Compare commits

...

4 Commits
0.1.3 ... main

Author SHA1 Message Date
Jose Conde
2346acc096 update 2024-07-25 16:55:36 +02:00
Jose Conde
8fb6fac75d fix 2024-07-25 16:20:22 +02:00
Jose Conde
f52bea91ac adding asocket.io room management 2024-07-24 22:38:34 +02:00
Jose Conde
0329e84548 v.0.1.4 2024-07-20 15:02:48 +02:00
27 changed files with 250 additions and 179 deletions

8
.hmrc
View File

@ -2,7 +2,7 @@
"path": "G:\\Other\\Development\\Projects\\[ideas]\\domino", "path": "G:\\Other\\Development\\Projects\\[ideas]\\domino",
"name": "domino-server", "name": "domino-server",
"initialVersion": "0.1.1", "initialVersion": "0.1.1",
"version": "0.1.3", "version": "0.1.5",
"docker": { "docker": {
"useRegistry": true, "useRegistry": true,
"registry": "192.168.1.115:5000", "registry": "192.168.1.115:5000",
@ -75,7 +75,7 @@
}, },
"_backup": { "_backup": {
"name": "domino-server", "name": "domino-server",
"version": "0.1.2", "version": "0.1.4",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
@ -88,8 +88,8 @@
"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 192.168.1.115:5000/arhuako/domino-server:latest .", "docker-build": "docker build -t 192.168.1.115:5000/arhuako/domino-server:latest .",
"docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-server:latest 192.168.1.115:5000/arhuako/domino-server:0.1.2", "docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-server:latest 192.168.1.115:5000/arhuako/domino-server:0.1.4",
"docker-push": "docker push 192.168.1.115:5000/arhuako/domino-server:latest && docker push 192.168.1.115:5000/arhuako/domino-server:0.1.2", "docker-push": "docker push 192.168.1.115:5000/arhuako/domino-server:latest && docker push 192.168.1.115:5000/arhuako/domino-server:0.1.4",
"publish": "npm run docker-build && npm run docker-tag && npm run docker-push" "publish": "npm run docker-build && npm run docker-tag && npm run docker-push"
}, },
"keywords": [], "keywords": [],

View File

@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file.
## Unreleased ## Unreleased
## 0.1.5 - 2024-07-25
### Added
- socket.io room management
### Fixed
- Blocked game not waiting for all turns to complete
## 0.1.4 - 2024-07-20
## 0.1.3 - 2024-07-18 ## 0.1.3 - 2024-07-18
### Added ### Added
- game by rounds - game by rounds

View File

@ -1,6 +1,6 @@
{ {
"name": "domino-server", "name": "domino-server",
"version": "0.1.3", "version": "0.1.5",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
@ -13,8 +13,8 @@
"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 192.168.1.115:5000/arhuako/domino-server:latest .", "docker-build": "docker build -t 192.168.1.115:5000/arhuako/domino-server:latest .",
"docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-server:latest 192.168.1.115:5000/arhuako/domino-server:0.1.3", "docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-server:latest 192.168.1.115:5000/arhuako/domino-server:0.1.5",
"docker-push": "docker push 192.168.1.115:5000/arhuako/domino-server:latest && docker push 192.168.1.115:5000/arhuako/domino-server:0.1.3", "docker-push": "docker push 192.168.1.115:5000/arhuako/domino-server:latest && docker push 192.168.1.115:5000/arhuako/domino-server:0.1.5",
"publish": "npm run docker-build && npm run docker-tag && npm run docker-push" "publish": "npm run docker-build && npm run docker-tag && npm run docker-push"
}, },
"keywords": [], "keywords": [],

View File

