Gestão de Usuários
O módulo de gestão de usuários permite visualizar, pesquisar, moderar e gerenciar todos os usuários da plataforma.
Funcionalidades
- Listar e pesquisar usuários
- Ver detalhes completos do usuário
- Banir/desbanir usuários
- Visualizar histórico de transações
- Ver batalhas e aberturas de caixas
- Ajustar saldo (em casos especiais)
Lista de Usuários
Endpoint
http
GET /api/admin/users?page=1&limit=20&search=player&status=active&sort=createdAt&order=descQuery Parameters
| Parâmetro | Tipo | Descrição |
|---|---|---|
| page | number | Página atual |
| limit | number | Itens por página (max 100) |
| search | string | Busca por username, email ou Steam ID |
| status | string | ACTIVE, BANNED, ALL |
| sort | string | Campo para ordenação |
| order | string | ASC ou DESC |
Response
json
{
"users": [
{
"id": "123456789",
"username": "player1",
"steamId": "76561198123456789",
"avatarUrl": "https://...",
"balanceCents": 15000,
"status": "ACTIVE",
"isAdmin": false,
"totalDeposited": 50000,
"totalWithdrawn": 25000,
"totalWagered": 100000,
"gamesPlayed": 150,
"createdAt": "2024-01-01T00:00:00Z",
"lastLoginAt": "2024-01-15T10:30:00Z"
}
],
"total": 1523,
"page": 1,
"limit": 20,
"totalPages": 77
}Implementação do Use Case
typescript
// get-users.use-case.ts
@Injectable()
export class GetUsersUseCase {
async execute(dto: GetUsersDto): Promise<PaginatedResult<UserAdmin>> {
const { page, limit, search, status, sort, order } = dto;
const where: Prisma.UserWhereInput = {};
// Filtro de busca
if (search) {
where.OR = [
{ username: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
{ steamId: { contains: search } },
];
}
// Filtro de status
if (status && status !== 'ALL') {
where.status = status;
}
const [users, total] = await Promise.all([
this.prisma.user.findMany({
where,
orderBy: { [sort]: order.toLowerCase() },
skip: (page - 1) * limit,
take: limit,
select: {
id: true,
username: true,
steamId: true,
avatarUrl: true,
balanceCents: true,
status: true,
isAdmin: true,
createdAt: true,
lastLoginAt: true,
_count: {
select: {
caseOpenings: true,
battles: true,
},
},
},
}),
this.prisma.user.count({ where }),
]);
// Calcular estatísticas de cada usuário
const enrichedUsers = await Promise.all(
users.map(async (user) => ({
...user,
id: user.id.toString(),
balanceCents: Number(user.balanceCents),
totalDeposited: await this.getTotalDeposited(user.id),
totalWithdrawn: await this.getTotalWithdrawn(user.id),
totalWagered: await this.getTotalWagered(user.id),
gamesPlayed: user._count.caseOpenings + user._count.battles,
})),
);
return {
users: enrichedUsers,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
}Detalhes do Usuário
Endpoint
http
GET /api/admin/users/:idResponse
json
{
"user": {
"id": "123456789",
"username": "player1",
"steamId": "76561198123456789",
"avatarUrl": "https://...",
"email": "player@email.com",
"balanceCents": 15000,
"status": "ACTIVE",
"isAdmin": false,
"adminRole": null,
"twoFactorEnabled": true,
"createdAt": "2024-01-01T00:00:00Z",
"lastLoginAt": "2024-01-15T10:30:00Z"
},
"stats": {
"totalDeposited": 50000,
"totalWithdrawn": 25000,
"totalWagered": 100000,
"totalProfit": -25000,
"casesOpened": 120,
"battlesPlayed": 30,
"upgradesAttempted": 15,
"biggestWin": {
"item": "AWP | Dragon Lore",
"value": 250000,
"date": "2024-01-10T15:30:00Z"
}
},
"recentTransactions": [...],
"recentGames": [...],
"sessions": [
{
"id": "session123",
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"createdAt": "2024-01-15T08:00:00Z",
"lastActivity": "2024-01-15T10:30:00Z"
}
],
"notes": [
{
"id": "note1",
"content": "Usuário reportou problema com saque",
"adminId": "admin123",
"adminName": "Admin",
"createdAt": "2024-01-14T12:00:00Z"
}
]
}Banir Usuário
Endpoint
http
POST /api/admin/users/:id/ban
Content-Type: application/json
{
"reason": "Uso de software malicioso",
"duration": null,
"notifyUser": true
}Implementação
typescript
// ban-user.use-case.ts
@Injectable()
export class BanUserUseCase {
async execute(
adminId: bigint,
userId: bigint,
dto: BanUserDto,
): Promise<void> {
const user = await this.userRepository.findById(userId);
if (!user) {
throw new UserNotFoundException();
}
if (user.isAdmin) {
throw new CannotBanAdminException();
}
if (user.status === UserStatus.BANNED) {
throw new UserAlreadyBannedException();
}
await this.prisma.$transaction(async (tx) => {
// 1. Atualizar status
await tx.user.update({
where: { id: userId },
data: {
status: UserStatus.BANNED,
bannedAt: new Date(),
bannedReason: dto.reason,
bannedUntil: dto.duration
? addDays(new Date(), dto.duration)
: null,
bannedBy: adminId,
},
});
// 2. Invalidar todas as sessões
await this.sessionService.invalidateAllUserSessions(userId);
// 3. Registrar auditoria
await this.auditService.log(tx, {
action: AdminAction.USER_BANNED,
adminId,
resourceId: userId,
resourceType: 'USER',
metadata: {
reason: dto.reason,
duration: dto.duration,
},
});
// 4. Notificar usuário (se solicitado)
if (dto.notifyUser) {
await this.notificationService.send(userId, {
type: NotificationType.ACCOUNT_BANNED,
title: 'Conta Suspensa',
message: `Sua conta foi suspensa. Motivo: ${dto.reason}`,
});
}
});
}
}Desbanir Usuário
Endpoint
http
POST /api/admin/users/:id/unban
Content-Type: application/json
{
"reason": "Apelação aceita após revisão"
}Implementação
typescript
// unban-user.use-case.ts
@Injectable()
export class UnbanUserUseCase {
async execute(
adminId: bigint,
userId: bigint,
dto: UnbanUserDto,
): Promise<void> {
const user = await this.userRepository.findById(userId);
if (!user) {
throw new UserNotFoundException();
}
if (user.status !== UserStatus.BANNED) {
throw new UserNotBannedException();
}
await this.prisma.$transaction(async (tx) => {
// 1. Atualizar status
await tx.user.update({
where: { id: userId },
data: {
status: UserStatus.ACTIVE,
bannedAt: null,
bannedReason: null,
bannedUntil: null,
bannedBy: null,
unbannedAt: new Date(),
unbannedBy: adminId,
unbannedReason: dto.reason,
},
});
// 2. Registrar auditoria
await this.auditService.log(tx, {
action: AdminAction.USER_UNBANNED,
adminId,
resourceId: userId,
resourceType: 'USER',
metadata: {
reason: dto.reason,
},
});
});
}
}Histórico de Transações do Usuário
Endpoint
http
GET /api/admin/users/:id/transactions?page=1&limit=20&type=allResponse
json
{
"transactions": [
{
"id": "tx123",
"type": "CASE_OPEN",
"amountCents": -1500,
"balanceAfter": 13500,
"referenceType": "CASE",
"referenceId": "case456",
"description": "Abertura - Caixa Premium",
"createdAt": "2024-01-15T10:30:00Z"
}
],
"total": 250,
"summary": {
"totalDeposits": 50000,
"totalWithdrawals": 25000,
"totalWagered": 100000,
"netResult": 25000
}
}Batalhas do Usuário
Endpoint
http
GET /api/admin/users/:id/battles?page=1&limit=20Aberturas de Caixas do Usuário
Endpoint
http
GET /api/admin/users/:id/case-openings?page=1&limit=20Frontend - Tabela de Usuários
tsx
// components/data-tables/users-table.tsx
'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { MoreHorizontal, Search, Ban, UserCheck } from 'lucide-react';
const columns: ColumnDef<UserAdmin>[] = [
{
accessorKey: 'username',
header: 'Usuário',
cell: ({ row }) => (
<div className="flex items-center gap-2">
<img
src={row.original.avatarUrl}
alt={row.original.username}
className="h-8 w-8 rounded-full"
/>
<div>
<p className="font-medium">{row.original.username}</p>
<p className="text-xs text-muted-foreground">
{row.original.steamId}
</p>
</div>
</div>
),
},
{
accessorKey: 'balanceCents',
header: 'Saldo',
cell: ({ row }) => (
<span>R$ {(row.original.balanceCents / 100).toFixed(2)}</span>
),
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<Badge
variant={row.original.status === 'ACTIVE' ? 'default' : 'destructive'}
>
{row.original.status}
</Badge>
),
},
{
accessorKey: 'gamesPlayed',
header: 'Jogos',
},
{
accessorKey: 'createdAt',
header: 'Cadastro',
cell: ({ row }) => format(new Date(row.original.createdAt), 'dd/MM/yyyy'),
},
{
id: 'actions',
cell: ({ row }) => <UserActions user={row.original} />,
},
];
function UserActions({ user }: { user: UserAdmin }) {
const queryClient = useQueryClient();
const banMutation = useMutation({
mutationFn: () => banUser(user.id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/users/${user.id}`}>Ver Detalhes</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/users/${user.id}/transactions`}>Transações</Link>
</DropdownMenuItem>
{user.status === 'ACTIVE' ? (
<DropdownMenuItem
onClick={() => banMutation.mutate()}
className="text-destructive"
>
<Ban className="mr-2 h-4 w-4" />
Banir Usuário
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={() => unbanUser(user.id)}>
<UserCheck className="mr-2 h-4 w-4" />
Desbanir
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
export function UsersTable() {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['users', { page, search }],
queryFn: () => fetchUsers({ page, search }),
});
const table = useReactTable({
data: data?.users || [],
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="space-y-4">
{/* Search */}
<div className="flex items-center gap-2">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por username, email ou Steam ID..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-8"
/>
</div>
</div>
{/* Table */}
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
{/* Pagination */}
<Pagination
page={page}
totalPages={data?.totalPages || 1}
onPageChange={setPage}
/>
</div>
);
}Notas do Usuário
Admins podem adicionar notas internas sobre usuários:
typescript
// add-user-note.use-case.ts
async execute(
adminId: bigint,
userId: bigint,
content: string,
): Promise<UserNote> {
return await this.prisma.userNote.create({
data: {
userId,
adminId,
content,
},
include: {
admin: {
select: { username: true },
},
},
});
}