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 }