@ -1,5 +1,11 @@
<h1>Changelog</h1> <h1>Changelog</h1>
<p>All notable changes to this project will be documented in this file.</p> <p>All notable changes to this project will be documented in this file.</p>
<h2>0.1.5 - 2024-07-24</h2>
<h3>Added</h3>
<ul>
<li>socket.io room management</li>
</ul>
<h2>0.1.4 - 2024-07-20</h2>
<h2>0.1.3 - 2024-07-18</h2> <h2>0.1.3 - 2024-07-18</h2>
<h3>Added</h3> <h3>Added</h3>
<ul> <ul>

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Domino Tiles</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="domino" data-top="1" data-bottom="5">
<div class="half top"></div>
<div class="half bottom"></div>
</div>
<div class="domino" data-top="1" data-bottom="5">
<div class="half top"></div>
<div class="half bottom"></div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@ -1,35 +0,0 @@
document.querySelectorAll('.domino').forEach(domino => {
const topValue = domino.getAttribute('data-top');
const bottomValue = domino.getAttribute('data-bottom');
addPips(domino.querySelector('.top'), topValue);
addPips(domino.querySelector('.bottom'), bottomValue);
});
function addPips(half, value) {
const pipPositions = getPipPositions(value);
pipPositions.forEach(pos => {
const pip = document.createElement('div');
pip.className = 'pip';
pip.style.left = pos[0];
pip.style.top = pos[1];
half.appendChild(pip);
});
}
function getPipPositions(value) {
const p1 = '17%';
const p2 = '42%';
const p3 = '67%';
const positions = {
0: [],
1: [[p2, p2]],
2: [[p1, p1], [p3, p3]],
3: [[p1, p1], [p2, p2], [p3, p3]],
4: [[p1, p1], [p1, p3], [p3, p1], [p3, p3]],
5: [[p1, p1], [p1, p3], [p2, p2], [p3, p1], [p3, p3]],
6: [[p1, p1], [p1, p3], [p2, p1], [p2, p3], [p3, p1], [p3, p3]],
};
return positions[value];
}

View File

