vek1 — autenticação
Autenticação e autorização
Stack atual: Better Auth 1.6 com HTTP adapter custom. Toda persistência de auth (user/session/account/verification) sai pra vek1-api
/internal/auth/*viaX-Auth-Tokenisolado do app token. Frontend não toca Postgres direto pra auth.
Configuração principal — src/lib/auth.ts
betterAuth({
database: httpAdapter, // delega tudo pra vek1-api
emailAndPassword: {
enabled: true,
autoSignIn: true,
sendResetPassword: async ({ user, url }) => {
await apiClient.email.sendResetPasswordEmail({
to: user.email,
reset_url: url,
name: user.name,
});
},
resetPasswordTokenExpiresIn: 60 * 60,
},
trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(',') ?? [],
baseURL: process.env.BETTER_AUTH_URL,
secret: process.env.BETTER_AUTH_SECRET,
})
HTTP adapter — src/lib/auth-http-adapter.ts
Implementa createAdapterFactory({...}) do Better Auth com handlers create / findOne / findMany / update / updateMany / delete / deleteMany / consumeOne / count. Responsabilidades:
- camelCase ↔ snake_case: Better Auth manda
emailVerified, Pydantic do vek1-api exigeemail_verified.snakeKeys()no outgoing,camelKeys()no incoming (recursivo). - Parse de timestamps: backend devolve string ISO; Better Auth precisa de
Date.parseTimestampField()converte campos terminados emexpiresAt|createdAt|updatedAt|...ExpiresAt. - Per-model dispatcher: cada operação mapeia pra endpoint purpose-built (não CRUD genérico).
findOne(user, where=[{id}])→apiClient.auth.findUserByIdfindOne(session, where=[{token}])→apiClient.auth.findSessionByTokenfindOne(account, where=[{providerId, accountId}])→apiClient.auth.findAccountByProviderfindMany(verification, where=[{identifier|value}])→ idem (Better Auth chama findMany no reset-password)consumeOne(verification, where=[{identifier, value}])→ DELETE...RETURNING atomic no vek1-api (elimina race PKCE)
- updateMany real: Better Auth chama
updateManynoupdatePassword(where=[userId, providerId='credential']). Adapter resolve TODOS os matches viafindManyRows+ filtro manual +updateRowem loop.
Endpoints /internal/auth/* no vek1-api
Todos exigem X-Auth-Token + Pydantic strict (extra='forbid'). Vide vek1-api/context para spec completa.
| Endpoint | Adapter call |
|---|---|
POST /users |
create(user) |
GET /users/by-id/{id} / /by-email/{email} |
findOne(user) |
PATCH /users/{id} / DELETE /users/{id} |
update/delete(user) |
POST /sessions |
create(session) |
GET /sessions/by-token/{token} / /by-id/{id} |
findOne(session) |
PATCH /sessions/{id} / DELETE /sessions/{id} |
update/delete(session) |
DELETE /sessions/by-user/{user_id} |
deleteMany(session) |
POST /accounts / GET /accounts/by-user/{user_id} / /by-provider?... |
create/findMany/findOne(account) |
GET /accounts/password-by-user/{user_id} |
retorna hash credential pro login |
POST /verifications |
create(verification) |
GET /verifications/by-value/{value} / /by-identifier/{identifier} |
findOne/findMany(verification) |
POST /verifications/consume |
consumeOne — DELETE..RETURNING atomic |
DELETE /verifications/by-identifier/{identifier} |
bulk delete |
GET /_cache-stats |
métrica interna (dev only) |
Fluxos
Signup
/register→authClient.signUp.email({ email, password, name })- Better Auth chama
create(user)→ vek1-apiPOST /internal/auth/users→ INSERT user - Better Auth chama
create(account)→ INSERT account (password hash bcrypt) autoSignIn: true→create(session)→ INSERT session + retorna cookie- Não cria
company_profilesautomaticamente (sem trigger SQL como tinha no Supabase). Cliente cria manualmente em/storesou via endpoint/internal/company/ensure.
Login
/login→authClient.signIn.email({ email, password })- Better Auth busca user por email + account com
providerId='credential', compara hash, cria session
Forgot password (PR #72)
/forgot-password→authClient.forgetPassword({ email, redirectTo })- Better Auth: cria row em
verification(identifier='reset-password:userId', value=token, expiresAt=+1h) + chamasendResetPassword sendResetPasswordemauth.tschamaapiClient.email.sendResetPasswordEmail({to, reset_url, name})→ vek1-api/internal/email/send-reset-password(webhook scope) → Resend- User clica no link →
/reset-password?token=...→authClient.resetPassword({ newPassword, token }) - Better Auth:
consumeOne(verification)(atomic) +updateMany(account)(atualiza hash)
OAuth callback
/auth/callback/route.tsainda existe mas não é usado (sem provider OAuth configurado).
Middleware / proxy (src/proxy.ts)
Em Next 16,
middleware.ts→proxy.ts. Função exportada se chamaproxy, nãomiddleware.
Whitelist de rotas públicas:
/, /login, /register, /forgot-password, /reset-password, /auth/callback
Prefixos protegidos:
/admin, /agents, /dashboard, /products, /stores, /upload, /leads, /orders, /settings,
/client-stats, /database-stats, /token-usage, /documents, /instance-settings
Rotas fora das duas listas passam livre (return NextResponse.next()).
Sem session em rota protegida → redirect /login?redirect={pathname}.
Matcher exclui api/* + assets estáticos.
Helpers SSR — src/lib/auth-server.ts
getCurrentUser(): Promise<User | null> // lê cookie session via Better Auth
requireUser(): Promise<User> // throw 401 se anon
Usado por Server Actions e Route Handlers pra validar antes de chamar apiClient com actorUserId: user.id.
Roles e permissões
Não há sistema de roles ainda. Modelo continua tenant = user = empresa:
better_auth.user.id == company_profiles.id == stores.company_id
Equipes/multi-usuário por company não suportados. Adicionar exigiria:
- Tabela
company_members(user_id, company_id, role) - Refactor todas as ownership checks no vek1-api (
assert_owns_store(actor, store_id)precisa virar JOIN com membership)
Autorização granular
Sem RLS no Postgres. Autorização vive no vek1-api via dependency injection:
Depends(require_app_token)validaX-Internal-Tokenactor: str = Depends(require_actor_user_id)extraiX-Actor-User-Idassert_owns_store(actor, store_id),assert_owns_order(actor, order_id),assert_owns_lead(actor, lead_id), etc. — JOIN comstores.company_id
Risco: ownership check vive 100% no backend. Bug aí = exposição cross-tenant. Mitigação: cobertura de testes integration em tests/test_*_endpoints.py no vek1-api.
Envs de auth
BETTER_AUTH_SECRET # secret HMAC pros tokens
BETTER_AUTH_URL # https://vek1.vercel.app (URL canônica)
NEXT_PUBLIC_BETTER_AUTH_URL # mesma URL pro client
BETTER_AUTH_TRUSTED_ORIGINS # comma-separated (Vercel previews etc)
INTERNAL_AUTH_TOKEN # dedicado pro httpAdapter (fallback: INTERNAL_API_TOKEN)
INTERNAL_API_TOKEN # app scope (apiClient default)
INTERNAL_WEBHOOK_TOKEN # webhook scope (email, AbacatePay relay)
VEK1_API_URL # https://vek1-api.kodama.solutions