Skip to content

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=desc

Query Parameters

ParâmetroTipoDescrição
pagenumberPágina atual
limitnumberItens por página (max 100)
searchstringBusca por username, email ou Steam ID
statusstringACTIVE, BANNED, ALL
sortstringCampo para ordenação
orderstringASC 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/:id

Response

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=all

Response

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=20

Aberturas de Caixas do Usuário

Endpoint

http
GET /api/admin/users/:id/case-openings?page=1&limit=20

Frontend - 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 },
      },
    },
  });
}

Documentação Técnica CSGOFlip