game flow revamp

This commit is contained in:
Jose Conde
2024-07-06 20:32:41 +02:00
parent 733ac3891f
commit a974f576b3
50 changed files with 2994 additions and 268 deletions

View File

@ -0,0 +1,90 @@
import { Request, Response } from "express";
import { ApiTokenMongoManager } from "../db/mongo/ApiTokenMongoManager";
import { SecurityManager } from "../managers/SecurityManager";
import { BaseController } from "./BaseController";
import { Token } from "../db/interfaces";
import toObjectId from "../db/mongo/common/mongoUtils";
export class ApiKeyController extends BaseController{
apiTokenManager = new ApiTokenMongoManager();
security = new SecurityManager();
async deleteApiKey(req: Request, res: Response) {
try {
const { id } = req.params;
await this.apiTokenManager.deleteToken(id);
res.status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async createApiKey(req: Request, res: Response) {
try {
const token: Token = this._createTokenObject(req);
await this.apiTokenManager.addToken(token);
res.status(201).end();
} catch (error) {
this.handleError(res, error);
}
}
async listUserApiKeys(req: Request, res: Response) {
try {
const { user } = req;
const response = await this.apiTokenManager.getTokens(user._id);
res.json(response).status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async listNamespaceApiKeys(req: Request, res: Response) {
try {
const { namespaceId } = req.user;
const response = await this.apiTokenManager.getTokensByNamespace(namespaceId);
res.json(response).status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async deleteNamespaceApiKey(req: Request, res: Response) {
try {
const { tokenId: id } = req.params;
await this.apiTokenManager.deleteToken(id);
res.status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async createNamespaceApiKey(req: Request, res: Response) {
try {
const token = this._createTokenObject(req);
await this.apiTokenManager.addToken(token);
res.status(201).end();
} catch (error) {
this.handleError(res, error);
}
}
_createTokenObject(req: Request): Token {
const { user, body } = req;
const { roles = [ 'client' ], type = 'user', description = '' } = body;
const { _id: userId, namespaceId } = user;
const token = this.security.generateApiToken();
const newToken: Token = {
token,
description,
roles,
type
};
if (type === 'namespace') {
newToken.namespaceId = toObjectId(namespaceId);
} else if (type === 'user') {
newToken.userId = toObjectId(userId);
}
return newToken;
}
}

View File

@ -0,0 +1,213 @@
import bcrypt from 'bcryptjs';
import { UsersMongoManager } from "../db/mongo/UsersMongoManager";
import { SecurityManager } from '../managers/SecurityManager';
import { ApiTokenMongoManager } from '../db/mongo/ApiTokenMongoManager';
import { TemporalTokenMongoManager } from '../db/mongo/TemporalTokenMongoManager';
import { BaseController } from './BaseController';
import { NextFunction, Request, Response } from 'express';
import { AuthenticationOption, Token, User } from '../db/interfaces';
export class AuthController extends BaseController {
security = new SecurityManager();
usersManager = new UsersMongoManager();
temporalTokenManager = new TemporalTokenMongoManager();
async login(req: Request, res: Response): Promise<void> {
const { log } = req;
try {
let token = null
const { username, password } = req.body;
this.logger.debug('login', username, password);
const { valid: isValidPassword, user } = await this._checkPassword(username, password);
this.logger.debug('isValidPassword', isValidPassword);
if (!isValidPassword) {
res.status(401).json({ error: 'Unauthorized' }).end();
log.error('Unauthorized login attempt for user: ', username);
return;
}
this._jwtSignUser(user, res)
} catch (error) {
this.handleError(res, error);
}
}
_jwtSignUser(user: User | null, res: Response) {
if (user === null) {
res.status(401).json({ error: 'Unauthorized' }).end();
return;
}
delete user.hash;
const token = this.security.signJwt(user);
if (token === null) {
res.status(401).json({ error: 'Unauthorized' }).end();
} else {
res.status(200).json({ token }).end();
}
return;
}
async twoFactorCodeAuthentication(req: Request, res: Response) {
const { code, username } = req.body;
const { valid: isValid, user } = await this._isValidTemporalCode(username, code);
if (!isValid) {
res.status(406).json({ error: 'Unauthorized' }).end();
return;
}
res.status(200).end();
}
async _isValidTemporalCode(username: string, code: string) {
const user = await this.usersManager.getByUsername(username);
if (user === null || user._id === undefined) {
return { valid: false, user: null };
}
const temporalToken = await this.temporalTokenManager.getByUserAndType(user._id.toString(), TemporalTokenMongoManager.Types.PASSWORD_RECOVERY);
if (temporalToken === null) {
return { valid: false, user: null };
}
const { token } = temporalToken;
const valid = bcrypt.compareSync(code, token);
return { valid, user: valid ? user : null};
}
async changePasswordWithCode(req: Request, res: Response) {
try {
const { username, newPassword, code } = req.body;
const { valid: isValid, user } = await this._isValidTemporalCode(username, code);
if (isValid) {
await this._setNewPassword(username, newPassword);
this._jwtSignUser(user, res);
} else {
res.status(400).json({ error: 'Code not valid.' }).end();
}
} catch (error) {
this.handleError(res, error);
}
}
async changePassword(req: Request, res: Response) {
try {
const { username, oldPassword, newPassword } = req.body;
const { valid: isValidPassword } = await this._checkPassword(username, oldPassword);
if (isValidPassword) {
await this._setNewPassword(username, newPassword);
res.status(200).end();
}
res.status(400).json({ error: 'Password not valid.' }).end();
} catch (error) {
this.handleError(res, error);
}
}
async _setNewPassword(username: string, newPassword: string) {
const hash = this.security.getHashedPassword(newPassword);
await this.usersManager.updatePassword(username, hash);
}
async _checkPassword(username: string, password: string) {
let valid = false;
const user = await this.usersManager.getByUsername(username);
if (user && user.hash) {
const { hash } = user;
valid = bcrypt.compareSync(password, hash);
}
return { valid, user };
}
static async checkRolesToken(token: Token, rolesToCheck: string[]) {
if (rolesToCheck.length === 0) {
return true;
}
if (!token._id) {
return false;
}
const tokenFromDb = await new ApiTokenMongoManager().getById(token._id.toString());
if (!tokenFromDb) {
return false;
}
const { roles } = tokenFromDb;
const validRoles = rolesToCheck.filter((r: string) => roles.includes(r));
return validRoles.length === rolesToCheck.length;
}
static async checkRoles(user: User, rolesToCheck: string[]) {
if (rolesToCheck.length === 0) {
return true;
}
if (!user._id) {
return false;
}
const usersManager = new UsersMongoManager();
const userFromDb = await usersManager.getById(user._id.toString());
if (!userFromDb) {
return false;
}
const { roles } = userFromDb;
const validRoles = rolesToCheck.filter((r: string) => roles.includes(r));
return validRoles.length === rolesToCheck.length;
}
static authenticate(options: AuthenticationOption = {}) {
return async function(req: Request, res: Response, next: NextFunction) {
const security = new SecurityManager();
const token = req.headers.authorization;
const { roles = [] } = options;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const user: User = await security.verifyJwt(token);
const validRoles = await AuthController.checkRoles(user, roles);
if (!validRoles) {
return res.status(403).json({ error: 'Forbidden' });
}
req.user = user;
next();
} catch (error) {
return res.status(403).json({ error: 'Forbidden' });
}
}
}
static tokenAuthenticate(options: AuthenticationOption = {}) {
return async function(req: Request, res: Response, next: NextFunction) {
const { log } = req;
// log.info('tokenAuthenticate')
try {
const token: string = req.headers['x-api-key'] as string;
const dm = new ApiTokenMongoManager();
const apiToken = await dm.getByToken(token);
const { roles = [] } = options;
const valid = !!apiToken && await AuthController.checkRolesToken(apiToken, roles);
if (!valid) {
return res.status(401).json({ error: 'Unauthorized' });
}
req.token = apiToken;
next();
} catch (error) {
return res.status(403).json({ error: 'Forbidden' });
}
}
}
static async withUser(req: Request, res: Response, next: NextFunction) {
try {
const token = req.token;
const dm = new UsersMongoManager();
const user = await dm.getById(token.userId);
req.user = user;
next();
} catch (error) {
return res.status(403).json({ error: 'Forbidden' });
}
}
}

View File

@ -0,0 +1,11 @@
import { Response } from "express";
import { LoggingService } from "../../common/LoggingService";
export class BaseController {
logger = new LoggingService().logger;
handleError(res: Response, error: any, data = {}) {
this.logger.error(error);
res.status(500).json({ error: error.message, ...data }).end();
}
}

View File

@ -0,0 +1,19 @@
import { Request, Response } from "express";
import { BaseController } from "./BaseController";
export class MatchSessionController extends BaseController {
// async createMatchSession(req: Request, res: Response): Promise<any> {
// const response = await this.sessionManager.createSession(data, socketId);
// return response;
// }
// async startMatchSession(data: any): Promise<any> {
// const response = await this.sessionManager.startSession(data);
// return response;
// }
// async joinMatchSession(data: any, socketId: string): Promise<any> {
// const response = await this.sessionManager.joinSession(data, socketId);
// return response;
// }
}

View File

@ -0,0 +1,62 @@
import { NamespacesService } from "../services/NamespacesService";
import { BaseController } from "./BaseController";
import { Request, Response } from "express";
export class NamespacesController extends BaseController{
private namespacesService: NamespacesService;
constructor() {
super();
this.namespacesService = new NamespacesService();
}
async getNamespaces(req: Request, res: Response) {
try {
const namespaces = await this.namespacesService.getNamespaces();
res.json(namespaces).status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async getNamespace(req: Request, res: Response) {
try {
const { id } = req.params;
const namespace = await this.namespacesService.getNamespace(id);
res.json(namespace).status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async createNamespace(req: Request, res: Response) {
try {
const namespace = req.body;
const user = req.user;
await this.namespacesService.createNamespace(namespace, user);
res.status(201).end();
} catch (error) {
this.handleError(res, error);
}
}
async updateNamespace(req: Request, res: Response) {
try {
const { body: namespace, params} = req;
const { id } = params;
const result = await this.namespacesService.updateNamespace(id, namespace);
res.status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async deleteNamespace(req: Request, res: Response) {
try {
const { id } = req.params;
await this.namespacesService.deleteNamespace(id);
res.status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
}

View File

@ -0,0 +1,152 @@
import { validationResult } from 'express-validator';
import { SecurityManager } from '../managers/SecurityManager';
import { CryptoService } from '../services/CryptoService';
import { TemporalTokenMongoManager } from '../db/mongo/TemporalTokenMongoManager';
import { BaseController } from './BaseController';
import { MailerService } from '../services/mailer/MailerService';
import { NamespacesService } from '../services/NamespacesService';
import { UsersService } from '../services/UsersService';
import { Request, Response } from 'express';
export class UserController extends BaseController {
security = new SecurityManager();
temporalTokenManager = new TemporalTokenMongoManager();
usersService = new UsersService();
mailService = new MailerService();
cryptoService = new CryptoService();
namespacesService = new NamespacesService();
constructor() {
super();
}
async getNamespaces(req: Request, res: Response) {
return await this.namespacesService.getNamespaces();
}
async getUser(req: Request, res: Response) {
try {
const { id } = req.params;
const user = await this.usersService.getById(id);
res.json(user).status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async createUser(req: Request, res: Response) {
const user = req.body;
try {
const validation = validationResult(req);
if (!validation.isEmpty()) {
res.status(400).json({ errors: validation.array() });
return;
}
await this.usersService.createUser(user);
res.status(201).end();
} catch (error) {
this.handleError(res, error);
}
}
async updateUserNamespace(req: Request, res: Response) {
try {
const { userId, namespaceId } = req.params;
return await this.usersService.updateUserNamespace(userId, namespaceId);
} catch (error) {
this.handleError(res, error);
}
}
async resetUserNamespace(req: Request, res: Response) {
try {
const { userId } = req.params;
const defaultNS = await this.namespacesService.getDefaultNamespace();
if (!defaultNS._id) {
throw new Error('Default namespace not found');
}
return await this.usersService.updateUserNamespace(userId, defaultNS._id.toString());
} catch (error) {
this.handleError(res, error);
}
}
async updateUser(req: Request, res: Response) {
const user = req.body;
try {
const validation = validationResult(req);
if (!validation.isEmpty()) {
res.status(400).json({ errors: validation.array() });
return;
}
const { id } = req.params;
await this.usersService.updateUser(user, id);
res.status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async deleteUser(req: Request, res: Response) {
try {
const { id } = req.params;
await this.usersService.deleteUser(id);
res.status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async listUsers(req: Request, res: Response): Promise<void> {
try {
const { available = false } = req.query;
let users = [];
if (available) {
const defaultNamespace = await this.namespacesService.getDefaultNamespace();
users = await this.usersService.listUsers({ not: defaultNamespace._id });
} else {
users = await this.usersService.listUsers();
}
res.json(users).status(200).end();
} catch (error) {
this.handleError(res, error);
}
}
async passwordRecovery(req: Request, res: Response) {
try {
const { username } = req.body;
const user = await this.usersService.getByUsername(username);
if (user === null) {
res.status(404).json({ message: 'User not found', code: 'user-not-found'}).end();
return;
}
if (!user.email) {
res.status(404).json({ message: 'Email not found', code: 'email-not-found'}).end();
return;
}
const { email, firstname, lastname, _id: userId } = user;
const pin = this.cryptoService.generateRandomPin(8);
const token = this.security.getHashedPassword(pin);
const temporalToken = {
userId,
token,
createdAt: new Date().getTime(),
validUntil: new Date().getTime() + 1000 * 60 * 60 * 1,
type: TemporalTokenMongoManager.Types.PASSWORD_RECOVERY,
}
if (!userId) {
throw new Error('User not found');
}
this.temporalTokenManager.deleteAllByUserAndType(userId.toString(), TemporalTokenMongoManager.Types.PASSWORD_RECOVERY);
this.temporalTokenManager.addToken(temporalToken);
await this.mailService.sendRecoveryPasswordEmail(firstname, lastname, email, pin);
res.status(200).end();
} catch (error: any) {
this.handleError(res, error, { code: 'critical', message: error.message });
}
}
}

View File

@ -0,0 +1,20 @@
import { MatchSession } from "../../game/MatchSession";
import { DbMatchSession } from "./interfaces";
export function matchSessionAdapter(session: MatchSession) : DbMatchSession {
return {
id: session.id,
name: session.name || '',
creator: session.creator.id,
players: session.players.map(player => player.id),
seed: session.seed,
mode: session.mode,
pointsToWin: session.pointsToWin,
maxPlayers: session.maxPlayers,
numPlayers: session.numPlayers,
scoreboard: Array.from(session.scoreboard.entries()).map(([player, score]) => ({ player, score })),
matchWinner: session.matchWinner ? session.matchWinner.id : null,
state: session.state
}
}

View File

@ -0,0 +1,98 @@
import { ObjectId } from "mongodb";
export interface Entity {
createdAt?: number | null;
modifiedAt?: number | null;
createdBy?: ObjectId | null;
modifiedBy?: ObjectId | null;
}
export interface EntityMongo extends Entity {
_id?: ObjectId;
}
export interface Score {
player: string;
score: number;
}
export interface Namespace extends EntityMongo {
name?: string;
description?: string;
default: boolean;
type: string | null;
ownerId?: ObjectId;
users?: any[];
}
export interface User extends EntityMongo {
id: string,
username: string;
namespaceId: ObjectId;
hash?: string;
roles: string[];
firstname?: string;
lastname?: string;
email?: string;
profileId?: string;
password?: string | null;
namespace?: Namespace;
}
export interface DbMatchSession extends EntityMongo {
id: string;
name: string;
creator: string;
players: string[];
seed: string;
mode: string;
pointsToWin: number;
maxPlayers: number;
numPlayers: number;
scoreboard: Score[];
matchWinner: string | null;
state: string;
}
export interface DbUser extends EntityMongo {
id: string,
username: string;
namespaceId: ObjectId;
hash?: string;
roles: string[];
firstname?: string;
lastname?: string;
email?: string;
profileId?: string;
password?: string | null;
namespace?: DbNamespace;
}
export interface DbNamespace extends EntityMongo {
name?: string;
description?: string;
default: boolean;
type: string | null;
ownerId?: ObjectId;
}
export interface Token extends EntityMongo {
token: string;
userId?: ObjectId;
roles?: string[];
expiresAt?: number | null;
type: string;
namespaceId?: ObjectId
description?: string;
}
export interface AuthenticationOption {
roles?: string[];
}
export interface Role {
name: string;
description: string;
permissions: string[];
}

View File

@ -0,0 +1,39 @@
import { mongoExecute } from './common/mongoDBPool';
import { BaseMongoManager } from './common/BaseMongoManager';
import { Token } from '../interfaces';
export class ApiTokenMongoManager extends BaseMongoManager{
collection = 'tokens';
async addToken(token: Token) {
return await mongoExecute(async ({ collection }) => {
await collection?.insertOne(token);
return token;
}, { colName: this.collection });
}
async getTokens(userId: string): Promise<Token[]> {
return await mongoExecute(async ({ collection }) => {
return await collection?.find({ userId: this.toObjectId(userId) }).toArray();
}, { colName: this.collection });
}
async getTokensByNamespace(namespaceId: string): Promise<Token[]> {
return await mongoExecute(async ({ collection }) => {
return await collection?.find({ namespaceId: this.toObjectId(namespaceId) }).toArray();
}, { colName: this.collection });
}
async getByToken(token: string): Promise<Token | null>{
return await mongoExecute(async ({ collection }) => {
return await collection?.findOne({ token });
}, { colName: this.collection });
}
async deleteToken(tokenId: string): Promise<number> {
return await mongoExecute(async ({ collection }) => {
const res = await collection?.deleteOne({ _id: this.toObjectId(tokenId) });
return res?.deletedCount || 0
}, { colName: this.collection });
}
}

View File

@ -0,0 +1,5 @@
import { BaseMongoManager } from './common/BaseMongoManager';
export class MatchSessionMongoManager extends BaseMongoManager {
protected collection = "matchSessions";
}

View File

@ -0,0 +1,77 @@
import { PipelineLibrary } from './common/PipelineLibrary';
import { mongoExecute } from './common/mongoDBPool';
import { BaseMongoManager } from './common/BaseMongoManager';
import { Namespace } from '../interfaces.js';
import { Document, ObjectId } from 'mongodb';
export class NamespacesMongoManager extends BaseMongoManager{
collection = 'namespaces';
constructor() {
super();
}
async createNamespace(namespace: Namespace): Promise<string> {
return await mongoExecute(async ({collection}) => {
const now = new Date().getTime();
delete namespace.users;
const result = await collection?.insertOne({
...namespace,
createdAt: now,
modifiedAt: now
});
return result?.insertedId.toString() || '';
}, {colName: this.collection})
}
async updateNamespace(id: string, namespace: Namespace): Promise<number> {
return await mongoExecute(async ({collection}) => {
const now = new Date().getTime();
const { name, description, type } = namespace;
const result = await collection?.updateOne({
_id: this.toObjectId(id),
}, {
$set: {
name,
description,
type,
modifiedAt: now
}
});
return result?.modifiedCount || 0;
}, {colName: this.collection})
}
async getNamespace(namespaceId: string): Promise<Document | null> {
const pipeline = PipelineLibrary.namespacesGetById(this.toObjectId((namespaceId)));
return await mongoExecute(async ({collection}) => {
const cursor: Document[] | undefined = await collection?.aggregate(pipeline).toArray();
if (cursor === undefined ||cursor.length === 0) {
return null;
}
return cursor[0];
}, {colName: this.collection})
}
async getDefaultNamespace(): Promise<Namespace> {
return await mongoExecute(async ({collection}) => {
return await collection?.findOne({
default: true
});
}, {colName: this.collection});
}
async getNamespaces(): Promise<Namespace[]> {
const pipeline = PipelineLibrary.namespacesGetNamespaces();
return await mongoExecute(async ({collection}) => {
return await collection?.aggregate(pipeline).toArray();
}, {colName: this.collection})
}
async deleteNamespace(_id: string): Promise<number> {
return await mongoExecute(async ({collection}) => {
const result = await collection?.deleteOne({ _id: this.toObjectId(_id) });
return result?.deletedCount || 0;
}, {colName: this.collection})
}
}

View File

@ -0,0 +1,35 @@
import { DeleteResult } from "mongodb";
import { ApiTokenMongoManager } from "./ApiTokenMongoManager";
import { mongoExecute } from "./common/mongoDBPool";
import { Token } from "../interfaces";
export class TemporalTokenMongoManager extends ApiTokenMongoManager{
collection = 'temporalTokens';
static Types = {
PASSWORD_RECOVERY: 'password-recovery',
};
async getTokens(): Promise<Token[]> {
return this.getAllTokens();
}
async getAllTokens(): Promise<Token[]> {
return await mongoExecute(async ({ collection }) => {
return await collection?.find({ }).toArray();
}, { colName: this.collection });
}
async getByUserAndType(userId: string, type: String): Promise<Token | null>{
return await mongoExecute(async ({ collection }) => {
return await collection?.findOne({ userId: this.toObjectId(userId) , type });
}, { colName: this.collection });
}
async deleteAllByUserAndType(userId: string, type: String): Promise<number>{
return await mongoExecute(async ({ collection }) => {
const res: DeleteResult | undefined = await collection?.deleteMany({ userId: this.toObjectId(userId), type });
return res?.deletedCount || 0;
}, { colName: this.collection });
}
}

View File

@ -0,0 +1,70 @@
import { mongoExecute } from './common/mongoDBPool';
import { PipelineLibrary } from './common/PipelineLibrary';
import { BaseMongoManager } from './common/BaseMongoManager';
import { User } from '../interfaces';
export class UsersMongoManager extends BaseMongoManager {
collection = 'users';
addUser(user: User) {
return this.create(user);
}
updateUser(user: User) {
return this.update(user);
}
async updateUserNamespace(userId: string, namespaceId: string) {
return await mongoExecute(async ({ collection }) => {
return await
collection?.updateOne({ _id: this.toObjectId(userId) }, { $set: { namespaceId: this.toObjectId(namespaceId) } });
}
, { colName: this.collection });
}
async deleteUser(_id: string) {
return await mongoExecute(async ({ collection }) => {
await collection?.deleteOne({ _id: this.toObjectId(_id) });
}, { colName: this.collection });
}
async getAvailableUsers(defaultId: string): Promise<User[]>{
return await mongoExecute(async ({ collection }) => {
const pipeline = PipelineLibrary.usersGetAvailableUsers(this.toObjectId(defaultId));
return await collection?.aggregate(pipeline).toArray();
}, { colName: this.collection });
}
async getUsers(): Promise<User[]> {
return await mongoExecute(async ({ collection }) => {
const pipeline = PipelineLibrary.usersGetUsers();
return await collection?.aggregate(pipeline).toArray();
}, { colName: this.collection });
}
async getById(id: string): Promise<User | null>{
const pipeline = PipelineLibrary.usersGetById(this.toObjectId(id));
return await mongoExecute(async ({ collection }) => {
const users = await collection?.aggregate(pipeline).toArray();
if (users === undefined || users.length === 0) {
return null;
}
const user = users[0];
delete user.hash;
return user;
}
, { colName: this.collection });
}
async getByUsername(username: string): Promise<User | null>{
return await mongoExecute(async ({ collection }) => {
return await collection?.findOne({ username });
}, { colName: this.collection });
}
async updatePassword(username: string, hash: string) {
return await mongoExecute(async ({ collection }) => {
return await collection?.updateOne({ username }, { $set: { hash } });
}, { colName: this.collection });
}
}

View File

@ -0,0 +1,123 @@
import { ObjectId } from "mongodb";
import { mongoExecute } from "./mongoDBPool";
import { Entity } from "../../interfaces";
import { LoggingService } from "../../../../common/LoggingService";
import toObjectId from "./mongoUtils";
export abstract class BaseMongoManager {
protected abstract collection?: string;
logger = new LoggingService().logger;
create(data: Entity) {
return mongoExecute(
async ({ collection }) => {
await collection?.insertOne(data as any);
return data;
},
{ colName: this.collection }
);
}
delete(id: string) {
return mongoExecute(
async ({ collection }) => {
await collection?.deleteOne({ _id: this.toObjectId(id) });
},
{ colName: this.collection }
);
}
deleteByFilter(filter: any) {
return mongoExecute(
async ({ collection }) => {
await collection?.deleteOne(filter);
},
{ colName: this.collection }
);
}
getById(id: string) {
return mongoExecute(
async ({ collection }) => {
return await collection?.findOne({ _id: this.toObjectId(id) });
},
{ colName: this.collection }
);
}
getByFilter(filter: any) {
return mongoExecute(
async ({ collection }) => {
return await collection?.findOne(filter);
},
{ colName: this.collection }
);
}
list() {
return mongoExecute(
async ({ collection }) => {
return await collection?.find().toArray();
},
{ colName: this.collection }
);
}
listByFilter(filter: any) {
return mongoExecute(
async ({ collection }) => {
return await collection?.find(filter).toArray();
},
{ colName: this.collection }
);
}
update(object: Entity) {
const data: any = { ...object };
const id = data._id;
delete data._id;
return mongoExecute(async ({ collection }) => {
return await collection?.updateOne(
{ _id: this.toObjectId(id) },
{ $set: data }
);
},
{ colName: this.collection });
}
updateMany(filter: any, data: Entity) {
return mongoExecute(async ({ collection }) => {
return await collection?.updateMany(filter, { $set: data as any });
},
{ colName: this.collection });
}
replaceOne(filter: any, object: Entity) {
return mongoExecute(async ({collection}) => {
return await collection?.replaceOne(filter, object);
}, {colName: this.collection});
}
aggregation(pipeline: any) {
return mongoExecute(
async ({ collection }) => {
return await collection?.aggregate(pipeline).toArray();
},
{ colName: this.collection }
);
}
aggregationOne(pipeline: any) {
return mongoExecute(
async ({ collection }) => {
return await collection?.aggregate(pipeline).next();
},
{ colName: this.collection }
);
}
protected toObjectId = (oid: string) => {
return toObjectId(oid);
};
}

View File

@ -0,0 +1,111 @@
import { ObjectId } from "mongodb";
export class PipelineLibrary {
static usersGetById(id: ObjectId) {
return [
{
'$match': {
'_id': id
}
}, {
'$lookup': {
'from': 'namespaces',
'localField': 'namespaceId',
'foreignField': '_id',
'as': 'namespace'
}
}, {
'$unwind': {
'path': '$namespace',
'preserveNullAndEmptyArrays': true
}
}
];
}
static namespacesGetNamespaces() {
return [
{
'$lookup': {
'from': 'users',
'localField': '_id',
'foreignField': 'namespaceId',
'as': 'users'
}
}, {
'$project': {
'_id': 1,
'name': 1,
'description': 1,
'ownerId': 1,
'default': 1,
'createdAt': 1,
'modifiedAt': 1,
'users': {
'_id': 1,
'id': 1,
'username': 1,
'firstname': 1,
'lastname': 1,
'email': 1,
'profileId': 1
}
}
}
];
}
static namespacesGetById(id: ObjectId) {
return [
{
'$match': {
'_id': id
}
},
... PipelineLibrary.namespacesGetNamespaces()
];
}
static usersGetAvailableUsers(defaultId: Object) {
return [
{
'$match': {
'namespaceId': defaultId
},
},
...PipelineLibrary.usersGetUsers(),
];
}
static usersGetUsers() {
return [
{
'$lookup': {
'from': 'namespaces',
'localField': 'namespaceId',
'foreignField': '_id',
'as': 'namespace'
}
}, {
'$unwind': {
'path': '$namespace',
'preserveNullAndEmptyArrays': true
}
}, {
'$project': {
'_id': 1,
'username': 1,
'roles': 1,
'firstname': 1,
'lastname': 1,
'email': 1,
'modifiedAt': 1,
'createdAt': 1,
'namespace': {
'_id': 1,
'name': 1
}
}
}
];
}
}

View File

@ -0,0 +1,55 @@
import { MongoClient, Collection, Db } from 'mongodb';
const {
MONGO_HOST,
MONGO_PORT,
MONGO_USER,
MONGO_PASS = '',
MONGO_DB,
} = process.env;
const uri = `mongodb://${MONGO_USER}:${MONGO_PASS.replace(/[^A-Za-z0-9\-_.!~*'()%]/g, (c) => encodeURIComponent(c))}@${MONGO_HOST}:${MONGO_PORT}/?maxPoolSize=20`;
interface MongoExecuteOptions {
dbName?: string;
colName?: string;
};
interface MongoExecuteParams {
collection?: Collection,
database?: Db,
connection?: MongoClient
}
type MongoExecuteFunction = (options: MongoExecuteParams) => void | Promise<void> | Promise<any> | any;
export const getMongoConnection = async() : Promise<MongoClient> => {
const client = new MongoClient(uri);
return await client.connect();
};
export const getMongoDatabase = (client: MongoClient, dbName: string): Db => {
const DB = dbName || MONGO_DB;
return client.db(DB);
};
export const mongoExecute = async function(fn: MongoExecuteFunction, opts: MongoExecuteOptions): Promise<any> {
const { dbName, colName } = { dbName: MONGO_DB, ...opts };
let connection: MongoClient | null = null;
try {
connection = await getMongoConnection();
const database = connection.db(dbName);
if (colName) {
const collection: Collection = database.collection(colName);
return await fn({ collection, database, connection });
}
return await fn({ database, connection });
} catch (err: any) {
console.log('MOMGODB ERROR:', err.message);
throw err;
} finally {
if (connection !== null) {
await connection.close();
}
}
};

View File

@ -0,0 +1,5 @@
import { ObjectId } from "mongodb";
export default function toObjectId(id: string) {
return ObjectId.createFromHexString(id);
}

View File

@ -5,21 +5,30 @@ import { join } from 'path';
import { NetworkClientNotifier } from '../game/NetworkClientNotifier';
import { SocketIoService } from './services/SocketIoService';
import { LoggingService } from '../common/LoggingService';
import { useRouter } from './router';
const clientNotifier = new NetworkClientNotifier();
const logger = new LoggingService();
const app = express();
const httpServer = http.createServer(app);
const socketIoService = new SocketIoService(httpServer);
clientNotifier.setSocket(socketIoService.getServer());
const PORT = process.env.PORT || 3000;
console.log('__dirname :>> ', __dirname);
app.use(cors());
app.use(logger.middleware());
app.use(express.json({ limit: '50mb'}));
app.use(express.text());
app.use(express.urlencoded({extended: true }));
app.use(useRouter())
app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'));
});
httpServer.listen(PORT, () => {
console.log(`listening on *:${PORT}`);
logger.info(`listening on *:${PORT}`);
});

View File

@ -1,5 +1,5 @@
import { LoggingService } from "../../common/LoggingService";
export class ControllerBase {
export class ManagerBase {
protected logger = new LoggingService();
}

View File

@ -0,0 +1,39 @@
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { User } from '../db/interfaces';
export class SecurityManager {
saltRounds = Number(process.env.SALT_ROUNDS);
jwtSecretKey = process.env.JWT_SECRET_KEY || '';
generateId() {
return crypto.randomBytes(16).toString('hex');
}
getHashedPassword(password: string) {
const salt = bcrypt.genSaltSync(this.saltRounds);
return bcrypt.hashSync(password, salt);
}
generateApiToken() {
return crypto.randomBytes(32).toString('hex');
}
signJwt(data: any) {
return jwt.sign(data, this.jwtSecretKey, { expiresIn: '3h' });
}
// TODO: verificar esto
async verifyJwt(token: string): Promise<User> {
return new Promise((resolve, reject) => {
jwt.verify(token, this.jwtSecretKey, (err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded as User);
}
});
});
}
}

View File

@ -1,11 +1,12 @@
import { LoggingService } from "../../common/LoggingService";
import { GameSession } from "../../game/GameSession";
import { MatchSession } from "../../game/MatchSession";
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
import { SessionService } from "../services/SessionService";
import { ControllerBase } from "./ControllerBase";
import { ManagerBase } from "./ManagerBase";
export class SessionController extends ControllerBase{
export class SessionManager extends ManagerBase {
private static sessions: any = {};
private sessionService: SessionService = new SessionService();
constructor() {
super();
@ -15,8 +16,9 @@ export class SessionController extends ControllerBase{
createSession(data: any, socketId: string): any {
const { user, sessionName } = data;
const player = new NetworkPlayer(user, socketId);
const session = new GameSession(player, sessionName);
SessionController.sessions[session.id] = session;
const session = new MatchSession(player, sessionName);
SessionManager.sessions[session.id] = session;
this.sessionService.createSession(session);
return {
status: 'ok',
@ -29,9 +31,10 @@ export class SessionController extends ControllerBase{
this.logger.debug('joinSession data :>> ')
this.logger.object(data);
const { user, sessionId } = data;
const session = SessionController.sessions[sessionId];
const session: MatchSession = SessionManager.sessions[sessionId];
const player = new NetworkPlayer(user, socketId);
session.addPlayer(player);
this.sessionService.updateSession(session);
return {
status: 'ok',
sessionId: session.id,
@ -39,10 +42,16 @@ export class SessionController extends ControllerBase{
};
}
setPlayerReady(data: any): any {
const { user, sessionId } = data;
const session: MatchSession = SessionManager.sessions[sessionId];
session.setPlayerReady(user)
}
startSession(data: any): any {
const sessionId: string = data.sessionId;
const seed: string | undefined = data.seed;
const session = SessionController.sessions[sessionId];
const session = SessionManager.sessions[sessionId];
if (!session) {
return ({
@ -68,10 +77,10 @@ export class SessionController extends ControllerBase{
getSession(id: string) {
return SessionController.sessions[id];
return SessionManager.sessions[id];
}
deleteSession(id: string) {
delete SessionController.sessions[id];
delete SessionManager.sessions[id];
}
}

View File

@ -0,0 +1,33 @@
import { Request, Response, Router } from 'express';
import { AuthController } from '../controllers/AuthController';
import { NamespacesController } from '../controllers/NamespacesController';
import { Validations } from './validations';
import { UserController } from '../controllers/UserController';
import { ApiKeyController } from '../controllers/ApiKeyController';
export default function(): Router {
const userController = new UserController();
const namespacesController = new NamespacesController();
const apiKeyController = new ApiKeyController();
const router = Router();
const { authenticate } = AuthController;
router.get('/users', authenticate({ roles: ['admin']}), (req: Request, res: Response) => userController.listUsers(req, res));
router.get('/user/:id', authenticate({ roles: ['admin']}), (req: Request, res: Response) => userController.getUser(req, res));
router.delete('/user/:id', authenticate({ roles: ['admin']}), (req: Request, res: Response) => userController.deleteUser(req, res));
router.post('/user', [ authenticate({ roles: ['admin']}), ...Validations.createUser ], (req: Request, res: Response) => userController.createUser(req, res));
router.patch('/user/:id', [ authenticate({ roles: ['admin']}), ...Validations.updateUser ], (req: Request, res: Response) => userController.updateUser(req, res));
router.patch('/user/:userId/namespace/:namespaceId/change', authenticate({ roles: ['admin']}), (req: Request, res: Response) => userController.updateUserNamespace(req, res));
router.patch('/user/:userId/namespace/reset', authenticate({ roles: ['admin']}), (req: Request, res: Response) => userController.resetUserNamespace(req, res));
router.get('/namespaces', authenticate({ roles: ['admin']}), (req: Request, res: Response) => namespacesController.getNamespaces(req, res));
router.post('/namespace', authenticate({ roles: ['admin']}), (req: Request, res: Response) => namespacesController.createNamespace(req, res));
router.get('/namespace/:id', authenticate({ roles: ['admin']}), (req: Request, res: Response) => namespacesController.getNamespace(req, res));
router.patch('/namespace/:id', authenticate({ roles: ['admin']}), (req: Request, res: Response) => namespacesController.updateNamespace(req, res));
router.delete('/namespace/:id', authenticate({ roles: ['admin']}), (req: Request, res: Response) => namespacesController.deleteNamespace(req, res));
router.get('/namespace/:id/tokens', authenticate({ roles: ['admin']}), (req: Request, res: Response) => apiKeyController.listNamespaceApiKeys(req, res));
router.delete('/namespace/:id/token/:tokenId', authenticate({ roles: ['admin']}), (req: Request, res: Response) => apiKeyController.deleteNamespaceApiKey(req, res));
router.post('/namespace/:id/token', authenticate({ roles: ['admin']}), (req: Request, res: Response) => apiKeyController.createNamespaceApiKey(req, res));
return router;
}

View File

@ -0,0 +1,22 @@
import { Request, Response, Router } from 'express';
import { AuthController } from '../controllers/AuthController';
import adminRouter from './adminRouter';
import userRouter from './userRouter';
export default function(): Router {
const router = Router();
const authController = new AuthController();
router.get('/version', async function(req: Request, res: Response){
res.send('1.0.0').end();
});
router.post('/auth/code', (req: Request, res: Response) => authController.twoFactorCodeAuthentication(req, res));
router.post('/login', (req: Request, res: Response) => authController.login(req, res));
router .use('/admin', adminRouter());
router .use('/user', userRouter());
return router;
}

View File

@ -0,0 +1,15 @@
import { Router } from "express";
import { join } from 'path';
import apiRouter from "./apiRouter";
export function useRouter(): Router {
const router = Router();
router.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'));
});
router.use('/api', apiRouter());
return router;
}

View File

@ -0,0 +1,23 @@
import { Request, Response, Router } from 'express';
import { UserController } from '../controllers/UserController';
import { AuthController } from '../controllers/AuthController';
import { ApiKeyController } from '../controllers/ApiKeyController';
export default function() : Router {
const userController = new UserController();
const authController = new AuthController();
const apiKeyController = new ApiKeyController();
const router = Router();
const { authenticate } = AuthController;
router.get('/tokens', authenticate({ roles: ['user']}), (req: Request, res: Response) => apiKeyController.listUserApiKeys(req, res));
router.post('/token', authenticate({ roles: ['user']}), (req: Request, res: Response) => apiKeyController.createApiKey(req, res));
router.delete('/token/:id', authenticate({ roles: ['user']}), (req: Request, res: Response) => apiKeyController.deleteApiKey(req, res));
router.post('/password/change', authenticate({ roles: ['user']}), (req: Request, res: Response) => authController.changePassword(req, res));
router.post('/password/recovery', (req: Request, res: Response) => userController.passwordRecovery(req, res));
router.post('/password/recovery/change', (req: Request, res: Response) => authController.changePasswordWithCode(req, res));
router.get('/namespaces', authenticate({ roles: ['user']}), (req: Request, res: Response) => userController.getNamespaces(req, res));
return router;
}

View File

@ -0,0 +1,22 @@
import { body } from 'express-validator';
const username = body("username")
.trim()
.isLength({ min: 5 })
.escape()
.withMessage("Username min 8 characters.")
.matches(/^[a-zA-Z0-9\-_]+$/).withMessage("First name has non-alphanumeric characters.");
const password = body("password")
.notEmpty().withMessage('Password is required')
.matches(/^[a-zA-Z0-9!@#$%^&*()_+{}\[\]:;<>,.?~\-]+$/).withMessage('Password must contain at least one special character');
const email = body('email')
.optional({values: 'falsy'})
.trim()
.isEmail();
export const Validations = {
createUser: [username, password, email],
updateUser: [username, email],
}

View File

@ -0,0 +1,25 @@
import crypto from 'crypto';
export class CryptoService {
generateEmailRecoveryToken(): string {
return this.generateToken(32);
}
generateRandomPin(length: number): string {
const randomBytes = crypto.randomBytes(length);
let pin = '';
for (let i = 0; i < randomBytes.length; i++) {
// Convert each byte to a number between 0-9
pin += (randomBytes[i] % 10).toString();
}
return pin;
}
generateToken(length: number): string {
return crypto.randomBytes(length).toString('hex');
}
}

View File

@ -0,0 +1,45 @@
import { Namespace, User } from "../db/interfaces";
import { NamespacesMongoManager } from "../db/mongo/NamespacesMongoManager";
import { UsersService } from "./UsersService";
export class NamespacesService {
namespacesManager = new NamespacesMongoManager();
usersService = new UsersService();
async createNamespace(namespace: Namespace, user: User) {
const insertedId = await this.namespacesManager.createNamespace({ ownerId: user._id, ...namespace, createdBy: user._id });
await this._updateNamespaceUsers(namespace.users ?? [], insertedId);
return insertedId;
}
async _updateNamespaceUsers(users: any[], id: string) {
const defaultNamespace: Namespace = await this.namespacesManager.getDefaultNamespace();
for (const user of users) {
if (defaultNamespace._id === undefined) continue;
const namespaceId = user.removed ? defaultNamespace._id?.toString() : id;
await this.usersService.updateUserNamespace(user.id, namespaceId);
}
}
async updateNamespace(id: string, namespace: Namespace) {
const result = await this.namespacesManager.updateNamespace(id, namespace);
await this._updateNamespaceUsers(namespace.users ?? [], id);
return result;
}
async getNamespace(namespaceId: string) {
return await this.namespacesManager.getNamespace(namespaceId);
}
async getNamespaces() {
return await this.namespacesManager.getNamespaces();
}
getDefaultNamespace(): Promise<Namespace> {
return this.namespacesManager.getDefaultNamespace();
}
async deleteNamespace(namespaceId: string) {
return await this.namespacesManager.deleteNamespace(namespaceId);
}
}

View File

@ -0,0 +1,19 @@
import { MatchSession } from "../../game/MatchSession";
import { matchSessionAdapter } from "../db/DbAdapter";
import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager";
import { ServiceBase } from "./ServiceBase";
export class SessionService extends ServiceBase{
private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager();
constructor() {
super()
}
public createSession(session: MatchSession): any {
this.dbManager.create(matchSessionAdapter(session));
}
public updateSession(session: MatchSession): any {
this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
}
}

View File

@ -1,10 +1,12 @@
import { Server as HttpServer } from "http";
import { ServiceBase } from "./ServiceBase";
import { Server } from "socket.io";
import { SessionController } from "../controllers/SessionController";
import { SessionManager } from "../managers/SessionManager";
export class SocketIoService extends ServiceBase{
io: Server
clients: Map<string, any> = new Map();
constructor(private httpServer: HttpServer) {
super()
this.io = this.socketIo(httpServer);
@ -16,23 +18,30 @@ export class SocketIoService extends ServiceBase{
}
private initListeners() {
const sessionController = new SessionController();
const sessionController = new SessionManager();
this.io.on('connection', (socket) => {
console.log(`connect ${socket.id}`);
this.logger.debug(`connect ${socket.id}`);
if (socket.recovered) {
// recovery was successful: socket.id, socket.rooms and socket.data were restored
console.log("recovered!");
console.log("socket.rooms:", socket.rooms);
console.log("socket.data:", socket.data);
this.logger.debug("recovered!");
this.logger.debug("socket.rooms:", socket.rooms);
this.logger.debug("socket.data:", socket.data);
} else {
console.log("new connection");
this.logger.debug("new connection");
this.clients.set(socket.id, { alive: true });
socket.join('room-general')
socket.data.foo = "bar";
}
socket.on('pong', () => {
if (this.clients.has(socket.id)) {
this.clients.set(socket.id, { alive: true });
}
})
socket.on('disconnect', () => {
console.log('user disconnected');
this.logger.debug('user disconnected');
this.clients.delete(socket.id);
});
socket.on('createSession', (data, callback) => {
@ -49,16 +58,30 @@ export class SocketIoService extends ServiceBase{
const response = sessionController.joinSession(data, socket.id);
callback(response);
});
// socket.on('chat message', (msg, callback) => {
// io.emit('chat message', msg);
// callback({
// status: 'ok',
// message: 'Message received',
// })
// });
socket.on('playerReady', (data, callback) => {
const response = sessionController.setPlayerReady(data);
callback(response);
});
this.pingClients()
});
}
private pingClients() {
setInterval(() => {
for (let [id, client] of this.clients.entries()) {
if (!client.alive) {
this.logger.debug(`Client ${id} did not respond. Disconnecting.`);
this.io.to(id).disconnectSockets(true); // Disconnect client
this.clients.delete(id);
} else {
client.alive = false; // Reset alive status for the next ping
this.io.to(id).emit('ping'); // Send ping message
}
}
}, 30000);
}
private socketIo(httpServer: HttpServer): Server {
return new Server(httpServer, {

View File

@ -0,0 +1,82 @@
import { UsersMongoManager } from "../db/mongo/UsersMongoManager";
import { SecurityManager } from "../managers/SecurityManager";
import { ServiceBase } from "./ServiceBase";
import { User } from "../db/interfaces";
import toObjectId from "../db/mongo/common/mongoUtils";
export class UsersService extends ServiceBase {
usersManager = new UsersMongoManager();
security = new SecurityManager();
listUsers(options?: any) {
if (options) {
return this.usersManager.getAvailableUsers(options.not);
}
return this.usersManager.getUsers();
}
getUser(id: string) {
return this.usersManager.getById(id);
}
getByUsername(username: string) {
return this.usersManager.getByUsername(username);
}
updateUser(user: any, id: string) {
const newUser = this._createUserObject(user, id);
const result = this.usersManager.updateUser(newUser);
return result;
}
updateUserNamespace(userId: string, namespaceId: string) {
return this.usersManager.updateUserNamespace(userId, namespaceId);
}
getById(id: string) {
return this.usersManager.getById(id);
}
createUser(user: any) {
const newUser = this._createUserObject(user);
return this.usersManager.addUser(newUser);
}
deleteUser(id: string) {
return this.usersManager.deleteUser(id);
}
_createUserObject(body: any, _id?: string) {
const { username, password, firstname, lastname, email, isadmin, namespaceId, character, roles } = body;
const id = this.security.generateId();
const createdAt = new Date().getTime();
const modifiedAt = createdAt
// const roles = isadmin ? ['admin', 'user'] : ['user'];
const user: User = {
id,
username,
password,
firstname,
lastname,
email,
roles,
createdAt,
modifiedAt,
namespaceId,
profileId: character,
};
this.logger.info(`${password === undefined}`);
if (_id !== undefined) {
user._id = toObjectId(_id);
}
if (password !== undefined && typeof password === 'string' && password.length > 0) {
user.hash = this.security.getHashedPassword(password);
delete user.password;
}
return user;
}
}

View File

@ -0,0 +1,75 @@
import nodemailer, { SentMessageInfo, Transporter } from 'nodemailer';
import hbs from 'nodemailer-express-handlebars';
import Mail from 'nodemailer/lib/mailer';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
export class MailerService {
transporter: Transporter;
host: string | undefined;
sender: string | undefined;
port: number | undefined;
user: string | undefined;
pass: string | undefined;
constructor() {
this.transporter = this._createTransporter();
this.sender = process.env.EMAIL_SENDER;
this.host = process.env.SMTP_HOST;
this.port = Number(process.env.SMTP_PORT || 587);
this.user = process.env.SMTP_USER;
this.pass = process.env.SMTP_PASS;
this._configureTemplates();
}
_createTransporter(): Transporter {
return nodemailer.createTransport({
host: this.host,
port: this.port,
secure: false,
auth: {
user: this.user,
pass: this.pass,
},
});
}
_configureTemplates() {
this.transporter.use('compile', hbs({
viewEngine: {
extname: '.hbs',
partialsDir: 'app/server/views/partials',
layoutsDir: 'app/server/views/layouts',
defaultLayout: 'email.hbs',
},
viewPath: 'app/server/views/emails',
extName: '.hbs',
}));
}
async sendRecoveryPasswordEmail(firstname: string = '', lastname: string = '', email: string, pin: string) {
const to = firstname ? `${firstname}${lastname ? ' ' + lastname : ''}} <${email}>` : email;
const mailOptions = {
from: this.sender,
to,
subject: 'Password Recovery',
template: 'passwordRecovery',
context: {
pin,
},
};
return this.send(mailOptions);
}
async send(mailOptions: any) {
return new Promise((resolve, reject) => {
this.transporter.sendMail(mailOptions, (error, info) => {
if (error) {
reject(error);
} else {
resolve(info);
}
});
});
}
}

11
src/server/types/environment.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
export {};
declare global {
namespace NodeJS {
interface ProcessEnv {
DB_PORT: number;
DB_USER: string;
ENV: 'test' | 'dev' | 'prod';
}
}
}

8
src/server/types/express/index.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import 'express-serve-static-core';
declare module 'express-serve-static-core' {
interface Request {
user?: any;
token?: any;
}
}