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 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()}:${

View File

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

View File

@ -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 {

View File

@ -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({

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 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<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 { 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;
}

View File

@ -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<User>;
@ -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<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;

View File

@ -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;

View File

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

View File

@ -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<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;
};
export type UpdateUserBody = Partial<Omit<UserInfoWithPassword, 'uuid'>>;
export type LoginUserBody = {
username: string;
password: string;