🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →

Aula 5: Primeiras Rotas - CSRF Token e Autenticação

Módulo 2⏱️ 30 minutos📚 Intermediário

🎯 Objetivo da Aula

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.

📋 Rotas que vamos criar

  • GET /csrf-token - Obter token CSRF
  • POST /auth-links/authenticate - Enviar link de autenticação

🚀 Implementando as Rotas

1. Rota do Token CSRF

Primeiro, 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(),
  };
});

2. Configuração do Banco de Dados

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";

3. Conexão com o Banco

// 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 });

4. Configuração de Email

// 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);
}

5. Variáveis de Ambiente

// 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);

6. Rota de Envio de Link de Autenticação

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" }),
    }),
  },
);

7. Atualizando o Servidor

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");
});

8. Configuração do Drizzle

// 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",
  },
});

9. Testando as Rotas

# 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"}'

🎯 Próximos Passos

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!