update + schema validate

This commit is contained in:
Yanis Rigaudeau 2022-11-06 18:40:30 +01:00
parent 5c21712344
commit 0bb28ce89a
Signed by: yanis
GPG Key ID: 4DD2841DF1C94D83
12 changed files with 163 additions and 39 deletions

View File

@ -1,19 +1,19 @@
import './paths'; import './paths';
import Server from './framework/express/server';
import ip from 'ip';
import UserModel from './framework/mongo/user';
import { MongoClient } from 'mongodb'; 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 { Config } from './config';
import { exit, env } from 'process';
export type Services = { export type Services = {
userModel: UserModel; userModel: UserModel;
}; };
const configFile = env.CONFIGFILE; const configFile = process.env.CONFIGFILE;
if (configFile === undefined) { if (configFile === undefined) {
console.log('env var CONFIGFILE not set'); console.log('env var CONFIGFILE not set');
exit(1); process.exit(1);
} }
const config = new Config(configFile); const config = new Config(configFile);
@ -27,6 +27,13 @@ const services: Services = {
const server = new Server(config.server, config.mongo, 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(() => server.start(() =>
console.log( console.log(
`Running on http://127.0.0.1:${config.server.port} http://${ip.address()}:${ `Running on http://127.0.0.1:${config.server.port} http://${ip.address()}:${

View File

@ -16,6 +16,15 @@ export class Config {
constructor(configFile: string) { constructor(configFile: string) {
const config = JSON.parse(readFileSync(configFile).toString()) as Config; 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.server = config.server;
this.mongo = config.mongo; this.mongo = config.mongo;
} }

View File

@ -5,7 +5,7 @@ import {
UserRoles, UserRoles,
UserWithPasswordCtor, UserWithPasswordCtor,
} from '@core'; } from '@core';
import { generateHash } from '../functions/password'; import { getHashedPassword } from '../functions/password';
import { Entity } from './entity'; import { Entity } from './entity';
export class User extends Entity implements UserInfo { export class User extends Entity implements UserInfo {
@ -34,7 +34,7 @@ export class UserWithPassword extends User implements UserInfoWithPassword {
constructor(raw: UserWithPasswordCtor) { constructor(raw: UserWithPasswordCtor) {
super(raw); super(raw);
this.password = generateHash(this.uuid, raw.password || ''); this.password = getHashedPassword(this.uuid, raw.password || '');
} }
Info(): UserInfoWithPassword { Info(): UserInfoWithPassword {

View File

@ -1,6 +1,6 @@
import { ErrorRequestHandler, Request, RequestHandler } from 'express'; import { ErrorRequestHandler, Request, RequestHandler } from 'express';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { validationResult } from 'express-validator'; import { validationResult, matchedData } from 'express-validator';
import { UserInfo, UserRoles } from '@core'; import { UserInfo, UserRoles } from '@core';
declare module 'express-session' { declare module 'express-session' {
@ -10,22 +10,21 @@ declare module 'express-session' {
} }
export function getRequestId(req: Request): string { export function getRequestId(req: Request): string {
return req.header('request-id') || 'unknown'; return req.header('x-request-id') || 'unknown';
} }
export function RequestId(): RequestHandler { export function RequestId(): RequestHandler {
return (req, res, next) => { return (req, res, next) => {
req.headers['request-id'] = randomUUID(); req.headers['x-request-id'] = randomUUID();
next(); next();
}; };
} }
export function CheckPermissions(): RequestHandler { export function CheckPermissions(): RequestHandler {
function getResourceId(req: Request): string | null { function getResourceId(req: Request): string | undefined {
if (req.method === 'GET' && req.params.uuid) return req.params.uuid; if (req.params.uuid) return req.params.uuid;
if ((req.method === 'POST' || req.method === 'PUT') && req.body.uuid) if (req.body.uuid) return req.body.uuid;
return req.body.uuid; return undefined;
return null;
} }
function canAccessRessource(user: UserInfo, uuid: string): boolean { 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) => { 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({ return next({
status: 400, status: 422,
message: `Found ${Object.keys(req.body).length} keys expected ${keys}`, message: 'Unprocessable Entity',
}); });
const error = validationResult(req);
error.isEmpty() error.isEmpty()
? next() ? next()
: next({ : next({

View File

@ -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,
},
});

View File

@ -1,15 +1,18 @@
import express, { Express } from 'express'; import express, { Express } from 'express';
import cors from 'cors'; import cors from 'cors';
import session from 'express-session'; import session from 'express-session';
import MongoStore from 'connect-mongo';
import { randomUUID } from 'crypto';
import * as http from 'http';
import { Routes } from './router'; import { Routes } from './router';
import { RequestId, ErrorHandler } from './middleware'; import { RequestId, ErrorHandler } from './middleware';
import { Services } from '../../app'; import { Services } from '../../app';
import { MongoConfig, ServerConfig } from '../../config'; import { MongoConfig, ServerConfig } from '../../config';
import { randomUUID } from 'crypto';
import MongoStore from 'connect-mongo';
class Server { class Server {
private app: Express; private app: Express;
private server: http.Server;
private config: ServerConfig; private config: ServerConfig;
constructor( constructor(
@ -54,7 +57,16 @@ class Server {
} }
start(func: () => void): void { start(func: () => void): void {
this.app.listen(this.config.port, func); this.server = this.app.listen(this.config.port, func);
}
async close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.server.close((error) => {
if (error) reject(error);
resolve();
});
});
} }
} }

View File

@ -1,13 +1,19 @@
import { RequestHandler, Router } from 'express'; import { RequestHandler, Router } from 'express';
import { Services } from '../../app'; import { Services } from '../../app';
import { CreateUser, ReadUser, LoginUser } from '../../functions/user'; import {
CreateUser,
ReadUser,
LoginUser,
UpdateUser,
} from '../../functions/user';
import { import {
LoginUserSchema, LoginUserSchema,
CreateUserSchema, CreateUserSchema,
ReadUserSchema, ReadUserSchema,
LogoutUserSchema, LogoutUserSchema,
UpdateUserSchema,
} from './schema/user'; } from './schema/user';
import { CheckPermissions, getRequestId, SchemaValidator } from './middleware'; import { CheckPermissions, getRequestId, ValidateSchema } from './middleware';
function LoginHandler(services: Services): RequestHandler { function LoginHandler(services: Services): RequestHandler {
const login = LoginUser(services); 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) { export function Routes(services: Services) {
const router = Router(); const router = Router();
router.post( router.post(
'/login', '/login',
LoginUserSchema(), LoginUserSchema(),
SchemaValidator(2), ValidateSchema(),
LoginHandler(services), LoginHandler(services),
); );
router.post( router.post(
'/logout', '/logout',
LogoutUserSchema(), LogoutUserSchema(),
SchemaValidator(), ValidateSchema(),
LogoutHandler(services), LogoutHandler(services),
); );
router.get( router.get(
'/read/:uuid', '/read/:uuid',
CheckPermissions(), CheckPermissions(),
ReadUserSchema(), ReadUserSchema(),
SchemaValidator(), ValidateSchema(),
ReadHandler(services), ReadHandler(services),
); );
@ -87,9 +110,17 @@ export function Routes(services: Services) {
'/create', '/create',
CheckPermissions(), CheckPermissions(),
CreateUserSchema(), CreateUserSchema(),
SchemaValidator(3), ValidateSchema(),
CreateHandler(services), CreateHandler(services),
); );
router.patch(
'/update/:uuid',
CheckPermissions(),
UpdateUserSchema(),
ValidateSchema(),
UpdateHandler(services),
);
return router; return router;
} }

View File

@ -1,8 +1,8 @@
import { Collection, Db } from 'mongodb'; import { Collection, Db } from 'mongodb';
import { LoginUserBody, UserInfo } from '@core'; import { LoginUserBody, UpdateUserBody } from '@core';
import { User, UserWithPassword } from '../../entities/user'; import { User, UserWithPassword } from '../../entities/user';
import log from '../../functions/logger'; import { log } from '../../functions/logger';
import { generateHash } from '../../functions/password'; import { getHashedPassword } from '../../functions/password';
class UserModel { class UserModel {
private collection: Collection<User>; private collection: Collection<User>;
@ -22,7 +22,7 @@ class UserModel {
const userDocument = await this.collection.findOne({ const userDocument = await this.collection.findOne({
uuid: checkUser.uuid, uuid: checkUser.uuid,
password: generateHash(checkUser.uuid, data.password), password: getHashedPassword(checkUser.uuid, data.password),
}); });
if (!userDocument) { if (!userDocument) {
log(tracker, 'Wrong Password'); log(tracker, 'Wrong Password');
@ -31,7 +31,7 @@ class UserModel {
const user = new User(userDocument); const user = new User(userDocument);
log(tracker, 'LOG IN', user); log(tracker, 'LOG IN', user);
return new User(user); return user;
} }
public async create( public async create(
@ -64,6 +64,35 @@ class UserModel {
log(tracker, 'READ USER', user); log(tracker, 'READ USER', user);
return user; return user;
} }
public async update(
tracker: string,
uuid: string,
userInfo: UpdateUserBody,
): Promise<User> {
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; export default UserModel;

View File

@ -6,7 +6,5 @@ export function log(tracker: string, ...message: unknown[]) {
} catch {} } catch {}
}); });
message ? console.log(`[${tracker}]`, ...message) : console.log(...tracker); message ? console.log(`[${tracker}]`, ...message) : console.log(tracker);
} }
export default log;

View File

@ -1,5 +1,9 @@
import { createHash } from 'crypto'; import { createHash } from 'crypto';
export function getHashedPassword(uuid: string, password: string) {
return generateHash(uuid, password);
}
export function generateHash(...values: string[]) { export function generateHash(...values: string[]) {
return createHash('sha256').update(values.join('')).digest('hex'); return createHash('sha256').update(values.join('')).digest('hex');
} }

View File

@ -1,4 +1,4 @@
import { CreateUserBody, LoginUserBody, UserInfo } from '@core'; import { CreateUserBody, LoginUserBody, UserInfo, UpdateUserBody } from '@core';
import { Services } from '../app'; import { Services } from '../app';
import { UserWithPassword } from '../entities/user'; import { UserWithPassword } from '../entities/user';
@ -34,3 +34,14 @@ export function ReadUser(
return user.Info(); return user.Info();
}; };
} }
export function UpdateUser(
services: Services,
): (tracker: string, uuid: string, raw: UpdateUserBody) => Promise<UserInfo> {
const { userModel } = services;
return async (tracker, uuid, raw) => {
const user = await userModel.update(tracker, uuid, raw);
return user.Info();
};
}

View File

@ -29,6 +29,8 @@ export type CreateUserBody = {
role: keyof typeof UserRoles; role: keyof typeof UserRoles;
}; };
export type UpdateUserBody = Partial<Omit<UserInfoWithPassword, 'uuid'>>;
export type LoginUserBody = { export type LoginUserBody = {
username: string; username: string;
password: string; password: string;