@ -1,77 +0,0 @@
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f0f0f0;
font-family: Arial, sans-serif;
}
.domino {
width: 100px;
height: 200px;
background: white;
border: 2px solid black;
border-radius: 10px;
position: relative;
display: flex;
flex-direction: column;
}
.half {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.top {
border-bottom: 2px solid black;
}
.pip {
width: 15px;
height: 15px;
background: black;
border-radius: 50%;
position: absolute;
}
.pip[data-number="1"] {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.pip[data-number="2"] {
left: 25%;
top: 25%;
}
.pip[data-number="3"] {
right: 25%;
top: 25%;
}
.pip[data-number="4"] {
left: 25%;
bottom: 25%;
}
.pip[data-number="5"] {
right: 25%;
bottom: 25%;
}
.pip[data-number="6"] {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.pip[data-number="7"] {
right: 50%;
top: 50%;
transform: translate(50%, -50%);
}

View File

@ -97,7 +97,7 @@ export class DominoesGame extends EventEmitter {
tiles.push(...player.hand); tiles.push(...player.hand);
} }
const canPlay = tiles.some(tile => tile.pips[0] === freeEnds[0] || tile.pips[1] === freeEnds[0] || tile.pips[0] === freeEnds[1] || tile.pips[1] === freeEnds[1]); const canPlay = tiles.some(tile => tile.pips[0] === freeEnds[0] || tile.pips[1] === freeEnds[0] || tile.pips[0] === freeEnds[1] || tile.pips[1] === freeEnds[1]);
return !canPlay; return this.blockedCount >= 4 && !canPlay;
} }
isGameOver(): boolean { isGameOver(): boolean {
@ -144,7 +144,7 @@ export class DominoesGame extends EventEmitter {
playTurn() { playTurn() {
try { try {
const player = this.players[this.currentPlayerIndex]; const player = this.players[this.currentPlayerIndex];
this.notificationService.sendEventToPlayers('server:next-turn', this.players, this.getGameState()); this.notificationService.sendEventToPlayers('server:next-turn', this.players, this.getState());
this.logger.debug(`${player.name}'s turn (${player.hand.length} tiles)`); this.logger.debug(`${player.name}'s turn (${player.hand.length} tiles)`);
this.printPlayerHand(player); this.printPlayerHand(player);
printBoard(this.board) printBoard(this.board)
@ -222,6 +222,7 @@ export class DominoesGame extends EventEmitter {
players: this.players.map(player => player.getState(true)), players: this.players.map(player => player.getState(true)),
board: this.board.tiles.map(tile => tile.getState(true)), board: this.board.tiles.map(tile => tile.getState(true)),
boneyard: this.board.boneyard.map(tile => tile.getState(true)), boneyard: this.board.boneyard.map(tile => tile.getState(true)),
movements: this.board.movements
} }
this.emit('game-over', summary); this.emit('game-over', summary);
} }
@ -264,7 +265,7 @@ export class DominoesGame extends EventEmitter {
this.deal(); this.deal();
const extractStates = (p: PlayerInterface) => ({ const extractStates = (p: PlayerInterface) => ({
player: p.getState(true), player: p.getState(true),
gameState: this.getGameState() gameState: this.getState()
}); });
await this.notificationService.sendEventToPlayers('server:hand-dealt', this.players, extractStates); await this.notificationService.sendEventToPlayers('server:hand-dealt', this.players, extractStates);
@ -325,11 +326,14 @@ export class DominoesGame extends EventEmitter {
} }
} }
getGameState(): GameState { getState(): GameState {
const currentPlayer = this.players[this.currentPlayerIndex] const currentPlayer = this.players[this.currentPlayerIndex]
const lastMove = this.lastMove?.getState() || null;
const movements = this.board.movements.map(move => move.getState());
return { return {
id: uuid(), id: uuid(),
lastMove: this.lastMove, lastMove,
gameInProgress: this.gameInProgress, gameInProgress: this.gameInProgress,
winner: this.winner, winner: this.winner,
tileSelectionPhase: this.tileSelectionPhase, tileSelectionPhase: this.tileSelectionPhase,
@ -337,10 +341,11 @@ export class DominoesGame extends EventEmitter {
gameTied: this.gameTied, gameTied: this.gameTied,
gameId: this.id, gameId: this.id,
boneyard: this.board.boneyard.map(tile => tile.getState(false)), boneyard: this.board.boneyard.map(tile => tile.getState(false)),
players: this.players.map(player => player.getState()), players: this.players.map(player => player.getState(false)),
currentPlayer: currentPlayer.getState(), currentPlayer: currentPlayer.getState(),
board: this.board.tiles.map(tile => tile.getState(true)), board: this.board.tiles.map(tile => tile.getState(true)),
boardFreeEnds: this.board.getFreeEnds(), boardFreeEnds: this.board.getFreeEnds(),
movements
} }
} }

View File

@ -26,6 +26,7 @@ export class MatchSession {
private clientsReady: string[] = []; private clientsReady: string[] = [];
private gameSummaries: GameSummary[] = []; private gameSummaries: GameSummary[] = [];
private sessionService: SessionService = new SessionService(); private sessionService: SessionService = new SessionService();
private room: string;
id: string; id: string;
matchInProgress: boolean = false; matchInProgress: boolean = false;
@ -46,6 +47,7 @@ export class MatchSession {
const { sessionName, seed, winType, winTarget } = options; const { sessionName, seed, winType, winTarget } = options;
this.seed = seed || getRandomSeed(); this.seed = seed || getRandomSeed();
this.id = uuid(); this.id = uuid();
this.room = `room-${this.id}`;
this.name = sessionName || `Match ${this.id}`; this.name = sessionName || `Match ${this.id}`;
this.addPlayerToSession(creator); this.addPlayerToSession(creator);
this.creator = creator; this.creator = creator;
@ -187,7 +189,6 @@ export class MatchSession {
}); });
} else { } else {
this.status = 'waiting' this.status = 'waiting'
// await this.playerNotificationManager.notifyMatchState(this);
this.notificationService.sendEventToPlayers('server:game-finished', this.players, { this.notificationService.sendEventToPlayers('server:game-finished', this.players, {
lastGame: gameSummary, lastGame: gameSummary,
sessionState: this.getState(true) sessionState: this.getState(true)
@ -397,6 +398,7 @@ export class MatchSession {
matchInProgress: this.matchInProgress, matchInProgress: this.matchInProgress,
gameSummaries: this.gameSummaries, gameSummaries: this.gameSummaries,
options: this.options, options: this.options,
room: this.room,
}; };
} }

View File

@ -1,5 +1,4 @@
import { PlayerMove } from "../entities/PlayerMove"; import { PlayerDto, PlayerMoveDto } from "./PlayerDto";
import { PlayerDto } from "./PlayerDto";
export interface GameState { export interface GameState {
id: string; id: string;
@ -14,5 +13,6 @@ export interface GameState {
gameId: string; gameId: string;
tileSelectionPhase: boolean; tileSelectionPhase: boolean;
boardFreeEnds: number[]; boardFreeEnds: number[];
lastMove: PlayerMove | null; lastMove: PlayerMoveDto | null;
movements: PlayerMoveDto[];
} }

View File

@ -1,4 +1,5 @@
import { Score } from "../../server/db/interfaces"; import { Score } from "../../server/db/interfaces";
import { PlayerMove } from "../entities/PlayerMove";
import { PlayerDto, TileDto } from "./PlayerDto"; import { PlayerDto, TileDto } from "./PlayerDto";
export interface GameSummary { export interface GameSummary {
@ -10,4 +11,5 @@ export interface GameSummary {
players?: PlayerDto[]; players?: PlayerDto[];
board: TileDto[] board: TileDto[]
boneyard: TileDto[] boneyard: TileDto[]
movements: PlayerMove[]
} }

View File

@ -10,4 +10,5 @@ export interface MatchSessionOptions {
seed: string seed: string
sessionName: string sessionName: string
numPlayers: 1 | 2 | 3 | 4 numPlayers: 1 | 2 | 3 | 4
turnWaitSeconds: number
} }

View File

@ -1,4 +1,5 @@
import { Score } from "../../server/db/interfaces"; import { Score } from "../../server/db/interfaces";
import { GameState } from "./GameState";
import { GameSummary } from "./GameSummary"; import { GameSummary } from "./GameSummary";
import { MatchSessionOptions } from "./MatchSessionOptions"; import { MatchSessionOptions } from "./MatchSessionOptions";
import { PlayerDto } from "./PlayerDto"; import { PlayerDto } from "./PlayerDto";
@ -22,4 +23,6 @@ export interface MatchSessionState {
playersReady: number, playersReady: number,
gameSummaries: GameSummary[]; gameSummaries: GameSummary[];
options: MatchSessionOptions options: MatchSessionOptions
currentGame?: GameState
room: string
} }

View File

@ -15,3 +15,11 @@ export interface PlayerDto {
ready: boolean; ready: boolean;
isHuman: boolean; isHuman: boolean;
} }
export interface PlayerMoveDto {
id: string;
tile: TileDto
type: string | null;
playerId: string;
direction?: string;
}

View File

@ -9,6 +9,7 @@ export class Board {
tiles: Tile[] = []; tiles: Tile[] = [];
boneyard: Tile[] = []; boneyard: Tile[] = [];
logger = new LoggingService(); logger = new LoggingService();
movements: PlayerMove[] = [];
constructor(private rng: PRNG) {} constructor(private rng: PRNG) {}
@ -64,6 +65,7 @@ export class Board {
this.playTileRight(tile); this.playTileRight(tile);
// printLine(`${tile} -- right`); // printLine(`${tile} -- right`);
} }
this.movements.push(playerMove);
} }
playTileLeft(tile: Tile) { playTileLeft(tile: Tile) {

View File

@ -1,5 +1,6 @@
import { uuid } from "../../common/utilities"; import { uuid } from "../../common/utilities";
import { PlayerMoveSideType } from "../constants"; import { PlayerMoveSideType } from "../constants";
import { PlayerMoveDto } from "../dto/PlayerDto";
import { Tile } from "./Tile"; import { Tile } from "./Tile";
export class PlayerMove { export class PlayerMove {
@ -9,4 +10,14 @@ export class PlayerMove {
toString() { toString() {
return `PlayerMove:([${this.tile.pips[0]}|${this.tile.pips[1]}] ${this.type})`; return `PlayerMove:([${this.tile.pips[0]}|${this.tile.pips[1]}] ${this.type})`;
} }
getState(): PlayerMoveDto {
return {
id: this.id,
tile: this.tile.getState(),
type: this.type,
playerId: this.playerId,
direction: this.direction,
};
}
} }

View File

@ -6,42 +6,83 @@ import { TemporalTokenMongoManager } from '../db/mongo/TemporalTokenMongoManager
import { BaseController } from './BaseController'; import { BaseController } from './BaseController';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { AuthenticationOption, Token, User } from '../db/interfaces'; import { AuthenticationOption, Token, User } from '../db/interfaces';
import { UserSessionMongoManager } from '../db/mongo/UserSessionMongoManager';
export class AuthController extends BaseController { export class AuthController extends BaseController {
security = new SecurityManager(); security = new SecurityManager();
usersManager = new UsersMongoManager(); usersManager = new UsersMongoManager();
temporalTokenManager = new TemporalTokenMongoManager(); temporalTokenManager = new TemporalTokenMongoManager();
userSessionManager = new UserSessionMongoManager();
async login(req: Request, res: Response): Promise<void> { async login(req: Request, res: Response): Promise<void> {
const { log } = req; const { log } = req;
try { try {
let token = null
const { username, password } = req.body; const { username, password } = req.body;
this.logger.debug('login', username, password);
const { valid: isValidPassword, user } = await this._checkPassword(username, password); const { valid: isValidPassword, user } = await this._checkPassword(username, password);
this.logger.debug('isValidPassword', isValidPassword); if (!isValidPassword || user === null) {
if (!isValidPassword) {
res.status(401).json({ error: 'Unauthorized' }).end(); res.status(401).json({ error: 'Unauthorized' }).end();
log.error('Unauthorized login attempt for user: ', username); log.error('Unauthorized login attempt for user: ', username);
return; return;
} }
this._jwtSignUser(user, res) delete user.hash;
let sessionId = await this.userSessionManager.renewSession(user.id);
const token = await this.security.signJwt({ sessionId, user}, false);
if (token === null) {
res.status(401).json({ error: 'Unauthorized' }).end();
return;
}
const refreshToken = await this.security.signJwt({ sessionId, user}, true);
res.status(200).json({ token, refreshToken }).end();
} catch (error) { } catch (error) {
this.handleError(res, error); this.handleError(res, error);
} }
} }
_jwtSignUser(user: User | null, res: Response) { async refresh(req: Request, res: Response): Promise<void> {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
res.status(401).json({ error: 'Unauthorized' }).end();
return;
}
const data = await this.security.verifyJwt(refreshToken);
const { uuid: sessionId } = await this.userSessionManager.getByUuid(data.sessionId) || { uuid: undefined };
if (!data.sessionId || !data.user || data.sessionId !== sessionId) {
res.status(401).json({ error: 'Unauthorized' }).end();
return;
}
const newToken = await this.security.signJwt(data, false);
res.status(200).json({ token: newToken }).end();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' }).end();
}
}
async _jwtSignUser(user: User | null, res: Response, isFromRefresh: boolean = false) {
if (user === null) { if (user === null) {
res.status(401).json({ error: 'Unauthorized' }).end(); res.status(401).json({ error: 'Unauthorized' }).end();
return; return;
} }
delete user.hash; delete user.hash;
const token = this.security.signJwt(user); let uuid;
if (!isFromRefresh) {
uuid = await this.userSessionManager.renewSession(user.id);
} else {
const userSession = await this.userSessionManager.getUserSession(user.id);
uuid = userSession?.uuid;
}
const token = this.security.signJwt({ sessionId: uuid, user}, false);
if (token === null) { if (token === null) {
res.status(401).json({ error: 'Unauthorized' }).end(); res.status(401).json({ error: 'Unauthorized' }).end();
} else { } else {
res.status(200).json({ token }).end(); const data: {
token: string,
refreshToken?: string
} = { token };
if (!isFromRefresh) {
data.refreshToken = this.security.signJwt({ sessionId: uuid, user}, true);
}
res.status(200).json(data).end();
} }
return; return;
} }
@ -159,21 +200,27 @@ export class AuthController extends BaseController {
static authenticate(options: AuthenticationOption = {}) { static authenticate(options: AuthenticationOption = {}) {
return async function(req: Request, res: Response, next: NextFunction) { return async function(req: Request, res: Response, next: NextFunction) {
const security = new SecurityManager(); const security = new SecurityManager();
const userSessionManager = new UserSessionMongoManager();
const token = req.headers.authorization; const token = req.headers.authorization;
const { roles = [] } = options; const { roles = [] } = options;
if (!token) { if (!token) {
return res.status(401).json({ error: 'Unauthorized' }); return res.status(401).json({ error: 'Unauthorized' });
} }
try { try {
const user: User = await security.verifyJwt(token); const parsed = await security.verifyJwt(token);
const user: User = parsed.user;
const userSession = await userSessionManager.getUserSession(user.id);
const validRoles = await AuthController.checkRoles(user, roles); const validRoles = await AuthController.checkRoles(user, roles);
if (!validRoles) { if (!validRoles) {
return res.status(403).json({ error: 'Forbidden' }); return res.status(403).json({ error: 'Forbidden' });
} }
if (userSession?.uuid !== parsed.sessionId) {
return res.status(401).json({ error: 'Unauthorized' });
}
req.user = user; req.user = user;
next(); next();
} catch (error) { } catch (error) {
return res.status(403).json({ error: 'Forbidden' }); return res.status(401).json({ error: 'Token expired' });
} }
} }
} }
@ -194,7 +241,7 @@ export class AuthController extends BaseController {
req.token = apiToken; req.token = apiToken;
next(); next();
} catch (error) { } catch (error) {
return res.status(403).json({ error: 'Forbidden' }); return res.status(401).json({ error: 'Unauthorized' });
} }
} }
} }

