Implemente autenticação JWT e Magic Links no ElysiaJS com Bun.js! Guia completo do sistema de login do Restaurantix, incluindo cookies e 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!