Conversation
Commit 990eba6 landed conflict markers in the account repository, breaking the TypeScript build on develop. The incoming side imported accountCacheKey/jwtPayloadCacheKey from core/lib/cache.ts, which da24aeb removed when the cache decorators replaced the manual helpers. Resolved in favour of the decorator pattern and converted updateAvatarUrl to @InvalidateCache, matching how credentials already invalidates those same keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds company-scoped integration keys following the existing module pattern (routes/controllers/services/repositories/errors + OpenAPI docs). - api_keys table: prefix is a uniquely indexed public handle, only the HMAC of the secret is persisted, revocation is a soft delete - GET/POST/DELETE /companies/:subdomain/api-keys behind a dedicated apiKeys:read|create|revoke permission group granted to manager and admin - authenticateApiKey middleware resolves the key by prefix in one indexed query, verifies the secret in constant time and populates request.ability Scopes can only narrow: the owning employee's role is resolved at request time and intersected with the stored scopes, so demoting an employee immediately narrows every key they issued. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Creates the api_keys table with a unique index on prefix, which backs the single-query lookup performed on every API key authentication. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The file landed in f303427 indented with spaces, which fails the biome check that `build:web` runs before next build — breaking the web build on develop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds /dashboard/:subdomain/settings/api-keys following the employees module pattern: a react-query list, a TanStack table and dialogs for the mutations. - The secret appears once, in the creation dialog, behind an explicit warning and a copy button; it is dropped from state as soon as the dialog closes - The scope picker only offers permissions the signed-in employee holds, so the UI cannot even express a key that outranks its creator - Revoking asks for confirmation naming the key, since it takes effect immediately and cannot be undone - Create and revoke controls are gated on apiKeys:create / apiKeys:revoke, deriving the ability from the session the way the sidebar does Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The task described keys as an admin/manager feature over a shared company pool. They are instead an extension of the employee who creates one, so the model now follows ownership rather than company membership. - apiKeys read/create/revoke moved into baseEmployee: every employee manages their own keys, and guest still holds none - Listing is filtered by employee, so nobody sees another person's keys — admins included - Revoke resolves by owner; a key belonging to someone else answers 404 rather than 403, so the endpoint cannot be used to probe for key IDs - Name uniqueness is per employee, not per company Scopes keep narrowing-only semantics, and the role is still resolved at request time, so a key never outlives or outranks its owner's access. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
805ce09 to
d1e0e9b
Compare
The middleware existed but no route used it, so every request carrying a fxr_ token hit authenticateEmployee, failed jwtVerify and came back as auth_jwt_invalid. The feature shipped a credential that opened nothing. authenticateEmployeeOrApiKey picks the credential by the shape of the token instead of trying one and falling back, so a bad key reports why it failed rather than masquerading as a bad JWT. Both paths populate request.user and request.ability identically, so requirePermission is unaware of the difference. Applied to the domain modules only. Routes tied to a human session and the api-keys routes themselves stay JWT-only, so a leaked key cannot mint more keys or perpetuate itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Revoking always failed with 400. The param schema required exactly 25 characters, a number taken from the varchar(25) column width, but cuid2 generates 24, so no real id ever passed validation. Switched to z.string().cuid2(), the convention already used in auth.ts, account.ts and models.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…IXR-47, FIXR-48) Creation moves from a centered dialog to a right side sheet, matching the service order form. While the secret is on screen it exists nowhere else, so closing by ESC or by clicking outside is blocked during that step and only the explicit button dismisses it. Visual and copy fixes from review: - Form fields had no breathing room, spacing widened - Page subtitle trimmed to one line; the detail about permissions and the secret already appears in the form and in the reveal alert - No icon in the creation heading - Status badges no longer use the primary colour. Revoked keeps a hint of destructive since it is a deliberate action; expired stays muted - The revoke icon now follows the destructive colour of its label - The reveal alert was destructive, which reads as failure. The user just succeeded and needs to act, so it is now a warning in amber, which meant adding a warning variant to the Alert component - Em dashes removed from every string in the feature Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
risixdzn
left a comment
There was a problem hiding this comment.
Ajustes
API
Ainda dá o mesmo erro na hora de revogar as chaves
500 DELETE /companies/acme/api-keys/raegm5rlhmw2qdb5kdz2f4f2
{"status":500,"error":"Internal Server Error","message":"[\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"subdomain\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n }\n]","code":"internal_error","data":{"message":"[\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"subdomain\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n }\n]","stack":"ZodError: [\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\","}}Não consigo acessar os endpoints da aplicação com o token gerado.
O comportamento desejado é que o token possa ser utilizado em TODOS endpoints, da mesma forma que um usuário interagiria com seu Bearer + refreshToken
O request seguinte
curl /account/ \
--header 'Accept: application/json' \
--header 'Authorization: Bearer fxr_Ytr-MUHCCu3P_SvcQmaiJVBOArSwxcb8m9I--NwskvikComL2xexTlcU'Tem essa resposta (mesma coisa para todos outros endpoints que testei)
{
"status": 401,
"error": "Unauthorized",
"message": "Authorization token is invalid or expired.",
"code": "auth_jwt_invalid",
"data": null
}Front
Ficou legal a Sheet no formulário de criação mas acho que ficaria melhor se fosse replicado o mesmo comportamento do form de nova ordem de serviço.
Uma rota /new que usa o conceito de parallel routes do NextJS, interceptada por um dialog / vaul headless que aparece com o formulário acima da página atual.
Também falta feedback de Toast na hora de criar.
Validating the id as cuid2 was only half the fix. The route schema declared params as apiKeyIdParamsSchema, which only knows apiKeyId, but the route is mounted under the /companies/:subdomain/api-keys prefix. Fastify replaces request.params with whatever the validator returns, and zod strips every key it does not know about, so subdomain was gone by the time the handler ran and getCompanyNestedDataSchema.parse(request.params) threw. Revoke kept failing, now with a 500 instead of a 400. The params schema now declares both, the way every other nested route already does. This also fixes the OpenAPI spec, which was missing the subdomain path param for this endpoint.
The side sheet is replaced by the same ResponsiveDialogDrawer the service
order and employee forms use, so every creation form opens the same way:
a centered dialog on desktop, a vaul drawer on mobile, with the icon
header.
That component only served intercepted route modals, so it now takes
optional open/onOpenChange, a trigger and a dismissible flag. Without
them it keeps closing through router.back(), leaving the existing route
modals untouched.
While the secret is on screen it exists nowhere else, so the reveal step
passes dismissible={false}: escape, outside clicks and the close button
are blocked, and only the explicit button closes.
It was the only package pointing at it through the catalog, which resolves to a version instead of the workspace, so the package was never linked and the tsconfig "extends" did not resolve from there.
…FIXR-45) A token is fxr_<prefix>_<secret> and both parts are base64url, an alphabet that includes "_". Parsing split the token on that character and required exactly three pieces, so any key that happened to draw a "_" was rejected: measured over 20k generated keys, 57% of them could never authenticate. The dispatch middleware reads a failed parse as "this is not an API key" and falls through to the JWT path, which is why the symptom was auth_jwt_invalid rather than an API key error. Both parts have a fixed encoded length, 12 and 43 characters, so the separator between them is located by offset instead of by splitting. Existing keys keep working, since neither the layout nor the encoding changes. The spec needs vitest to transform an import of @fixr/env, hence the config: the server had the runner wired but no suite yet.
Implementa chaves de API para acesso programático ao Fixr (FIXR-35), cobrindo schema, API, autenticação e tela de gerenciamento.
Modelo de acesso
As chaves são user-scoped: pertencem a quem as criou e carregam exatamente as permissões do cargo desse usuário.
scopes[]é opcional e só restringe — nunca amplia além do que o criador possui. Vazio significa "herda o cargo como está".404em vez de403, para o endpoint não virar um oráculo de IDs.apiKeys:read|create|revokeentrou embaseEmployee, então é self-service para todo funcionário (guestsegue sem).Endpoints
GET/companies/:subdomain/api-keys— lista as chaves do próprio usuário (só o prefixo público)POST/companies/:subdomain/api-keys— cria e devolve o secret uma única vezDELETE/companies/:subdomain/api-keys/:id— revoga (soft delete, preserva auditoria)Autenticação via
x-api-keyouAuthorization: Bearer <token>.Decisão técnica: HMAC-SHA256 em vez de bcrypt
A issue pedia bcrypt. Bcrypt é lento de propósito, o que é correto para senha de usuário (baixa entropia), mas custa ~100ms em toda requisição autenticada — inviável numa API de integração e um vetor de DoS.
Chaves têm 256 bits de entropia aleatória: não há dicionário a atacar, então a lentidão não compra segurança. HMAC-SHA256 com pepper (
API_KEY_SECRET) mantém a verificação em microssegundos e torna um dump da tabela inútil sem a env var. É o que GitHub, Stripe e AWS fazem.Está isolado em
hashApiKeySecret/verifyApiKeySecret, em um único arquivo, caso a decisão mude.Performance e segurança
prefixcom índice único: uma query por autenticação, e só então a verificação do secret em tempo constante (timingSafeEqual).last_used_atcom throttle de 60s viaSET NXno Redis — sem isso, toda chamada de leitura viraria uma escrita, com uma linha sob contenção constante.Frontend
/dashboard/:subdomain/settings/api-keys, seguindo o módulo de funcionários (react-query + TanStack Table + dialogs). O seletor de permissões só oferece o que o usuário logado possui, então a UI nem consegue expressar uma chave que supere o criador.Correções de bugs pré-existentes
Três defeitos que bloqueavam o build e vieram de commits anteriores nesta base. Estão em commits separados para facilitar cherry-pick:
3075673— marcadores de merge (<<<<<<<) commitados emaccount/repositories, quebrando o build do server2a8374c—use-as-ref.tsindentado com espaços, quebrando o biome check que roda antes donext buildbun run db:migratenão roda num banco limpo. A migration0012_wooden_goliathfazRENAME COLUMN uploads.purposeeMODIFY model_images.upload_id— colunas que nenhuma migration anterior cria. Ela foi gerada contra um banco sincronizado viadb:push. Isso quebra CI e qualquer ambiente novo, e merece uma issue própria.Setup necessário
Nova env var obrigatória no server (já documentada no
.env.example):Validação
15.9 kB/openapi.jsonFora de escopo
Rate limiting foi extraído para a FIXR-44, em branch própria. É uma capacidade transversal a toda a API e não deveria entrar como efeito colateral desta feature.
Testes automatizados. A cobertura que eu priorizaria: scope não pode escalar privilégio, e chave revogada/expirada é recusada.