/
kochkatech
/
CorpGame
Обзор
Документация
Войти
/
kochkatech
/
CorpGame
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
master
backend/src/admin/admin.controller.ts
205 строк
6 KB
kochkareal
chore: добавлен .gitattributes, нормализованы переносы строк (LF везде, кроме .bat/.cmd)
06 авг 2026, 15:18
06 авг 2026, 15:18
c25ef1b
Код
Авторство
О чём код?
import { BadRequestException, Body, Controller, Delete, Get, Param, Patch, Post, Req, Res, UnauthorizedException, UseGuards, } from '@nestjs/common'; import { Request, Response } from 'express'; import { AdminGuard } from '../auth/admin.guard'; import { signAdminToken } from '../auth/jwt.util'; import { passwordsMatch } from '../auth/password'; import { LoginRateLimiter } from '../auth/login-rate-limiter'; import { GameService } from '../game/game.service'; import { RevealService } from '../game/reveal.service'; import { EventsService } from '../events/events.service'; import { AdminTeamService } from './admin-team.service'; const COOKIE_OPTIONS = { httpOnly: true, sameSite: 'lax' as const, maxAge: 12 * 60 * 60 * 1000 }; @Controller('admin') export class AdminController { // Один инстанс на процесс (не DI-сервис): состояние — только счётчик неудачных попыток // в памяти, переживать рестарт или шариться между запросами через DI не требуется. private readonly loginLimiter = new LoginRateLimiter(); constructor( private readonly game: GameService, private readonly events: EventsService, private readonly teams: AdminTeamService, private readonly reveal: RevealService, ) {} @Post('login') login(@Body('password') password: string, @Req() req: Request, @Res({ passthrough: true }) res: Response) { const ip = req.ip ?? 'unknown'; const waitMs = this.loginLimiter.msUntilAllowed(ip); if (waitMs > 0) { throw new UnauthorizedException( `Слишком много неудачных попыток. Повторите через ${Math.ceil(waitMs / 1000)} с.`, ); } if (!password || !passwordsMatch(password, process.env.ADMIN_PASSWORD ?? '')) { this.loginLimiter.recordFailure(ip); throw new UnauthorizedException('Неверный пароль'); } this.loginLimiter.recordSuccess(ip); res.cookie('admin_token', signAdminToken(), COOKIE_OPTIONS); return { ok: true }; } @Post('logout') logout(@Res({ passthrough: true }) res: Response) { res.clearCookie('admin_token'); return { ok: true }; } @UseGuards(AdminGuard) @Get('sessions') list() { return this.game.list(); } @UseGuards(AdminGuard) @Post('sessions') create(@Body('teamsCount') teamsCount: number, @Body('durationSec') durationSec: number) { return this.game.createSession(Number(teamsCount), Number(durationSec)); } @UseGuards(AdminGuard) @Get('sessions/:id') get(@Param('id') id: string) { return this.game.getFullState(id); } @UseGuards(AdminGuard) @Get('sessions/:id/events') events_(@Param('id') id: string) { return this.events.recent(id); } @UseGuards(AdminGuard) @Post('sessions/:id/start') start(@Param('id') id: string) { return this.game.start(id); } @UseGuards(AdminGuard) @Post('sessions/:id/pause') pause(@Param('id') id: string) { return this.game.pause(id); } @UseGuards(AdminGuard) @Post('sessions/:id/resume') resume(@Param('id') id: string) { return this.game.resume(id); } @UseGuards(AdminGuard) @Post('sessions/:id/finish') finish(@Param('id') id: string) { return this.game.finish(id); } @UseGuards(AdminGuard) @Post('sessions/:id/extend') extend(@Param('id') id: string, @Body('addSec') addSec: number) { const value = Number(addSec); if (!Number.isFinite(value)) { throw new BadRequestException('Некорректное количество секунд'); } return this.game.extendDuration(id, value); } @UseGuards(AdminGuard) @Get('sessions/:id/reveal') revealCurrent(@Param('id') id: string) { return this.reveal.current(id); } @UseGuards(AdminGuard) @Post('sessions/:id/reveal/start') revealStart(@Param('id') id: string) { return this.reveal.start(id); } @UseGuards(AdminGuard) @Post('sessions/:id/reveal/next') revealNext(@Param('id') id: string) { return this.reveal.next(id); } @UseGuards(AdminGuard) @Post('sessions/:id/reveal/prev') revealPrev(@Param('id') id: string) { return this.reveal.prev(id); } @UseGuards(AdminGuard) @Delete('sessions/:id') deleteSession(@Param('id') id: string) { return this.game.deleteSession(id); } @UseGuards(AdminGuard) @Patch('sessions/:id/flags') flags( @Param('id') id: string, @Body() body: Partial<{ transferEnabled: boolean; sanctionsEnabled: boolean; theftEnabled: boolean }>, ) { return this.game.setFlags(id, body); } @UseGuards(AdminGuard) @Patch('sessions/:id/config') updateConfig( @Param('id') id: string, @Body() body: Partial<{ bankRate: number; maxTheftsPerTeam: number; maxSanctionsPerTeam: number }>, ) { return this.game.updateConfig(id, body); } @UseGuards(AdminGuard) @Patch('sessions/:id/dashboard') updateDashboardConfig( @Param('id') id: string, @Body() body: Partial<{ showResources: boolean; showScore: boolean }>, ) { return this.game.updateDashboardConfig(id, body); } @UseGuards(AdminGuard) @Patch('teams/:id') renameTeam(@Param('id') id: string, @Body('name') name: string) { return this.teams.rename(id, name); } @UseGuards(AdminGuard) @Delete('teams/:id') removeTeam(@Param('id') id: string) { return this.teams.remove(id); } @UseGuards(AdminGuard) @Post('teams/:id/grant') grant(@Param('id') id: string, @Body('resource') resource: string, @Body('amount') amount: number) { return this.teams.grant(id, resource, Number(amount)); } @UseGuards(AdminGuard) @Post('teams/:id/effect') effect(@Param('id') id: string, @Body('multiplier') multiplier: number, @Body('durationSec') durationSec: number) { return this.teams.applyEffect(id, Number(multiplier), Number(durationSec)); } }