View File

@ -0,0 +1,50 @@
import { Request, Response } from "express";
import { BaseController } from "./BaseController";
const json = {
"version": "0.2.0",
"notes": `- v0.2.0
Added: session expiration and renew handling
Added: Allow unique session per user
Added: Socket.io room management
Added: Game: added turn waiting timer
Added: Game: added timed button on game summaries
Added: Added new header
Added: New sound library (Howlerjs)
Fixed: pp data persistance
Fixed: Some other Fixes
- v0.1.12
Added: I18n translations
Added: Win conditions
Fixed: Multiplayer join button not accessible
- v0.1.8
Added: Updater
Added: Refresh authentication when expires
Added: Match summary page phase 1`,
"pub_date": "2024-07-20T10:25:57Z",
"platforms": {
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTdDh5VEM1Y1hnUUEzRXpPWDhLQ0JDSjU0UDhwdnFqLzQ0NEV1RFRudSsyS3AwcmdpbDd2SmVkV1VwdmNXWVdnbkF2S3ZPMWZKTDRzdHZ0djVjZWNENk51TGlWbjRsWXdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzIxOTE4NDU2CWZpbGU6ZG9taW5vLWNsaWVudF8wLjIuMF94NjQtc2V0dXAubnNpcy56aXAKTnpuMkJueXBJaGVEYkpMS2phMm5ybkp5aHRKVUtLbHJYaVVrYXRXMXg1R1owQjN2VzRVVmlOMnJMWUlIMzBCSE5WeHpIYUY5dmVzTG5SdDEyNGNPQXc9PQo=",
"url": "https://domserv.xintanalabs.net/updates/domino-client_0.2.0_x64-setup.nsis.zip"
}
}
}
export class UpdaterController extends BaseController {
async checkUpdate(req: Request, res: Response): Promise<any> {
this.logger.info('Checking for updates');
return res.json(json).status(200).end();
// return res.status(204).end();
}
// async startMatchSession(data: any): Promise<any> {
// const response = await this.sessionManager.startSession(data);
// return response;
// }
// async joinMatchSession(data: any, socketId: string): Promise<any> {
// const response = await this.sessionManager.joinSession(data, socketId);
// return response;
// }
}

