16 KiB
Toptran
Aplicação full-stack composta por um backend REST em Node.js/Express com autenticação JWT e um aplicativo mobile em React Native/Expo, voltado para o gerenciamento de corridas de trabalhadores do transporte.
Estrutura do projeto
toptran/
├── backend/ → API REST (Node.js, Express, Prisma, PostgreSQL)
└── toptran-app/ → App mobile (React Native, Expo)
Backend
Tecnologias
| Pacote | Versão | Função |
|---|---|---|
| Node.js | LTS | Runtime |
| Express | 5 | Framework HTTP |
| TypeScript | 5 | Tipagem |
| Prisma | 7 | ORM |
| PostgreSQL | — | Banco de dados |
| bcryptjs | — | Hash de senha |
| jsonwebtoken | — | Access token (JWT) |
| zod | — | Validação de input |
| @prisma/adapter-pg | — | Driver nativo do Prisma 7 |
Arquitetura em camadas
src/
├── server.ts → Inicialização do Express e registro das rotas
├── lib/
│ └── prisma.ts → Singleton do PrismaClient com adapter PG
├── repositories/
│ ├── users.repository.ts → Queries de usuários no Prisma
│ ├── tokens.repository.ts → Queries de tokens no Prisma
│ ├── companies.repository.ts → Queries de empresas no Prisma
│ └── rides.repository.ts → Queries de corridas no Prisma
├── services/
│ ├── auth.service.ts → Lógica de registro, login, logout e JWT
│ ├── users.service.ts → Lógica de leitura, atualização e remoção de usuários
│ └── sync.service.ts → Lógica de sincronização de empresas e corridas
├── controllers/
│ ├── auth.controller.ts → Recebe requests e chama auth.service
│ ├── users.controller.ts → Recebe requests e chama users.service
│ └── sync.controller.ts → Recebe requests e chama sync.service
├── middlewares/
│ └── auth.middleware.ts → Valida o Bearer token nas rotas protegidas
└── routes/
├── auth.routes.ts → Rotas públicas de autenticação
├── users.routes.ts → Rotas protegidas de usuário
└── sync.routes.ts → Rotas protegidas de sincronização
Fluxo de uma requisição:
Request → routes → middleware (se protegida) → controller → service → repository → Prisma → PostgreSQL
Modelos do banco de dados
model users {
id String @id
name String
email String @unique
password String
profilePhoto String? @default("")
bio String? @default("")
createdAt DateTime @default(now())
updatedAt DateTime
rides rides[]
tokens tokens[]
}
model tokens {
id String @id
token String @unique
type TokenType
userId String
expiresAt DateTime
createdAt DateTime @default(now())
users users @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model companies {
id String @id
name String
cost_per_km Decimal @db.Decimal(10, 2)
notes String? @default("")
createdAt DateTime? @default(now()) @db.Timestamptz(6)
updatedAt DateTime? @default(now()) @db.Timestamptz(6)
}
model rides {
id String @id
user_id String
company String
km Decimal @db.Decimal(10, 2)
cost_per_km Decimal @db.Decimal(10, 2)
total Decimal @db.Decimal(10, 2)
ride_date String
departures Int? @default(0)
failed_service Int? @default(0)
idle_hours Decimal? @default(0) @db.Decimal(10, 2)
synced Int? @default(0) @db.SmallInt
createdAt DateTime? @default(now()) @db.Timestamptz(6)
updatedAt DateTime? @default(now()) @db.Timestamptz(6)
users users @relation(fields: [user_id], references: [id], onDelete: Cascade)
}
enum TokenType {
REFRESH
}
Variáveis de ambiente
O backend trabalha com dois bancos — um de homologação (toptrandev) e um de produção (toptranprod) — e dois arquivos .env separados:
| Arquivo | Banco | Porta |
|---|---|---|
backend/.env.development |
toptrandev |
4000 |
backend/.env.production |
toptranprod |
5000 |
Modelo do .env.development:
DATABASE_URL=postgresql://USER:PASS@HOST:5432/toptrandev
DB_USER=...
DB_PASSWORD=...
DB_HOST=...
DB_PORT=5432
DB_NAME=toptrandev
PORT=4000
NODE_ENV=development
JWT_SECRET=...
JWT_REFRESH_SECRET=...
Modelo do .env.production: idêntico, trocando DB_NAME=toptranprod, PORT=5000 e NODE_ENV=production.
JWT_SECRETé obrigatório em produção. O access token expira em 15 minutos e o refresh token em 7 dias.
Instalação e execução
cd backend
# Instalar dependências
npm install
# Gerar cliente Prisma
npx prisma generate
# Rodar a API apontando pro banco de homologação (toptrandev)
npm run dev
# Rodar a API apontando pro banco de produção (toptranprod)
npm run dev:prod
# Build + start em produção
npm run build
npm start # usa .env.production por padrão
npm run start:dev # caso queira o build rodando contra o banco de dev
Migrations e Prisma CLI por ambiente (via dotenv-cli):
# Homologação
npm run prisma:dev migrate dev
npm run prisma:dev studio
# Produção
npm run prisma:prod migrate deploy
npm run prisma:prod studio
API Reference
Autenticação — /auth
Rotas públicas, não requerem token.
POST /auth/register
Cria uma nova conta de usuário.
// Request body
{
"name": "João Silva",
"email": "joao@email.com",
"password": "minhasenha"
}
// Response 201
{
"id": "uuid",
"name": "João Silva",
"email": "joao@email.com"
}
POST /auth/login
Autentica o usuário e retorna os tokens.
// Request body
{
"email": "joao@email.com",
"password": "minhasenha"
}
// Response 200
{
"accessToken": "eyJ...",
"refreshToken": "uuid"
}
POST /auth/logout
Revoga o refresh token.
// Request body
{
"refreshToken": "uuid"
}
// Response 204 (sem body)
Usuários — /users
Rotas protegidas. Enviar o header
Authorization: Bearer <accessToken>.
GET /users/me
Retorna os dados básicos do usuário autenticado.
// Response 200
{
"id": "uuid",
"name": "João Silva",
"email": "joao@email.com",
"createdAt": "2026-04-27T00:00:00.000Z"
}
GET /users/profile
Retorna o perfil completo do usuário, incluindo foto e bio.
// Response 200
{
"id": "uuid",
"name": "João Silva",
"email": "joao@email.com",
"profilePhoto": "base64_ou_url",
"bio": "Motorista há 5 anos.",
"createdAt": "2026-04-27T00:00:00.000Z"
}
PUT /users/me
Atualiza nome, e-mail e/ou senha. Todos os campos são opcionais.
// Request body
{
"name": "João Atualizado",
"email": "novo@email.com",
"password": "novasenha"
}
// Response 200
{
"id": "uuid",
"name": "João Atualizado",
"email": "novo@email.com"
}
PATCH /users/profile
Atualiza foto de perfil e/ou bio. Todos os campos são opcionais.
// Request body
{
"profilePhoto": "base64_ou_url",
"bio": "Nova bio aqui."
}
// Response 200
{
"id": "uuid",
"name": "João Silva",
"profilePhoto": "base64_ou_url",
"bio": "Nova bio aqui."
}
DELETE /users/me
Remove a conta do usuário autenticado.
// Response 204 (sem body)
Sincronização — /sync
Rotas protegidas. Enviar o header
Authorization: Bearer <accessToken>.
GET /sync/companies
Retorna todas as empresas cadastradas no servidor.
// Response 200
{
"success": true,
"data": [
{ "id": "uuid", "name": "Empresa X", "cost_per_km": "2.50", "notes": "" }
]
}
POST /sync/companies
Sincroniza (upsert) uma lista de empresas no servidor.
// Request body
{
"companies": [
{ "id": "uuid", "name": "Empresa X", "cost_per_km": 2.50, "notes": "" }
]
}
// Response 200
{
"success": true,
"message": "1 company/companies synced",
"data": [...]
}
GET /sync/rides
Retorna todas as corridas cadastradas no servidor.
// Response 200
{
"success": true,
"data": [
{
"id": "uuid",
"user_id": "uuid",
"company": "Empresa X",
"km": "12.5",
"cost_per_km": "2.50",
"total": "31.25",
"ride_date": "2026-05-01",
"departures": 3,
"failed_service": 1,
"idle_hours": "1.50",
"synced": 1
}
]
}
POST /sync/rides
Sincroniza (upsert) uma lista de corridas no servidor. Os campos departures, failed_service e idle_hours são opcionais e assumem 0 por padrão.
// Request body
{
"rides": [
{
"id": "uuid",
"user_id": "uuid",
"company": "Empresa X",
"km": 12.5,
"cost_per_km": 2.50,
"total": 31.25,
"ride_date": "2026-05-01",
"departures": 3,
"failed_service": 1,
"idle_hours": 1.5
}
]
}
// Response 200
{
"success": true,
"message": "1 ride(s) synced",
"data": [...]
}
Campos da corrida:
| Campo | Tipo | Descrição |
|---|---|---|
id |
string | Identificador único da corrida |
user_id |
string | ID do usuário dono da corrida |
company |
string | Nome da empresa |
km |
number|string | Quilômetros rodados |
cost_per_km |
number|string | Custo por km no momento da corrida |
total |
number|string | Valor total da corrida |
ride_date |
string | Data da corrida (formato YYYY-MM-DD) |
departures |
number? | Quantidade de partidas/saídas (default 0) |
failed_service |
number? | Quantidade de serviços não realizados (default 0) |
idle_hours |
number|string? | Horas ociosas, com 2 casas decimais (default 0) |
synced |
number? | Flag de sincronização (0 ou 1) |
Respostas de erro
| Status | Situação |
|---|---|
| 400 | Input inválido (falha na validação Zod) |
| 401 | Token ausente, inválido ou expirado |
| 404 | Recurso não encontrado |
| 409 | E-mail já cadastrado |
| 500 | Erro interno no servidor |
Deploy com Docker / Podman
cd backend
# Build da imagem
podman build -t top-tran-backend .
# Subir com compose
podman-compose up -d
O container expõe a porta 3000 e o serviço é reiniciado automaticamente em caso de falha.
Mobile
Tecnologias
| Pacote | Versão | Função |
|---|---|---|
| React Native | 0.83.6 | Framework mobile |
| Expo | 55 | Plataforma e toolchain |
| Expo Router | 55 | Navegação baseada em arquivos |
| Expo SQLite | 55 | Banco de dados local (offline-first) |
| Expo Secure Store | 55 | Armazenamento seguro de tokens |
| Expo Image Picker | 55 | Seleção de foto de perfil |
| Expo Print | 55 | Geração de PDF |
| Expo Sharing | 55 | Compartilhamento de arquivos |
| Axios | 1 | Cliente HTTP |
| TypeScript | 5 | Tipagem |
Estrutura
src/
├── app/
│ ├── _layout.tsx → Layout raiz: inicializa DB e provedor de auth
│ ├── index.tsx → Tela de login
│ ├── signup.tsx → Tela de cadastro
│ ├── home.tsx → Tela inicial com resumo de corridas
│ ├── lancamento.tsx → Lançamento de nova corrida
│ ├── historico.tsx → Histórico de corridas
│ ├── empresas.tsx → Gerenciamento de empresas (CRUD local)
│ ├── relatorio.tsx → Relatório mensal com exportação em PDF
│ ├── perfil.tsx → Edição de perfil (nome, foto, bio)
│ ├── sincronizar.tsx → Sincronização com o servidor (timeline visual)
│ ├── cadastros.tsx → Hub de cadastros
│ └── corrida.tsx → Rota de corrida (redirect)
├── components/
│ ├── Button.tsx → Botão reutilizável
│ ├── Header.tsx → Cabeçalho de tela
│ ├── Input.tsx → Campo de texto reutilizável
│ └── Select.tsx → Seletor (Picker) reutilizável
├── constants/
│ └── theme.ts → Cores, espaçamentos e border-radius globais
├── contexts/
│ └── AuthContext.tsx → Estado de autenticação global (JWT + usuário)
├── server/
│ └── api.ts → Instância do Axios configurada para o backend
├── services/
│ └── db.ts → Serviço SQLite: tabelas, CRUD de corridas, empresas e configurações
└── utils/
└── jwt.ts → Utilitário para decode do JWT
Telas
| Arquivo | Rota | Descrição |
|---|---|---|
src/app/index.tsx |
/ |
Login |
src/app/signup.tsx |
/signup |
Cadastro de conta |
src/app/home.tsx |
/home |
Dashboard com resumo do dia |
src/app/lancamento.tsx |
/lancamento |
Lançar nova corrida |
src/app/historico.tsx |
/historico |
Histórico completo de corridas |
src/app/empresas.tsx |
/empresas |
CRUD de empresas locais |
src/app/relatorio.tsx |
/relatorio |
Relatório mensal + exportar PDF |
src/app/perfil.tsx |
/perfil |
Edição de nome, foto e bio |
src/app/sincronizar.tsx |
/sincronizar |
Sincronização com o servidor |
src/app/cadastros.tsx |
/cadastros |
Hub de cadastros |
Banco de dados local (SQLite)
O app usa expo-sqlite para persistência offline. Ao iniciar, o _layout.tsx chama initDB() que cria as seguintes tabelas:
| Tabela | Descrição |
|---|---|
users |
Dados do usuário logado e token de acesso |
rides |
Corridas com flag synced (0 = pendente, 1 = sincronizado) |
companies |
Empresas cadastradas localmente |
settings |
Configurações chave-valor (ex: foto de perfil, bio) |
Sincronização
A tela /sincronizar executa três etapas em sequência com feedback visual em timeline:
- Upload de empresas — envia empresas locais para o servidor via
POST /sync/companies - Download de empresas — baixa empresas do servidor se o cadastro local estiver vazio
- Upload de corridas — compara IDs locais com os do servidor e envia apenas as corridas ausentes via
POST /sync/rides
Ambientes (dev / prod)
O app não conversa direto com o PostgreSQL — ele consome a API do backend. Alternar de banco no app significa apontar pra um backend diferente via EXPO_PUBLIC_API_URL.
| Arquivo | API | Banco |
|---|---|---|
toptran-app/.env.development |
http://175.15.15.93:4000/api |
toptrandev |
toptran-app/.env.production |
https://toptran.olymp.com.br/api |
toptranprod |
O Expo carrega o arquivo certo automaticamente conforme o modo:
| Comando | Arquivo carregado |
|---|---|
npx expo start (dev) |
.env.development |
npx expo export / EAS Build production |
.env.production |
Variáveis
EXPO_PUBLIC_*são inlined no bundle em build time. Reinicie sempre comnpx expo start -capós trocar o.env.
Perfis EAS
Configurados em toptran-app/eas.json:
| Perfil | Formato | API/Banco | Comando | Pra quê serve |
|---|---|---|---|---|
development |
APK + dev client | dev (:4000 → toptrandev) |
eas build --profile development |
Rodar com expo start plugado, hot reload |
preview |
APK standalone | prod (toptranprod) |
eas build --profile preview |
APK pra testar/distribuir fora da Play |
production |
AAB | prod (toptranprod) |
eas build --profile production |
Bundle pra Google Play (eas submit) |
Execução
cd toptran-app
# Instalar dependências
npm install
# Iniciar o servidor Expo (carrega .env.development → backend dev)
npm start
# Build e abrir no Android
npm run android
# Build e abrir no iOS
npm run ios
Conectar ao dispositivo Android via AVD na VM
- Ligue o dispositivo Android (AVD)
- Rodar o comando
adb tcpip 5555 - No terminal local, crie o túnel SSH reverso:
ssh -R 5555:localhost:5555 dev@175.15.15.93 - Conecte o ADB ao dispositivo tunelado:
adb connect localhost:5555