Vamos completar o sistema de autenticação implementando as rotas de login via magic link, logout e obtenção do perfil do usuÔrio.
GET /auth-links/authenticate
- Autenticar via magic linkPOST /sign-out
- Fazer logoutGET /me
- Obter perfil do usuÔrioEsta é a rota mais complexa - ela valida o código do magic link e autentica o usuÔrio:
// src/http/routes/authenticate-from-link.ts import { t } from "elysia"; import { db } from "@/application/infra/db/connection"; import { authLinks, users } from "@/application/infra/db/schema"; import { eq, and, gt } from "drizzle-orm"; import dayjs from "dayjs"; import { auth } from "../auth"; export const authenticateFromLink = auth.get( "/auth-links/authenticate", async ({ query, signUser, set }) => { const { code, redirect } = query; // Buscar link de autenticação vÔlido const [authLinkFound] = await db .select({ id: authLinks.id, userId: authLinks.userId, expiresAt: authLinks.expiresAt, }) .from(authLinks) .where( and( eq(authLinks.code, code), gt(authLinks.expiresAt, new Date()) // Verificar se não expirou ) ); if (!authLinkFound) { throw new Error("Invalid or expired authentication link"); } // Buscar dados do usuÔrio const [user] = await db .select({ id: users.id, name: users.name, email: users.email, role: users.role, }) .from(users) .where(eq(users.id, authLinkFound.userId)); if (!user) { throw new Error("User not found"); } // Gerar JWT e salvar em cookie const token = await signUser(user.id); // Definir cookie de autenticação set.cookie = { auth: { value: token, httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 60 * 60 * 24 * 7, // 7 dias path: "/", }, }; // Remover link usado (one-time use) await db.delete(authLinks).where(eq(authLinks.id, authLinkFound.id)); // Redirecionar para o frontend set.redirect = redirect; }, { query: t.Object({ code: t.String(), redirect: t.String(), }), } );
Rota simples para fazer logout removendo o cookie de autenticação:
// src/http/routes/sign-out.ts import { auth } from "../auth"; export const signOut = auth.post("/sign-out", async ({ set }) => { // Remover cookie de autenticação set.cookie = { auth: { value: "", httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 0, // Expira imediatamente path: "/", }, }; return { message: "Signed out successfully" }; });
Rota protegida que retorna os dados do usuƔrio autenticado:
// src/http/routes/get-profile.ts import { db } from "@/application/infra/db/connection"; import { users } from "@/application/infra/db/schema"; import { auth } from "../auth"; import { eq } from "drizzle-orm"; export const getProfile = auth.get("/me", async ({ getCurrentUser }) => { const { sub: userId } = await getCurrentUser(); const [user] = await db .select({ id: users.id, name: users.name, email: users.email, role: users.role, createdAt: users.createdAt, }) .from(users) .where(eq(users.id, userId)); if (!user) { throw new Error("User not found"); } return { user }; });
Vamos melhorar o middleware de autenticação para trabalhar com cookies:
// src/http/auth.ts (atualizado) import { Elysia } from "elysia"; import { jwt } from "@elysiajs/jwt"; import { cookie } from "@elysiajs/cookie"; export const auth = new Elysia({ name: "auth" }) .use( jwt({ name: "jwt", secret: process.env.JWT_SECRET || "your-secret-key", }) ) .use(cookie()) .derive(({ jwt, cookie }) => { return { getCurrentUser: async () => { const token = cookie.auth; if (!token) { throw new Error("Unauthorized"); } const payload = await jwt.verify(token); if (!payload) { throw new Error("Unauthorized"); } return payload as { sub: string }; }, signUser: async (userId: string) => { return await jwt.sign({ sub: userId }); }, }; }) .macro(({ onBeforeHandle }) => ({ isSignedIn: (enabled: boolean = true) => { if (!enabled) return; onBeforeHandle(async ({ getCurrentUser, error }) => { try { await getCurrentUser(); } catch { return error(401, "Unauthorized"); } }); }, }));
Vamos criar um script para inserir um usuƔrio de teste no banco:
// src/application/infra/db/seed.ts import { db } from "./connection"; import { users } from "./schema"; async function seed() { console.log("š± Seeding database..."); // Inserir usuĆ”rio de teste await db.insert(users).values({ name: "JoĆ£o Silva", email: "joao@example.com", role: "manager", }); console.log("ā Database seeded successfully!"); process.exit(0); } seed().catch((error) => { console.error("ā Error seeding database:", error); process.exit(1); });
Vamos adicionar as novas rotas ao servidor:
// src/http/server.ts (atualizado) 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"; import { authenticateFromLink } from "./routes/authenticate-from-link"; import { signOut } from "./routes/sign-out"; import { getProfile } from "./routes/get-profile"; const app = new Elysia() .use(cors()) .use(csrf) .use(auth) .use(getCsrfToken) .use(sendAuthLink) .use(authenticateFromLink) .use(signOut) .use(getProfile) .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"); });
# 1. Executar seed para criar usuÔrio bun run src/application/infra/db/seed.ts # 2. Executar servidor bun run src/http/server.ts # 3. Obter token CSRF curl http://localhost:3000/csrf-token # 4. Solicitar magic link curl -X POST http://localhost:3000/auth-links/authenticate \ -H "Content-Type: application/json" \ -H "x-csrf-token: SEU_TOKEN_CSRF" \ -d '{"email":"joao@example.com"}' # 5. Copiar o link do console e acessar no navegador # O link serÔ algo como: # http://localhost:3000/auth-links/authenticate?code=...&redirect=... # 6. Após autenticação, testar perfil curl http://localhost:3000/me \ -H "Cookie: auth=SEU_JWT_TOKEN" # 7. Testar logout curl -X POST http://localhost:3000/sign-out \ -H "Cookie: auth=SEU_JWT_TOKEN"
// package.json (scripts) { "scripts": { "dev": "bun --watch src/http/server.ts", "build": "bun build src/http/server.ts --target=bun", "start": "NODE_ENV=production bun src/http/server.ts", "generate": "drizzle-kit generate", "migrate": "drizzle-kit migrate", "studio": "drizzle-kit studio", "seed": "bun run src/application/infra/db/seed.ts" } }
Na próxima aula, vamos começar a implementar as rotas de restaurantes: add-restaurant e get-managed-restaurant.
š” Dica: O JWT Ć© armazenado em cookie httpOnly para maior seguranƧa contra ataques XSS.
ā Checkpoint: Sistema de autenticação completo funcionando com magic links, JWT e cookies seguros!