From 0bb28ce89a0321462ab114eac304e1de897b8795 Mon Sep 17 00:00:00 2001 From: Yanis Rigaudeau Date: Sun, 6 Nov 2022 18:40:30 +0100 Subject: [PATCH] update + schema validate --- api/src/app.ts | 19 +++++++---- api/src/config.ts | 9 +++++ api/src/entities/user.ts | 4 +-- api/src/framework/express/middleware.ts | 29 +++++++++------- api/src/framework/express/schema/user.ts | 18 ++++++++++ api/src/framework/express/server.ts | 18 ++++++++-- api/src/framework/express/user.ts | 43 ++++++++++++++++++++---- api/src/framework/mongo/user.ts | 39 ++++++++++++++++++--- api/src/functions/logger.ts | 4 +-- api/src/functions/password.ts | 4 +++ api/src/functions/user.ts | 13 ++++++- core/src/user.ts | 2 ++ 12 files changed, 163 insertions(+), 39 deletions(-) diff --git a/api/src/app.ts b/api/src/app.ts index df325cb..7ee06ff 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -1,19 +1,19 @@ import './paths'; -import Server from './framework/express/server'; -import ip from 'ip'; -import UserModel from './framework/mongo/user'; import { MongoClient } from 'mongodb'; +import ip from 'ip'; + +import Server from './framework/express/server'; +import UserModel from './framework/mongo/user'; import { Config } from './config'; -import { exit, env } from 'process'; export type Services = { userModel: UserModel; }; -const configFile = env.CONFIGFILE; +const configFile = process.env.CONFIGFILE; if (configFile === undefined) { console.log('env var CONFIGFILE not set'); - exit(1); + process.exit(1); } const config = new Config(configFile); @@ -27,6 +27,13 @@ const services: Services = { const server = new Server(config.server, config.mongo, services); +process.on('SIGINT', async () => { + console.log('\nClosing Api...'); + await mongo.close(); + await server.close(); + process.exit(); +}); + server.start(() => console.log( `Running on http://127.0.0.1:${config.server.port} http://${ip.address()}:${ diff --git a/api/src/config.ts b/api/src/config.ts index a697bda..26ce6cb 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -16,6 +16,15 @@ export class Config { constructor(configFile: string) { const config = JSON.parse(readFileSync(configFile).toString()) as Config; + if ( + config.mongo.dbName === undefined || + config.mongo.uri === undefined || + config.server.origin === undefined || + config.server.port === undefined + ) { + console.log('Error in Config File'); + process.exit(1); + } this.server = config.server; this.mongo = config.mongo; } diff --git a/api/src/entities/user.ts b/api/src/entities/user.ts index 22ea6b8..5045d78 100644 --- a/api/src/entities/user.ts +++ b/api/src/entities/user.ts @@ -5,7 +5,7 @@ import { UserRoles, UserWithPasswordCtor, } from '@core'; -import { generateHash } from '../functions/password'; +import { getHashedPassword } from '../functions/password'; import { Entity } from './entity'; export class User extends Entity implements UserInfo { @@ -34,7 +34,7 @@ export class UserWithPassword extends User implements UserInfoWithPassword { constructor(raw: UserWithPasswordCtor) { super(raw); - this.password = generateHash(this.uuid, raw.password || ''); + this.password = getHashedPassword(this.uuid, raw.password || ''); } Info(): UserInfoWithPassword { diff --git a/api/src/framework/express/middleware.ts b/api/src/framework/express/middleware.ts index 54a3e3f..11b8605 100644 --- a/api/src/framework/express/middleware.ts +++ b/api/src/framework/express/middleware.ts @@ -1,6 +1,6 @@ import { ErrorRequestHandler, Request, RequestHandler } from 'express'; import { randomUUID } from 'crypto'; -import { validationResult } from 'express-validator'; +import { validationResult, matchedData } from 'express-validator'; import { UserInfo, UserRoles } from '@core'; declare module 'express-session' { @@ -10,22 +10,21 @@ declare module 'express-session' { } export function getRequestId(req: Request): string { - return req.header('request-id') || 'unknown'; + return req.header('x-request-id') || 'unknown'; } export function RequestId(): RequestHandler { return (req, res, next) => { - req.headers['request-id'] = randomUUID(); + req.headers['x-request-id'] = randomUUID(); next(); }; } export function CheckPermissions(): RequestHandler { - function getResourceId(req: Request): string | null { - if (req.method === 'GET' && req.params.uuid) return req.params.uuid; - if ((req.method === 'POST' || req.method === 'PUT') && req.body.uuid) - return req.body.uuid; - return null; + function getResourceId(req: Request): string | undefined { + if (req.params.uuid) return req.params.uuid; + if (req.body.uuid) return req.body.uuid; + return undefined; } function canAccessRessource(user: UserInfo, uuid: string): boolean { @@ -55,15 +54,19 @@ export function CheckPermissions(): RequestHandler { }; } -export function SchemaValidator(keys: number = 0): RequestHandler { +export function ValidateSchema(): RequestHandler { return (req, res, next) => { - if (Object.keys(req.body).length > keys) + const error = validationResult(req); + + const oldBody = req.body; + req.body = matchedData(req, { locations: ['body'] }); + + if (JSON.stringify(oldBody) !== JSON.stringify(req.body)) return next({ - status: 400, - message: `Found ${Object.keys(req.body).length} keys expected ${keys}`, + status: 422, + message: 'Unprocessable Entity', }); - const error = validationResult(req); error.isEmpty() ? next() : next({ diff --git a/api/src/framework/express/schema/user.ts b/api/src/framework/express/schema/user.ts index eb9a541..11d134d 100644 --- a/api/src/framework/express/schema/user.ts +++ b/api/src/framework/express/schema/user.ts @@ -36,3 +36,21 @@ export const CreateUserSchema = () => }, }, }); + +export const UpdateUserSchema = () => + checkSchema({ + username: { + isString: true, + optional: true, + }, + password: { + isString: true, + optional: true, + }, + role: { + isIn: { + options: [Object.values(UserRoles)], + }, + optional: true, + }, + }); diff --git a/api/src/framework/express/server.ts b/api/src/framework/express/server.ts index 64478bd..7587eb6 100644 --- a/api/src/framework/express/server.ts +++ b/api/src/framework/express/server.ts @@ -1,15 +1,18 @@ import express, { Express } from 'express'; import cors from 'cors'; import session from 'express-session'; +import MongoStore from 'connect-mongo'; +import { randomUUID } from 'crypto'; +import * as http from 'http'; + import { Routes } from './router'; import { RequestId, ErrorHandler } from './middleware'; import { Services } from '../../app'; import { MongoConfig, ServerConfig } from '../../config'; -import { randomUUID } from 'crypto'; -import MongoStore from 'connect-mongo'; class Server { private app: Express; + private server: http.Server; private config: ServerConfig; constructor( @@ -54,7 +57,16 @@ class Server { } start(func: () => void): void { - this.app.listen(this.config.port, func); + this.server = this.app.listen(this.config.port, func); + } + + async close(): Promise { + return new Promise((resolve, reject) => { + this.server.close((error) => { + if (error) reject(error); + resolve(); + }); + }); } } diff --git a/api/src/framework/express/user.ts b/api/src/framework/express/user.ts index e6ba214..ae75a14 100644 --- a/api/src/framework/express/user.ts +++ b/api/src/framework/express/user.ts @@ -1,13 +1,19 @@ import { RequestHandler, Router } from 'express'; import { Services } from '../../app'; -import { CreateUser, ReadUser, LoginUser } from '../../functions/user'; +import { + CreateUser, + ReadUser, + LoginUser, + UpdateUser, +} from '../../functions/user'; import { LoginUserSchema, CreateUserSchema, ReadUserSchema, LogoutUserSchema, + UpdateUserSchema, } from './schema/user'; -import { CheckPermissions, getRequestId, SchemaValidator } from './middleware'; +import { CheckPermissions, getRequestId, ValidateSchema } from './middleware'; function LoginHandler(services: Services): RequestHandler { const login = LoginUser(services); @@ -60,26 +66,43 @@ function ReadHandler(services: Services): RequestHandler { }; } +function UpdateHandler(services: Services): RequestHandler { + const updateUser = UpdateUser(services); + + return async (req, res, next) => { + try { + const user = await updateUser( + getRequestId(req), + req.params.uuid, + req.body, + ); + res.status(200).send(user); + } catch (error) { + next({ status: 404, message: 'User Not Found' }); + } + }; +} + export function Routes(services: Services) { const router = Router(); router.post( '/login', LoginUserSchema(), - SchemaValidator(2), + ValidateSchema(), LoginHandler(services), ); router.post( '/logout', LogoutUserSchema(), - SchemaValidator(), + ValidateSchema(), LogoutHandler(services), ); router.get( '/read/:uuid', CheckPermissions(), ReadUserSchema(), - SchemaValidator(), + ValidateSchema(), ReadHandler(services), ); @@ -87,9 +110,17 @@ export function Routes(services: Services) { '/create', CheckPermissions(), CreateUserSchema(), - SchemaValidator(3), + ValidateSchema(), CreateHandler(services), ); + router.patch( + '/update/:uuid', + CheckPermissions(), + UpdateUserSchema(), + ValidateSchema(), + UpdateHandler(services), + ); + return router; } diff --git a/api/src/framework/mongo/user.ts b/api/src/framework/mongo/user.ts index 2c24d5d..e065e61 100644 --- a/api/src/framework/mongo/user.ts +++ b/api/src/framework/mongo/user.ts @@ -1,8 +1,8 @@ import { Collection, Db } from 'mongodb'; -import { LoginUserBody, UserInfo } from '@core'; +import { LoginUserBody, UpdateUserBody } from '@core'; import { User, UserWithPassword } from '../../entities/user'; -import log from '../../functions/logger'; -import { generateHash } from '../../functions/password'; +import { log } from '../../functions/logger'; +import { getHashedPassword } from '../../functions/password'; class UserModel { private collection: Collection; @@ -22,7 +22,7 @@ class UserModel { const userDocument = await this.collection.findOne({ uuid: checkUser.uuid, - password: generateHash(checkUser.uuid, data.password), + password: getHashedPassword(checkUser.uuid, data.password), }); if (!userDocument) { log(tracker, 'Wrong Password'); @@ -31,7 +31,7 @@ class UserModel { const user = new User(userDocument); log(tracker, 'LOG IN', user); - return new User(user); + return user; } public async create( @@ -64,6 +64,35 @@ class UserModel { log(tracker, 'READ USER', user); return user; } + + public async update( + tracker: string, + uuid: string, + userInfo: UpdateUserBody, + ): Promise { + const checkUser = await this.collection.findOne({ uuid }); + if (!checkUser) { + log(tracker, 'User Does Not Exist'); + throw new Error(); + } + + if (userInfo.password) + userInfo.password = getHashedPassword(uuid, userInfo.password); + + await this.collection.updateOne( + { uuid }, + { + $set: { + ...userInfo, + updatedAt: new Date(), + }, + }, + ); + + const user = await this.read(tracker, uuid); + log(tracker, 'UPDATE USER', user); + return user; + } } export default UserModel; diff --git a/api/src/functions/logger.ts b/api/src/functions/logger.ts index ccba982..6a92cfb 100644 --- a/api/src/functions/logger.ts +++ b/api/src/functions/logger.ts @@ -6,7 +6,5 @@ export function log(tracker: string, ...message: unknown[]) { } catch {} }); - message ? console.log(`[${tracker}]`, ...message) : console.log(...tracker); + message ? console.log(`[${tracker}]`, ...message) : console.log(tracker); } - -export default log; diff --git a/api/src/functions/password.ts b/api/src/functions/password.ts index 5a3ac14..cfdba9e 100644 --- a/api/src/functions/password.ts +++ b/api/src/functions/password.ts @@ -1,5 +1,9 @@ import { createHash } from 'crypto'; +export function getHashedPassword(uuid: string, password: string) { + return generateHash(uuid, password); +} + export function generateHash(...values: string[]) { return createHash('sha256').update(values.join('')).digest('hex'); } diff --git a/api/src/functions/user.ts b/api/src/functions/user.ts index 3026970..2fe1ba2 100644 --- a/api/src/functions/user.ts +++ b/api/src/functions/user.ts @@ -1,4 +1,4 @@ -import { CreateUserBody, LoginUserBody, UserInfo } from '@core'; +import { CreateUserBody, LoginUserBody, UserInfo, UpdateUserBody } from '@core'; import { Services } from '../app'; import { UserWithPassword } from '../entities/user'; @@ -34,3 +34,14 @@ export function ReadUser( return user.Info(); }; } + +export function UpdateUser( + services: Services, +): (tracker: string, uuid: string, raw: UpdateUserBody) => Promise { + const { userModel } = services; + + return async (tracker, uuid, raw) => { + const user = await userModel.update(tracker, uuid, raw); + return user.Info(); + }; +} diff --git a/core/src/user.ts b/core/src/user.ts index 77e326b..77a2528 100644 --- a/core/src/user.ts +++ b/core/src/user.ts @@ -29,6 +29,8 @@ export type CreateUserBody = { role: keyof typeof UserRoles; }; +export type UpdateUserBody = Partial>; + export type LoginUserBody = { username: string; password: string;