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

Aula 6: Completando a Autenticação - Login e Perfil

Módulo 3ā±ļø 35 minutosšŸ“š IntermediĆ”rio

šŸŽÆ Objetivo da Aula

Vamos completar o sistema de autenticação implementando as rotas de login via magic link, logout e obtenção do perfil do usuÔrio.

šŸ“‹ Rotas que vamos criar

  • GET /auth-links/authenticate - Autenticar via magic link
  • POST /sign-out - Fazer logout
  • GET /me - Obter perfil do usuĆ”rio

šŸš€ Implementando as Rotas

1. Rota de Autenticação via Magic Link

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

2. Rota de Logout

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

3. Rota de Perfil do UsuƔrio

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

4. Atualizando o Middleware de Auth

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

5. Criando um UsuƔrio de Teste

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

6. Atualizando o Servidor

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

7. Testando o Fluxo Completo

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

8. Adicionando Scripts ao package.json

// 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"
  }
}

šŸŽÆ Próximos Passos

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!