Skip to content

Admin vs Customer — Guia de Separação para Frontend

A FDPlay API serve duas aplicações completamente diferentes para dois tipos de usuário:

Painel Administrativo App de Streaming
Quem usa Admin (user_type: "admin") Customer (user_type: "customer")
Propósito Gerenciar a plataforma Consumir vídeos
Precisa de assinatura? Nunca Sim (para acessar vídeos)
Como é criado Por outro admin (POST /users) Self-service (POST /customers/signup)
Endpoint de perfil GET /my-user GET /customers/me
Listagem (admin vê todos) GET /users (só admins) GET /customers (admin lista customers)
Acesso a vídeos Total (bypass de assinatura) Somente com subscription status: "active"
Login POST /token (mesmo endpoint) POST /token (mesmo endpoint)

Regra fundamental

O frontend deve tratar Admin e Customer como dois produtos distintos que compartilham apenas o login (POST /token). Após o login, o campo user_type da resposta de GET /current-user determina qual interface carregar.


Identificando o Tipo de Usuário

Após login, consulte GET /api/v1/current-user. O campo user_type é o discriminador:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> routeByUserType(String token) async {
  final response = await http.get(
    Uri.parse('$baseUrl/current-user'),
    headers: {'Authorization': 'Bearer $token'},
  );
  final user = jsonDecode(response.body);

  if (user['user_type'] == 'admin') {
    // → Carregar Painel Administrativo
    Navigator.pushReplacementNamed(context, '/admin');
  } else if (user['user_type'] == 'customer') {
    // → Carregar App de Streaming
    Navigator.pushReplacementNamed(context, '/videos');
  }
}

Segurança

O roteamento no frontend é apenas UX. O backend valida user_type em toda requisição. Customer chamando rota admin recebe 403 ADMIN_REQUIRED. Customer sem assinatura chamando /videos recebe 403 SUBSCRIPTION_REQUIRED.


Parte 1 — App de Streaming (Customer)

O Customer é o assinante/consumidor. Ele se cadastra, assina um plano e assiste vídeos.

Jornada do Customer

graph TD
    A[Acessa o site] --> B{Tem conta?}
    B -->|Não| C["Signup (2 passos com verificação de email)"]
    C --> D[Recebe JWT]

    B -->|Sim| E[Login]
    E --> F{password_reset_required?}
    F -->|Sim| F1{Sabe a senha atual?}
    F1 -->|Sim| F2["POST /auth/change-password"]
    F1 -->|Não — Legado| F3["POST /auth/forgot-password → código por email"]
    F3 --> F4["POST /auth/reset-password → nova senha"]
    F2 --> D
    F4 --> D
    F -->|Não| D

    D --> G{Tem assinatura ativa?}
    G -->|Não| H[Tela de planos → Assinar]
    H --> G

    G -->|Sim| I[Catálogo de vídeos]
    I --> J[Assistir vídeo]
    I --> K[Gerenciar conta]

Endpoints do Customer

Onboarding (Público — sem JWT)

Endpoint Método O que faz
/customers/signup POST Cadastro em 2 passos (dados → código de email → JWT)
/customers/check-availability GET Verificar se email/CPF já está em uso
/plans GET Listar planos disponíveis com preços
/auth/forgot-password POST Solicitar reset de senha
/auth/reset-password POST Resetar senha com token do email
/auth/resend-verification POST Reenviar código de verificação de email
/token POST Login (retorna JWT)

Área Logada (JWT obrigatório)

Endpoint Método O que faz
/customers/me GET Ver dados do perfil
/customers/me PUT Atualizar dados pessoais (email bloqueado — usar fluxo abaixo)
/customers/me DELETE Deletar conta (com cascade de assinatura)
/customers/me/change-email POST Solicitar troca de email (envia código ao novo email)
/customers/me/confirm-email POST Confirmar troca de email (valida código)
/auth/change-password POST Alterar senha
/current-user GET Dados do usuário autenticado (retorna user_type — usar para rotear)
/my-user GET Dados do próprio perfil (alternativo)
/my-user PUT Atualizar perfil (alternativo)

Assinatura (JWT obrigatório)

