feat: game creation and login

This commit is contained in:
2024-12-28 17:38:25 +01:00
parent a3dc01171a
commit c7c778a55b
29 changed files with 1735 additions and 1104 deletions
-33
View File
@@ -1,33 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'warn', // or "error"
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
};
+1 -2
View File
@@ -18,7 +18,6 @@
"console": "internalConsole", "console": "internalConsole",
"outputCapture": "std", "outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart", "internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/.env" },
}
] ]
} }
+23 -1
View File
@@ -1,4 +1,5 @@
services: services:
postgres: postgres:
image: postgres image: postgres
ports: ports:
@@ -7,6 +8,27 @@ services:
- pg_data:/var/lib/postgresql/data - pg_data:/var/lib/postgresql/data
environment: environment:
- POSTGRES_PASSWORD=test - POSTGRES_PASSWORD=test
pgadmin:
image: dpage/pgadmin4
ports:
- 8080:80
environment:
- PGADMIN_DEFAULT_EMAIL=postgres@postgres.com
- PGADMIN_DEFAULT_PASSWORD=postgres
- PGADMIN_DISABLE_POSTFIX=true
volumes:
- pgadmin_data:/var/lib/pgadmin
redis:
image: redis
ports:
- 6379:6379
volumes:
- redis_data:/data
command: "redis-server --save 60 1"
volumes: volumes:
pg_data: pg_data:
pgadmin_data:
redis_data:
+62
View File
@@ -0,0 +1,62 @@
import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin';
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: ['**/.eslintrc.js'],
},
...compat.extends(
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
),
{
plugins: {
'@typescript-eslint': typescriptEslintEslintPlugin,
},
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
parser: tsParser,
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: '/home/legonzaur/Documents/code/concept/back',
},
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
];
+1354 -948
View File
File diff suppressed because it is too large Load Diff
+15 -4
View File
@@ -16,7 +16,11 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json",
"migration:generate": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:generate -d ./src/db/typeorm.config.ts ./src/db/migrations/migration",
"migration:create": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:create -d ./src/db/typeorm.config.ts ./src/db/migrations/migration",
"migration:run": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:run -d ./src/db/typeorm.config.ts",
"migration:revert": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:revert -d ./src/db/typeorm.config.ts"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^10.3.2", "@nestjs/common": "^10.3.2",
@@ -27,14 +31,19 @@
"@nestjs/platform-express": "^10.3.2", "@nestjs/platform-express": "^10.3.2",
"@nestjs/swagger": "^7.3.1", "@nestjs/swagger": "^7.3.1",
"@nestjs/typeorm": "^10.0.2", "@nestjs/typeorm": "^10.0.2",
"connect-mongo": "^5.1.0", "@types/ranjs": "^1.22.7",
"connect-redis": "^8.0.1",
"express-session": "^1.18.0", "express-session": "^1.18.0",
"mongoose-autopopulate": "^1.1.0", "pg": "^8.13.1",
"ranjs": "^1.24.5",
"redis": "^4.7.0",
"reflect-metadata": "^0.2.1", "reflect-metadata": "^0.2.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"typeorm": "^0.3.20" "typeorm": "^0.3.20"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@nestjs/cli": "^10.3.1", "@nestjs/cli": "^10.3.1",
"@nestjs/schematics": "^10.1.0", "@nestjs/schematics": "^10.1.0",
"@nestjs/testing": "^10.3.2", "@nestjs/testing": "^10.3.2",
@@ -47,9 +56,11 @@
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.56.0", "@typescript-eslint/typescript-estree": "^8.18.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"globals": "^15.14.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
+21 -11
View File
@@ -6,23 +6,33 @@ import { GamesModule } from './games/games.module';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/schemas/user.entity'; import { User } from './users/entities/user.entity';
import { Game } from './games/schemas/game.schema'; import { Game } from './games/entities/game.entity';
import { Topic } from './words/schemas/topic.schema'; import { Topic } from './words/entities/topic.entity';
import { Word } from './words/schemas/word.schema'; import { Word } from './words/entities/word.entity';
import { Player } from './games/schemas/player.schema'; import { Player } from './games/entities/player.entity';
import { Concept } from './games/entities/concept.entity';
import { ConceptInTurn } from './games/entities/concept_in_turn.entity';
import { ConfigService } from '@nestjs/config';
const configService = new ConfigService();
@Module({ @Module({
imports: [ imports: [
EventEmitterModule.forRoot(), EventEmitterModule.forRoot(),
ConfigModule.forRoot(), ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
TypeOrmModule.forRoot({ TypeOrmModule.forRoot({
type: 'postgres', type: 'postgres',
url: process.env.PG_ENDPOINT, host: configService.get('PG_HOST'),
password: process.env.PG_PASSWORD, // url: process.env.PG_ENDPOINT,
username: process.env.PG_USER, password: configService.get('PG_PASSWORD'),
synchronize: process.env.DEV_MODE == 'true', username: configService.get('PG_USER'),
entities: [User, Player, Game, Topic, Word], database: 'concept',
synchronize: configService.get('DEV_MODE') == 'true',
entities: [User, Player, Game, Concept, ConceptInTurn, Topic, Word],
}), }),
GamesModule, GamesModule,
UsersModule, UsersModule,
@@ -0,0 +1,103 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Migration1735396726613 implements MigrationInterface {
name = 'Migration1735396726613';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user" ("userId" character varying NOT NULL, "avatar" character varying NOT NULL, "username" character varying NOT NULL, CONSTRAINT "PK_d72ea127f30e21753c9e229891e" PRIMARY KEY ("userId"))`,
);
await queryRunner.query(
`CREATE TABLE "topic" ("id" SERIAL NOT NULL, CONSTRAINT "PK_33aa4ecb4e4f20aa0157ea7ef61" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "word" ("id" SERIAL NOT NULL, "value" character varying NOT NULL, "ownerUserId" character varying, CONSTRAINT "PK_ad026d65e30f80b7056ca31f666" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "concept" ("id" SERIAL NOT NULL, "value" character varying NOT NULL, CONSTRAINT "UQ_3275d953a4dc4dadeef5a8aeed4" UNIQUE ("value"), CONSTRAINT "PK_83c1330866b9866ac2d0ed2b36b" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "concept_in_turn" ("id" SERIAL NOT NULL, "order" integer NOT NULL DEFAULT '0', "subconcept" integer NOT NULL DEFAULT '0', "markers" integer NOT NULL DEFAULT '1', "conceptId" integer, "gameId" integer, CONSTRAINT "PK_c44692f972ab31a6245c8599b4b" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "game" ("id" SERIAL NOT NULL, "seed" character varying NOT NULL, "currentWord" character varying NOT NULL, "started" boolean NOT NULL, "ownerUserId" character varying, CONSTRAINT "PK_352a30652cd352f552fef73dec5" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "player" ("id" SERIAL NOT NULL, "score" integer NOT NULL, "userUserId" character varying, "gameId" integer, CONSTRAINT "PK_65edadc946a7faf4b638d5e8885" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "word_topics_topic" ("wordId" integer NOT NULL, "topicId" integer NOT NULL, CONSTRAINT "PK_cc5d13565912abf7d0e1dcfc3a7" PRIMARY KEY ("wordId", "topicId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_66e5a60dcdb89fc3be327eb139" ON "word_topics_topic" ("wordId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_3346a237bd507b395347a64ddc" ON "word_topics_topic" ("topicId") `,
);
await queryRunner.query(
`ALTER TABLE "word" ADD CONSTRAINT "FK_4472fdeaf534031039106aa04de" FOREIGN KEY ("ownerUserId") REFERENCES "user"("userId") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "concept_in_turn" ADD CONSTRAINT "FK_6dfaa966f86139a62aba3b2114a" FOREIGN KEY ("conceptId") REFERENCES "concept"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "concept_in_turn" ADD CONSTRAINT "FK_f1c8614584bd8afcac818bacfd9" FOREIGN KEY ("gameId") REFERENCES "game"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "game" ADD CONSTRAINT "FK_9b45658e1fea65880fd35c9bacc" FOREIGN KEY ("ownerUserId") REFERENCES "user"("userId") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "player" ADD CONSTRAINT "FK_2b8a672d52e840e6c4e3bf79f58" FOREIGN KEY ("userUserId") REFERENCES "user"("userId") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "player" ADD CONSTRAINT "FK_7dfdd31fcd2b5aa3b08ed15fe8a" FOREIGN KEY ("gameId") REFERENCES "game"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "word_topics_topic" ADD CONSTRAINT "FK_66e5a60dcdb89fc3be327eb139a" FOREIGN KEY ("wordId") REFERENCES "word"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "word_topics_topic" ADD CONSTRAINT "FK_3346a237bd507b395347a64ddcb" FOREIGN KEY ("topicId") REFERENCES "topic"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "word_topics_topic" DROP CONSTRAINT "FK_3346a237bd507b395347a64ddcb"`,
);
await queryRunner.query(
`ALTER TABLE "word_topics_topic" DROP CONSTRAINT "FK_66e5a60dcdb89fc3be327eb139a"`,
);
await queryRunner.query(
`ALTER TABLE "player" DROP CONSTRAINT "FK_7dfdd31fcd2b5aa3b08ed15fe8a"`,
);
await queryRunner.query(
`ALTER TABLE "player" DROP CONSTRAINT "FK_2b8a672d52e840e6c4e3bf79f58"`,
);
await queryRunner.query(
`ALTER TABLE "game" DROP CONSTRAINT "FK_9b45658e1fea65880fd35c9bacc"`,
);
await queryRunner.query(
`ALTER TABLE "concept_in_turn" DROP CONSTRAINT "FK_f1c8614584bd8afcac818bacfd9"`,
);
await queryRunner.query(
`ALTER TABLE "concept_in_turn" DROP CONSTRAINT "FK_6dfaa966f86139a62aba3b2114a"`,
);
await queryRunner.query(
`ALTER TABLE "word" DROP CONSTRAINT "FK_4472fdeaf534031039106aa04de"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_3346a237bd507b395347a64ddc"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_66e5a60dcdb89fc3be327eb139"`,
);
await queryRunner.query(`DROP TABLE "word_topics_topic"`);
await queryRunner.query(`DROP TABLE "player"`);
await queryRunner.query(`DROP TABLE "game"`);
await queryRunner.query(`DROP TABLE "concept_in_turn"`);
await queryRunner.query(`DROP TABLE "concept"`);
await queryRunner.query(`DROP TABLE "word"`);
await queryRunner.query(`DROP TABLE "topic"`);
await queryRunner.query(`DROP TABLE "user"`);
}
}
+25
View File
@@ -0,0 +1,25 @@
import { DataSource } from 'typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
});
const configService = new ConfigService();
const AppDataSource = new DataSource({
type: 'postgres',
host: configService.get('PG_HOST'),
// url: process.env.PG_ENDPOINT,
password: configService.get('PG_PASSWORD'),
username: configService.get('PG_USER'),
database: 'concept',
synchronize: false,
entities: ['**/*.entity.ts'],
migrations: ['src/db/migrations/*-migration.ts'],
migrationsRun: false,
logging: true,
});
export default AppDataSource;
+4 -2
View File
@@ -12,12 +12,13 @@ import {
import { LobbyService } from '../services/lobby.service'; import { LobbyService } from '../services/lobby.service';
import { AuthGuard } from '../guards/auth.guard'; import { AuthGuard } from '../guards/auth.guard';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Game } from '../schemas/game.schema'; import { Game } from '../entities/game.entity';
import { GamePipe, GameStartedPipe } from '../pipe/game.pipe'; import { GamePipe, GameStartedPipe } from '../pipe/game.pipe';
import { Observable, fromEvent, map } from 'rxjs'; import { Observable, fromEvent, map } from 'rxjs';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { GameService } from '../services/games.service'; import { GameService } from '../services/games.service';
import { ConfigService } from '@nestjs/config';
@ApiTags('game') @ApiTags('game')
@Controller('games/:id') @Controller('games/:id')
@@ -26,6 +27,7 @@ export class GamesController {
private readonly lobbyService: LobbyService, private readonly lobbyService: LobbyService,
private readonly gameService: GameService, private readonly gameService: GameService,
private eventEmitter: EventEmitter2, private eventEmitter: EventEmitter2,
private configService: ConfigService,
) {} ) {}
@Post('/start') @Post('/start')
@@ -51,7 +53,7 @@ export class GamesController {
if (game.started) { if (game.started) {
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST); throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
} }
if (game.players.length < 2 && !process.env.DEV_MODE) { if (game.players.length < 2 && !this.configService.get('DEV_MODE')) {
throw new HttpException( throw new HttpException(
'Not enough teams to start the game', 'Not enough teams to start the game',
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
@@ -1,5 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { LobbyController } from './games.lobby.controller'; import { LobbyController } from './lobby.controller';
import { LobbyService } from '../services/lobby.service'; import { LobbyService } from '../services/lobby.service';
describe('GamesController', () => { describe('GamesController', () => {
@@ -8,15 +8,13 @@ import {
Session, Session,
HttpException, HttpException,
HttpStatus, HttpStatus,
Patch,
Body,
Sse, Sse,
} from '@nestjs/common'; } from '@nestjs/common';
import { LobbyService } from '../services/lobby.service'; import { LobbyService } from '../services/lobby.service';
import { AuthGuard } from '../guards/auth.guard'; import { AuthGuard } from '../guards/auth.guard';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { GamePipe } from '../pipe/game.pipe'; import { GamePipe } from '../pipe/game.pipe';
import { Game } from '../schemas/game.schema'; import { Game } from '../entities/game.entity';
import { Observable, fromEvent, map } from 'rxjs'; import { Observable, fromEvent, map } from 'rxjs';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
@@ -43,7 +41,7 @@ export class LobbyController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async create(@Session() session: Record<string, any>) { async create(@Session() session: Record<string, any>) {
const games = await this.lobbyService.findByOwner(session.user); const games = await this.lobbyService.findByOwner(session.user);
if (games) { if (games.length > 0) {
throw new HttpException( throw new HttpException(
'You can only create one game at a time', 'You can only create one game at a time',
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
@@ -8,7 +8,7 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Unique, Unique,
} from 'typeorm'; } from 'typeorm';
import { Player } from './player.schema'; import { Player } from './player.entity';
@Entity() @Entity()
@Unique(['value']) @Unique(['value'])
@@ -8,8 +8,8 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Unique, Unique,
} from 'typeorm'; } from 'typeorm';
import { Concept } from './concept.schema'; import { Concept } from './concept.entity';
import { Game } from './game.schema'; import { Game } from './game.entity';
@Entity() @Entity()
export class ConceptInTurn { export class ConceptInTurn {
@@ -7,9 +7,9 @@ import {
OneToOne, OneToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
} from 'typeorm'; } from 'typeorm';
import { Player } from './player.schema'; import { Player } from './player.entity';
import { ConceptInTurn } from './concept_in_turn.schema'; import { ConceptInTurn } from './concept_in_turn.entity';
import { User } from 'src/users/schemas/user.entity'; import { User } from 'src/users/entities/user.entity';
@Entity() @Entity()
export class Game { export class Game {
@@ -29,10 +29,10 @@ export class Game {
@OneToOne(() => Player) @OneToOne(() => Player)
currentPlayer: Player; currentPlayer: Player;
@Column() @Column({ nullable: true })
currentWord: string; currentWord: string;
@Column() @Column({ default: false })
started: boolean; started: boolean;
@OneToMany(() => ConceptInTurn, (cit) => cit.game) @OneToMany(() => ConceptInTurn, (cit) => cit.game)
@@ -1,4 +1,4 @@
import { User } from 'src/users/schemas/user.entity'; import { User } from 'src/users/entities/user.entity';
import { import {
Column, Column,
Entity, Entity,
@@ -9,20 +9,20 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Unique, Unique,
} from 'typeorm'; } from 'typeorm';
import { Game } from './game.schema'; import { Game } from './game.entity';
@Entity() @Entity()
@Unique(['user, game']) // @Unique(['user, game'])
export class Player { export class Player {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ManyToOne((type) => User) @ManyToOne(() => User)
user: User; user: User;
@ManyToOne((type) => Game) @ManyToOne(() => Game)
game: Promise<Game>; game: Game;
@Column() @Column({ default: 0 })
score: number; score: number;
} }
+8 -3
View File
@@ -1,14 +1,19 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { LobbyService } from './services/lobby.service'; import { LobbyService } from './services/lobby.service';
import { LobbyController } from './controllers/games.lobby.controller'; import { LobbyController } from './controllers/lobby.controller';
import { Game } from './schemas/game.schema'; import { Game } from './entities/game.entity';
import { UsersModule } from 'src/users/users.module'; import { UsersModule } from 'src/users/users.module';
import { GamesController } from './controllers/games.controller'; import { GamesController } from './controllers/games.controller';
import { GameService } from './services/games.service'; import { GameService } from './services/games.service';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { Player } from './entities/player.entity';
@Module({ @Module({
imports: [UsersModule, TypeOrmModule.forFeature([Game])], imports: [
UsersModule,
TypeOrmModule.forFeature([Game]),
TypeOrmModule.forFeature([Player]),
],
controllers: [LobbyController, GamesController], controllers: [LobbyController, GamesController],
providers: [LobbyService, GameService], providers: [LobbyService, GameService],
}) })
+1 -1
View File
@@ -5,7 +5,7 @@ import {
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { Game } from '../schemas/game.schema'; import { Game } from '../entities/game.entity';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
+3 -3
View File
@@ -6,11 +6,11 @@ import {
Injectable, Injectable,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Game } from '../schemas/game.schema'; import { Game } from '../entities/game.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { Player } from '../schemas/player.schema'; import { Player } from '../entities/player.entity';
import { User } from 'src/users/schemas/user.entity'; import { User } from 'src/users/entities/user.entity';
@Injectable() @Injectable()
export class GameService { export class GameService {
+7 -5
View File
@@ -3,9 +3,10 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { GameService } from './games.service'; import { GameService } from './games.service';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { Game } from '../schemas/game.schema'; import { Game } from '../entities/game.entity';
import { User } from 'src/users/schemas/user.entity'; import { User } from 'src/users/entities/user.entity';
import { Player } from '../schemas/player.schema'; import { Player } from '../entities/player.entity';
import { randomUUID } from 'node:crypto';
@Injectable() @Injectable()
export class LobbyService { export class LobbyService {
@@ -19,6 +20,8 @@ export class LobbyService {
async create(owner: User) { async create(owner: User) {
const createdGame = this.gameRepository.create(); const createdGame = this.gameRepository.create();
createdGame.owner = owner; createdGame.owner = owner;
createdGame.seed = randomUUID();
this.gameRepository.save(createdGame);
await this.joinGame(createdGame, owner); await this.joinGame(createdGame, owner);
this.eventEmitter.emit('sse.lobby', { type: 'create', ...createdGame }); this.eventEmitter.emit('sse.lobby', { type: 'create', ...createdGame });
return createdGame; return createdGame;
@@ -27,7 +30,7 @@ export class LobbyService {
async joinGame(game: Game, user: User) { async joinGame(game: Game, user: User) {
const player = this.playerRepository.create(); const player = this.playerRepository.create();
player.user = user; player.user = user;
game.players.push(); player.game = game;
this.eventEmitter.emit('sse.lobby', { this.eventEmitter.emit('sse.lobby', {
type: 'joinGame', type: 'joinGame',
@@ -39,7 +42,6 @@ export class LobbyService {
player: player, player: player,
}); });
this.playerRepository.save(player); this.playerRepository.save(player);
this.gameRepository.save(game);
} }
findByOwner(user: User) { findByOwner(user: User) {
+9 -18
View File
@@ -2,40 +2,31 @@ import { NestFactory } from '@nestjs/core';
import * as session from 'express-session'; import * as session from 'express-session';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import MongoStore from 'connect-mongo'; import { RedisStore } from 'connect-redis';
import mongoose from 'mongoose'; import { createClient } from 'redis';
async function bootstrap() { async function bootstrap() {
const redisClient = createClient();
redisClient.connect().catch(console.error);
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.enableCors({ origin: 'http://localhost:3000', credentials: true }); app.enableCors({ origin: 'http://localhost:3000', credentials: true });
app.setGlobalPrefix(process.env.API_PREFIX); app.setGlobalPrefix(process.env.API_PREFIX);
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Heron Game') .setTitle('Concept Game')
.setDescription('Heron') .setDescription('Concept')
.setVersion('1.0') .setVersion('1.0')
.build(); .build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document); SwaggerModule.setup('api', app, document);
mongoose.set('transactionAsyncLocalStorage', true);
app.use( app.use(
session.default({ session.default({
secret: process.env.SESSION_SECRET, secret: process.env.SESSION_SECRET,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
name: 'heron.session', name: 'concept.session',
store: MongoStore.create({ store: new RedisStore({ client: redisClient, prefix: 'concept:' }),
collectionName: 'sessions',
dbName: 'heron',
mongoUrl: process.env.MONGO_ENDPOINT,
mongoOptions: {
auth: {
password: process.env.MONGO_PASSWORD,
username: process.env.MONGO_USER,
},
},
}),
}), }),
); );
@@ -1,4 +1,3 @@
import { Entity, Column, PrimaryColumn } from 'typeorm'; import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity() @Entity()
+10 -4
View File
@@ -11,25 +11,31 @@ import { UsersService } from './users.service';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
const configService = new ConfigService();
@ApiTags('auth') @ApiTags('auth')
@Controller() @Controller()
export class UsersController { export class UsersController {
constructor(private readonly usersService: UsersService) {} constructor(private readonly usersService: UsersService) {}
@Get('/login') @Get('/login')
@Redirect(process.env.CLIENT_ENDPOINT) @Redirect()
async login( async login(
@Query('code') code: string, @Query('code') code: string,
@Session() session: Record<string, any>, @Session() session: Record<string, any>,
): Promise<void> { ) {
await this.usersService.createDiscordToken({ code }, session); await this.usersService.createDiscordToken({ code }, session);
await this.usersService.discordLogin(session); await this.usersService.discordLogin(session);
return { url: configService.get('CLIENT_ENDPOINT') };
} }
@Get('/logout') @Get('/logout')
@Redirect(process.env.CLIENT_ENDPOINT) @Redirect()
async logout(@Session() session: Record<string, any>): Promise<void> { async logout(@Session() session: Record<string, any>) {
await this.usersService.discordLogout(session); await this.usersService.discordLogout(session);
return { url: configService.get('CLIENT_ENDPOINT') };
} }
@Get('/whoami') @Get('/whoami')
+2 -3
View File
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { UsersService } from './users.service'; import { UsersService } from './users.service';
import { UsersController } from './users.controller'; import { UsersController } from './users.controller';
import { User } from './schemas/user.entity'; import { User } from './entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
@Module({ @Module({
@@ -9,5 +9,4 @@ import { TypeOrmModule } from '@nestjs/typeorm';
controllers: [UsersController], controllers: [UsersController],
providers: [UsersService], providers: [UsersService],
}) })
export class UsersModule { export class UsersModule {}
}
+18 -12
View File
@@ -1,13 +1,17 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
import { User } from './schemas/user.entity'; import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor(@InjectRepository(User) private usersRepository: Repository<User>) {} constructor(
@InjectRepository(User) private usersRepository: Repository<User>,
private configService: ConfigService,
) {}
async createDiscordToken( async createDiscordToken(
{ {
@@ -22,17 +26,19 @@ export class UsersService {
let body; let body;
if (code) { if (code) {
body = new URLSearchParams({ body = new URLSearchParams({
client_id: process.env.DISCORD_CLIENTID, // client_id: this.configService.get('DISCORD_CLIENTID'),
client_secret: process.env.DISCORD_CLIENTSECRET, // client_secret: this.configService.get('DISCORD_CLIENTSECRET'),
code, code,
grant_type: 'authorization_code', grant_type: 'authorization_code',
redirect_uri: redirect_uri:
process.env.API_ENDPOINT + process.env.API_PREFIX + '/login', this.configService.get('API_ENDPOINT') +
this.configService.get('API_PREFIX') +
'/login',
}); });
} else if (refresh_token) { } else if (refresh_token) {
body = new URLSearchParams({ body = new URLSearchParams({
client_id: process.env.DISCORD_CLIENTID, // client_id: this.configService.get('DISCORD_CLIENTID'),
client_secret: process.env.DISCORD_CLIENTSECRET, // client_secret: this.configService.get('DISCORD_CLIENTSECRET'),
refresh_token, refresh_token,
grant_type: 'refresh_token', grant_type: 'refresh_token',
}); });
@@ -42,10 +48,11 @@ export class UsersService {
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
const req = await fetch('https://discord.com/api/oauth2/token', { const req = await fetch('https://discord.com/api/v10/oauth2/token', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(this.configService.get('DISCORD_CLIENTID') + ':' + this.configService.get('DISCORD_CLIENTSECRET')).toString('base64')}`,
}, },
body, body,
}); });
@@ -83,8 +90,8 @@ export class UsersService {
async revokeDiscordToken(refresh_token: string) { async revokeDiscordToken(refresh_token: string) {
const body = new URLSearchParams({ const body = new URLSearchParams({
client_id: process.env.DISCORD_CLIENTID, client_id: this.configService.get('DISCORD_CLIENTID'),
client_secret: process.env.DISCORD_CLIENTSECRET, client_secret: this.configService.get('DISCORD_CLIENTSECRET'),
token: refresh_token, token: refresh_token,
token_type_hint: 'refresh_token', token_type_hint: 'refresh_token',
}); });
@@ -138,11 +145,10 @@ export class UsersService {
let user = await this.usersRepository.findOneBy({ userId }); let user = await this.usersRepository.findOneBy({ userId });
if (!user) { if (!user) {
user = this.usersRepository.create({ ...data, userId }); user = this.usersRepository.create({ ...data, userId });
} else { } else {
Object.assign(user, data); Object.assign(user, data);
} }
this.usersRepository.save(user) this.usersRepository.save(user);
return user; return user;
} }
+23
View File
@@ -0,0 +1,23 @@
import { User } from 'src/users/entities/user.entity';
import {
Column,
Entity,
JoinTable,
ManyToMany,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';
import { Word } from './word.entity';
@Entity()
export class Topic {
@PrimaryGeneratedColumn()
id: number;
name: string;
@ManyToMany((type) => Word, (word) => word.topics)
words: Word[];
}
+28
View File
@@ -0,0 +1,28 @@
import { User } from 'src/users/entities/user.entity';
import {
Column,
Entity,
JoinTable,
ManyToMany,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';
import { Topic } from './topic.entity';
@Entity()
export class Word {
@PrimaryGeneratedColumn()
id: number;
@Column()
value: string;
@ManyToOne((type) => User)
owner: User;
@ManyToMany((type) => Topic, (topic) => topic.words)
@JoinTable()
topics: Topic[];
}
-14
View File
@@ -1,14 +0,0 @@
import { User } from 'src/users/schemas/user.entity';
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { Word } from './word.schema';
@Entity()
export class Topic{
@PrimaryGeneratedColumn()
id: number
name: string
@ManyToMany(type=>Word, word=>word.topics)
words: Word[]
}
-19
View File
@@ -1,19 +0,0 @@
import { User } from 'src/users/schemas/user.entity';
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { Topic } from './topic.schema';
@Entity()
export class Word{
@PrimaryGeneratedColumn()
id: number
@Column()
value: string
@ManyToOne(type=>User)
owner: User
@ManyToMany(type=>Topic, topic => topic.words)
@JoinTable()
topics: Topic[]
}