View File

@ -18,6 +18,7 @@ export function matchSessionAdapter(session: MatchSession, showPips: boolean = f
status: state.status, status: state.status,
matchInProgress: state.matchInProgress, matchInProgress: state.matchInProgress,
gameSummaries: state.gameSummaries, gameSummaries: state.gameSummaries,
room: state.room
} }
} }

View File

@ -61,6 +61,7 @@ export interface DbMatchSession extends EntityMongo {
status: string; status: string;
matchInProgress: boolean; matchInProgress: boolean;
gameSummaries: GameSummary[]; gameSummaries: GameSummary[];
room: string;
} }
export interface DbUser extends EntityMongo { export interface DbUser extends EntityMongo {
@ -116,3 +117,8 @@ export interface DbListResponse{
sort?: any; sort?: any;
data: EntityMongo[]; data: EntityMongo[];
} }
export interface UserSession extends EntityMongo {
userId: string;
uuid: string;
}

View File

@ -0,0 +1,37 @@
import { uuid } from "../../../common/utilities";
import { UserSession } from "../interfaces";
import { BaseMongoManager } from "./common/BaseMongoManager";
export class UserSessionMongoManager extends BaseMongoManager {
collection = 'userSessions';
constructor() {
super();
}
async createSession(userId: string): Promise<string> {
const uid: string = uuid();
const userSession: UserSession = {
userId,
uuid: uid
}
await this.create(userSession);
return uid;
}
async removeSession(userId: string): Promise<number> {
return this.deleteByFilter({ userId });
}
async renewSession(userId: string): Promise<string> {
await this.removeSession(userId);
return this.createSession(userId);
}
async getUserSession(userId: string): Promise<UserSession | null> {
return this.getByFilter({ userId }) as Promise<UserSession | null>;
}
async getByUuid(uuid: string): Promise<UserSession | null> {
return this.getByFilter({ uuid }) as Promise<UserSession | null>;
}
}

View File

@ -24,6 +24,7 @@ app.use(express.text());
app.use(express.urlencoded({extended: true })); app.use(express.urlencoded({extended: true }));
app.use(useRouter()) app.use(useRouter())
app.use(express.static(join(process.cwd(), 'public')));
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html')); res.sendFile(join(__dirname, 'index.html'));

View File

@ -2,8 +2,9 @@ import crypto from 'crypto';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { User } from '../db/interfaces'; import { User } from '../db/interfaces';
import { ManagerBase } from './ManagerBase';
export class SecurityManager { export class SecurityManager extends ManagerBase {
saltRounds = Number(process.env.SALT_ROUNDS); saltRounds = Number(process.env.SALT_ROUNDS);
jwtSecretKey = process.env.JWT_SECRET_KEY || ''; jwtSecretKey = process.env.JWT_SECRET_KEY || '';
@ -20,18 +21,21 @@ export class SecurityManager {
return crypto.randomBytes(32).toString('hex'); return crypto.randomBytes(32).toString('hex');
} }
signJwt(data: any) { signJwt(data: any, longTerm: boolean = false): string {
return jwt.sign(data, this.jwtSecretKey, { expiresIn: '3h' }); const expiresIn: string = longTerm ? '7d' : '3h'
delete data.iat;
delete data.exp;
return jwt.sign(data, this.jwtSecretKey, { expiresIn });
} }
// TODO: verificar esto // TODO: verificar esto
async verifyJwt(token: string): Promise<User> { async verifyJwt(token: string): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
jwt.verify(token, this.jwtSecretKey, (err, decoded) => { jwt.verify(token, this.jwtSecretKey, (err, decoded) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
resolve(decoded as User); resolve(decoded);
} }
}); });
}); });

