Vamos implementar as duas primeiras rotas do sistema: obtenção do token CSRF e envio de link de autenticação por email. Estas são as bases do sistema de segurança.
GET /csrf-token
- Obter token CSRFPOST /auth-links/authenticate
- Enviar link de autenticaçãoPrimeiro, vamos criar a rota que fornece o token CSRF para o frontend:
// src/http/routes/get-csrf-token.ts import { csrf } from "../csrf"; export const getCsrfToken = csrf.get("/csrf-token", ({ csrfToken }) => { return { csrfToken: csrfToken(), }; });
Antes da rota de autenticação, precisamos configurar o banco. Vamos criar os schemas necessários:
// src/application/infra/db/schema/users.ts import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { createId } from "@paralleldrive/cuid2"; export const users = pgTable("users", { id: text("id") .$defaultFn(() => createId()) .primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), phone: text("phone"), role: text("role", { enum: ["manager", "customer"] }) .default("customer") .notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), });
// src/application/infra/db/schema/auth-links.ts import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { createId } from "@paralleldrive/cuid2"; import { users } from "./users"; export const authLinks = pgTable("auth_links", { id: text("id") .$defaultFn(() => createId()) .primaryKey(), code: text("code").notNull().unique(), userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), expiresAt: timestamp("expires_at").notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), });
// src/application/infra/db/schema/index.ts export * from "./users"; export * from "./auth-links";
// src/application/infra/db/connection.ts import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; const connectionString = process.env.DATABASE_URL || "postgresql://localhost:5432/restaurantix"; export const connection = postgres(connectionString); export const db = drizzle(connection, { schema });
// src/application/infra/messaging/factories/emailFactory.ts import { Resend } from "resend"; export interface EmailService { sendEmail(to: string, subject: string, html: string): Promise<void>; } class ResendEmailService implements EmailService { private resend: Resend; constructor(apiKey: string) { this.resend = new Resend(apiKey); } async sendEmail(to: string, subject: string, html: string): Promise<void> { await this.resend.emails.send({ from: "Restaurantix <noreply@restaurantix.com>", to, subject, html, }); } } export function makeEmailService(): EmailService { const apiKey = process.env.RESEND_API_KEY; if (!apiKey) { throw new Error("RESEND_API_KEY is required"); } return new ResendEmailService(apiKey); }
// src/env.ts import { z } from "zod"; export const envSchema = z.object({ DATABASE_URL: z.string().url().min(1), API_BASE_URL: z.string().url().min(1), AUTH_REDIRECT_URL: z.string().url().min(1), JWT_SECRET: z.string().min(1), RESEND_API_KEY: z.string().min(1).optional(), }); export const env = envSchema.parse(process.env);
Agora a rota mais complexa - envio do link de autenticação:
// src/http/routes/send-auth-link.ts import { t } from "elysia"; import { db } from "@/application/infra/db/connection"; import { authLinks, users } from "@/application/infra/db/schema"; import { eq } from "drizzle-orm"; import { createId } from "@paralleldrive/cuid2"; import { env } from "@/env"; import dayjs from "dayjs"; import { csrf } from "../csrf"; import { makeEmailService } from "@/application/infra/messaging/factories/emailFactory"; export const sendAuthLink = csrf.post( "/auth-links/authenticate", async ({ body }) => { const { email } = body; // Buscar usuário pelo email const [userFound] = await db .select() .from(users) .where(eq(users.email, email)); if (!userFound) { // Por segurança, sempre retornamos sucesso return { message: "Authentication link sent", }; } // Gerar código único const code = createId(); // Salvar link de autenticação no banco await db.insert(authLinks).values({ userId: userFound.id, code, expiresAt: dayjs().add(1, "days").toDate(), }); // Construir URL de autenticação const authLink = new URL(`auth-links/authenticate`, env.API_BASE_URL); authLink.searchParams.set("code", code); authLink.searchParams.set("redirect", env.AUTH_REDIRECT_URL); // Enviar email se configurado if (env.RESEND_API_KEY) { try { const emailSender = makeEmailService(); await emailSender.sendEmail( email, "Authentication Link", ` <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;"> <h1 style="color: #4a5568;">Welcome to Restaurantix</h1> <p>Click the button below to sign in to your account:</p> <a href="${authLink.toString()}" style="display: inline-block; background-color: #3182ce; color: white; font-weight: bold; padding: 12px 24px; border-radius: 4px; text-decoration: none;" > Sign In </a> <p style="margin-top: 24px; color: #718096; font-size: 14px;"> If you didn't request this link, you can safely ignore this email. </p> </div> `, ); } catch (error) { console.error("Failed to send authentication email:", error); } } console.log("Auth link:", authLink.toString()); return { message: "Authentication link sent", }; }, { body: t.Object({ email: t.String({ format: "email" }), }), }, );
Agora vamos adicionar as rotas ao servidor principal:
// src/http/server.ts import { Elysia } from "elysia"; import { cors } from "@elysiajs/cors"; import { auth } from "./auth"; import { csrf } from "./csrf"; import { getCsrfToken } from "./routes/get-csrf-token"; import { sendAuthLink } from "./routes/send-auth-link"; const app = new Elysia() .use(cors()) .use(csrf) .use(auth) .use(getCsrfToken) .use(sendAuthLink) .get("/", () => ({ message: "Restaurantix API" })) .onError(({ error, code, set }) => { console.error(error); switch (code) { case "VALIDATION": set.status = error.status; return error.toResponse(); default: set.status = 500; console.error(error); return new Response(null, { status: 500 }); } }); app.listen(3000, () => { console.log("Server is running on port 3000"); });
// drizzle.config.ts import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./src/application/infra/db/schema/index.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL || "postgresql://localhost:5432/restaurantix", }, });
# 1. Gerar migrations bun run drizzle-kit generate # 2. Executar migrations bun run drizzle-kit migrate # 3. Executar servidor bun run src/http/server.ts # 4. Testar CSRF token curl http://localhost:3000/csrf-token # 5. Testar envio de auth link (precisa do token CSRF) curl -X POST http://localhost:3000/auth-links/authenticate \ -H "Content-Type: application/json" \ -H "x-csrf-token: SEU_TOKEN_AQUI" \ -d '{"email":"test@example.com"}'
Na próxima aula, vamos implementar as rotas de autenticação completa: authenticate-from-link, sign-out e get-profile.
💡 Dica: O sistema de magic link é mais seguro que senhas tradicionais e oferece melhor UX.
✅ Checkpoint: Você implementou as primeiras rotas do sistema com segurança CSRF e autenticação por email!