Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,15 @@ CLERK_SECRET_KEY=sk_test_xxxxxxxx

# Clerk webhook: Dashboard > Webhooks > your endpoint > Signing Secret (whsec_...)
CLERK_WEBHOOK_SIGNING_SECRET=whsec_xxxxxxxx

# SPIKE (ADR-033, branch spike/better-auth only)
BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
BETTER_AUTH_URL=http://localhost:3000
# Optional: without RESEND_API_KEY the e-mails (OTP, invitation) are printed in the server console
RESEND_API_KEY=
MAIL_FROM=
# Optional social login (redirect: http://localhost:3000/api/auth/callback/<provider>)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
25 changes: 25 additions & 0 deletions SPIKE-BETTER-AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Spike: Better Auth (ADR-033) — NÃO MERGEAR EM `develop`

Branch de investigação. Nada aqui vai para produção: o merge em `develop` faz deploy e a migration `0005_better_auth_spike` não deve rodar no Neon `production`.

## O que foi montado
- `src/db/auth-schema.ts` + `drizzle/0005_better_auth_spike.sql`: tabelas `ba_*` (user, session, account, verification, organization, member, invitation), geradas por `npx auth@latest generate` e só renomeadas com prefixo. Tabelas públicas do app intocadas.
- `src/server/ba/auth.ts`: `createAuth()`/`getAuth()` — Drizzle adapter `pg`, `emailOTP`, `organization` (convite por e-mail), Google/GitHub condicionais por env, hook que ativa a primeira organização no login, `nextCookies()` por último.
- `src/server/ba/context.ts`: `requireBaWorkspaceContext()` com o mesmo contrato de `requireWorkspaceContext()`; espelha em `users`/`workspaces` reutilizando os upserts de `clerk-sync.ts` (ids com prefixo `ba:` nas colunas `clerk_*`, para não migrar tabelas públicas no spike).
- `src/app/api/auth/[...all]/route.ts`, `src/lib/ba/auth-client.ts`, páginas `/spike`, `/spike/sign-in`, `/spike/accept/[id]`.
- Testes: `src/server/ba/auth.smoke.test.ts` (PGlite, Better Auth real) e `context.test.ts`.

## Como testar localmente (banco `development` do Neon!)
1. No `.env.local`: `BETTER_AUTH_SECRET` (`openssl rand -base64 32`), `BETTER_AUTH_URL=http://localhost:3000`; opcionais `RESEND_API_KEY`, `GOOGLE_*`, `GITHUB_*` (callback `http://localhost:3000/api/auth/callback/<provider>`).
2. Conferir que `DATABASE_URL` aponta para `development`; `npm run db:migrate` (ou `npx drizzle-kit migrate`).
3. `npm run dev`; abrir `/spike/sign-in`. Sem `RESEND_API_KEY`, o código e o link de convite saem no console do servidor.

## Critérios (ADR-033)
| | Critério | Estado |
|---|---|---|
| a | Login por código + Google + GitHub | Código: validado em teste (PGlite). Google/GitHub: configurado, **não testado** (precisa de credenciais OAuth) |
| b | Funciona com neon-http | Pelo código do adapter, `pg` só usa transação com `transaction: true` (desligado) → provável OK. **Não testado contra Neon** |
| c | Organizações ↔ workspaces, admin/member | Validado em teste (owner/admin → admin; member). Ponte `requireBaWorkspaceContext()` só typecheck + teste de papel; **rodar no app** |
| d | Convite por e-mail aceito por 2º usuário | Fluxo validado em teste (inclui recusa de e-mail diferente). Envio real via Resend **não testado** |
| e | Sessão lida em `getTenant()` sem mexer no resto | Contrato igual; páginas `/spike` usam a ponte. **Rodar no app** |
| f | Estimativa de migração | Ver ADR-033 (atualizado após o teste manual) |
2 changes: 1 addition & 1 deletion drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ try {

export default defineConfig({
dialect: "postgresql",
schema: "./src/db/schema.ts",
schema: ["./src/db/schema.ts", "./src/db/auth-schema.ts"],
out: "./drizzle",
dbCredentials: { url: process.env.DATABASE_URL ?? "" },
strict: true,
Expand Down
91 changes: 91 additions & 0 deletions drizzle/0005_better_auth_spike.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
CREATE TABLE "ba_account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp,
"refresh_token_expires_at" timestamp,
"scope" text,
"password" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ba_invitation" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"email" text NOT NULL,
"role" text,
"status" text DEFAULT 'pending' NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"inviter_id" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ba_member" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"user_id" text NOT NULL,
"role" text DEFAULT 'member' NOT NULL,
"created_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ba_organization" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"slug" text NOT NULL,
"logo" text,
"created_at" timestamp NOT NULL,
"metadata" text,
CONSTRAINT "ba_organization_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "ba_session" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
"active_organization_id" text,
CONSTRAINT "ba_session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "ba_user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"image" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "ba_user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "ba_verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "ba_account" ADD CONSTRAINT "ba_account_user_id_ba_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."ba_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ba_invitation" ADD CONSTRAINT "ba_invitation_organization_id_ba_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."ba_organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ba_invitation" ADD CONSTRAINT "ba_invitation_inviter_id_ba_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."ba_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ba_member" ADD CONSTRAINT "ba_member_organization_id_ba_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."ba_organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ba_member" ADD CONSTRAINT "ba_member_user_id_ba_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."ba_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ba_session" ADD CONSTRAINT "ba_session_user_id_ba_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."ba_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "account_userId_idx" ON "ba_account" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "invitation_organizationId_idx" ON "ba_invitation" USING btree ("organization_id");--> statement-breakpoint
CREATE INDEX "invitation_email_idx" ON "ba_invitation" USING btree ("email");--> statement-breakpoint
CREATE INDEX "member_organizationId_idx" ON "ba_member" USING btree ("organization_id");--> statement-breakpoint
CREATE INDEX "member_userId_idx" ON "ba_member" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "session_userId_idx" ON "ba_session" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "verification_identifier_idx" ON "ba_verification" USING btree ("identifier");
Loading
Loading