View File

@ -1,5 +1,6 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { AuthController } from '../controllers/AuthController'; import { AuthController } from '../controllers/AuthController';
import { UpdaterController } from '../controllers/UpdaterController';
import adminRouter from './adminRouter'; import adminRouter from './adminRouter';
import userRouter from './userRouter'; import userRouter from './userRouter';
@ -8,13 +9,19 @@ import gameRouter from './gameRouter';
export default function(): Router { export default function(): Router {
const router = Router(); const router = Router();
const authController = new AuthController(); const authController = new AuthController();
const updaterController = new UpdaterController();
router.get('/version', async function(req: Request, res: Response){ router.get('/version', async function(req: Request, res: Response){
res.send('1.0.0').end(); res.json({
app: 'domino',
version: '0.1.4-test',
}).end();
}); });
router.post('/auth/code', (req: Request, res: Response) => authController.twoFactorCodeAuthentication(req, res)); router.post('/auth/code', (req: Request, res: Response) => authController.twoFactorCodeAuthentication(req, res));
router.post('/login', (req: Request, res: Response) => authController.login(req, res)); router.post('/login', (req: Request, res: Response) => authController.login(req, res));
router.post('/refresh', (req: Request, res: Response) => authController.refresh(req, res));
router.get('/updater/:target/:arch/:currentVersion', (req: Request, res: Response) => updaterController.checkUpdate(req, res));
router.use('/admin', adminRouter()); router.use('/admin', adminRouter());
router.use('/user', userRouter()); router.use('/user', userRouter());

View File

@ -97,7 +97,6 @@ export class InteractionService extends ServiceBase{
}; };
} }
} }
private onPlayerReady(data: any): any { private onPlayerReady(data: any): any {
const { userId, sessionId } = data; const { userId, sessionId } = data;
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId); const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);

