Implemente autenticação completa em Bun.js: login via magic link, JWT, logout e perfil de usuário. Guia prático com ElysiaJS e cookies seguros. Domine a segurança backend!
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";
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Aula 6 | CrazyStack',
description: 'Curso completo sobre aula 6. Aprenda técnicas práticas e melhores práticas para desenvolvimento moderno. Guia atualizado 2025.',
keywords: ['aula', '6', 'programação', 'desenvolvimento', 'tutorial', 'crazystack'],
openGraph: {
title: 'Aula 6',
description: 'Curso completo sobre aula 6. Aprenda técnicas práticas e melhores práticas para desenvolvimento moderno. Guia atualizado 2025.',
type: 'article'
},
};
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!