feat: add sample endpoint to test JWT
This commit is contained in:
@@ -188,3 +188,43 @@ export async function createUserHandler(
|
|||||||
return res.status(201)
|
return res.status(201)
|
||||||
.send(userResponse);
|
.send(userResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `GET /api/v1/user/account`
|
||||||
|
*
|
||||||
|
* Handles requests for user info retrieval
|
||||||
|
*
|
||||||
|
* @param {Request} req The Express request
|
||||||
|
* @param {Response} res The Express resource
|
||||||
|
*/
|
||||||
|
export async function getUserHandler(
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) {
|
||||||
|
|
||||||
|
const userService = new UserService();
|
||||||
|
const user: jwt.JwtDecoded = res.locals.user.decoded;
|
||||||
|
|
||||||
|
// Get detailed data from DB
|
||||||
|
let existingUser: User | null = await userService.findById(user.sub);
|
||||||
|
|
||||||
|
if (existingUser === null) {
|
||||||
|
const error: ms.ErrorDTO = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'User does not exist',
|
||||||
|
code: 'deleted_user'
|
||||||
|
};
|
||||||
|
return res.status(404)
|
||||||
|
.json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond with ShortUserInfoDTO
|
||||||
|
const userResponse: as.ShortUserInfoDTO = {
|
||||||
|
status: 'ok',
|
||||||
|
id: existingUser.id!,
|
||||||
|
name: existingUser.name,
|
||||||
|
role: existingUser.role
|
||||||
|
}
|
||||||
|
return res.status(201)
|
||||||
|
.send(userResponse);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// https://github.com/TomDoesTech/REST-API-Tutorial-Updated/blob/7b5f040e1acd94d267df585516b33ee7e3b75f70/src/middleware/deserializeUser.ts
|
// https://github.com/TomDoesTech/REST-API-Tutorial-Updated/blob/7b5f040e1acd94d267df585516b33ee7e3b75f70/src/middleware/deserializeUser.ts
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { verifyJwt } from '../tools/jwt';
|
import * as jwt from '../tools/jwt';
|
||||||
|
|
||||||
const inferUser = async (
|
const inferUser = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -17,12 +17,9 @@ const inferUser = async (
|
|||||||
|
|
||||||
if (!accessToken) return next();
|
if (!accessToken) return next();
|
||||||
|
|
||||||
const { decoded } = verifyJwt(accessToken, 'accessTokenPublicKey');
|
const token = jwt.verifyJwt(accessToken, 'accessTokenPrivateKey');
|
||||||
|
if (token) {
|
||||||
// console.log('decoded user:', decoded);
|
res.locals.user = token;
|
||||||
|
|
||||||
if (decoded) {
|
|
||||||
res.locals.user = decoded;
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
src/middleware/requireUser.ts
Normal file
41
src/middleware/requireUser.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { ErrorDTO } from "../schemas/miscSchema";
|
||||||
|
import * as jwt from "../tools/jwt";
|
||||||
|
|
||||||
|
const requireUser = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const user: jwt.JwtStatus = res.locals.user;
|
||||||
|
let error: ErrorDTO | null = null;
|
||||||
|
|
||||||
|
// No user? Something errored partway. Display an error.
|
||||||
|
if (!user)
|
||||||
|
error = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'Unauthorized, please sign in',
|
||||||
|
code: 'unauthorized_generic'
|
||||||
|
};
|
||||||
|
// Check if token is expired first.
|
||||||
|
// This is because a token can be valid
|
||||||
|
// (if signature matches) while being expired.
|
||||||
|
else if (user.expired)
|
||||||
|
error = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'Token expired, please sign in again',
|
||||||
|
code: 'expired_token'
|
||||||
|
};
|
||||||
|
// Previous checks failed?
|
||||||
|
// As a last resort, check if the token is valid.
|
||||||
|
else if (!user.valid)
|
||||||
|
error = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'Invalid token, please sign in',
|
||||||
|
code: 'invalid_token'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error !== null)
|
||||||
|
return res.status(401)
|
||||||
|
.send(error);
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireUser;
|
||||||
@@ -2,6 +2,7 @@ import { Router } from 'express';
|
|||||||
import validateSchema from '../tools/validateSchema';
|
import validateSchema from '../tools/validateSchema';
|
||||||
import * as ac from '../controllers/authController';
|
import * as ac from '../controllers/authController';
|
||||||
import * as as from '../schemas/authSchema';
|
import * as as from '../schemas/authSchema';
|
||||||
|
import requireUser from '../middleware/requireUser';
|
||||||
|
|
||||||
const userRouter = Router();
|
const userRouter = Router();
|
||||||
|
|
||||||
@@ -69,4 +70,33 @@ userRouter.post('/api/v1/user/signUp', validateSchema(as.loginRequestSchema), ac
|
|||||||
*/
|
*/
|
||||||
userRouter.post('/api/v1/user/signIn', validateSchema(as.loginRequestSchema), ac.loginUserHandler);
|
userRouter.post('/api/v1/user/signIn', validateSchema(as.loginRequestSchema), ac.loginUserHandler);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
*
|
||||||
|
* /api/v1/user/account:
|
||||||
|
* get:
|
||||||
|
* description: Get authenticated user info
|
||||||
|
* tags: [User]
|
||||||
|
* summary: "[AUTHED] Get user info"
|
||||||
|
* produces:
|
||||||
|
* - application/json
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: User logged in successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/ShortUserInfoDTO'
|
||||||
|
* 400:
|
||||||
|
* description: Wrong password/non-existent user
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/ErrorDTO'
|
||||||
|
*/
|
||||||
|
userRouter.get('/api/v1/user/account',
|
||||||
|
requireUser,
|
||||||
|
ac.getUserHandler
|
||||||
|
);
|
||||||
|
|
||||||
export default userRouter;
|
export default userRouter;
|
||||||
@@ -80,3 +80,33 @@ export type UserInfoDTO = {
|
|||||||
ttl: number | null;
|
ttl: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* ShortUserInfoDTO:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - status
|
||||||
|
* - id
|
||||||
|
* - name
|
||||||
|
* - role
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* default: ok on success, otherwise ErrorDTO with error
|
||||||
|
* id:
|
||||||
|
* type: number
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* default: username
|
||||||
|
* role:
|
||||||
|
* type: number
|
||||||
|
* default: 0 # 0 - standard user, 1 - administrator
|
||||||
|
*/
|
||||||
|
export type ShortUserInfoDTO = {
|
||||||
|
status: 'ok';
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
role: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export type SentenceLinkRequestDTO = z.TypeOf<typeof sentenceLinkRequestSchema>;
|
|||||||
* default: ok on success, otherwise ErrorDTO with error
|
* default: ok on success, otherwise ErrorDTO with error
|
||||||
* uri:
|
* uri:
|
||||||
* type: string
|
* type: string
|
||||||
* default: username
|
* default: generated uri
|
||||||
* subdomain:
|
* subdomain:
|
||||||
* type: string
|
* type: string
|
||||||
* default: subdomain or null
|
* default: subdomain or null
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ export class UserService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a User by it's identifier.
|
||||||
|
*
|
||||||
|
* @param {number} id The identifier
|
||||||
|
* @return {(Promise<User|null>)} User (or null if not found)
|
||||||
|
*/
|
||||||
|
async findById(id: number): Promise<User | null> {
|
||||||
|
return await this.userRepo.findOneBy({id});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts all the user entities in DB.
|
* Counts all the user entities in DB.
|
||||||
* Used to tell whether the next created account should be an admin
|
* Used to tell whether the next created account should be an admin
|
||||||
|
|||||||
@@ -4,10 +4,17 @@ import jwt from 'jsonwebtoken';
|
|||||||
import { DEFAULT_TOKEN_LIFETIME } from '../schemas/authSchema';
|
import { DEFAULT_TOKEN_LIFETIME } from '../schemas/authSchema';
|
||||||
import * as env from './env';
|
import * as env from './env';
|
||||||
|
|
||||||
type JwtStatus = {
|
export type JwtDecoded = {
|
||||||
|
sub: number;
|
||||||
|
role: number;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JwtStatus = {
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
expired: boolean;
|
expired: boolean;
|
||||||
decoded: string | jwt.JwtPayload | null;
|
decoded: JwtDecoded | null; // null if decoding failed
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +59,7 @@ export function signJwt(
|
|||||||
*/
|
*/
|
||||||
export function verifyJwt(
|
export function verifyJwt(
|
||||||
token: string,
|
token: string,
|
||||||
keyName: 'accessTokenPublicKey' | 'refreshTokenPublicKey'
|
keyName: 'accessTokenPrivateKey' | 'refreshTokenPrivateKey'
|
||||||
): JwtStatus {
|
): JwtStatus {
|
||||||
|
|
||||||
// refresh tokens aren't (yet) supported
|
// refresh tokens aren't (yet) supported
|
||||||
@@ -64,18 +71,21 @@ export function verifyJwt(
|
|||||||
const secret: string = env.getString(keyName, true)!;
|
const secret: string = env.getString(keyName, true)!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded: string | jwt.JwtPayload = jwt.verify(token, secret);
|
const decoded: jwt.JwtPayload | string = jwt.verify(token, secret);
|
||||||
|
|
||||||
|
// TODO: Can this be done better, smarter?
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
expired: false,
|
expired: false,
|
||||||
decoded,
|
decoded: decoded as unknown as JwtDecoded
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('JWT verify error:', e);
|
console.error('JWT verify error:', e);
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: e.message !== 'jwt malformed',
|
||||||
expired: e.message === 'jwt expired',
|
expired: e.message === 'jwt expired',
|
||||||
decoded: null,
|
decoded: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user