Endpoint Método O que faz
/customers/me/subscribe POST Criar assinatura (cartão, boleto ou PIX anual)
/customers/me/subscription GET Ver status da assinatura atual
/customers/me/subscription/renew PUT Trocar método de pagamento (cancela antiga → cria nova)
/customers/me/subscription DELETE Cancelar assinatura

Consumo de Conteúdo (JWT + assinatura ativa)

Endpoint Método O que faz
/videos GET Listar vídeos (filtrado por tier do plano)
/categories GET Listar categorias
/tags GET Listar tags

Vídeos exigem assinatura ativa

GET /videos valida current_subscription_id → subscription com status: "active". Sem assinatura, retorna 403 com código específico que indica a ação necessária (ver seção de erros).

Campos do Customer

{
  "_id": "507f1f77bcf86cd799439020",
  "username": "maria_silva",
  "email": "maria@email.com",
  "user_type": "customer",
  "full_name": "Maria da Silva",
  "tax_id": "12345678900",
  "phone": "+5511999887766",
  "address": {
    "street": "Av. Paulista",
    "number": "1578",
    "complement": "Apto 22",
    "locality": "Bela Vista",
    "city": "São Paulo",
    "region_code": "SP",
    "country": "BRA",
    "postal_code": "01310200"
  },
  "avatar_id": null,
  "email_verified": true,
  "current_subscription_id": "507f1f77bcf86cd799439030",
  "created_at": "2026-01-10T14:00:00Z",
  "updated_at": "2026-01-20T08:15:00Z"
}
Campo Tipo Descrição
full_name string Nome completo (billing)
tax_id string | null CPF/CNPJ
phone string | null Telefone com código do país
address object | null Endereço de cobrança
avatar_id string | null ID do avatar (GridFS)
email_verified boolean Email verificado via código
current_subscription_id string | null FK para subscription ativa
password_reset_required boolean Se true, forçar troca de senha no login (ver nota abaixo)
legacy_id string | null UUID do sistema legado (migração)

Usuários legados NÃO sabem a senha atual

Usuários migrados do sistema SQL anterior têm password_reset_required: true e senha placeholder (hash de string interna). Eles não conseguem usar POST /auth/change-password diretamente (exige senha atual). O frontend deve oferecer dois caminhos:

  1. Se sabe a senha atual (raro — apenas se definiu após migração): POST /auth/change-password
  2. Se NÃO sabe a senha (caso padrão para legados): Redirecionar para POST /auth/forgot-password → código por email → POST /auth/reset-password

Ambos os fluxos limpam a flag password_reset_required automaticamente.

Campos mais relevantes para o frontend

  • current_subscription_id: Se null → customer não tem assinatura (mostrar CTA de assinar)
  • email_verified: Se false → signup incompleto (pedir verificação de email)
  • password_reset_required: Se true → redirecionar para troca de senha obrigatória após login
  • address: Pré-preencher no formulário de assinatura se já cadastrado

Erros Específicos do Customer

Estes erros indicam ações concretas que o frontend deve executar:

Código HTTP Ação do frontend
PASSWORD_RESET_REQUIRED 401 Redirecionar para forgot-password (usuário legado sem senha)
SUBSCRIPTION_REQUIRED 403 Redirecionar para página de planos
SUBSCRIPTION_INACTIVE 403 Redirecionar para renovação de assinatura
SUBSCRIPTION_EXPIRED 403 Redirecionar para página de planos (PIX vencido)
PAYMENT_FAILURES_EXCEEDED 403 Redirecionar para troca de método de pagamento
PLAN_TIER_INSUFFICIENT 403 Mostrar modal de upgrade de plano
EMAIL_NOT_VERIFIED 403 Redirecionar para tela de verificação de email
VERIFICATION_CODE_EXPIRED 400 Mostrar botão "Reenviar código"
VERIFICATION_MAX_ATTEMPTS 400 Reenviar código automaticamente
VERIFICATION_COOLDOWN 429 Mostrar timer de espera (60 segundos)

Parte 2 — Painel Administrativo (Admin)