View File

@ -10,23 +10,23 @@ export class PlayerNotificationService extends ServiceBase {
clientNotifier: NetworkClientNotifier = new NetworkClientNotifier(); clientNotifier: NetworkClientNotifier = new NetworkClientNotifier();
notifyGameState(game: DominoesGame) { notifyGameState(game: DominoesGame) {
const gameState: GameState = game.getGameState(); const gameState: GameState = game.getState();
const { players } = game; const { players } = game;
players.map(player => player.sendEvent('update-game-state', gameState)); players.filter(p => p instanceof NetworkPlayer).map(player => player.sendEvent('update-game-state', gameState));
} }
notifyPlayersState(players: PlayerInterface[]) { notifyPlayersState(players: PlayerInterface[]) {
players.map(player => player.sendEvent('update-player-state', player.getState())); players.filter(p => p instanceof NetworkPlayer).map(player => player.sendEvent('update-player-state', player.getState()));
} }
notifyMatchState(session: MatchSession) { notifyMatchState(session: MatchSession) {
const { players } = session; const { players } = session;
players.map(player => player.sendEvent('update-match-session-state', session.getState())); players.filter(p => p instanceof NetworkPlayer).forEach(player => player.sendEvent('update-match-session-state', session.getState()));
} }
async sendEventToPlayers(event: string, players: PlayerInterface[], data: Function | any = {}) { async sendEventToPlayers(event: string, players: PlayerInterface[], data: Function | any = {}) {
players.forEach((player) => { players.filter(p => p instanceof NetworkPlayer).forEach((player) => {
let dataTosend = data; let dataTosend = data;
if (typeof data === 'function') { if (typeof data === 'function') {
dataTosend = data(player); dataTosend = data(player);
@ -37,6 +37,8 @@ export class PlayerNotificationService extends ServiceBase {
sendEvent(event: string, player: PlayerInterface, data: any = {}) { sendEvent(event: string, player: PlayerInterface, data: any = {}) {
this.logger.debug(`Sending event '${event}' to player ${player.id}`); this.logger.debug(`Sending event '${event}' to player ${player.id}`);
if (player instanceof NetworkPlayer) {
this.clientNotifier.sendEvent(player as NetworkPlayer, event, data); this.clientNotifier.sendEvent(player as NetworkPlayer, event, data);
} }
}
} }

View File

@ -2,7 +2,6 @@ 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 { SecurityManager } from "../managers/SecurityManager"; import { SecurityManager } from "../managers/SecurityManager";
import { User } from "../db/interfaces";
import { Socket } from "socket.io"; import { Socket } from "socket.io";
import { InteractionService } from "./InteractionService"; import { InteractionService } from "./InteractionService";
import { ClientEvents } from "../../game/constants"; import { ClientEvents } from "../../game/constants";
@ -24,7 +23,7 @@ export class SocketIoService extends ServiceBase {
if (socket.handshake.auth && socket.handshake.auth.token) { if (socket.handshake.auth && socket.handshake.auth.token) {
const token = socket.handshake.auth.token; const token = socket.handshake.auth.token;
try { try {
const user: User = await this.security.verifyJwt(token); const { user } = await this.security.verifyJwt(token);
socket.user = user; socket.user = user;
next(); next();
} catch (err) { } catch (err) {
@ -62,7 +61,6 @@ export class SocketIoService extends ServiceBase {
const userId = user._id.toString(); const userId = user._id.toString();
if (!SocketIoService.clients.has(userId)) { if (!SocketIoService.clients.has(userId)) {
SocketIoService.clients.set(userId, { socketId, alive: true, user: socket.user }); SocketIoService.clients.set(userId, { socketId, alive: true, user: socket.user });
socket.join('room-general')
} else { } else {
const client = SocketIoService.clients.get(userId); const client = SocketIoService.clients.get(userId);
this.interactionService.updateSocketId(client.sessionId, userId, socketId); this.interactionService.updateSocketId(client.sessionId, userId, socketId);
@ -83,6 +81,11 @@ export class SocketIoService extends ServiceBase {
} }
}); });
socket.on('client:join-room', (room) => {
socket.join(room);
this.logger.debug(`User ${socket.user?.username} joined room ${room}`);
});
socket.on(ClientEvents.CLIENT_EVENT, (data) => { socket.on(ClientEvents.CLIENT_EVENT, (data) => {
this.interactionService.handleClientEvent(data); this.interactionService.handleClientEvent(data);
}); });