game flow revamp
This commit is contained in:
parent
733ac3891f
commit
a974f576b3
902
package-lock.json
generated
902
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -9,6 +9,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "node --env-file=.env --watch -r ts-node/register src/server/index.ts",
|
||||
"create": "node --env-file=.env -r ts-node/register ./create.ts",
|
||||
"test": "node --env-file=.env -r ts-node/register src/test.ts",
|
||||
"test:watch": "node --env-file=.env --watch -r ts-node/register src/test.ts",
|
||||
"docker-build": "docker build -t arhuako/domino:latest .",
|
||||
@ -22,17 +23,29 @@
|
||||
"type": "commonjs",
|
||||
"reposityory": "github:jmconde/domino",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express-validator": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongodb": "^6.8.0",
|
||||
"nodemailer": "^6.9.14",
|
||||
"nodemailer-express-handlebars": "^6.1.2",
|
||||
"pino": "^9.2.0",
|
||||
"pino-http": "^10.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"pino-rotating-file-stream": "^0.0.2",
|
||||
"seedrandom": "^3.0.5",
|
||||
"socket.io": "^4.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/nodemailer-express-handlebars": "^4.0.5",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.2"
|
||||
|
@ -1,19 +1,20 @@
|
||||
import pino, { BaseLogger } from 'pino';
|
||||
import path from 'path';
|
||||
import httpPino from 'pino-http';
|
||||
|
||||
export class LoggingService {
|
||||
static instance: LoggingService;
|
||||
logsPath: string = path.join(process.cwd(), 'app', 'server', 'logs');
|
||||
logsPath: string = path.join(process.cwd(), 'logs');
|
||||
logger!: BaseLogger;
|
||||
level: string = process.env.LOG_LEVEL || 'info';
|
||||
|
||||
/*
|
||||
* ogger.fatal('fatal');
|
||||
logger.error('error');
|
||||
logger.warn('warn');
|
||||
logger.info('info');
|
||||
logger.debug('debug');
|
||||
logger.trace('trace');
|
||||
* 1 - fatal
|
||||
2 - error
|
||||
3 - warn
|
||||
4 - info
|
||||
5 - debug
|
||||
6 - trace
|
||||
*/
|
||||
constructor() {
|
||||
if ((!LoggingService.instance)) {
|
||||
@ -26,7 +27,7 @@ export class LoggingService {
|
||||
return LoggingService.instance;
|
||||
}
|
||||
|
||||
get commonRorationOptions() : any {
|
||||
get commonRotationOptions() : any {
|
||||
return {
|
||||
interval: '1d',
|
||||
maxFiles: 10,
|
||||
@ -39,14 +40,14 @@ export class LoggingService {
|
||||
get transports() {
|
||||
return pino.transport({
|
||||
targets: [
|
||||
// {
|
||||
// target: 'pino-rotating-file-stream',
|
||||
// level: this.level,
|
||||
// options: {
|
||||
// filename: 'app.log',
|
||||
// ...this.commonRorationOptions
|
||||
// },
|
||||
// },
|
||||
{
|
||||
target: 'pino-rotating-file-stream',
|
||||
level: this.level,
|
||||
options: {
|
||||
filename: 'app.log',
|
||||
...this.commonRotationOptions
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'pino-pretty',
|
||||
level: this.level,
|
||||
@ -59,6 +60,29 @@ export class LoggingService {
|
||||
});
|
||||
}
|
||||
|
||||
get httpTransports() : any {
|
||||
return pino.transport({
|
||||
targets: [
|
||||
{
|
||||
target: 'pino-rotating-file-stream',
|
||||
level: this.level,
|
||||
options: {
|
||||
filename: 'http.log',
|
||||
...this.commonRotationOptions
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
middleware(): any {
|
||||
return httpPino({
|
||||
logger: pino({
|
||||
level: this.level,
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
}, this.httpTransports) });
|
||||
}
|
||||
|
||||
debug(message: string, data?: any) {
|
||||
this.logger.debug(this._getMessageWidthObject(message, data));
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ export class DominoesGame {
|
||||
winner: PlayerInterface | null = null;
|
||||
rng: PRNG;
|
||||
handSize: number = 7;
|
||||
notificationManager: PlayerNotificationManager = new PlayerNotificationManager(this);
|
||||
notificationManager: PlayerNotificationManager = new PlayerNotificationManager();
|
||||
lastMove: PlayerMove | null = null;
|
||||
|
||||
constructor(public players: PlayerInterface[], seed: PRNG) {
|
||||
@ -44,6 +44,14 @@ export class DominoesGame {
|
||||
this.board.boneyard = this.generateTiles();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.board.reset();
|
||||
this.initializeGame();
|
||||
for (let player of this.players) {
|
||||
player.hand = [];
|
||||
}
|
||||
}
|
||||
|
||||
generateTiles(): Tile[] {
|
||||
const tiles: Tile[] = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
@ -142,18 +150,25 @@ export class DominoesGame {
|
||||
this.nextPlayer();
|
||||
}
|
||||
|
||||
resetPlayersScore() {
|
||||
for (let player of this.players) {
|
||||
player.score = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<GameSummary> {
|
||||
this.resetPlayersScore();
|
||||
this.gameInProgress = false;
|
||||
this.tileSelectionPhase = true;
|
||||
await this.notificationManager.notifyGameState();
|
||||
await this.notificationManager.notifyPlayersState();
|
||||
await this.notificationManager.notifyGameState(this);
|
||||
await this.notificationManager.notifyPlayersState(this.players);
|
||||
this.logger.debug('clients received boneyard :>> ' + this.board.boneyard);
|
||||
await wait(1000);
|
||||
|
||||
if (this.autoDeal) {
|
||||
this.dealTiles();
|
||||
await this.notificationManager.notifyGameState();
|
||||
await this.notificationManager.notifyPlayersState();
|
||||
await this.notificationManager.notifyGameState(this);
|
||||
await this.notificationManager.notifyPlayersState(this.players);
|
||||
} else {
|
||||
await this.tilesSelection();
|
||||
}
|
||||
@ -164,8 +179,8 @@ export class DominoesGame {
|
||||
printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`);
|
||||
while (!this.gameOver) {
|
||||
await this.playTurn();
|
||||
await this.notificationManager.notifyGameState();
|
||||
await this.notificationManager.notifyPlayersState();
|
||||
await this.notificationManager.notifyGameState(this);
|
||||
await this.notificationManager.notifyPlayersState(this.players);
|
||||
this.gameBlocked = this.isBlocked();
|
||||
this.gameOver = this.isGameOver();
|
||||
}
|
||||
@ -196,8 +211,8 @@ export class DominoesGame {
|
||||
while (this.board.boneyard.length > 0) {
|
||||
for (let player of this.players) {
|
||||
const choosen = await player.chooseTile(this.board);
|
||||
await this.notificationManager.notifyGameState();
|
||||
await this.notificationManager.notifyPlayersState();
|
||||
await this.notificationManager.notifyGameState(this);
|
||||
await this.notificationManager.notifyPlayersState(this.players);
|
||||
if (this.board.boneyard.length === 0) {
|
||||
break;
|
||||
}
|
||||
@ -217,16 +232,8 @@ export class DominoesGame {
|
||||
gameTied: this.gameTied,
|
||||
gameId: this.id,
|
||||
boneyard: this.board.boneyard.map(tile => ({ id: tile.id})),
|
||||
players: this.players.map(player => ({
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
score: player.score,
|
||||
hand: player.hand.map(tile => tile.id),
|
||||
})),
|
||||
currentPlayer: {
|
||||
id: currentPlayer.id,
|
||||
name: currentPlayer.name
|
||||
},
|
||||
players: this.players.map(player => player.getState()),
|
||||
currentPlayer: currentPlayer.getState(),
|
||||
board: this.board.tiles.map(tile => ({
|
||||
id: tile.id,
|
||||
pips: tile.pips
|
||||
|
@ -1,149 +0,0 @@
|
||||
import { DominoesGame } from "./DominoesGame";
|
||||
import { PlayerAI } from "./entities/player/PlayerAI";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
import { LoggingService } from "../common/LoggingService";
|
||||
import { getRandomSeed, uuid, wait } from "../common/utilities";
|
||||
import { GameSessionState } from "./dto/GameSessionState";
|
||||
import { PlayerNotificationManager } from './PlayerNotificationManager';
|
||||
import seedrandom, { PRNG } from "seedrandom";
|
||||
|
||||
export class GameSession {
|
||||
private game: DominoesGame | null = null;
|
||||
private minHumanPlayers: number = 1;
|
||||
private waitingForPlayers: boolean = true;
|
||||
private waitingSeconds: number = 0;
|
||||
private logger: LoggingService = new LoggingService();
|
||||
private mode: string = 'classic';
|
||||
private pointsToWin: number = 100;
|
||||
private playerNotificationManager: PlayerNotificationManager;
|
||||
id: string;
|
||||
players: PlayerInterface[] = [];
|
||||
sessionInProgress: boolean = false;
|
||||
maxPlayers: number = 4;
|
||||
seed!: string
|
||||
rng!: PRNG
|
||||
|
||||
constructor(public creator: PlayerInterface, public name?: string) {
|
||||
this.playerNotificationManager = new PlayerNotificationManager(this);
|
||||
this.id = uuid();
|
||||
this.name = name || `Game ${this.id}`;
|
||||
this.addPlayer(creator);
|
||||
this.logger.info(`GameSession created by: ${creator.name}`);
|
||||
this.creator = creator;
|
||||
this.playerNotificationManager.notifySessionState();
|
||||
}
|
||||
|
||||
get numPlayers() {
|
||||
return this.players.length;
|
||||
}
|
||||
|
||||
private async startGame(seed: string) {
|
||||
this.rng = seedrandom(seed);
|
||||
const missingPlayers = this.maxPlayers - this.numPlayers;
|
||||
for (let i = 0; i < missingPlayers; i++) {
|
||||
this.addPlayer(this.createPlayerAI(i));
|
||||
}
|
||||
this.game = new DominoesGame(this.players, this.rng);
|
||||
this.sessionInProgress = true;
|
||||
this.logger.info('Game started');
|
||||
this.playerNotificationManager.notifySessionState();
|
||||
await this.game.start();
|
||||
return this.endGame();
|
||||
}
|
||||
|
||||
private endGame(): any {
|
||||
if (this.game !== null) {
|
||||
this.sessionInProgress = false;
|
||||
const { gameBlocked, gameTied, winner } = this.game;
|
||||
|
||||
gameBlocked ? console.log('Game blocked!') : gameTied ? console.log('Game tied!') : console.log('Game over!');
|
||||
console.log('Winner: ' + winner?.name + ' with ' + winner?.pipsCount() + ' points');
|
||||
this.getScore(this.game);
|
||||
this.sessionInProgress = false;
|
||||
this.logger.info('Game ended');
|
||||
this.game = null;
|
||||
this.playerNotificationManager.notifySessionState();
|
||||
|
||||
return {
|
||||
gameBlocked,
|
||||
gameTied,
|
||||
winner
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getScore(game: DominoesGame) {
|
||||
const pips = game.players
|
||||
.sort((a,b) => (b.pipsCount() - a.pipsCount()))
|
||||
.map(player => {
|
||||
return `${player.name}: ${player.pipsCount()}`;
|
||||
});
|
||||
console.log(`Pips count: ${pips.join(', ')}`);
|
||||
const totalPoints = game.players.reduce((acc, player) => acc + player.pipsCount(), 0);
|
||||
if (game.winner !== null) {
|
||||
game.winner.score += totalPoints;
|
||||
}
|
||||
const scores = game.players
|
||||
.sort((a,b) => (b.score - a.score))
|
||||
.map(player => {
|
||||
return `${player.name}: ${player.score}`;
|
||||
});
|
||||
console.log(`Scores: ${scores.join(', ')}`);
|
||||
}
|
||||
|
||||
createPlayerAI(i: number) {
|
||||
const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
|
||||
return new PlayerAI(AInames[i], this.rng);
|
||||
}
|
||||
|
||||
async start(seed?: string) {
|
||||
this.seed = seed || getRandomSeed();
|
||||
console.log('seed :>> ', this.seed);
|
||||
if (this.sessionInProgress) {
|
||||
throw new Error("Game already in progress");
|
||||
}
|
||||
this.waitingForPlayers = true;
|
||||
this.logger.info('Waiting for players to join');
|
||||
while (this.numPlayers < this.maxPlayers) {
|
||||
this.waitingSeconds += 1;
|
||||
this.logger.info(`Waiting for players to join: ${this.waitingSeconds}`);
|
||||
await wait(1000);
|
||||
}
|
||||
this.waitingForPlayers = false;
|
||||
this.logger.info('All players joined');
|
||||
this.startGame(this.seed);
|
||||
}
|
||||
|
||||
addPlayer(player: PlayerInterface) {
|
||||
if (this.numPlayers >= this.maxPlayers) {
|
||||
throw new Error("GameSession is full");
|
||||
}
|
||||
this.players.push(player);
|
||||
this.logger.info(`${player.name} joined the game!`);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `GameSession:(${this.id} ${this.name})`;
|
||||
}
|
||||
|
||||
getState(): GameSessionState {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name!,
|
||||
creator: this.creator.id,
|
||||
players: this.players.map(player =>( {
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
})),
|
||||
sessionInProgress: this.sessionInProgress,
|
||||
maxPlayers: this.maxPlayers,
|
||||
numPlayers: this.numPlayers,
|
||||
waitingForPlayers: this.waitingForPlayers,
|
||||
waitingSeconds: this.waitingSeconds,
|
||||
seed: this.seed,
|
||||
mode: this.mode,
|
||||
pointsToWin: this.pointsToWin,
|
||||
status: this.sessionInProgress ? 'in progress' : 'waiting'
|
||||
};
|
||||
}
|
||||
}
|
250
src/game/MatchSession.ts
Normal file
250
src/game/MatchSession.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import { DominoesGame } from "./DominoesGame";
|
||||
import { PlayerAI } from "./entities/player/PlayerAI";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
import { LoggingService } from "../common/LoggingService";
|
||||
import { getRandomSeed, uuid, wait } from "../common/utilities";
|
||||
import { MatchSessionState } from "./dto/MatchSessionState";
|
||||
import { PlayerNotificationManager } from './PlayerNotificationManager';
|
||||
import seedrandom, { PRNG } from "seedrandom";
|
||||
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
|
||||
import { PlayerHuman } from "./entities/player/PlayerHuman";
|
||||
|
||||
|
||||
export class MatchSession {
|
||||
private currentGame: DominoesGame | null = null;
|
||||
private minHumanPlayers: number = 1;
|
||||
private waitingForPlayers: boolean = true;
|
||||
private waitingSeconds: number = 0;
|
||||
private logger: LoggingService = new LoggingService();
|
||||
private playerNotificationManager = new PlayerNotificationManager();
|
||||
|
||||
id: string;
|
||||
matchInProgress: boolean = false;
|
||||
matchWinner: PlayerInterface | null = null;
|
||||
maxPlayers: number = 4;
|
||||
mode: string = 'classic';
|
||||
players: PlayerInterface[] = [];
|
||||
pointsToWin: number = 50;
|
||||
rng!: PRNG
|
||||
scoreboard: Map<string, number> = new Map();
|
||||
seed!: string
|
||||
sessionInProgress: boolean = false;
|
||||
state: string = 'created'
|
||||
|
||||
constructor(public creator: PlayerInterface, public name?: string) {
|
||||
this.id = uuid();
|
||||
this.name = name || `Game ${this.id}`;
|
||||
this.addPlayer(creator);
|
||||
|
||||
this.creator = creator;
|
||||
this.logger.info(`Match session created by: ${creator.name}`);
|
||||
this.logger.info(`Match session ID: ${this.id}`);
|
||||
this.logger.info(`Match session name: ${this.name}`);
|
||||
this.logger.info(`Points to win: ${this.pointsToWin}`);
|
||||
this.sessionInProgress = true;
|
||||
this.matchInProgress = false;
|
||||
this.playerNotificationManager.notifyMatchState(this);
|
||||
this.playerNotificationManager.notifyPlayersState(this.players);
|
||||
}
|
||||
|
||||
get numPlayers() {
|
||||
return this.players.length;
|
||||
}
|
||||
|
||||
get numPlayersReady() {
|
||||
return this.players.filter(player => player.ready).length;
|
||||
}
|
||||
|
||||
get numHumanPlayers() {
|
||||
return this.players.filter(player => player instanceof PlayerHuman).length;
|
||||
}
|
||||
|
||||
private async startMatch(seed: string) {
|
||||
this.rng = seedrandom(seed);
|
||||
const missingPlayers = this.maxPlayers - this.numPlayers;
|
||||
for (let i = 0; i < missingPlayers; i++) {
|
||||
this.addPlayer(this.createPlayerAI(i));
|
||||
}
|
||||
this.state = 'ready'
|
||||
this.resetScoreboard()
|
||||
let gameNumber: number = 0;
|
||||
this.matchInProgress = true
|
||||
this.playerNotificationManager.notifyMatchState(this);
|
||||
while (this.matchInProgress) {
|
||||
|
||||
this.currentGame = new DominoesGame(this.players, this.rng);
|
||||
gameNumber += 1;
|
||||
this.state = 'started'
|
||||
this.logger.info(`Game #${gameNumber} started`);
|
||||
// this.game.reset()
|
||||
await this.currentGame.start();
|
||||
this.setScores();
|
||||
this.checkMatchWinner();
|
||||
this.resetReadiness();
|
||||
this.state = 'waiting'
|
||||
await this.playerNotificationManager.notifyMatchState(this);
|
||||
this.playerNotificationManager.sendEventToPlayers('game-finished', this.players);
|
||||
await this.checkHumanPlayersReady();
|
||||
}
|
||||
this.state = 'end'
|
||||
// await this.game.start();
|
||||
return this.endGame();
|
||||
}
|
||||
|
||||
async checkHumanPlayersReady() {
|
||||
this.logger.info('Waiting for human players to be ready');
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
this.logger.debug(`Human players ready: ${this.numPlayersReady}/${this.numHumanPlayers}`)
|
||||
if (this.numPlayersReady === this.numHumanPlayers) {
|
||||
clearInterval(interval);
|
||||
resolve(true);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
resetReadiness() {
|
||||
this.players.forEach(player => {
|
||||
player.ready = false
|
||||
});
|
||||
}
|
||||
|
||||
checkMatchWinner() {
|
||||
const scores = Array.from(this.scoreboard.values());
|
||||
const maxScore = Math.max(...scores);
|
||||
if (maxScore >= this.pointsToWin) {
|
||||
this.matchWinner = this.players.find(player => this.scoreboard.get(player.id) === maxScore)!;
|
||||
this.logger.info(`Match winner: ${this.matchWinner.name} with ${maxScore} points`);
|
||||
this.matchInProgress = false;
|
||||
}
|
||||
}
|
||||
resetScoreboard() {
|
||||
this.scoreboard = new Map();
|
||||
this.players.forEach(player => {
|
||||
this.scoreboard.set(player.id, 0);
|
||||
});
|
||||
}
|
||||
|
||||
setScores() {
|
||||
const totalPips = this.currentGame?.players.reduce((acc, player) => acc + player.pipsCount(), 0);
|
||||
if (this.currentGame && this.currentGame.winner !== null) {
|
||||
const winner = this.currentGame.winner;
|
||||
this.scoreboard.set(winner.id, this.scoreboard.get(winner.id)! + totalPips!);
|
||||
if (winner.teamedWith !== null) {
|
||||
this.scoreboard.set(winner.teamedWith.id, this.scoreboard.get(winner.teamedWith.id)! + totalPips!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private endGame(): any {
|
||||
if (this.currentGame !== null) {
|
||||
const { gameBlocked, gameTied, winner } = this.currentGame;
|
||||
gameBlocked ? this.logger.info('Game blocked!') : gameTied ? this.logger.info('Game tied!') : this.logger.info('Game over!');
|
||||
this.logger.info('Winner: ' + winner?.name + ' with ' + winner?.pipsCount() + ' points');
|
||||
this.getScore(this.currentGame);
|
||||
this.logger.info('Game ended');
|
||||
this.currentGame = null;
|
||||
this.playerNotificationManager.notifyMatchState(this);
|
||||
|
||||
return {
|
||||
gameBlocked,
|
||||
gameTied,
|
||||
winner
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private getScore(game: DominoesGame) {
|
||||
const pips = game.players
|
||||
.sort((a,b) => (b.pipsCount() - a.pipsCount()))
|
||||
.map(player => {
|
||||
return `${player.name}: ${player.pipsCount()}`;
|
||||
});
|
||||
this.logger.info(`Pips count: ${pips.join(', ')}`);
|
||||
const totalPoints = game.players.reduce((acc, player) => acc + player.pipsCount(), 0);
|
||||
if (game.winner !== null) {
|
||||
game.winner.score += totalPoints;
|
||||
}
|
||||
const scores = game.players
|
||||
.sort((a,b) => (b.score - a.score))
|
||||
.map(player => {
|
||||
return `${player.name}: ${player.score}`;
|
||||
});
|
||||
this.logger.info(`Scores: ${scores.join(', ')}`);
|
||||
}
|
||||
|
||||
createPlayerAI(i: number) {
|
||||
const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
|
||||
const player = new PlayerAI(AInames[i], this.rng);
|
||||
player.ready = true;
|
||||
return player;
|
||||
}
|
||||
|
||||
async start(seed?: string) {
|
||||
this.seed = seed || getRandomSeed();
|
||||
console.log('seed :>> ', this.seed);
|
||||
if (this.matchInProgress) {
|
||||
throw new Error("Game already in progress");
|
||||
}
|
||||
this.waitingForPlayers = true;
|
||||
this.logger.info('Waiting for players to be ready');
|
||||
while (this.numPlayers < this.maxPlayers) {
|
||||
this.waitingSeconds += 1;
|
||||
this.logger.info(`Waiting for players to join: ${this.waitingSeconds}`);
|
||||
await wait(1000);
|
||||
}
|
||||
this.waitingForPlayers = false;
|
||||
this.logger.info('All players joined');
|
||||
await this.startMatch(this.seed);
|
||||
}
|
||||
|
||||
addPlayer(player: PlayerInterface) {
|
||||
if (this.numPlayers >= this.maxPlayers) {
|
||||
throw new Error("GameSession is full");
|
||||
}
|
||||
this.players.push(player);
|
||||
this.logger.info(`${player.name} joined the game!`);
|
||||
}
|
||||
|
||||
setPlayerReady(user: string) {
|
||||
const player = this.players.find(player => player.name === user);
|
||||
if (!player) {
|
||||
throw new Error("Player not found");
|
||||
}
|
||||
player.ready = true;
|
||||
this.logger.info(`${player.name} is ready!`);
|
||||
this.playerNotificationManager.notifyMatchState(this);
|
||||
}
|
||||
|
||||
|
||||
toString() {
|
||||
return `GameSession:(${this.id} ${this.name})`;
|
||||
}
|
||||
|
||||
getState(): MatchSessionState {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name!,
|
||||
creator: this.creator.id,
|
||||
players: this.players.map(player =>( {
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
ready: player.ready,
|
||||
})),
|
||||
playersReady: this.numPlayersReady,
|
||||
sessionInProgress: this.sessionInProgress,
|
||||
maxPlayers: this.maxPlayers,
|
||||
numPlayers: this.numPlayers,
|
||||
waitingForPlayers: this.waitingForPlayers,
|
||||
waitingSeconds: this.waitingSeconds,
|
||||
seed: this.seed,
|
||||
mode: this.mode,
|
||||
pointsToWin: this.pointsToWin,
|
||||
status: this.sessionInProgress ? 'in progress' : 'waiting',
|
||||
scoreboard: this.scoreboard,
|
||||
matchWinner: this.matchWinner?.getState() || null,
|
||||
matchInProgress: this.matchInProgress
|
||||
};
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ export class NetworkClientNotifier {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
async notifyPlayer(player: NetworkPlayer, event: string, data: any = {}, timeoutSecs: number = 300): Promise<any> {
|
||||
async notifyPlayer(player: NetworkPlayer, event: string, data: any = {}, timeoutSecs: number = 900): Promise<any> {
|
||||
try {
|
||||
const response = await this.io.to(player.socketId)
|
||||
.timeout(timeoutSecs * 1000)
|
||||
@ -29,6 +29,10 @@ export class NetworkClientNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
async sendEvent(player: NetworkPlayer, event: string, data?: any) {
|
||||
this.io.to(player.socketId).emit(event, data);
|
||||
}
|
||||
|
||||
async broadcast(event: string, data: any) {
|
||||
const responses = await this.io.emit(event, data);
|
||||
this.logger.debug('responses :>> ', responses);
|
||||
|
@ -1,39 +1,36 @@
|
||||
import { DominoesGame } from "./DominoesGame";
|
||||
import { GameSession } from "./GameSession";
|
||||
import { MatchSession } from "./MatchSession";
|
||||
import { GameState } from "./dto/GameState";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
|
||||
export class PlayerNotificationManager {
|
||||
game!: DominoesGame;
|
||||
session!: GameSession;
|
||||
|
||||
constructor(game: DominoesGame | GameSession) {
|
||||
if (game instanceof GameSession) {
|
||||
this.session = game;
|
||||
} else {
|
||||
this.game = game;
|
||||
}
|
||||
}
|
||||
|
||||
async notifyGameState() {
|
||||
if(!this.game) throw new Error('Game not initialized');
|
||||
const gameState: GameState = this.game.getGameState();
|
||||
const { players } = this.game;
|
||||
async notifyGameState(game: DominoesGame) {
|
||||
const gameState: GameState = game.getGameState();
|
||||
const { players } = game;
|
||||
let promises: Promise<void>[] = players.map(player => player.notifyGameState(gameState));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
async notifyPlayersState() {
|
||||
if(!this.game) throw new Error('Game not initialized');
|
||||
const { players } = this.game;
|
||||
async notifyPlayersState(players: PlayerInterface[]) {
|
||||
let promises: Promise<void>[] = players.map(player => player.notifyPlayerState(player.getState()));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
async notifySessionState() {
|
||||
if(!this.session) throw new Error('Session not initialized');
|
||||
const { players } = this.session;
|
||||
let promises: Promise<void>[] = players.map(player => player.notifySessionState(this.session.getState()));
|
||||
async notifyMatchState(session: MatchSession) {
|
||||
const { players } = session;
|
||||
let promises: Promise<void>[] = players.map(player => player.notifyMatchState(session.getState()));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
async waitForPlayersAction(actionId: string, data: any = {}, players: PlayerInterface[]) {
|
||||
let promises: Promise<boolean>[] = players.map(player => player.waitForAction(actionId, data));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
async sendEventToPlayers(event: string, players: PlayerInterface[]) {
|
||||
let promises: Promise<void>[] = players.map(player => player.sendEvent(event));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { PlayerDto } from "./PlayerDto";
|
||||
|
||||
export interface GameSessionState {
|
||||
export interface MatchSessionState {
|
||||
id: string;
|
||||
name: string;
|
||||
creator: string;
|
||||
@ -14,4 +14,8 @@ export interface GameSessionState {
|
||||
maxPlayers: number;
|
||||
numPlayers: number;
|
||||
waitingSeconds: number;
|
||||
scoreboard: Map<string, number>;
|
||||
matchWinner: PlayerDto | null;
|
||||
matchInProgress: boolean;
|
||||
playersReady: number
|
||||
}
|
@ -2,5 +2,7 @@ export interface PlayerDto {
|
||||
id: string;
|
||||
name: string;
|
||||
score?: number;
|
||||
hand?: string[];
|
||||
hand?: any[];
|
||||
teamedWith?: PlayerDto | null;
|
||||
ready: boolean;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
export interface PlayerState {
|
||||
id: string;
|
||||
name: string;
|
||||
score: number;
|
||||
hand: any[];
|
||||
teamedWith: string | undefined;
|
||||
}
|
@ -38,6 +38,11 @@ export class Board {
|
||||
return this.rightEnd?.flippedPips[1];
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.tiles = [];
|
||||
this.boneyard = [];
|
||||
}
|
||||
|
||||
getFreeEnds() {
|
||||
if(this.count === 0) {
|
||||
return [];
|
||||
|
@ -7,8 +7,8 @@ import { EventEmitter } from "stream";
|
||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||
import { uuid } from "../../../common/utilities";
|
||||
import { GameState } from "../../dto/GameState";
|
||||
import { PlayerState } from "../../dto/PlayerState";
|
||||
import { GameSessionState } from "../../dto/GameSessionState";
|
||||
import { MatchSessionState } from "../../dto/MatchSessionState";
|
||||
import { PlayerDto } from "../../dto/PlayerDto";
|
||||
|
||||
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
|
||||
hand: Tile[] = [];
|
||||
@ -17,6 +17,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
||||
teamedWith: PlayerInterface | null = null;
|
||||
playerInteraction: PlayerInteractionInterface = undefined as any;
|
||||
id: string = uuid();
|
||||
ready: boolean = false;
|
||||
|
||||
constructor(public name: string) {
|
||||
super();
|
||||
@ -29,10 +30,17 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
||||
async notifyGameState(state: GameState): Promise<void> {
|
||||
}
|
||||
|
||||
async notifyPlayerState(state: PlayerState): Promise<void> {
|
||||
async notifyPlayerState(state: PlayerDto): Promise<void> {
|
||||
}
|
||||
|
||||
async notifySessionState(state: GameSessionState): Promise<void> {
|
||||
async notifyMatchState(state: MatchSessionState): Promise<void> {
|
||||
}
|
||||
|
||||
async waitForAction(actionId: string): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async sendEvent(event: string): Promise<void> {
|
||||
}
|
||||
|
||||
pipsCount(): number {
|
||||
@ -54,17 +62,24 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
||||
return highestPair;
|
||||
}
|
||||
|
||||
getState(): PlayerState {
|
||||
getState(showPips: boolean = false): PlayerDto {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
score: this.score,
|
||||
hand: this.hand.map(tile => ({
|
||||
id: tile.id,
|
||||
pips: tile.pips,
|
||||
flipped: tile.revealed,
|
||||
})),
|
||||
teamedWith: this.teamedWith?.id,
|
||||
hand: this.hand.map(tile => {
|
||||
const d = {
|
||||
id: tile.id,
|
||||
pips: tile.pips,
|
||||
flipped: tile.revealed,
|
||||
};
|
||||
if (showPips) {
|
||||
d.pips = tile.pips;
|
||||
}
|
||||
return d;
|
||||
}),
|
||||
teamedWith: this.teamedWith?.getState() ?? null,
|
||||
ready: this.ready,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import { NetworkClientNotifier } from "../../NetworkClientNotifier";
|
||||
import { Tile } from "../Tile";
|
||||
import { Board } from "../Board";
|
||||
import { GameState } from "../../dto/GameState";
|
||||
import { PlayerState } from "../../dto/PlayerState";
|
||||
import { GameSessionState } from "../../dto/GameSessionState";
|
||||
import { PlayerDto } from "../../dto/PlayerDto";
|
||||
import { MatchSessionState } from "../../dto/MatchSessionState";
|
||||
import { SocketDisconnectedError } from "../../../common/exceptions/SocketDisconnectedError";
|
||||
|
||||
export class NetworkPlayer extends PlayerHuman {
|
||||
@ -27,7 +27,7 @@ export class NetworkPlayer extends PlayerHuman {
|
||||
}
|
||||
}
|
||||
|
||||
async notifyPlayerState(state: PlayerState): Promise<void> {
|
||||
async notifyPlayerState(state: PlayerDto): Promise<void> {
|
||||
const response = await this.clientNotifier.notifyPlayer(this, 'playerState', state);
|
||||
console.log('player state notified :>> ', response);
|
||||
if (response === undefined || response.status !== 'ok' ) {
|
||||
@ -35,14 +35,25 @@ export class NetworkPlayer extends PlayerHuman {
|
||||
}
|
||||
}
|
||||
|
||||
async notifySessionState(state: GameSessionState): Promise<void> {
|
||||
const response = await this.clientNotifier.notifyPlayer(this, 'sessionState', state);
|
||||
async notifyMatchState(state: MatchSessionState): Promise<void> {
|
||||
const response = await this.clientNotifier.notifyPlayer(this, 'matchState', state);
|
||||
console.log('session state notified :>> ', response);
|
||||
if (response === undefined || response.status !== 'ok' ) {
|
||||
throw new SocketDisconnectedError();
|
||||
}
|
||||
}
|
||||
async waitForAction(actionId: string): Promise<boolean> {
|
||||
const response = await this.clientNotifier.notifyPlayer(this, actionId);
|
||||
if (response === undefined || response.status !== 'ok' ) {
|
||||
throw new SocketDisconnectedError();
|
||||
}
|
||||
const { actionResult } = response;
|
||||
return actionResult;
|
||||
}
|
||||
|
||||
async sendEvent(event: string): Promise<void> {
|
||||
this.clientNotifier.sendEvent(this, event);
|
||||
}
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
return await this.playerInteraction.chooseTile(board);
|
||||
|
@ -2,9 +2,9 @@ import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||
import { Board } from "../Board";
|
||||
import { GameState } from "../../dto/GameState";
|
||||
import { PlayerMove } from "../PlayerMove";
|
||||
import { PlayerState } from "../../dto/PlayerState";
|
||||
import { Tile } from "../Tile";
|
||||
import { GameSessionState } from "../../dto/GameSessionState";
|
||||
import { MatchSessionState } from "../../dto/MatchSessionState";
|
||||
import { PlayerDto } from "../../dto/PlayerDto";
|
||||
|
||||
export interface PlayerInterface {
|
||||
id: string;
|
||||
@ -13,12 +13,15 @@ export interface PlayerInterface {
|
||||
hand: Tile[];
|
||||
teamedWith: PlayerInterface | null;
|
||||
playerInteraction: PlayerInteractionInterface;
|
||||
ready: boolean;
|
||||
|
||||
makeMove(gameState: Board): Promise<PlayerMove | null>;
|
||||
chooseTile(board: Board): Promise<Tile>;
|
||||
pipsCount(): number;
|
||||
notifyGameState(state: GameState): Promise<void>;
|
||||
notifyPlayerState(state: PlayerState): Promise<void>;
|
||||
notifySessionState(state: GameSessionState): Promise<void>;
|
||||
getState(): PlayerState;
|
||||
notifyPlayerState(state: PlayerDto): Promise<void>;
|
||||
notifyMatchState(state: MatchSessionState): Promise<void>;
|
||||
waitForAction(actionId: string, data: any): Promise<boolean>;
|
||||
sendEvent(event: string): Promise<void>;
|
||||
getState(): PlayerDto;
|
||||
}
|
90
src/server/controllers/ApiKeyController.ts
Normal file
90
src/server/controllers/ApiKeyController.ts
Normal 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;
|
||||
}
|
||||
}
|
213
src/server/controllers/AuthController.ts
Normal file
213
src/server/controllers/AuthController.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
}
|
11
src/server/controllers/BaseController.ts
Normal file
11
src/server/controllers/BaseController.ts
Normal 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();
|
||||
}
|
||||
}
|
19
src/server/controllers/MatchSessionController.ts
Normal file
19
src/server/controllers/MatchSessionController.ts
Normal 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;
|
||||
// }
|
||||
}
|
62
src/server/controllers/NamespacesController.ts
Normal file
62
src/server/controllers/NamespacesController.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
152
src/server/controllers/UserController.ts
Normal file
152
src/server/controllers/UserController.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
20
src/server/db/DbAdapter.ts
Normal file
20
src/server/db/DbAdapter.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
98
src/server/db/interfaces.ts
Normal file
98
src/server/db/interfaces.ts
Normal 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[];
|
||||
}
|
||||
|
39
src/server/db/mongo/ApiTokenMongoManager.ts
Normal file
39
src/server/db/mongo/ApiTokenMongoManager.ts
Normal 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 });
|
||||
}
|
||||
}
|
5
src/server/db/mongo/MatchSessionMongoManager.ts
Normal file
5
src/server/db/mongo/MatchSessionMongoManager.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { BaseMongoManager } from './common/BaseMongoManager';
|
||||
|
||||
export class MatchSessionMongoManager extends BaseMongoManager {
|
||||
protected collection = "matchSessions";
|
||||
}
|
77
src/server/db/mongo/NamespacesMongoManager.ts
Normal file
77
src/server/db/mongo/NamespacesMongoManager.ts
Normal 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})
|
||||
}
|
||||
}
|
35
src/server/db/mongo/TemporalTokenMongoManager.ts
Normal file
35
src/server/db/mongo/TemporalTokenMongoManager.ts
Normal 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 });
|
||||
}
|
||||
}
|
70
src/server/db/mongo/UsersMongoManager.ts
Normal file
70
src/server/db/mongo/UsersMongoManager.ts
Normal 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 });
|
||||
}
|
||||
}
|
123
src/server/db/mongo/common/BaseMongoManager.ts
Normal file
123
src/server/db/mongo/common/BaseMongoManager.ts
Normal 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);
|
||||
};
|
||||
}
|
111
src/server/db/mongo/common/PipelineLibrary.ts
Normal file
111
src/server/db/mongo/common/PipelineLibrary.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
55
src/server/db/mongo/common/mongoDBPool.ts
Normal file
55
src/server/db/mongo/common/mongoDBPool.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
};
|
5
src/server/db/mongo/common/mongoUtils.ts
Normal file
5
src/server/db/mongo/common/mongoUtils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
export default function toObjectId(id: string) {
|
||||
return ObjectId.createFromHexString(id);
|
||||
}
|
@ -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}`);
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { LoggingService } from "../../common/LoggingService";
|
||||
|
||||
export class ControllerBase {
|
||||
export class ManagerBase {
|
||||
protected logger = new LoggingService();
|
||||
}
|
39
src/server/managers/SecurityManager.ts
Normal file
39
src/server/managers/SecurityManager.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
33
src/server/router/adminRouter.ts
Normal file
33
src/server/router/adminRouter.ts
Normal 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;
|
||||
}
|
22
src/server/router/apiRouter.ts
Normal file
22
src/server/router/apiRouter.ts
Normal 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;
|
||||
}
|
15
src/server/router/index.ts
Normal file
15
src/server/router/index.ts
Normal 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;
|
||||
}
|
23
src/server/router/userRouter.ts
Normal file
23
src/server/router/userRouter.ts
Normal 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;
|
||||
}
|
22
src/server/router/validations.ts
Normal file
22
src/server/router/validations.ts
Normal 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],
|
||||
}
|
25
src/server/services/CryptoService.ts
Normal file
25
src/server/services/CryptoService.ts
Normal 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');
|
||||
}
|
||||
|
||||
}
|
45
src/server/services/NamespacesService.ts
Normal file
45
src/server/services/NamespacesService.ts
Normal 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);
|
||||
}
|
||||
}
|
19
src/server/services/SessionService.ts
Normal file
19
src/server/services/SessionService.ts
Normal 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));
|
||||
}
|
||||
}
|
@ -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) => {
|
||||
@ -50,16 +59,30 @@ export class SocketIoService extends ServiceBase{
|
||||
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, {
|
||||
cors: {
|
||||
|
82
src/server/services/UsersService.ts
Normal file
82
src/server/services/UsersService.ts
Normal 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;
|
||||
}
|
||||
}
|
75
src/server/services/mailer/MailerService.ts
Normal file
75
src/server/services/mailer/MailerService.ts
Normal 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
11
src/server/types/environment.d.ts
vendored
Normal 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
8
src/server/types/express/index.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import 'express-serve-static-core';
|
||||
|
||||
declare module 'express-serve-static-core' {
|
||||
interface Request {
|
||||
user?: any;
|
||||
token?: any;
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"typeRoots": ["./src/server/types"], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
|
Loading…
x
Reference in New Issue
Block a user