O Admin gerencia a plataforma: cria vídeos, planos, monitora assinaturas, processa reembolsos.

Jornada do Admin

graph TD
    A[Login] --> B[Dashboard]

    B --> C[Gestão de Vídeos]
    C --> C1[CRUD vídeos + upload thumbnails]
    C --> C2[Definir tier de acesso por vídeo]

    B --> D[Gestão de Planos]
    D --> D1[CRUD planos + sync Stripe/Asaas]
    D --> D2[Ativar/Desativar planos]

    B --> E[Gestão de Assinaturas]
    E --> E1[Listar + estatísticas]
    E --> E2[Cancelar / Reembolsar / Chargeback]

    B --> F[Gestão de Customers]
    F --> F1[Listar + editar dados]

    B --> G[Conteúdo]
    G --> G1[Categorias / Tags / Séries / Grupos]

    B --> H[Auditoria]
    H --> H1[Webhook logs + estatísticas]

Endpoints do Admin

Gestão de Vídeos

Endpoint Método O que faz
/videos GET Listar todos (sem validação de assinatura — bypass)
/videos POST Criar vídeo
/videos PUT Atualizar vídeo
/videos DELETE Deletar vídeo

Gestão de Conteúdo

Endpoint Método O que faz
/categories GET/POST/PUT/DELETE CRUD de categorias
/tags GET/POST/PUT/DELETE CRUD de tags
/groups GET/POST/PUT/DELETE CRUD de grupos
/seasons POST/PUT/DELETE Mutações admin (GET aberto a customer com assinatura ativa)

Gestão de Planos

Endpoint Método O que faz
/plans/admin GET Listar todos (ativos e inativos)
/plans POST Criar plano
/plans PUT Atualizar plano
/plans/{id}/activate POST Ativar plano
/plans/{id}/inactivate POST Desativar plano
/plans/{id}/sync-stripe POST Sincronizar plano existente com Stripe

Gestão de Usuários

Endpoint Método O que faz
/users GET Listar admins
/users POST Criar novo admin
/users PUT Atualizar admin
/users DELETE Deletar admin
/customers GET Listar todos os customers
/customers PUT Atualizar customer (admin pode alterar email diretamente)
/customers DELETE Deletar customer (cascade de assinatura)

Gestão de Assinaturas

Endpoint Método O que faz
/subscriptions GET Listar todas as assinaturas
/subscriptions/stats GET Estatísticas básicas legado (MRR, status counts)
/admin/subscriptions/dashboard GET Visão 360 (totals, by_gateway, by_payment_method, alerts)
/admin/subscriptions/insights GET Análise interpretada com labels, severities, oportunidades
/subscriptions PUT Atualizar assinatura
/subscriptions/{id}/cancel POST Cancelar assinatura
/subscriptions/{id}/refund POST Processar reembolso (PIX ou recorrente)
/subscriptions/{id}/refunds GET Histórico de reembolsos e chargebacks
/subscriptions/{id}/chargeback POST Gerenciar chargeback (suspend/resolve)
/subscriptions/{id}/payments GET Histórico de pagamentos

Quando usar cada um: - dashboard → tabelas, gráficos, drill-down de números brutos. - insights → cards de saúde, alertas priorizados, recomendações com impacto $. - stats (legado) → mantido para retrocompat; novos consumos devem usar dashboard/insights.

Gestão de Clientes

Endpoint Método O que faz
/admin/customers/{id}/verify-email POST Verificar email manualmente
/admin/customers/{id}/toggle-active POST Suspender/reativar conta
/admin/subscriptions/{id}/extend POST Estender prazo de assinatura
/admin/subscriptions/{customer_id}/activate POST Ativar assinatura manualmente (cortesia)
/admin/customers/export GET Exportar clientes segmentados (CSV/XLSX/JSON)

Logs e Auditoria

Endpoint Método O que faz
/webhook-logs GET Logs de webhooks
/webhook-logs/stats GET Estatísticas de webhooks

Outras Rotas Admin

