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": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "node --env-file=.env --watch -r ts-node/register src/server/index.ts",
|
"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": "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",
|
"test:watch": "node --env-file=.env --watch -r ts-node/register src/test.ts",
|
||||||
"docker-build": "docker build -t arhuako/domino:latest .",
|
"docker-build": "docker build -t arhuako/domino:latest .",
|
||||||
@ -22,17 +23,29 @@
|
|||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"reposityory": "github:jmconde/domino",
|
"reposityory": "github:jmconde/domino",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2",
|
"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": "^9.2.0",
|
||||||
|
"pino-http": "^10.2.0",
|
||||||
"pino-pretty": "^11.2.1",
|
"pino-pretty": "^11.2.1",
|
||||||
|
"pino-rotating-file-stream": "^0.0.2",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
"socket.io": "^4.7.5"
|
"socket.io": "^4.7.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
|
"@types/nodemailer": "^6.4.15",
|
||||||
|
"@types/nodemailer-express-handlebars": "^4.0.5",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.5.2"
|
"typescript": "^5.5.2"
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import pino, { BaseLogger } from 'pino';
|
import pino, { BaseLogger } from 'pino';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import httpPino from 'pino-http';
|
||||||
|
|
||||||
export class LoggingService {
|
export class LoggingService {
|
||||||
static instance: LoggingService;
|
static instance: LoggingService;
|
||||||
logsPath: string = path.join(process.cwd(), 'app', 'server', 'logs');
|
logsPath: string = path.join(process.cwd(), 'logs');
|
||||||
logger!: BaseLogger;
|
logger!: BaseLogger;
|
||||||
level: string = process.env.LOG_LEVEL || 'info';
|
level: string = process.env.LOG_LEVEL || 'info';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ogger.fatal('fatal');
|
* 1 - fatal
|
||||||
logger.error('error');
|
2 - error
|
||||||
logger.warn('warn');
|
3 - warn
|
||||||
logger.info('info');
|
4 - info
|
||||||
logger.debug('debug');
|
5 - debug
|
||||||
logger.trace('trace');
|
6 - trace
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
if ((!LoggingService.instance)) {
|
if ((!LoggingService.instance)) {
|
||||||
@ -26,7 +27,7 @@ export class LoggingService {
|
|||||||
return LoggingService.instance;
|
return LoggingService.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
get commonRorationOptions() : any {
|
get commonRotationOptions() : any {
|
||||||
return {
|
return {
|
||||||
interval: '1d',
|
interval: '1d',
|
||||||
maxFiles: 10,
|
maxFiles: 10,
|
||||||
@ -39,14 +40,14 @@ export class LoggingService {
|
|||||||
get transports() {
|
get transports() {
|
||||||
return pino.transport({
|
return pino.transport({
|
||||||
targets: [
|
targets: [
|
||||||
// {
|
{
|
||||||
// target: 'pino-rotating-file-stream',
|
target: 'pino-rotating-file-stream',
|
||||||
// level: this.level,
|
level: this.level,
|
||||||
// options: {
|
options: {
|
||||||
// filename: 'app.log',
|
filename: 'app.log',
|
||||||
// ...this.commonRorationOptions
|
...this.commonRotationOptions
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
target: 'pino-pretty',
|
target: 'pino-pretty',
|
||||||
level: this.level,
|
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) {
|
debug(message: string, data?: any) {
|
||||||
this.logger.debug(this._getMessageWidthObject(message, data));
|
this.logger.debug(this._getMessageWidthObject(message, data));
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ export class DominoesGame {
|
|||||||
winner: PlayerInterface | null = null;
|
winner: PlayerInterface | null = null;
|
||||||
rng: PRNG;
|
rng: PRNG;
|
||||||
handSize: number = 7;
|
handSize: number = 7;
|
||||||
notificationManager: PlayerNotificationManager = new PlayerNotificationManager(this);
|
notificationManager: PlayerNotificationManager = new PlayerNotificationManager();
|
||||||
lastMove: PlayerMove | null = null;
|
lastMove: PlayerMove | null = null;
|
||||||
|
|
||||||
constructor(public players: PlayerInterface[], seed: PRNG) {
|
constructor(public players: PlayerInterface[], seed: PRNG) {
|
||||||
@ -44,6 +44,14 @@ export class DominoesGame {
|
|||||||
this.board.boneyard = this.generateTiles();
|
this.board.boneyard = this.generateTiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.board.reset();
|
||||||
|
this.initializeGame();
|
||||||
|
for (let player of this.players) {
|
||||||
|
player.hand = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
generateTiles(): Tile[] {
|
generateTiles(): Tile[] {
|
||||||
const tiles: Tile[] = [];
|
const tiles: Tile[] = [];
|
||||||
for (let i = 6; i >= 0; i--) {
|
for (let i = 6; i >= 0; i--) {
|
||||||
@ -142,18 +150,25 @@ export class DominoesGame {
|
|||||||
this.nextPlayer();
|
this.nextPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetPlayersScore() {
|
||||||
|
for (let player of this.players) {
|
||||||
|
player.score = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async start(): Promise<GameSummary> {
|
async start(): Promise<GameSummary> {
|
||||||
|
this.resetPlayersScore();
|
||||||
this.gameInProgress = false;
|
this.gameInProgress = false;
|
||||||
this.tileSelectionPhase = true;
|
this.tileSelectionPhase = true;
|
||||||
await this.notificationManager.notifyGameState();
|
await this.notificationManager.notifyGameState(this);
|
||||||
await this.notificationManager.notifyPlayersState();
|
await this.notificationManager.notifyPlayersState(this.players);
|
||||||
this.logger.debug('clients received boneyard :>> ' + this.board.boneyard);
|
this.logger.debug('clients received boneyard :>> ' + this.board.boneyard);
|
||||||
await wait(1000);
|
await wait(1000);
|
||||||
|
|
||||||
if (this.autoDeal) {
|
if (this.autoDeal) {
|
||||||
this.dealTiles();
|
this.dealTiles();
|
||||||
await this.notificationManager.notifyGameState();
|
await this.notificationManager.notifyGameState(this);
|
||||||
await this.notificationManager.notifyPlayersState();
|
await this.notificationManager.notifyPlayersState(this.players);
|
||||||
} else {
|
} else {
|
||||||
await this.tilesSelection();
|
await this.tilesSelection();
|
||||||
}
|
}
|
||||||
@ -164,8 +179,8 @@ export class DominoesGame {
|
|||||||
printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`);
|
printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`);
|
||||||
while (!this.gameOver) {
|
while (!this.gameOver) {
|
||||||
await this.playTurn();
|
await this.playTurn();
|
||||||
await this.notificationManager.notifyGameState();
|
await this.notificationManager.notifyGameState(this);
|
||||||
await this.notificationManager.notifyPlayersState();
|
await this.notificationManager.notifyPlayersState(this.players);
|
||||||
this.gameBlocked = this.isBlocked();
|
this.gameBlocked = this.isBlocked();
|
||||||
this.gameOver = this.isGameOver();
|
this.gameOver = this.isGameOver();
|
||||||
}
|
}
|
||||||
@ -196,8 +211,8 @@ export class DominoesGame {
|
|||||||
while (this.board.boneyard.length > 0) {
|
while (this.board.boneyard.length > 0) {
|
||||||
for (let player of this.players) {
|
for (let player of this.players) {
|
||||||
const choosen = await player.chooseTile(this.board);
|
const choosen = await player.chooseTile(this.board);
|
||||||
await this.notificationManager.notifyGameState();
|
await this.notificationManager.notifyGameState(this);
|
||||||
await this.notificationManager.notifyPlayersState();
|
await this.notificationManager.notifyPlayersState(this.players);
|
||||||
if (this.board.boneyard.length === 0) {
|
if (this.board.boneyard.length === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -217,16 +232,8 @@ export class DominoesGame {
|
|||||||
gameTied: this.gameTied,
|
gameTied: this.gameTied,
|
||||||
gameId: this.id,
|
gameId: this.id,
|
||||||
boneyard: this.board.boneyard.map(tile => ({ id: tile.id})),
|
boneyard: this.board.boneyard.map(tile => ({ id: tile.id})),
|
||||||
players: this.players.map(player => ({
|
players: this.players.map(player => player.getState()),
|
||||||
id: player.id,
|
currentPlayer: currentPlayer.getState(),
|
||||||
name: player.name,
|
|
||||||
score: player.score,
|
|
||||||
hand: player.hand.map(tile => tile.id),
|
|
||||||
})),
|
|
||||||
currentPlayer: {
|
|
||||||
id: currentPlayer.id,
|
|
||||||
name: currentPlayer.name
|
|
||||||
},
|
|
||||||
board: this.board.tiles.map(tile => ({
|
board: this.board.tiles.map(tile => ({
|
||||||
id: tile.id,
|
id: tile.id,
|
||||||
pips: tile.pips
|
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;
|
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 {
|
try {
|
||||||
const response = await this.io.to(player.socketId)
|
const response = await this.io.to(player.socketId)
|
||||||
.timeout(timeoutSecs * 1000)
|
.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) {
|
async broadcast(event: string, data: any) {
|
||||||
const responses = await this.io.emit(event, data);
|
const responses = await this.io.emit(event, data);
|
||||||
this.logger.debug('responses :>> ', responses);
|
this.logger.debug('responses :>> ', responses);
|
||||||
|
@ -1,39 +1,36 @@
|
|||||||
import { DominoesGame } from "./DominoesGame";
|
import { DominoesGame } from "./DominoesGame";
|
||||||
import { GameSession } from "./GameSession";
|
import { MatchSession } from "./MatchSession";
|
||||||
import { GameState } from "./dto/GameState";
|
import { GameState } from "./dto/GameState";
|
||||||
|
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||||
|
|
||||||
export class PlayerNotificationManager {
|
export class PlayerNotificationManager {
|
||||||
game!: DominoesGame;
|
|
||||||
session!: GameSession;
|
|
||||||
|
|
||||||
constructor(game: DominoesGame | GameSession) {
|
async notifyGameState(game: DominoesGame) {
|
||||||
if (game instanceof GameSession) {
|
const gameState: GameState = game.getGameState();
|
||||||
this.session = game;
|
const { players } = 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;
|
|
||||||
let promises: Promise<void>[] = players.map(player => player.notifyGameState(gameState));
|
let promises: Promise<void>[] = players.map(player => player.notifyGameState(gameState));
|
||||||
return await Promise.all(promises);
|
return await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
async notifyPlayersState() {
|
async notifyPlayersState(players: PlayerInterface[]) {
|
||||||
if(!this.game) throw new Error('Game not initialized');
|
|
||||||
const { players } = this.game;
|
|
||||||
let promises: Promise<void>[] = players.map(player => player.notifyPlayerState(player.getState()));
|
let promises: Promise<void>[] = players.map(player => player.notifyPlayerState(player.getState()));
|
||||||
return await Promise.all(promises);
|
return await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async notifySessionState() {
|
async notifyMatchState(session: MatchSession) {
|
||||||
if(!this.session) throw new Error('Session not initialized');
|
const { players } = session;
|
||||||
const { players } = this.session;
|
let promises: Promise<void>[] = players.map(player => player.notifyMatchState(session.getState()));
|
||||||
let promises: Promise<void>[] = players.map(player => player.notifySessionState(this.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);
|
return await Promise.all(promises);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { PlayerDto } from "./PlayerDto";
|
import { PlayerDto } from "./PlayerDto";
|
||||||
|
|
||||||
export interface GameSessionState {
|
export interface MatchSessionState {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
creator: string;
|
creator: string;
|
||||||
@ -14,4 +14,8 @@ export interface GameSessionState {
|
|||||||
maxPlayers: number;
|
maxPlayers: number;
|
||||||
numPlayers: number;
|
numPlayers: number;
|
||||||
waitingSeconds: number;
|
waitingSeconds: number;
|
||||||
|
scoreboard: Map<string, number>;
|
||||||
|
matchWinner: PlayerDto | null;
|
||||||
|
matchInProgress: boolean;
|
||||||
|
playersReady: number
|
||||||
}
|
}
|
@ -2,5 +2,7 @@ export interface PlayerDto {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
score?: number;
|
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];
|
return this.rightEnd?.flippedPips[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.tiles = [];
|
||||||
|
this.boneyard = [];
|
||||||
|
}
|
||||||
|
|
||||||
getFreeEnds() {
|
getFreeEnds() {
|
||||||
if(this.count === 0) {
|
if(this.count === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -7,8 +7,8 @@ import { EventEmitter } from "stream";
|
|||||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||||
import { uuid } from "../../../common/utilities";
|
import { uuid } from "../../../common/utilities";
|
||||||
import { GameState } from "../../dto/GameState";
|
import { GameState } from "../../dto/GameState";
|
||||||
import { PlayerState } from "../../dto/PlayerState";
|
import { MatchSessionState } from "../../dto/MatchSessionState";
|
||||||
import { GameSessionState } from "../../dto/GameSessionState";
|
import { PlayerDto } from "../../dto/PlayerDto";
|
||||||
|
|
||||||
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
|
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
|
||||||
hand: Tile[] = [];
|
hand: Tile[] = [];
|
||||||
@ -17,6 +17,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
|||||||
teamedWith: PlayerInterface | null = null;
|
teamedWith: PlayerInterface | null = null;
|
||||||
playerInteraction: PlayerInteractionInterface = undefined as any;
|
playerInteraction: PlayerInteractionInterface = undefined as any;
|
||||||
id: string = uuid();
|
id: string = uuid();
|
||||||
|
ready: boolean = false;
|
||||||
|
|
||||||
constructor(public name: string) {
|
constructor(public name: string) {
|
||||||
super();
|
super();
|
||||||
@ -29,10 +30,17 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
|||||||
async notifyGameState(state: GameState): Promise<void> {
|
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 {
|
pipsCount(): number {
|
||||||
@ -54,17 +62,24 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
|||||||
return highestPair;
|
return highestPair;
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(): PlayerState {
|
getState(showPips: boolean = false): PlayerDto {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
score: this.score,
|
score: this.score,
|
||||||
hand: this.hand.map(tile => ({
|
hand: this.hand.map(tile => {
|
||||||
id: tile.id,
|
const d = {
|
||||||
pips: tile.pips,
|
id: tile.id,
|
||||||
flipped: tile.revealed,
|
pips: tile.pips,
|
||||||
})),
|
flipped: tile.revealed,
|
||||||
teamedWith: this.teamedWith?.id,
|
};
|
||||||
|
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 { Tile } from "../Tile";
|
||||||
import { Board } from "../Board";
|
import { Board } from "../Board";
|
||||||
import { GameState } from "../../dto/GameState";
|
import { GameState } from "../../dto/GameState";
|
||||||
import { PlayerState } from "../../dto/PlayerState";
|
import { PlayerDto } from "../../dto/PlayerDto";
|
||||||
import { GameSessionState } from "../../dto/GameSessionState";
|
import { MatchSessionState } from "../../dto/MatchSessionState";
|
||||||
import { SocketDisconnectedError } from "../../../common/exceptions/SocketDisconnectedError";
|
import { SocketDisconnectedError } from "../../../common/exceptions/SocketDisconnectedError";
|
||||||
|
|
||||||
export class NetworkPlayer extends PlayerHuman {
|
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);
|
const response = await this.clientNotifier.notifyPlayer(this, 'playerState', state);
|
||||||
console.log('player state notified :>> ', response);
|
console.log('player state notified :>> ', response);
|
||||||
if (response === undefined || response.status !== 'ok' ) {
|
if (response === undefined || response.status !== 'ok' ) {
|
||||||
@ -35,14 +35,25 @@ export class NetworkPlayer extends PlayerHuman {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async notifySessionState(state: GameSessionState): Promise<void> {
|
async notifyMatchState(state: MatchSessionState): Promise<void> {
|
||||||
const response = await this.clientNotifier.notifyPlayer(this, 'sessionState', state);
|
const response = await this.clientNotifier.notifyPlayer(this, 'matchState', state);
|
||||||
console.log('session state notified :>> ', response);
|
console.log('session state notified :>> ', response);
|
||||||
if (response === undefined || response.status !== 'ok' ) {
|
if (response === undefined || response.status !== 'ok' ) {
|
||||||
throw new SocketDisconnectedError();
|
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> {
|
async chooseTile(board: Board): Promise<Tile> {
|
||||||
return await this.playerInteraction.chooseTile(board);
|
return await this.playerInteraction.chooseTile(board);
|
||||||
|
@ -2,9 +2,9 @@ import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
|||||||
import { Board } from "../Board";
|
import { Board } from "../Board";
|
||||||
import { GameState } from "../../dto/GameState";
|
import { GameState } from "../../dto/GameState";
|
||||||
import { PlayerMove } from "../PlayerMove";
|
import { PlayerMove } from "../PlayerMove";
|
||||||
import { PlayerState } from "../../dto/PlayerState";
|
|
||||||
import { Tile } from "../Tile";
|
import { Tile } from "../Tile";
|
||||||
import { GameSessionState } from "../../dto/GameSessionState";
|
import { MatchSessionState } from "../../dto/MatchSessionState";
|
||||||
|
import { PlayerDto } from "../../dto/PlayerDto";
|
||||||
|
|
||||||
export interface PlayerInterface {
|
export interface PlayerInterface {
|
||||||
id: string;
|
id: string;
|
||||||
@ -13,12 +13,15 @@ export interface PlayerInterface {
|
|||||||
hand: Tile[];
|
hand: Tile[];
|
||||||
teamedWith: PlayerInterface | null;
|
teamedWith: PlayerInterface | null;
|
||||||
playerInteraction: PlayerInteractionInterface;
|
playerInteraction: PlayerInteractionInterface;
|
||||||
|
ready: boolean;
|
||||||
|
|
||||||
makeMove(gameState: Board): Promise<PlayerMove | null>;
|
makeMove(gameState: Board): Promise<PlayerMove | null>;
|
||||||
chooseTile(board: Board): Promise<Tile>;
|
chooseTile(board: Board): Promise<Tile>;
|
||||||
pipsCount(): number;
|
pipsCount(): number;
|
||||||
notifyGameState(state: GameState): Promise<void>;
|
notifyGameState(state: GameState): Promise<void>;
|
||||||
notifyPlayerState(state: PlayerState): Promise<void>;
|
notifyPlayerState(state: PlayerDto): Promise<void>;
|
||||||
notifySessionState(state: GameSessionState): Promise<void>;
|
notifyMatchState(state: MatchSessionState): Promise<void>;
|
||||||
getState(): PlayerState;
|
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 { NetworkClientNotifier } from '../game/NetworkClientNotifier';
|
||||||
import { SocketIoService } from './services/SocketIoService';
|
import { SocketIoService } from './services/SocketIoService';
|
||||||
|
import { LoggingService } from '../common/LoggingService';
|
||||||
|
import { useRouter } from './router';
|
||||||
|
|
||||||
const clientNotifier = new NetworkClientNotifier();
|
const clientNotifier = new NetworkClientNotifier();
|
||||||
|
const logger = new LoggingService();
|
||||||
const app = express();
|
const app = express();
|
||||||
const httpServer = http.createServer(app);
|
const httpServer = http.createServer(app);
|
||||||
const socketIoService = new SocketIoService(httpServer);
|
const socketIoService = new SocketIoService(httpServer);
|
||||||
clientNotifier.setSocket(socketIoService.getServer());
|
clientNotifier.setSocket(socketIoService.getServer());
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
console.log('__dirname :>> ', __dirname);
|
|
||||||
|
|
||||||
app.use(cors());
|
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) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile(join(__dirname, 'index.html'));
|
res.sendFile(join(__dirname, 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
httpServer.listen(PORT, () => {
|
httpServer.listen(PORT, () => {
|
||||||
console.log(`listening on *:${PORT}`);
|
logger.info(`listening on *:${PORT}`);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { LoggingService } from "../../common/LoggingService";
|
import { LoggingService } from "../../common/LoggingService";
|
||||||
|
|
||||||
export class ControllerBase {
|
export class ManagerBase {
|
||||||
protected logger = new LoggingService();
|
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 { MatchSession } from "../../game/MatchSession";
|
||||||
import { GameSession } from "../../game/GameSession";
|
|
||||||
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
|
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 static sessions: any = {};
|
||||||
|
private sessionService: SessionService = new SessionService();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -15,8 +16,9 @@ export class SessionController extends ControllerBase{
|
|||||||
createSession(data: any, socketId: string): any {
|
createSession(data: any, socketId: string): any {
|
||||||
const { user, sessionName } = data;
|
const { user, sessionName } = data;
|
||||||
const player = new NetworkPlayer(user, socketId);
|
const player = new NetworkPlayer(user, socketId);
|
||||||
const session = new GameSession(player, sessionName);
|
const session = new MatchSession(player, sessionName);
|
||||||
SessionController.sessions[session.id] = session;
|
SessionManager.sessions[session.id] = session;
|
||||||
|
this.sessionService.createSession(session);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
@ -29,9 +31,10 @@ export class SessionController extends ControllerBase{
|
|||||||
this.logger.debug('joinSession data :>> ')
|
this.logger.debug('joinSession data :>> ')
|
||||||
this.logger.object(data);
|
this.logger.object(data);
|
||||||
const { user, sessionId } = data;
|
const { user, sessionId } = data;
|
||||||
const session = SessionController.sessions[sessionId];
|
const session: MatchSession = SessionManager.sessions[sessionId];
|
||||||
const player = new NetworkPlayer(user, socketId);
|
const player = new NetworkPlayer(user, socketId);
|
||||||
session.addPlayer(player);
|
session.addPlayer(player);
|
||||||
|
this.sessionService.updateSession(session);
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
sessionId: session.id,
|
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 {
|
startSession(data: any): any {
|
||||||
const sessionId: string = data.sessionId;
|
const sessionId: string = data.sessionId;
|
||||||
const seed: string | undefined = data.seed;
|
const seed: string | undefined = data.seed;
|
||||||
const session = SessionController.sessions[sessionId];
|
const session = SessionManager.sessions[sessionId];
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return ({
|
return ({
|
||||||
@ -68,10 +77,10 @@ export class SessionController extends ControllerBase{
|
|||||||
|
|
||||||
|
|
||||||
getSession(id: string) {
|
getSession(id: string) {
|
||||||
return SessionController.sessions[id];
|
return SessionManager.sessions[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSession(id: string) {
|
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 { Server as HttpServer } from "http";
|
||||||
import { ServiceBase } from "./ServiceBase";
|
import { ServiceBase } from "./ServiceBase";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { SessionController } from "../controllers/SessionController";
|
import { SessionManager } from "../managers/SessionManager";
|
||||||
|
|
||||||
export class SocketIoService extends ServiceBase{
|
export class SocketIoService extends ServiceBase{
|
||||||
io: Server
|
io: Server
|
||||||
|
clients: Map<string, any> = new Map();
|
||||||
|
|
||||||
constructor(private httpServer: HttpServer) {
|
constructor(private httpServer: HttpServer) {
|
||||||
super()
|
super()
|
||||||
this.io = this.socketIo(httpServer);
|
this.io = this.socketIo(httpServer);
|
||||||
@ -16,23 +18,30 @@ export class SocketIoService extends ServiceBase{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private initListeners() {
|
private initListeners() {
|
||||||
const sessionController = new SessionController();
|
const sessionController = new SessionManager();
|
||||||
this.io.on('connection', (socket) => {
|
this.io.on('connection', (socket) => {
|
||||||
console.log(`connect ${socket.id}`);
|
this.logger.debug(`connect ${socket.id}`);
|
||||||
if (socket.recovered) {
|
if (socket.recovered) {
|
||||||
// recovery was successful: socket.id, socket.rooms and socket.data were restored
|
// recovery was successful: socket.id, socket.rooms and socket.data were restored
|
||||||
console.log("recovered!");
|
this.logger.debug("recovered!");
|
||||||
console.log("socket.rooms:", socket.rooms);
|
this.logger.debug("socket.rooms:", socket.rooms);
|
||||||
console.log("socket.data:", socket.data);
|
this.logger.debug("socket.data:", socket.data);
|
||||||
} else {
|
} else {
|
||||||
console.log("new connection");
|
this.logger.debug("new connection");
|
||||||
|
this.clients.set(socket.id, { alive: true });
|
||||||
socket.join('room-general')
|
socket.join('room-general')
|
||||||
socket.data.foo = "bar";
|
socket.data.foo = "bar";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
socket.on('pong', () => {
|
||||||
|
if (this.clients.has(socket.id)) {
|
||||||
|
this.clients.set(socket.id, { alive: true });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
console.log('user disconnected');
|
this.logger.debug('user disconnected');
|
||||||
|
this.clients.delete(socket.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('createSession', (data, callback) => {
|
socket.on('createSession', (data, callback) => {
|
||||||
@ -50,16 +59,30 @@ export class SocketIoService extends ServiceBase{
|
|||||||
callback(response);
|
callback(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
// socket.on('chat message', (msg, callback) => {
|
socket.on('playerReady', (data, callback) => {
|
||||||
// io.emit('chat message', msg);
|
const response = sessionController.setPlayerReady(data);
|
||||||
// callback({
|
callback(response);
|
||||||
// status: 'ok',
|
});
|
||||||
// message: 'Message received',
|
|
||||||
// })
|
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 {
|
private socketIo(httpServer: HttpServer): Server {
|
||||||
return new Server(httpServer, {
|
return new Server(httpServer, {
|
||||||
cors: {
|
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. */
|
// "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. */
|
// "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. */
|
// "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. */
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user