error handler + login

This commit is contained in:
Yanis Rigaudeau 2022-10-15 22:04:27 +02:00
parent b2024bf4aa
commit f6a7415884
Signed by: yanis
GPG Key ID: 4DD2841DF1C94D83
15 changed files with 197 additions and 43 deletions

1
.gitignore vendored
View File

@ -3,5 +3,6 @@
**/node_modules
**/public/build
**/dist
**/config
!.prettierrc.json

View File

@ -9,6 +9,7 @@ Restart=on-failure
User=node
Group=node
Environment="CONFIGFILE=/home/node/config/config.json"
WorkingDirectory=/home/node/app
ExecStart=/usr/bin/node /home/node/app/dist/app.js

45
api/package-lock.json generated
View File

@ -11,6 +11,7 @@
"cors": "^2.8.5",
"express": "^4.18.1",
"express-session": "^1.17.3",
"express-validator": "^6.14.2",
"ip": "^1.1.8",
"mongodb": "^4.10.0",
"uuid": "^9.0.0"
@ -636,6 +637,18 @@
"node": ">= 0.6"
}
},
"node_modules/express-validator": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.2.tgz",
"integrity": "sha512-8XfAUrQ6Y7dIIuy9KcUPCfG/uCbvREctrxf5EeeME+ulanJ4iiW71lWmm9r4YcKKYOCBMan0WpVg7FtHu4Z4Wg==",
"dependencies": {
"lodash": "^4.17.21",
"validator": "^13.7.0"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -900,6 +913,11 @@
"node": ">=0.12.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@ -1629,6 +1647,14 @@
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"node_modules/validator": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -2175,6 +2201,15 @@
}
}
},
"express-validator": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.2.tgz",
"integrity": "sha512-8XfAUrQ6Y7dIIuy9KcUPCfG/uCbvREctrxf5EeeME+ulanJ4iiW71lWmm9r4YcKKYOCBMan0WpVg7FtHu4Z4Wg==",
"requires": {
"lodash": "^4.17.21",
"validator": "^13.7.0"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -2361,6 +2396,11 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@ -2866,6 +2906,11 @@
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"validator": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -6,8 +6,8 @@
"private": true,
"scripts": {
"build": "tsc",
"dev": "ts-node-dev --respawn --transpile-only ./src/app.ts",
"start": "npm run build && node ./dist/app.js",
"dev": "CONFIGFILE=config/config.json ts-node-dev --respawn --transpile-only ./src/app.ts",
"start": "npm run build && CONFIGFILE=config/config.json node ./dist/app.js",
"prettier": "prettier -w ./src"
},
"devDependencies": {
@ -25,6 +25,7 @@
"cors": "^2.8.5",
"express": "^4.18.1",
"express-session": "^1.17.3",
"express-validator": "^6.14.2",
"ip": "^1.1.8",
"mongodb": "^4.10.0",
"uuid": "^9.0.0"

View File

@ -2,26 +2,34 @@ import Server from './framework/express/server';
import ip from 'ip';
import UserModel from './framework/mongo/user';
import { MongoClient } from 'mongodb';
const PORT = 8000;
const MONGOURI = 'mongodb://localhost:27017';
const DBNAME = 'dev';
import { Config } from './config';
import { exit, env } from 'process';
export type Services = {
userModel: UserModel;
};
const mongo = new MongoClient(MONGOURI);
const db = mongo.db(DBNAME);
const configFile = env.CONFIGFILE;
if (configFile === undefined) {
console.log('env var CONFIGFILE not set');
exit(1);
}
const config = new Config(configFile);
const mongo = new MongoClient(config.mongo.uri);
const db = mongo.db(config.mongo.dbName);
const services: Services = {
userModel: new UserModel(db),
};
const server = new Server(services);
const server = new Server(config.server, services);
server.start(PORT, () =>
server.start(() =>
console.log(
`Running on http://127.0.0.1:${PORT} http://${ip.address()}:${PORT}`,
`Running on http://127.0.0.1:${config.server.port} http://${ip.address()}:${
config.server.port
}`,
),
);

21
api/src/config.ts Normal file
View File

@ -0,0 +1,21 @@
import { readFileSync } from 'fs';
export type ServerConfig = {
port: number;
};
export type MongoConfig = {
uri: string;
dbName: string;
};
export class Config {
server: ServerConfig;
mongo: MongoConfig;
constructor(configFile: string) {
const config = JSON.parse(readFileSync(configFile).toString()) as Config;
this.server = config.server;
this.mongo = config.mongo;
}
}

View File

@ -4,22 +4,22 @@ import {
UserInfoWithPassword,
UserWithPasswordCtor,
} from '@core';
import { createHash } from 'crypto';
import { generateHash } from '../functions/password';
import { Entity } from './entity';
export class User extends Entity implements UserInfo {
name: string;
username: string;
constructor(raw: UserCtor) {
super(raw);
this.name = raw.name ? raw.name : '';
this.username = raw.username ? raw.username : '';
}
Info(): UserInfo {
return {
uuid: this.uuid,
name: this.name,
username: this.username,
};
}
}
@ -30,15 +30,13 @@ export class UserWithPassword extends User implements UserInfoWithPassword {
constructor(raw: UserWithPasswordCtor) {
super(raw);
this.password = createHash('sha256')
.update(`${this.uuid}+${raw.password}`)
.digest('hex');
this.password = generateHash(this.uuid, raw.password || '');
}
Info(): UserInfoWithPassword {
return {
uuid: this.uuid,
name: this.name,
username: this.username,
password: this.password,
};
}

View File

@ -1,4 +1,4 @@
import { Request, RequestHandler } from 'express';
import { ErrorRequestHandler, Request, RequestHandler } from 'express';
import { randomUUID } from 'crypto';
export function getId(req: Request): string {
@ -11,3 +11,11 @@ export function BeforeEach(): RequestHandler {
next();
};
}
export function ErrorHandler(): ErrorRequestHandler {
return (error, req, res, next) => {
error.status
? res.status(error.status).send(error)
: res.status(500).send(error);
};
}

View File

@ -1,22 +1,27 @@
import express, { Express } from 'express';
import cors from 'cors';
import * as router from './router';
import { BeforeEach } from './middleware';
import { BeforeEach, ErrorHandler } from './middleware';
import { Services } from '../../app';
import { ServerConfig } from '../../config';
class Server {
private app: Express;
private config: ServerConfig;
constructor(config: ServerConfig, services: Services) {
this.config = config;
constructor(services: Services) {
this.app = express();
this.app.use(express.json());
this.app.use(cors());
this.app.use(BeforeEach());
this.app.use(router.getRoutes(services));
this.app.use(ErrorHandler());
}
start(port: number, func: () => void): void {
this.app.listen(port, func);
start(func: () => void): void {
this.app.listen(this.config.port, func);
}
}

View File

@ -1,31 +1,36 @@
import { RequestHandler, Router } from 'express';
import { Services } from '../../app';
import { Create, Read } from '../../functions/user';
import { Create, Read, Login } from '../../functions/user';
import { getId } from './middleware';
export function LoginHandler(services: Services): RequestHandler {
return async (req, res) => {
return res.send('Hey!\n');
const login = Login(services);
return async (req, res, next) => {
const user = await login(getId(req), req.body);
user
? res.status(200).send(user)
: next({ status: 404, message: 'bad user or password' });
};
}
export function CreateHandler(services: Services): RequestHandler {
const createUser = Create(services);
return async (req, res) => {
return async (req, res, next) => {
const user = await createUser(getId(req), req.body);
res.send(user);
user ? res.status(201).send(user) : next();
};
}
export function ReadHandler(services: Services): RequestHandler {
const readUser = Read(services);
return async (req, res) => {
return async (req, res, next) => {
const user = await readUser(getId(req), req.params.uuid);
res.send(user);
user
? res.status(200).send(user)
: next({ status: 404, message: 'user not found' });
};
}

View File

@ -1,5 +1,8 @@
import { Collection, Db } from 'mongodb';
import { LoginUserBody } from '../../../../core/src/user';
import { User, UserWithPassword } from '../../entities/user';
import log from '../../functions/logger';
import { generateHash } from '../../functions/password';
class UserModel {
private collection: Collection<User>;
@ -8,14 +11,42 @@ class UserModel {
this.collection = db.collection<User>('users');
}
public async login(tracker: string, data: LoginUserBody) {
const checkUser = await this.collection.findOne({
username: data.username,
});
if (checkUser === null) {
log(tracker, 'User Not Found');
return null;
}
const user = await this.collection.findOne({
uuid: checkUser.uuid,
password: generateHash(checkUser.uuid, data.password),
});
if (user === null) {
log(tracker, 'Wrong Password');
return null;
}
log(tracker, 'LOG IN', user);
return new User(user);
}
public async create(tracker: string, user: UserWithPassword) {
await this.collection.insertOne(user);
log(tracker, 'CREATE USER', user);
return this.read(tracker, user.uuid);
}
public async read(tracker: string, uuid: string) {
const user = await this.collection.findOne({ uuid });
return new User(user || { name: 'not found' });
if (user === null) {
log(tracker, 'User Not Found');
return null;
}
log(tracker, 'READ USER', user);
return new User(user);
}
}

View File

@ -0,0 +1,11 @@
export function log(tracker: string, ...message: unknown[]) {
message.forEach((obj, index) => {
try {
message[index] = JSON.stringify(obj);
} catch {}
});
tracker ? console.log(`[${tracker}]`, ...message) : console.log(...message);
}
export default log;

View File

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

View File

@ -1,25 +1,36 @@
import { CreateUserBody, UserInfo } from '@core';
import { CreateUserBody, LoginUserBody, UserInfo } from '@core';
import { Services } from '../app';
import { User, UserWithPassword } from '../entities/user';
import { UserWithPassword } from '../entities/user';
export function Login(
services: Services,
): (tracker: string, raw: LoginUserBody) => Promise<UserInfo | null> {
const { userModel } = services;
return async (tracker, raw) => {
const user = await userModel.login(tracker, raw);
return user ? user.Info() : null;
};
}
export function Create(
services: Services,
): (tracker: string, raw: CreateUserBody) => Promise<UserInfo> {
): (tracker: string, raw: CreateUserBody) => Promise<UserInfo | null> {
const { userModel } = services;
return async (tracker, raw) => {
const user = await userModel.create(tracker, new UserWithPassword(raw));
return user.Info();
return user ? user.Info() : null;
};
}
export function Read(
services: Services,
): (tracker: string, uuid: string) => Promise<UserInfo> {
): (tracker: string, uuid: string) => Promise<UserInfo | null> {
const { userModel } = services;
return async (tracker, uuid) => {
const user = await userModel.read(tracker, uuid);
return user.Info();
return user ? user.Info() : null;
};
}

View File

@ -2,7 +2,7 @@ import { EntityInfo } from './entity';
export type UserInfo = {
uuid: string;
name: string;
username: string;
};
export type UserInfoWithPassword = {
@ -14,11 +14,14 @@ export type User = UserInfo & EntityInfo;
export type UserWithPassword = UserInfoWithPassword & EntityInfo;
export type CreateUserBody = {
name: string;
username: string;
password: string;
};
export type LoginUserBody = CreateUserBody;
export type LoginUserBody = {
username: string;
password: string;
};
export type UserCtor = Partial<User>;