Endpoint Método O que faz
/archive-records GET/POST/PUT/DELETE CRUD de registros de arquivo
/upload-file/archive-records/{id} POST Upload de arquivo (GridFS)
/upload-file/archive-records/{id}/{file_id} PUT/DELETE Atualizar/deletar arquivo
/archive-records/{file_id} GET Download de arquivo
/status GET/POST/PUT/DELETE CRUD de status de workflow
/flows GET/POST/PUT/DELETE CRUD de fluxos
/types-of-registers GET/POST/PUT/DELETE CRUD de tipos de registro
/users-type GET/POST/PUT/DELETE CRUD de tipos/perfis de usuário

Campos do Admin

{
  "_id": "507f1f77bcf86cd799439011",
  "username": "admin_joao",
  "email": "joao@fdplay.com",
  "user_type": "admin",
  "name": "João Administrador",
  "avatar_id": "507f1f77bcf86cd799439012",
  "created_at": "2026-01-01T00:00:00Z",
  "updated_at": "2026-01-15T10:30:00Z"
}
Campo Tipo Descrição
name string Nome de exibição no painel (Customer usa full_name)
avatar_id string | null ID do avatar (GridFS)

Admin não possui campos de billing

Campos como tax_id, phone, address, current_subscription_id existem apenas no Customer. O Admin não participa do fluxo de pagamento.

Erros Específicos do Admin

Código HTTP Quando
ADMIN_REQUIRED 403 Customer tentou acessar rota admin
REFUND_FAILED 502 Falha na comunicação com o gateway de pagamento ao reembolsar
REFUND_NOT_ALLOWED 422 Status da subscription não permite reembolso
CHARGEBACK_DETECTED 409 Chargeback ativo na assinatura

Parte 3 — Roteamento no Frontend

Estrutura de Rotas Sugerida

PÚBLICO (sem auth)
├── /                      Landing page
├── /plans                 Página de planos
├── /signup                Cadastro de customer
├── /login                 Login (admin ou customer)
├── /forgot-password       Recuperar senha
└── /reset-password        Resetar com token

APP DE STREAMING (customer)
├── /videos                Catálogo de vídeos (requer assinatura)
├── /videos/:id            Player
├── /account               Perfil
├── /account/subscription  Gerenciar assinatura
├── /account/change-email  Alterar email (2 passos)
├── /account/change-password  Alterar senha
└── /change-password       Troca obrigatória (usuários legados)

PAINEL ADMIN
├── /admin                 Dashboard
├── /admin/videos          CRUD de vídeos
├── /admin/plans           CRUD de planos
├── /admin/customers       Lista de customers
├── /admin/subscriptions   Gestão de assinaturas
├── /admin/users           CRUD de admins
├── /admin/categories      CRUD de categorias
├── /admin/tags            CRUD de tags
├── /admin/seasons          CRUD de temporadas
└── /admin/webhooks        Logs de webhooks

Guard de Rota

/// Middleware de rota — valida acesso antes de navegar.
/// Retorna a rota de destino (pode redirecionar).
String? canActivateRoute(String route, Map<String, dynamic>? user) {
  // Públicas — sempre OK
  const publicRoutes = ['/', '/plans', '/signup', '/login', '/forgot-password', '/reset-password'];
  if (publicRoutes.contains(route)) return null; // permitido

  // Sem auth → login
  if (user == null) return '/login';

  // /admin/* → somente admin
  if (route.startsWith('/admin')) {
    if (user['user_type'] != 'admin') return '/videos';
    return null; // permitido
  }

  // /videos, /account → somente customer
  if (route.startsWith('/videos') || route.startsWith('/account')) {
    if (user['user_type'] == 'admin') return '/admin';
    return null; // permitido
  }

  return null; // permitido
}

// Uso no MaterialApp (GoRouter, auto_route, ou Navigator)
void navigateTo(BuildContext context, String route, Map<String, dynamic>? user) {
  final redirect = canActivateRoute(route, user);
  final target = redirect ?? route;
  Navigator.pushNamed(context, target);
}

Interceptor de Erros 403

import 'dart:convert';
import 'package:http/http.dart' as http;

