- README: o que a aplicação faz, migrations, BP_WEB_PORT e testes com
go.sum/package-lock.json (sem go mod tidy)
- ARQUITETURA: esquema real (00001 a 00003), rotas que faltavam, Compose
e imagens atuais, cadeia de proxies, retenção, situação dos testes,
decisões 30 a 33 e estado da implementação
- DEPLOY: cópia por tar, BP_WEB_PORT em todas as etapas e atualização
para a versão com exclusão de contas
- MANUAL: papéis do owner e navegação por ano nos feriados
- openapi.yaml: DELETE /accounts/{id} e regras da conta restrita
- Dockerfiles: comentários sobre go.sum e package-lock.json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
588 lines
20 KiB
YAML
588 lines
20 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: Bot de Pontos API
|
|
version: 1.0.0
|
|
description: |
|
|
API REST consumida pelo painel Next.js (ARQUITETURA.md, seção 7).
|
|
|
|
Autenticação: cookie de sessão `bp_session` (HttpOnly). Toda requisição que
|
|
altera dados exige também o header `X-CSRF-Token` com o valor do cookie
|
|
`bp_csrf` e um `Origin` igual a BP_PUBLIC_URL.
|
|
|
|
`x-min-role` indica o papel mínimo: viewer < admin < owner.
|
|
Conta restrita (viewer com user_id): só vê e altera o próprio usuário em
|
|
/users/{id}*, recebe só os próprios dados em /users, /notifications e
|
|
/dashboard, e não acessa /audit, /settings nem /accounts (ARQUITETURA.md, 9.6).
|
|
Erros seguem application/problem+json (RFC 9457).
|
|
servers:
|
|
- url: /api/v1
|
|
|
|
components:
|
|
securitySchemes:
|
|
session:
|
|
type: apiKey
|
|
in: cookie
|
|
name: bp_session
|
|
responses:
|
|
Problem:
|
|
description: Erro
|
|
content:
|
|
application/problem+json:
|
|
schema: { $ref: "#/components/schemas/Problem" }
|
|
UserDetail:
|
|
description: Usuário atualizado
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/UserDetail" }
|
|
parameters:
|
|
id:
|
|
name: id
|
|
in: path
|
|
required: true
|
|
schema: { type: integer, format: int64 }
|
|
schemas:
|
|
Problem:
|
|
type: object
|
|
required: [type, title, status]
|
|
properties:
|
|
type: { type: string, example: about:blank }
|
|
title: { type: string }
|
|
status: { type: integer }
|
|
code:
|
|
type: string
|
|
description: |
|
|
Código estável: VALIDATION, NOT_FOUND, UNAUTHENTICATED, FORBIDDEN, BAD_ORIGIN, BAD_CSRF,
|
|
INVALID_LOGIN, ACCOUNT_LOCKED, CREDENTIALS_REQUIRED, CREDENTIALS_REJECTED, POINTS_UNAVAILABLE,
|
|
USER_NOT_ACTIVE, INVALID_STATE, LAST_OWNER, NOT_CONFIGURED, DB_UNAVAILABLE, INTERNAL
|
|
errors:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
field: { type: string }
|
|
message: { type: string }
|
|
Account:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
username: { type: string }
|
|
role: { type: string, enum: [owner, admin, viewer] }
|
|
user_id: { type: [integer, "null"] }
|
|
disabled: { type: boolean }
|
|
locked_until: { type: [string, "null"], format: date-time }
|
|
last_login_at: { type: [string, "null"], format: date-time }
|
|
created_at: { type: string, format: date-time }
|
|
User:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
telegram_chat_id: { type: integer }
|
|
display_name: { type: string }
|
|
status: { type: string, enum: [pending, active, disabled] }
|
|
is_bot_admin: { type: boolean }
|
|
notifications_enabled: { type: boolean }
|
|
paused_reason: { type: [string, "null"], enum: [manual, api_failures, admin, user_disabled, null] }
|
|
timezone: { type: string }
|
|
created_at: { type: string, format: date-time }
|
|
UserSummary:
|
|
allOf:
|
|
- $ref: "#/components/schemas/User"
|
|
- type: object
|
|
properties:
|
|
has_credential: { type: boolean }
|
|
credential_validated: { type: boolean }
|
|
consecutive_failures: { type: integer }
|
|
custom_window: { type: boolean }
|
|
QueryWindow:
|
|
type: object
|
|
required: [query_window_start, query_window_end, day_check_deadline, no_points_recheck_until]
|
|
properties:
|
|
query_window_start: { type: string, example: "11:30" }
|
|
query_window_end: { type: string, example: "19:00" }
|
|
day_check_deadline: { type: string, example: "11:45" }
|
|
no_points_recheck_until: { type: string, example: "15:00" }
|
|
Schedule:
|
|
type: object
|
|
properties:
|
|
lunch_minutes: { type: integer, minimum: 15, maximum: 180 }
|
|
daily_work_minutes: { type: integer, minimum: 60, maximum: 720 }
|
|
entrada2_lead_minutes: { type: integer, minimum: 0, maximum: 60 }
|
|
saida2_lead_minutes: { type: integer, minimum: 0, maximum: 60 }
|
|
window:
|
|
description: null = usa a janela global
|
|
oneOf:
|
|
- $ref: "#/components/schemas/QueryWindow"
|
|
- type: "null"
|
|
Day:
|
|
type: object
|
|
properties:
|
|
day: { type: string, format: date }
|
|
status: { type: string, enum: [awaiting, no_points, working, not_working, finished] }
|
|
zero_point_checks: { type: integer }
|
|
last_point_count: { type: integer }
|
|
next_check_at: { type: [string, "null"], format: date-time }
|
|
resumed_by: { type: [string, "null"] }
|
|
UserDetail:
|
|
type: object
|
|
properties:
|
|
user: { $ref: "#/components/schemas/User" }
|
|
schedule: { $ref: "#/components/schemas/Schedule" }
|
|
credential:
|
|
type: [object, "null"]
|
|
description: A senha nunca é devolvida.
|
|
properties:
|
|
api_email: { type: string }
|
|
validated: { type: boolean }
|
|
last_validated_at: { type: [string, "null"], format: date-time }
|
|
last_error: { type: [string, "null"] }
|
|
consecutive_failures: { type: integer }
|
|
last_failure_at: { type: [string, "null"], format: date-time }
|
|
updated_at: { type: string, format: date-time }
|
|
today: { $ref: "#/components/schemas/Day" }
|
|
effective_window: { $ref: "#/components/schemas/QueryWindow" }
|
|
custom_window: { type: boolean }
|
|
Today:
|
|
type: object
|
|
properties:
|
|
day: { type: string, format: date }
|
|
timezone: { type: string }
|
|
points: { type: array, items: { type: string, format: date-time } }
|
|
invalid: { type: boolean }
|
|
overtime: { type: boolean }
|
|
entrada2_at: { type: [string, "null"], format: date-time }
|
|
saida2_at: { type: [string, "null"], format: date-time }
|
|
saida2_error: { type: string }
|
|
day_state: { $ref: "#/components/schemas/Day" }
|
|
window: { $ref: "#/components/schemas/QueryWindow" }
|
|
custom_window: { type: boolean }
|
|
Notification:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
user_id: { type: integer }
|
|
user_name: { type: string }
|
|
reference_date: { type: string, format: date }
|
|
type: { type: string, enum: [entrada2, saida2] }
|
|
scheduled_at: { type: string, format: date-time }
|
|
predicted_at: { type: string, format: date-time }
|
|
status: { type: string, enum: [scheduled, sending, sent, failed, cancelled, expired] }
|
|
attempts: { type: integer }
|
|
sent_at: { type: [string, "null"], format: date-time }
|
|
error_message: { type: [string, "null"] }
|
|
AuditLog:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
occurred_at: { type: string, format: date-time }
|
|
source: { type: string, enum: [telegram, web, worker, cli] }
|
|
actor_user_id: { type: [integer, "null"] }
|
|
actor_user_name: { type: [string, "null"] }
|
|
actor_account: { type: [integer, "null"] }
|
|
actor_username: { type: [string, "null"] }
|
|
target_user_id: { type: [integer, "null"] }
|
|
target_user_name: { type: [string, "null"] }
|
|
event_type: { type: string }
|
|
success: { type: boolean }
|
|
details: { type: object }
|
|
Holiday:
|
|
type: object
|
|
properties:
|
|
day: { type: string, format: date }
|
|
name: { type: string }
|
|
source: { type: string, enum: [feriadosapi, feriados.dev, manual] }
|
|
|
|
security:
|
|
- session: []
|
|
|
|
paths:
|
|
/health/live:
|
|
get:
|
|
summary: Processo da API está de pé
|
|
security: []
|
|
responses:
|
|
"200": { description: OK }
|
|
/health/ready:
|
|
get:
|
|
summary: API pronta (Postgres respondendo)
|
|
security: []
|
|
responses:
|
|
"200": { description: OK }
|
|
"503": { $ref: "#/components/responses/Problem" }
|
|
|
|
/auth/login:
|
|
post:
|
|
summary: Login (Argon2id; 5 falhas bloqueiam por 15 min)
|
|
security: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [username, password]
|
|
properties:
|
|
username: { type: string }
|
|
password: { type: string }
|
|
responses:
|
|
"200": { description: Sessão criada; define os cookies bp_session e bp_csrf }
|
|
"401": { $ref: "#/components/responses/Problem" }
|
|
"423": { $ref: "#/components/responses/Problem" }
|
|
/auth/logout:
|
|
post:
|
|
summary: Revoga a sessão e limpa os cookies
|
|
x-min-role: viewer
|
|
responses:
|
|
"204": { description: OK }
|
|
/auth/me:
|
|
get:
|
|
summary: Conta logada
|
|
x-min-role: viewer
|
|
responses:
|
|
"200": { description: OK }
|
|
|
|
/dashboard:
|
|
get:
|
|
summary: Contadores, queda da PontoMais e fallback de feriados
|
|
x-min-role: viewer
|
|
responses:
|
|
"200": { description: OK }
|
|
|
|
/users:
|
|
get:
|
|
summary: Lista de usuários
|
|
x-min-role: viewer
|
|
parameters:
|
|
- { name: status, in: query, schema: { type: string, enum: [pending, active, disabled] } }
|
|
- { name: q, in: query, schema: { type: string }, description: Nome ou chat ID }
|
|
- { name: custom_window, in: query, schema: { type: boolean } }
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
users: { type: array, items: { $ref: "#/components/schemas/UserSummary" } }
|
|
/users/{id}:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
get:
|
|
summary: Detalhe do usuário
|
|
x-min-role: viewer
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
"404": { $ref: "#/components/responses/Problem" }
|
|
/users/{id}/activate:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Ativa (exige credencial validada — mesma regra do /ativarusuario)
|
|
x-min-role: admin
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
"409": { $ref: "#/components/responses/Problem" }
|
|
/users/{id}/disable:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Desativa e cancela os lembretes agendados
|
|
x-min-role: admin
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
/users/{id}/schedule:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
put:
|
|
summary: Jornada, fuso e exceção de janela (9.8, 9.14)
|
|
x-min-role: admin
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
allOf:
|
|
- $ref: "#/components/schemas/Schedule"
|
|
- type: object
|
|
properties:
|
|
timezone: { type: string }
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
"422": { $ref: "#/components/responses/Problem" }
|
|
/users/{id}/credentials:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
put:
|
|
summary: Valida na PontoMais e grava cifrada; em falha a anterior é mantida
|
|
x-min-role: admin
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [email, password]
|
|
properties:
|
|
email: { type: string }
|
|
password: { type: string }
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
"422": { $ref: "#/components/responses/Problem" }
|
|
"502": { $ref: "#/components/responses/Problem" }
|
|
/users/{id}/credentials/test:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Consulta a PontoMais com a credencial atual
|
|
x-min-role: admin
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Today" }
|
|
/users/{id}/notifications/enable:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Liga os lembretes, zera falhas e retoma o dia se estava sem pontos
|
|
x-min-role: admin
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
/users/{id}/notifications/disable:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Pausa os lembretes e cancela os agendados
|
|
x-min-role: admin
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
/users/{id}/bot-admin:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
put:
|
|
summary: Liga ou desliga o admin do bot no Telegram
|
|
x-min-role: admin
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
enabled: { type: boolean }
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
/users/{id}/today:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
get:
|
|
summary: Consulta ao vivo dos pontos e previsões do dia
|
|
x-min-role: viewer
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Today" }
|
|
"502": { $ref: "#/components/responses/Problem" }
|
|
/users/{id}/today/resume:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Retoma um dia sem pontos (9.12)
|
|
x-min-role: admin
|
|
responses:
|
|
"200": { $ref: "#/components/responses/UserDetail" }
|
|
"409": { $ref: "#/components/responses/Problem" }
|
|
|
|
/notifications:
|
|
get:
|
|
summary: Lembretes (até 200)
|
|
x-min-role: viewer
|
|
parameters:
|
|
- { name: day, in: query, schema: { type: string, format: date } }
|
|
- { name: status, in: query, schema: { type: string } }
|
|
- { name: type, in: query, schema: { type: string, enum: [entrada2, saida2] } }
|
|
- { name: user_id, in: query, schema: { type: integer } }
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
notifications: { type: array, items: { $ref: "#/components/schemas/Notification" } }
|
|
/notifications/{id}/cancel:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Cancela (só scheduled)
|
|
x-min-role: admin
|
|
responses:
|
|
"200": { description: OK }
|
|
"409": { $ref: "#/components/responses/Problem" }
|
|
/notifications/{id}/resend:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Reenvio manual (só failed do dia atual, 9.5)
|
|
x-min-role: admin
|
|
responses:
|
|
"200": { description: OK }
|
|
"409": { $ref: "#/components/responses/Problem" }
|
|
|
|
/audit:
|
|
get:
|
|
summary: Auditoria, paginada por cursor (next_before)
|
|
x-min-role: viewer
|
|
parameters:
|
|
- { name: from, in: query, schema: { type: string, format: date } }
|
|
- { name: to, in: query, schema: { type: string, format: date } }
|
|
- { name: source, in: query, schema: { type: string } }
|
|
- { name: event_type, in: query, schema: { type: string } }
|
|
- { name: success, in: query, schema: { type: boolean } }
|
|
- { name: target_user_id, in: query, schema: { type: integer } }
|
|
- { name: actor_account, in: query, schema: { type: integer } }
|
|
- { name: before, in: query, schema: { type: integer } }
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
logs: { type: array, items: { $ref: "#/components/schemas/AuditLog" } }
|
|
next_before: { type: [integer, "null"] }
|
|
|
|
/holidays:
|
|
get:
|
|
summary: Feriados do ano
|
|
x-min-role: viewer
|
|
parameters:
|
|
- { name: year, in: query, schema: { type: integer } }
|
|
responses:
|
|
"200": { description: OK }
|
|
post:
|
|
summary: Cadastra feriado manual
|
|
x-min-role: admin
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Holiday" }
|
|
responses:
|
|
"201": { description: Criado }
|
|
"422": { $ref: "#/components/responses/Problem" }
|
|
/holidays/sync:
|
|
post:
|
|
summary: Sincroniza o ano com a feriadosapi.com
|
|
x-min-role: admin
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
year: { type: integer }
|
|
responses:
|
|
"200": { description: OK }
|
|
"503": { $ref: "#/components/responses/Problem" }
|
|
/holidays/{day}:
|
|
delete:
|
|
summary: Remove feriado manual
|
|
x-min-role: admin
|
|
parameters:
|
|
- { name: day, in: path, required: true, schema: { type: string, format: date } }
|
|
responses:
|
|
"204": { description: OK }
|
|
|
|
/settings:
|
|
get:
|
|
summary: Janela global e parâmetros do servidor
|
|
x-min-role: viewer
|
|
responses:
|
|
"200": { description: OK }
|
|
put:
|
|
summary: Altera a janela global (9.13)
|
|
x-min-role: admin
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/QueryWindow" }
|
|
responses:
|
|
"200": { description: OK }
|
|
"422": { $ref: "#/components/responses/Problem" }
|
|
/settings/preview:
|
|
post:
|
|
summary: Efeito estimado de uma nova janela global
|
|
x-min-role: admin
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/QueryWindow" }
|
|
responses:
|
|
"200": { description: OK }
|
|
|
|
/accounts:
|
|
get:
|
|
summary: Contas do painel
|
|
x-min-role: owner
|
|
responses:
|
|
"200": { description: OK }
|
|
post:
|
|
summary: Cria conta
|
|
x-min-role: owner
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [username, password, role]
|
|
properties:
|
|
username: { type: string }
|
|
password: { type: string, minLength: 12 }
|
|
role: { type: string, enum: [owner, admin, viewer] }
|
|
user_id: { type: [integer, "null"] }
|
|
responses:
|
|
"201":
|
|
description: Criada
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Account" }
|
|
/accounts/{id}:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
patch:
|
|
summary: Altera papel, desativa e/ou troca o usuário ligado (mantém pelo menos um owner ativo)
|
|
x-min-role: owner
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
role: { type: string, enum: [owner, admin, viewer] }
|
|
disabled: { type: boolean }
|
|
user_id: { type: [integer, "null"], description: "Usuário do Telegram ligado à conta; null remove o vínculo" }
|
|
responses:
|
|
"200": { description: OK }
|
|
"409": { $ref: "#/components/responses/Problem" }
|
|
delete:
|
|
summary: Exclui a conta e as sessões dela; a auditoria mantém o nome (actor_account_name)
|
|
description: Recusa excluir a própria conta (INVALID_STATE) e o último owner ativo (LAST_OWNER).
|
|
x-min-role: owner
|
|
responses:
|
|
"204": { description: Excluída }
|
|
"404": { $ref: "#/components/responses/Problem" }
|
|
"409": { $ref: "#/components/responses/Problem" }
|
|
/accounts/{id}/reset-password:
|
|
parameters: [{ $ref: "#/components/parameters/id" }]
|
|
post:
|
|
summary: Redefine a senha e encerra as sessões da conta
|
|
x-min-role: owner
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [password]
|
|
properties:
|
|
password: { type: string, minLength: 12 }
|
|
responses:
|
|
"204": { description: OK }
|