diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 8080931..be36cdc 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -188,3 +188,43 @@ export async function createUserHandler( return res.status(201) .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); +} diff --git a/src/middleware/inferUser.ts b/src/middleware/inferUser.ts index 3b0bddb..44c08a1 100644 --- a/src/middleware/inferUser.ts +++ b/src/middleware/inferUser.ts @@ -2,7 +2,7 @@ // https://github.com/TomDoesTech/REST-API-Tutorial-Updated/blob/7b5f040e1acd94d267df585516b33ee7e3b75f70/src/middleware/deserializeUser.ts import { get } from 'lodash'; import { Request, Response, NextFunction } from 'express'; -import { verifyJwt } from '../tools/jwt'; +import * as jwt from '../tools/jwt'; const inferUser = async ( req: Request, @@ -17,12 +17,9 @@ const inferUser = async ( if (!accessToken) return next(); - const { decoded } = verifyJwt(accessToken, 'accessTokenPublicKey'); - - // console.log('decoded user:', decoded); - - if (decoded) { - res.locals.user = decoded; + const token = jwt.verifyJwt(accessToken, 'accessTokenPrivateKey'); + if (token) { + res.locals.user = token; return next(); } diff --git a/src/middleware/requireUser.ts b/src/middleware/requireUser.ts new file mode 100644 index 0000000..0d2895b --- /dev/null +++ b/src/middleware/requireUser.ts @@ -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; \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index ca8dbfa..300c732 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -2,6 +2,7 @@ import { Router } from 'express'; import validateSchema from '../tools/validateSchema'; import * as ac from '../controllers/authController'; import * as as from '../schemas/authSchema'; +import requireUser from '../middleware/requireUser'; 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); +/** + * @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; \ No newline at end of file diff --git a/src/schemas/authSchema.ts b/src/schemas/authSchema.ts index e6d18fc..f829891 100644 --- a/src/schemas/authSchema.ts +++ b/src/schemas/authSchema.ts @@ -80,3 +80,33 @@ export type UserInfoDTO = { 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; +}; diff --git a/src/schemas/linkSchema.ts b/src/schemas/linkSchema.ts index 83a9c02..5ced090 100644 --- a/src/schemas/linkSchema.ts +++ b/src/schemas/linkSchema.ts @@ -51,7 +51,7 @@ export type SentenceLinkRequestDTO = z.TypeOf; * default: ok on success, otherwise ErrorDTO with error * uri: * type: string - * default: username + * default: generated uri * subdomain: * type: string * default: subdomain or null diff --git a/src/services/userService.ts b/src/services/userService.ts index 5cb9ae0..f86730d 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -75,6 +75,16 @@ export class UserService { } + /** + * Finds a User by it's identifier. + * + * @param {number} id The identifier + * @return {(Promise)} User (or null if not found) + */ + async findById(id: number): Promise { + return await this.userRepo.findOneBy({id}); + } + /** * Counts all the user entities in DB. * Used to tell whether the next created account should be an admin diff --git a/src/tools/jwt.ts b/src/tools/jwt.ts index 9c733c6..9f4c001 100644 --- a/src/tools/jwt.ts +++ b/src/tools/jwt.ts @@ -4,10 +4,17 @@ import jwt from 'jsonwebtoken'; import { DEFAULT_TOKEN_LIFETIME } from '../schemas/authSchema'; import * as env from './env'; -type JwtStatus = { +export type JwtDecoded = { + sub: number; + role: number; + iat: number; + exp: number; +}; + +export type JwtStatus = { valid: 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( token: string, - keyName: 'accessTokenPublicKey' | 'refreshTokenPublicKey' + keyName: 'accessTokenPrivateKey' | 'refreshTokenPrivateKey' ): JwtStatus { // refresh tokens aren't (yet) supported @@ -64,18 +71,21 @@ export function verifyJwt( const secret: string = env.getString(keyName, true)!; 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 { valid: true, expired: false, - decoded, + decoded: decoded as unknown as JwtDecoded }; } catch (e: any) { console.error('JWT verify error:', e); return { - valid: false, + valid: e.message !== 'jwt malformed', expired: e.message === 'jwt expired', - decoded: null, + decoded: null }; } }