/// Interceptor global para respostas 403.
/// Chamar após toda requisição autenticada.
void handleForbidden(BuildContext context, http.Response response) {
  if (response.statusCode != 403) return;

  final body = jsonDecode(response.body);
  final code = body['error']?['code'] as String?;

  switch (code) {
    // Customer sem acesso a vídeos
    case 'SUBSCRIPTION_REQUIRED':
    case 'SUBSCRIPTION_INACTIVE':
    case 'SUBSCRIPTION_EXPIRED':
      Navigator.pushReplacementNamed(context, '/plans');
      _showSnackBar(context, 'Assine um plano para acessar os vídeos.');
      break;

    // Pagamento com problema
    case 'PAYMENT_FAILURES_EXCEEDED':
      Navigator.pushReplacementNamed(context, '/account/subscription');
      _showSnackBar(context, 'Atualize seu método de pagamento.');
      break;

    // Plano insuficiente
    case 'PLAN_TIER_INSUFFICIENT':
      _showUpgradeDialog(context);
      break;

    // Customer tentou acessar admin
    case 'ADMIN_REQUIRED':
      Navigator.pushReplacementNamed(context, '/videos');
      break;

    default:
      _showSnackBar(context, 'Acesso negado.');
  }
}

void _showSnackBar(BuildContext context, String message) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(message)),
  );
}

void _showUpgradeDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (_) => AlertDialog(
      title: const Text('Upgrade necessário'),
      content: const Text('Faça upgrade do seu plano para acessar este conteúdo.'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pushNamed(context, '/plans'),
          child: const Text('Ver planos'),
        ),
      ],
    ),
  );
}

Erros Comuns (Ambos os Perfis)

Código HTTP Quando
UNAUTHORIZED 401 Token ausente ou inválido
INVALID_CREDENTIALS 401 Login com senha incorreta
TOKEN_EXPIRED 401 Token JWT expirado
FORBIDDEN 403 Sem permissão genérica
NOT_FOUND 404 Recurso não encontrado
CONFLICT 409 Conflito (ex: email já cadastrado)
DUPLICATE_KEY 422 Chave duplicada (username, email)
VALIDATION_ERROR 422 Dados inválidos no payload

Resumo Visual

┌──────────────────────────────────────────────────────────────┐
│                         FDPlay API                           │
├─────────────────────────────┬────────────────────────────────┤
│   PAINEL ADMIN (Gestão)     │   APP STREAMING (Consumo)      │
│   user_type: "admin"        │   user_type: "customer"        │
├─────────────────────────────┼────────────────────────────────┤
│                             │                                │
│ CRUD /videos                │ GET /videos (requer assinatura)│
│ CRUD /categories            │ GET /categories                │
│ CRUD /tags                  │ GET /tags                      │
│ CRUD /groups                │ GET /seasons (requer assinatura)│
│ CRUD /seasons                │ POST /customers/me/subscribe   │
│ CRUD /plans                 │ GET  /customers/me/subscription│
│ POST /plans/{id}/sync-stripe│ PUT  /.../subscription/renew   │
│                             │ DELETE /.../subscription       │
│ CRUD /users (admins)        │                                │
│ GET/PUT/DELETE /customers   │ GET/PUT/DELETE /customers/me   │
│                             │ POST /.../change-email         │
│ GET /subscriptions          │ POST /.../confirm-email        │
│ GET /subscriptions/stats    │                                │
│ POST /.../cancel            │ POST /auth/change-password     │
│ POST /.../refund            │ GET /current-user              │
│ POST /.../chargeback        │ GET/PUT /my-user               │
│                             │                                │
│ GET /webhook-logs           │                                │
│ GET /webhook-logs/stats     │                                │
│                             │                                │
│ CRUD /archive-records       │                                │
│ CRUD /status                │                                │
│ CRUD /flows                 │                                │
│ CRUD /types-of-registers    │                                │
│ CRUD /users-type            │                                │
├─────────────────────────────┴────────────────────────────────┤
│                    PÚBLICO (Sem auth)                        │
├──────────────────────────────────────────────────────────────┤
│ POST /token              GET /plans            POST /auth/*  │
│ POST /customers/signup   POST /customers/check-availability  │
└──────────────────────────────────────────────────────────────┘

Referências