From 8968609d622ba9f37473fdb98a8ed278e781ba28 Mon Sep 17 00:00:00 2001 From: Giuseppe Lanna <49965024+King-witcher@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:30:29 -0300 Subject: [PATCH 1/4] =?UTF-8?q?Prompt=201=20desenvolva=20uma=20funcionalid?= =?UTF-8?q?ade=20que=20permite=20a=20usu=C3=A1rios=20com=20role=20creator?= =?UTF-8?q?=20banir=20outros=20usu=C3=A1rios.=20o=20banimento=20deve=20pod?= =?UTF-8?q?er=20ser=20de=20dois=20tipos:=20tempor=C3=A1rio=20por=20tempo?= =?UTF-8?q?=20determinado,=20e=20permanente.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/modules/auth/auth.guard.ts | 25 ++++++++++++- .../modules/user/swagger/ban-user-command.ts | 20 ++++++++++ .../src/modules/user/swagger/user-commands.ts | 2 - backend/src/modules/user/user.controller.ts | 37 +++++++++++++++++++ packages/database-types/src/rows/index.ts | 2 + packages/database-types/src/rows/user-ban.ts | 9 +++++ packages/database-types/src/rows/user-row.ts | 2 + 7 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 backend/src/modules/user/swagger/ban-user-command.ts create mode 100644 packages/database-types/src/rows/user-ban.ts diff --git a/backend/src/modules/auth/auth.guard.ts b/backend/src/modules/auth/auth.guard.ts index c74bca14..84b77cc2 100644 --- a/backend/src/modules/auth/auth.guard.ts +++ b/backend/src/modules/auth/auth.guard.ts @@ -6,13 +6,16 @@ import { AuthService } from './auth.service' import { AuthenticRequest } from './auth-request' import { SKIP_AUTH_KEY } from './skip-auth.decorator' +import { UserRepository } from '@/infra/database' + @Injectable() export class AuthGuard implements CanActivate { private readonly logger = new Logger(AuthGuard.name, { timestamp: true }) constructor( private readonly authService: AuthService, - private readonly reflector: Reflector + private readonly reflector: Reflector, + private readonly userRepository: UserRepository ) {} async canActivate(context: ExecutionContext) { @@ -48,6 +51,15 @@ export class AuthGuard implements CanActivate { if (!token) respondError('unauthorized', 401, '"Authorization" header is missing') const userId = await this.authService.validateToken(token.replace('Bearer ', '')) if (!userId) respondError('unauthorized', 401, 'Invalid auth token') + // Ban check + const user = await this.userRepository.getById(userId) + if (user?.data?.ban) { + const ban = user.data.ban + const now = new Date() + if (ban.type === 'permanent' || (ban.type === 'temporary' && ban.expiresAt && now < new Date(ban.expiresAt))) { + respondError('banned', 403, ban.type === 'permanent' ? 'User is permanently banned' : `User is banned until ${ban.expiresAt}`) + } + } request.userId = userId return true } @@ -60,6 +72,17 @@ export class AuthGuard implements CanActivate { return false } + // Ban check + const user = await this.userRepository.getById(socket.data.userId) + if (user?.data?.ban) { + const ban = user.data.ban + const now = new Date() + if (ban.type === 'permanent' || (ban.type === 'temporary' && ban.expiresAt && now < new Date(ban.expiresAt))) { + this.logger.warn(`banned user ${socket.data.userId} tried to connect`) + return false + } + } + return true } } diff --git a/backend/src/modules/user/swagger/ban-user-command.ts b/backend/src/modules/user/swagger/ban-user-command.ts new file mode 100644 index 00000000..233acef1 --- /dev/null +++ b/backend/src/modules/user/swagger/ban-user-command.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsDefined, IsEnum, IsOptional, IsString, IsDateString } from 'class-validator' +import { BanType } from '@magic3t/database-types' + +export class BanUserCommand { + @ApiProperty({ enum: ['temporary', 'permanent'] }) + @IsDefined() + @IsEnum(['temporary', 'permanent']) + type: BanType + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + reason?: string + + @ApiProperty({ required: false, description: 'Expiration date for temporary bans (ISO string)' }) + @IsOptional() + @IsDateString() + expiresAt?: string +} diff --git a/backend/src/modules/user/swagger/user-commands.ts b/backend/src/modules/user/swagger/user-commands.ts index 65b16cab..817c3c75 100644 --- a/backend/src/modules/user/swagger/user-commands.ts +++ b/backend/src/modules/user/swagger/user-commands.ts @@ -5,9 +5,7 @@ import { IsNumber, IsString, Matches, - Max, MaxLength, - Min, MinLength, } from 'class-validator' diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts index 75865e93..bb97e6b3 100644 --- a/backend/src/modules/user/user.controller.ts +++ b/backend/src/modules/user/user.controller.ts @@ -14,6 +14,9 @@ import { } from './swagger/user-commands' import { UserService } from './user.service' +import { BanUserCommand } from './swagger/ban-user-command' +import { AdminGuard } from '@/modules/admin/admin.guard' + const baseIcons = new Set([...range(59, 79), ...range(0, 30)]) @Controller('users') @@ -24,6 +27,40 @@ export class UserController { private readonly configRepository: ConfigRepository ) {} + @Post(':id/ban') + @UseGuards(AuthGuard, AdminGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Ban a user (creator only)' }) + async banUser( + @UserId() creatorId: string, + @Param('id') userId: string, + @Body() body: BanUserCommand + ) { + if (creatorId === userId) respondError('cannot-ban-self', 400, 'You cannot ban yourself') + const user = await this.userRepository.getById(userId) + if (!user) respondError('user-not-found', 404, 'User not found') + if (user.data.role === 'creator') respondError('cannot-ban-creator', 403, 'Cannot ban another creator') + + const now = new Date() + let expiresAt: Date | undefined = undefined + if (body.type === 'temporary') { + if (!body.expiresAt) respondError('missing-expiry', 400, 'expiresAt is required for temporary bans') + expiresAt = new Date(body.expiresAt) + if (Number.isNaN(expiresAt.getTime()) || expiresAt <= now) respondError('invalid-expiry', 400, 'expiresAt must be a future date') + } + + await this.userRepository.update(userId, { + ban: { + type: body.type, + reason: body.reason, + bannedAt: now, + expiresAt, + bannedBy: creatorId, + }, + }) + return { success: true } + } + @Get('id/:id') @ApiOperation({ summary: 'Get a user by id', diff --git a/packages/database-types/src/rows/index.ts b/packages/database-types/src/rows/index.ts index bde4b613..8094d402 100644 --- a/packages/database-types/src/rows/index.ts +++ b/packages/database-types/src/rows/index.ts @@ -4,3 +4,5 @@ export * from './icon-assignment-row' export * from './match-row' export * from './user-row' export * from './with-id' + +export * from './user-ban' diff --git a/packages/database-types/src/rows/user-ban.ts b/packages/database-types/src/rows/user-ban.ts new file mode 100644 index 00000000..7e0c5dca --- /dev/null +++ b/packages/database-types/src/rows/user-ban.ts @@ -0,0 +1,9 @@ +export type BanType = 'temporary' | 'permanent'; + +export interface UserBan { + type: BanType; + reason?: string; + bannedAt: Date; + expiresAt?: Date; // Only for temporary bans + bannedBy: string; // userId of the creator +} diff --git a/packages/database-types/src/rows/user-row.ts b/packages/database-types/src/rows/user-row.ts index 6772212d..692894ba 100644 --- a/packages/database-types/src/rows/user-row.ts +++ b/packages/database-types/src/rows/user-row.ts @@ -47,4 +47,6 @@ export type UserRow = { draws: number defeats: number } + /** Ban info, if user is banned */ + ban?: import('./user-ban').UserBan } From 75230a18a46be207fa7513805902fdc8b7c54d79 Mon Sep 17 00:00:00 2001 From: Giuseppe Lanna <49965024+King-witcher@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:31:01 -0300 Subject: [PATCH 2/4] fix linting --- backend/src/modules/auth/auth.guard.ts | 21 ++++++++++++++----- .../modules/user/swagger/ban-user-command.ts | 4 ++-- .../src/modules/user/swagger/user-commands.ts | 9 +------- backend/src/modules/user/user.controller.ts | 16 +++++++------- packages/database-types/src/rows/index.ts | 3 +-- packages/database-types/src/rows/user-ban.ts | 12 +++++------ 6 files changed, 35 insertions(+), 30 deletions(-) diff --git a/backend/src/modules/auth/auth.guard.ts b/backend/src/modules/auth/auth.guard.ts index 84b77cc2..b8c3311d 100644 --- a/backend/src/modules/auth/auth.guard.ts +++ b/backend/src/modules/auth/auth.guard.ts @@ -2,12 +2,11 @@ import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/commo import { Reflector } from '@nestjs/core' import { Socket } from 'socket.io' import { respondError } from '@/common' +import { UserRepository } from '@/infra/database' import { AuthService } from './auth.service' import { AuthenticRequest } from './auth-request' import { SKIP_AUTH_KEY } from './skip-auth.decorator' -import { UserRepository } from '@/infra/database' - @Injectable() export class AuthGuard implements CanActivate { private readonly logger = new Logger(AuthGuard.name, { timestamp: true }) @@ -56,8 +55,17 @@ export class AuthGuard implements CanActivate { if (user?.data?.ban) { const ban = user.data.ban const now = new Date() - if (ban.type === 'permanent' || (ban.type === 'temporary' && ban.expiresAt && now < new Date(ban.expiresAt))) { - respondError('banned', 403, ban.type === 'permanent' ? 'User is permanently banned' : `User is banned until ${ban.expiresAt}`) + if ( + ban.type === 'permanent' || + (ban.type === 'temporary' && ban.expiresAt && now < new Date(ban.expiresAt)) + ) { + respondError( + 'banned', + 403, + ban.type === 'permanent' + ? 'User is permanently banned' + : `User is banned until ${ban.expiresAt}` + ) } } request.userId = userId @@ -77,7 +85,10 @@ export class AuthGuard implements CanActivate { if (user?.data?.ban) { const ban = user.data.ban const now = new Date() - if (ban.type === 'permanent' || (ban.type === 'temporary' && ban.expiresAt && now < new Date(ban.expiresAt))) { + if ( + ban.type === 'permanent' || + (ban.type === 'temporary' && ban.expiresAt && now < new Date(ban.expiresAt)) + ) { this.logger.warn(`banned user ${socket.data.userId} tried to connect`) return false } diff --git a/backend/src/modules/user/swagger/ban-user-command.ts b/backend/src/modules/user/swagger/ban-user-command.ts index 233acef1..eb392496 100644 --- a/backend/src/modules/user/swagger/ban-user-command.ts +++ b/backend/src/modules/user/swagger/ban-user-command.ts @@ -1,6 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsEnum, IsOptional, IsString, IsDateString } from 'class-validator' import { BanType } from '@magic3t/database-types' +import { ApiProperty } from '@nestjs/swagger' +import { IsDateString, IsDefined, IsEnum, IsOptional, IsString } from 'class-validator' export class BanUserCommand { @ApiProperty({ enum: ['temporary', 'permanent'] }) diff --git a/backend/src/modules/user/swagger/user-commands.ts b/backend/src/modules/user/swagger/user-commands.ts index 817c3c75..e4842402 100644 --- a/backend/src/modules/user/swagger/user-commands.ts +++ b/backend/src/modules/user/swagger/user-commands.ts @@ -1,13 +1,6 @@ import { ChangeIconCommand, ChangeNicknameCommand, RegisterUserCommand } from '@magic3t/api-types' import { ApiProperty } from '@nestjs/swagger' -import { - IsDefined, - IsNumber, - IsString, - Matches, - MaxLength, - MinLength, -} from 'class-validator' +import { IsDefined, IsNumber, IsString, Matches, MaxLength, MinLength } from 'class-validator' export class RegisterUserCommandClass implements RegisterUserCommand { @IsDefined() diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts index bb97e6b3..15e85139 100644 --- a/backend/src/modules/user/user.controller.ts +++ b/backend/src/modules/user/user.controller.ts @@ -5,8 +5,10 @@ import { ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger' import { range } from 'lodash' import { respondError } from '@/common' import { ConfigRepository, UserRepository } from '@/infra/database' +import { AdminGuard } from '@/modules/admin/admin.guard' import { AuthGuard } from '@/modules/auth/auth.guard' import { UserId } from '@/modules/auth/user-id.decorator' +import { BanUserCommand } from './swagger/ban-user-command' import { ChangeIconCommandClass, ChangeNickCommandClass, @@ -14,9 +16,6 @@ import { } from './swagger/user-commands' import { UserService } from './user.service' -import { BanUserCommand } from './swagger/ban-user-command' -import { AdminGuard } from '@/modules/admin/admin.guard' - const baseIcons = new Set([...range(59, 79), ...range(0, 30)]) @Controller('users') @@ -39,14 +38,17 @@ export class UserController { if (creatorId === userId) respondError('cannot-ban-self', 400, 'You cannot ban yourself') const user = await this.userRepository.getById(userId) if (!user) respondError('user-not-found', 404, 'User not found') - if (user.data.role === 'creator') respondError('cannot-ban-creator', 403, 'Cannot ban another creator') + if (user.data.role === 'creator') + respondError('cannot-ban-creator', 403, 'Cannot ban another creator') const now = new Date() - let expiresAt: Date | undefined = undefined + let expiresAt: Date | undefined if (body.type === 'temporary') { - if (!body.expiresAt) respondError('missing-expiry', 400, 'expiresAt is required for temporary bans') + if (!body.expiresAt) + respondError('missing-expiry', 400, 'expiresAt is required for temporary bans') expiresAt = new Date(body.expiresAt) - if (Number.isNaN(expiresAt.getTime()) || expiresAt <= now) respondError('invalid-expiry', 400, 'expiresAt must be a future date') + if (Number.isNaN(expiresAt.getTime()) || expiresAt <= now) + respondError('invalid-expiry', 400, 'expiresAt must be a future date') } await this.userRepository.update(userId, { diff --git a/packages/database-types/src/rows/index.ts b/packages/database-types/src/rows/index.ts index 8094d402..69cc64da 100644 --- a/packages/database-types/src/rows/index.ts +++ b/packages/database-types/src/rows/index.ts @@ -2,7 +2,6 @@ export * from './config' export * from './crash-report-row' export * from './icon-assignment-row' export * from './match-row' +export * from './user-ban' export * from './user-row' export * from './with-id' - -export * from './user-ban' diff --git a/packages/database-types/src/rows/user-ban.ts b/packages/database-types/src/rows/user-ban.ts index 7e0c5dca..59db6ff7 100644 --- a/packages/database-types/src/rows/user-ban.ts +++ b/packages/database-types/src/rows/user-ban.ts @@ -1,9 +1,9 @@ -export type BanType = 'temporary' | 'permanent'; +export type BanType = 'temporary' | 'permanent' export interface UserBan { - type: BanType; - reason?: string; - bannedAt: Date; - expiresAt?: Date; // Only for temporary bans - bannedBy: string; // userId of the creator + type: BanType + reason?: string + bannedAt: Date + expiresAt?: Date // Only for temporary bans + bannedBy: string // userId of the creator } From 83c2bfbf4c4a120de8a6c98b81c4f1d13fb367ed Mon Sep 17 00:00:00 2001 From: Giuseppe Lanna <49965024+King-witcher@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:35:57 -0300 Subject: [PATCH 3/4] Prompt 2 agora implementa no frontend --- .../components/templates/profile/profile.tsx | 82 ++++++++++++++++++- frontend/src/services/clients/ban-user.ts | 12 +++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 frontend/src/services/clients/ban-user.ts diff --git a/frontend/src/components/templates/profile/profile.tsx b/frontend/src/components/templates/profile/profile.tsx index cf1d0600..61fe26b2 100644 --- a/frontend/src/components/templates/profile/profile.tsx +++ b/frontend/src/components/templates/profile/profile.tsx @@ -6,7 +6,13 @@ import { Console } from '@/lib/console' import { MatchHistory } from './components/match-history' import { ProfileHeader } from './components/profile-header' import { ProfileSearch } from './components/profile-search' -import { ProfileStats } from './components/profile-stats' + +import { useAuth } from '@/contexts/auth-context' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { useState } from 'react' +import { banUser, BanUserPayload } from '@/services/clients/ban-user' interface Props { user: GetUserResult @@ -14,6 +20,35 @@ interface Props { } export function ProfileTemplate({ user, matchesQuery }: Props) { + const auth = useAuth() + const [banOpen, setBanOpen] = useState(false) + const [banType, setBanType] = useState<'temporary' | 'permanent'>('permanent') + const [reason, setReason] = useState('') + const [expiresAt, setExpiresAt] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + const isCreator = auth.user?.role === 'creator' + const isSelf = auth.user?.id === user.id + const canBan = isCreator && !isSelf && user.role !== 'creator' && !user.ban + + async function handleBan() { + setLoading(true) + setError(null) + setSuccess(false) + try { + const payload: BanUserPayload = { type: banType, reason } + if (banType === 'temporary') payload.expiresAt = expiresAt + await banUser(user.id, payload) + setSuccess(true) + setBanOpen(false) + } catch (e: any) { + setError(e?.message || 'Erro ao banir usuário') + } finally { + setLoading(false) + } + } // Registers a console command to log the user ID useRegisterCommand( { @@ -35,6 +70,51 @@ export function ProfileTemplate({ user, matchesQuery }: Props) { + {canBan && ( +
+ + + + + + + Banir usuário + + Escolha o tipo de banimento e, se temporário, defina a data de expiração. + + +
+ + + {banType === 'temporary' && ( + setExpiresAt(e.target.value)} + placeholder="Data/hora de expiração" + /> + )} + setReason(e.target.value)} + placeholder="Motivo (opcional)" + /> + {error &&
{error}
} + {success &&
Usuário banido com sucesso!
} +
+ + + +
+
+
+ )} {/* Match History Card */} diff --git a/frontend/src/services/clients/ban-user.ts b/frontend/src/services/clients/ban-user.ts new file mode 100644 index 00000000..47e812c2 --- /dev/null +++ b/frontend/src/services/clients/ban-user.ts @@ -0,0 +1,12 @@ +import { apiClient } from '@/services/clients/api-client' +import { BanType } from '@magic3t/database-types' + +export interface BanUserPayload { + type: BanType + reason?: string + expiresAt?: string +} + +export async function banUser(userId: string, payload: BanUserPayload): Promise { + await apiClient.user.post(`${userId}/ban`, payload) +} From 13219790fa0f2461a47766a709581f3741cee1f5 Mon Sep 17 00:00:00 2001 From: Giuseppe Lanna <49965024+King-witcher@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:32:59 -0300 Subject: [PATCH 4/4] fix some firebase bugs --- backend/src/modules/user/user.controller.ts | 2 +- frontend/src/components/templates/profile/profile.tsx | 1 + packages/database-types/src/rows/user-ban.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts index 15e85139..4514e46c 100644 --- a/backend/src/modules/user/user.controller.ts +++ b/backend/src/modules/user/user.controller.ts @@ -56,7 +56,7 @@ export class UserController { type: body.type, reason: body.reason, bannedAt: now, - expiresAt, + expiresAt: expiresAt ?? null, bannedBy: creatorId, }, }) diff --git a/frontend/src/components/templates/profile/profile.tsx b/frontend/src/components/templates/profile/profile.tsx index 61fe26b2..2b92d901 100644 --- a/frontend/src/components/templates/profile/profile.tsx +++ b/frontend/src/components/templates/profile/profile.tsx @@ -13,6 +13,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { Input } from '@/components/ui/input' import { useState } from 'react' import { banUser, BanUserPayload } from '@/services/clients/ban-user' +import { ProfileStats } from './components/profile-stats' interface Props { user: GetUserResult diff --git a/packages/database-types/src/rows/user-ban.ts b/packages/database-types/src/rows/user-ban.ts index 59db6ff7..7580e60b 100644 --- a/packages/database-types/src/rows/user-ban.ts +++ b/packages/database-types/src/rows/user-ban.ts @@ -4,6 +4,6 @@ export interface UserBan { type: BanType reason?: string bannedAt: Date - expiresAt?: Date // Only for temporary bans + expiresAt: Date | null // Only for temporary bans bannedBy: string // userId of the creator }