From ff12f0d4a137d14297219e471b69759da4e1b939 Mon Sep 17 00:00:00 2001 From: Ronald Tomaz Date: Tue, 17 Mar 2026 11:20:29 -0300 Subject: [PATCH] test: refactor auth integration tests to validate httpOnly cookies --- .github/copilot-instructions.md | 131 ------------- CLAUDE.md | 60 ++++++ package-lock.json | 31 +++ package.json | 2 + src/__tests__/auth.integration.test.ts | 211 +++++++++++++-------- src/controllers/auth.controller.ts | 59 ++++-- src/lib/create-server.ts | 5 +- src/middlewares/authenticate.middleware.ts | 42 ++-- src/routes/auth.routes.ts | 5 + src/server.ts | 37 ++-- 10 files changed, 318 insertions(+), 265 deletions(-) delete mode 100644 .github/copilot-instructions.md create mode 100644 CLAUDE.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index e5541a2..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,131 +0,0 @@ -# Copilot Instructions - Task Manager API - -## Architecture Overview - -This is an Express.js REST API with a **layered architecture**: -- **Routes** → **Controllers** → **Services** → **Repositories** → **Prisma Client** - -Key principle: Business logic lives in **Services**, data access in **Repositories**. Controllers are thin, handling only HTTP concerns (parsing, status codes). - -### Data Flow Example -Route defines endpoint and middleware → Controller receives request → validates input with Zod → calls Service → Service executes business logic, throws custom errors → Repository handles Prisma queries → Response returned through middleware (error handler catches failures). - -## Critical Patterns - -### 1. Dependency Injection (Routes Layer) -All dependencies instantiated in route files, not globally. Example from [src/routes/auth.routes.ts](src/routes/auth.routes.ts): -```typescript -const authRepository = new AuthRepository() -const authService = new AuthService(authRepository) -router.post('/auth/register', authController.signUp) -``` -**Pattern**: Services receive repositories as constructor args; controllers receive services. - -### 2. Error Handling -- Custom errors: [BadRequestError](src/errors/bad-request-error.ts), [UnauthorizedError](src/errors/unauthorized-error.ts) -- Catch all at [error.handler.middleware.ts](src/middlewares/error.handler.middleware.ts) -- Zod errors automatically formatted by `formatZodError()` -- **Don't use `throw new Error()`** – use custom error classes to enable proper HTTP status mapping - -### 3. Authentication & Authorization -- **authenticate** middleware ([authenticate.middleware.ts](src/middlewares/authenticate.middleware.ts)): Verifies JWT, attaches `req.user` with `{id, email, role}` -- **authorize(roles)** middleware ([authorize.middleware.ts](src/middlewares/authorize.middleware.ts)): Checks if user role in allowed list -- **Record-level authorization** in Services: Members can only update own tasks (checked in [tasks.service.ts](src/services/tasks.service.ts) `update()` method) - -### 4. Input Validation (Zod) -All validator schemas in [src/validators/](src/validators/). Use `schema.parse(input)` in controllers – throws ZodError if invalid (handled by error middleware). -Example: [signUpSchema](src/validators/auth.validator.ts) enforces email format, min password length. - -### 5. Repository Pattern -Repositories ([src/repositories/](src/repositories/)) encapsulate all Prisma queries. Services never call prisma directly. -Example: [AuthRepository.register()](src/repositories/auth.repository.ts) handles `prisma.user.create()`. - -### 6. Database & Migrations -- **Schema**: [prisma/schema.prisma](prisma/schema.prisma) – defines models (User, Team, Task, TeamMember) and relations -- **Generated types**: `/generated/prisma/client.ts` (auto-generated from schema, do not edit) -- **Workflow**: Edit schema → `npm run db:migrate "description"` → generates migration in `prisma/migrations/` -- Use `npm run db:studio` to visually inspect/manage data - -### 7. Request Type Augmentation -Express Request extended with user context ([src/types/express.d.ts](src/types/express.d.ts)): -```typescript -declare namespace Express { - export interface Request { - user?: { id: string; email: string; role: 'ADMIN' | 'MEMBER' } - } -} -``` -Access in controllers via `req.user` after auth middleware. - -## Development Workflow - -| Task | Command | -|------|---------| -| **Dev server** (watch mode) | `npm run dev` – runs `tsx watch .` | -| **Build** | `npm run build` – compiles TypeScript to `dist/` | -| **Migrate DB** | `npm run db:migrate` – creates migration from schema changes | -| **Generate Prisma** | `npm run db:generate` – regenerates `/generated/prisma` | -| **View DB** | `npm run db:studio` – opens Prisma Studio UI | - -**Setup checklist**: Install → `.env` config (JWT_SECRET, DATABASE_URL) → `npm run db:generate` → `npm run db:migrate dev` → `npm run dev` - -## Code Patterns & Conventions - -### Service Method Signature -Services take a structured input type (usually from validator): -```typescript -async register(data: RegisterTaskInput) { ... } -// or with context: -async update({ taskId, data, userId, userRole }: { ... }) { ... } -``` - -### Controller Pattern -1. Parse request body with Zod schema -2. Extract params/user from request -3. Call service method -4. Return response with status code -```typescript -async signUp = async (req: Request, res: Response) => { - const { email, password, name, role } = signUpSchema.parse(req.body) - const user = await this.authService.register({ email, password, name, role }) - const token = this.JWTService.generateToken({ userId: user.id, ... }) - return res.status(201).json({ user, token }) -} -``` - -### Authorization in Services -Check role-based permissions: -```typescript -if (userRole === 'MEMBER' && task.assigned_to !== userId) { - throw new UnauthorizedError('You do not have permission...') -} -``` - -## Key Files Reference - -| File | Purpose | -|------|---------| -| [src/server.ts](src/server.ts) | Express app setup, middleware registration | -| [prisma/schema.prisma](prisma/schema.prisma) | Data model definitions | -| [src/validators/](src/validators/) | Zod input schemas (auth, tasks, teams) | -| [src/services/](src/services/) | Business logic, error throwing | -| [src/repositories/](src/repositories/) | Prisma queries only | -| [src/controllers/](src/controllers/) | HTTP handling, validation triggers | -| [src/routes/](src/routes/) | Endpoint definitions, middleware stacking, DI | -| [src/middlewares/](src/middlewares/) | Auth, authorization, error handling | -| [src/errors/](src/errors/) | Custom error classes | -| [src/lib/prisma.ts](src/lib/prisma.ts) | Prisma client singleton | - -## When Adding Features - -1. **New endpoint**: Create validator schema → service method → controller method → route definition -2. **New model**: Update [schema.prisma](prisma/schema.prisma) → migrate → create repository methods -3. **New error type**: Extend Error class in [src/errors/](src/errors/), update error handler if special status needed -4. **Database query**: Add to repository, never call `prisma` from controller/service directly -5. **Authorization**: Check role in middleware stack; for record ownership, check in service logic - -## Role-Based Access Control - -- **ADMIN**: All operations on users, teams, and tasks -- **MEMBER**: View team tasks, edit only own assigned tasks -- Middleware checks endpoint access; service checks record ownership diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5377787 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Task Manager API - A RESTful API for managing tasks, teams, and users built with Express.js, TypeScript, PostgreSQL, and Prisma ORM. See [README.md](README.md) for full feature documentation. + +## Commands + +See @package.json for available npm commands for this project + +**Setup**: `npm install` → copy `.env.example` to `.env` → `npm run db:generate` → `npm run db:migrate` → `npm run dev` + +## Architecture + +**Layered architecture**: Routes → Controllers → Services → Repositories → Prisma Client + +- **Routes** (`src/routes/`): Endpoint definitions, middleware stacking, dependency injection +- **Controllers** (`src/controllers/`): HTTP concerns only - parse request with Zod, call service, return response +- **Services** (`src/services/`): Business logic, role-based authorization checks, throws custom errors +- **Repositories** (`src/repositories/`): All Prisma queries - services never call Prisma directly +- **Validators** (`src/validators/`): Zod schemas for input validation +- **Middlewares** (`src/middlewares/`): `authenticate` (JWT verification, attaches `req.user`), `authorize(roles)` (role checking), error handler +- **Errors** (`src/errors/`): Custom error classes (`BadRequestError`, `UnauthorizedError`, `TokenError`) + +### Dependency Injection Pattern + +Dependencies are instantiated in route files, not globally: + +```typescript +const authRepository = new AuthRepository() +const authService = new AuthService(authRepository) +const authController = new AuthController(authService) +``` + +### Error Handling + +Use custom error classes from `src/errors/` - never use bare `throw new Error()`. The error handler middleware maps custom errors to appropriate HTTP status codes. Zod errors are automatically formatted by `formatZodError()`. + +### Authorization + +- Endpoint-level: `authorize(['ADMIN'])` middleware +- Record-level: Services check ownership (e.g., members can only edit their own tasks) + +## Data Models + +Defined in `prisma/schema.prisma`. Generated client output: `generated/prisma/` + +- **User**: id, name, email, password, role (ADMIN/MEMBER) +- **Team**: id, name, description +- **TeamMember**: Composite key (user_id, team_id) +- **Task**: id, title, description, status (PENDING/IN_PROGRESS/COMPLETED), priority (HIGH/MEDIUM/LOW), assigned_to, teamId + +## Testing + +- **Unit tests**: `src/**/*.test.ts` - mocked dependencies, uses `vitest-mock-extended` +- **Integration tests**: `src/__tests__/*.integration.test.ts` - real PostgreSQL via Docker +- **Test factories**: `src/test/factories/` - helpers for creating test data +- **Integration test helpers**: `src/__tests__/helpers/` - auth helpers, DB reset, seeding diff --git a/package-lock.json b/package-lock.json index 1defbe1..443ff52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.2.3", "express": "^5.2.1", @@ -22,6 +23,7 @@ }, "devDependencies": { "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", @@ -1271,6 +1273,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -1935,6 +1947,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", diff --git a/package.json b/package.json index 72d075d..013255d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.2.3", "express": "^5.2.1", @@ -33,6 +34,7 @@ }, "devDependencies": { "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", diff --git a/src/__tests__/auth.integration.test.ts b/src/__tests__/auth.integration.test.ts index 6d64f7e..e1b6e4e 100644 --- a/src/__tests__/auth.integration.test.ts +++ b/src/__tests__/auth.integration.test.ts @@ -1,32 +1,33 @@ -import { prisma } from "../lib/prisma.js" -import app from "../lib/create-server.js" -import request from "supertest" -import jwt from "jsonwebtoken" -import bcrypt from "bcrypt" +import { prisma } from "../lib/prisma.js"; +import app from "../lib/create-server.js"; +import request from "supertest"; +import jwt from "jsonwebtoken"; +import bcrypt from "bcrypt"; import { makeRegisterUserData, - makeSignInUserData -} from "../test/factories/index.js" -import { SALT_ROUNDS } from "../services/auth.service.js" + makeSignInUserData, +} from "../test/factories/index.js"; +import { seedUser } from "./helpers/seed.js"; -const PATH_AUTH_REGISTER = "/auth/register" -const PATH_AUTH_LOGIN = "/auth/login" +const PATH_AUTH_REGISTER = "/auth/register"; +const PATH_AUTH_LOGIN = "/auth/login"; +const PATH_AUTH_LOGOUT = "/auth/logout"; describe("Integration: Auth", () => { describe("[POST] /auth/register", () => { it("should return 400 when email is already registered", async () => { - const payload = makeRegisterUserData({ email: "maria@gmail.com" }) + const payload = makeRegisterUserData({ email: "maria@gmail.com" }); - await prisma.user.create({ data: payload }) + await prisma.user.create({ data: payload }); const response = await request(app) .post(PATH_AUTH_REGISTER) .send(payload) - .expect(400) + .expect(400); - expect(response.body).toHaveProperty("message", "Email já cadastrado") - }) + expect(response.body).toHaveProperty("message", "Email já cadastrado"); + }); it("should return 400 when input data is invalid", async () => { const response = await request(app) @@ -35,132 +36,192 @@ describe("Integration: Auth", () => { role: "no-exists-role", name: "John Doe", email: "johndoe", - password: "1234" + password: "1234", }) - .expect(400) + .expect(400); expect(response.body).toMatchObject({ message: "Validation failed", errors: expect.arrayContaining([ expect.objectContaining({ field: "email" }), expect.objectContaining({ field: "password" }), - expect.objectContaining({ field: "role" }) - ]) - }) - }) + expect.objectContaining({ field: "role" }), + ]), + }); + }); it("should return 201 with user details on successful register", async () => { const payload = makeRegisterUserData({ name: "John Doe", email: "johndoe@gmail.com", - role: "ADMIN" - }) + role: "ADMIN", + }); const response = await request(app) .post(PATH_AUTH_REGISTER) .send(payload) - .expect(201) + .expect(201); - const newUser = await prisma.user.findFirst() - expect(newUser).not.toBeNull() + const newUser = await prisma.user.findFirst(); + expect(newUser).not.toBeNull(); expect(response.body.user).toMatchObject({ role: payload.role, email: payload.email, - name: payload.name - }) - }) + name: payload.name, + }); + }); - it("should return a signed JWT token with user on successful register", async () => { + it("should return a signed JWT token in httpOnly cookie on successful register", async () => { const payload = makeRegisterUserData({ email: "johndoe@gmail.com", - role: "ADMIN" - }) + role: "ADMIN", + }); - const { body } = await request(app) + const response = await request(app) .post(PATH_AUTH_REGISTER) .send(payload) - .expect(201) + .expect(201); + + const cookies = response.headers["set-cookie"]; + expect(cookies).toBeDefined(); + expect(Array.isArray(cookies)).toBe(true); - expect(body.token).toBeDefined() + const tokenCookie = cookies.find((cookie: string) => + cookie.startsWith("token="), + ); + expect(tokenCookie).toBeDefined(); + expect(tokenCookie).toContain("HttpOnly"); - const decoded = jwt.verify(body.token, process.env.JWT_SECRET as string) + const token = tokenCookie.split(";")[0].split("=")[1]; + const decoded = jwt.verify(token, process.env.JWT_SECRET as string); expect(decoded).toMatchObject({ - userId: body.user.id, + userId: response.body.user.id, email: "johndoe@gmail.com", - role: "ADMIN" - }) - }) + role: "ADMIN", + }); + }); it("should store password as a bcrypt hash, not plain text", async () => { - const user = makeRegisterUserData({ password: "123456" }) + const user = makeRegisterUserData({ password: "123456" }); - await request(app).post(PATH_AUTH_REGISTER).send(user).expect(201) + await request(app).post(PATH_AUTH_REGISTER).send(user).expect(201); const userFounded = await prisma.user.findUnique({ - where: { email: user.email } - }) + where: { email: user.email }, + }); - expect(userFounded?.password).not.toBe(user.password) + expect(userFounded?.password).not.toBe(user.password); expect(await bcrypt.compare(user.password, userFounded!.password)).toBe( - true - ) - }) - }) + true, + ); + }); + }); describe("[POST] /auth/login", () => { const user = { email: "maria@gmail.com", - password: "maria1234" - } + password: "maria1234", + }; beforeEach(async () => { - await prisma.user.create({ - data: makeRegisterUserData({ - ...user, - password: await bcrypt.hash(user.password, SALT_ROUNDS) - }) - }) - }) + await seedUser({ + email: user.email, + password: user.password, + }); + }); it("should return 400 when no account exists with given email", async () => { - const signInData = makeSignInUserData() + const signInData = makeSignInUserData(); const response = await request(app) .post(PATH_AUTH_LOGIN) .send(signInData) - .expect(400) + .expect(400); - expect(response.body.message).toBe("Credenciais inválidas") - }) + expect(response.body.message).toBe("Credenciais inválidas"); + }); it("should return 400 when password does not match", async () => { const response = await request(app) .post(PATH_AUTH_LOGIN) .send({ email: user.email, password: "wrong-password" }) - .expect(400) + .expect(400); - expect(response.body.message).toBe("Credenciais inválidas") - }) + expect(response.body.message).toBe("Credenciais inválidas"); + }); - it("should return 200 with user data and signed token on successful login", async () => { + it("should return 200 with user data and set httpOnly cookie on successful login", async () => { const response = await request(app) .post(PATH_AUTH_LOGIN) .send({ email: user.email, password: user.password }) - .expect(200) + .expect(200); const userFounded = await prisma.user.findUnique({ - where: { email: user.email } - }) + where: { email: user.email }, + }); expect(response.body.user).toMatchObject({ name: userFounded!.name, email: userFounded!.email, - role: userFounded!.role - }) + role: userFounded!.role, + }); + + const cookies = response.headers["set-cookie"]; + expect(cookies).toBeDefined(); + expect(Array.isArray(cookies)).toBe(true); + + const tokenCookie = cookies.find((cookie: string) => + cookie.startsWith("token="), + ); + expect(tokenCookie).toBeDefined(); + expect(tokenCookie).toContain("HttpOnly"); + }); + }); + + describe("[POST] /auth/logout", () => { + const user = { + email: "logout-test@gmail.com", + password: "test1234", + }; - expect(response.body.token).toBeDefined() - }) - }) -}) + beforeEach(async () => { + await seedUser({ + email: user.email, + password: user.password, + }); + }); + + it("should return 200 and clear the token cookie", async () => { + const loginResponse = await request(app) + .post(PATH_AUTH_LOGIN) + .send({ email: user.email, password: user.password }) + .expect(200); + + const loginCookies = loginResponse.headers["set-cookie"]; + expect(loginCookies).toBeDefined(); + + const logoutResponse = await request(app) + .post(PATH_AUTH_LOGOUT) + .set("Cookie", loginCookies) + .expect(200); + + const logoutCookies = logoutResponse.headers["set-cookie"]; + expect(logoutCookies).toBeDefined(); + + const clearedCookie = logoutCookies.find((cookie: string) => + cookie.startsWith("token="), + ); + expect(clearedCookie).toBeDefined(); + expect( + clearedCookie.includes("Max-Age=0") || + clearedCookie.includes("Expires=Thu, 01 Jan 1970"), + ).toBe(true); + }); + + it("should return 200 even without a token cookie", async () => { + await request(app).post(PATH_AUTH_LOGOUT).expect(200); + }); + }); +}); diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 648629d..53602c0 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,50 +1,69 @@ -import { Request, Response } from "express" +import { Request, Response } from "express"; -import { AuthService } from "../services/auth.service.js" -import { JWTService } from "../services/jwt.service.js" -import { signInSchema, signUpSchema } from "../validators/auth.validator.js" +import { AuthService } from "../services/auth.service.js"; +import { JWTService } from "../services/jwt.service.js"; +import { signInSchema, signUpSchema } from "../validators/auth.validator.js"; export class AuthController { constructor( private authService: AuthService, - private JWTService: JWTService + private JWTService: JWTService, ) {} signUp = async (req: Request, res: Response) => { - const { email, password, role, name } = signUpSchema.parse(req.body) + const { email, password, role, name } = signUpSchema.parse(req.body); const user = await this.authService.register({ email, password, name, - role - }) + role, + }); - const { password: _, ...userWithoutPassword } = user + const { password: _, ...userWithoutPassword } = user; const token = this.JWTService.generateToken({ userId: user.id, email: user.email, - role: user.role - }) + role: user.role, + }); - return res.status(201).json({ user: userWithoutPassword, token }) - } + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 24 * 60 * 60 * 1000, // 24h in ms + }); + + return res.status(201).json({ user: userWithoutPassword }); + }; signIn = async (req: Request, res: Response) => { - const { email, password } = signInSchema.parse(req.body) + const { email, password } = signInSchema.parse(req.body); const user = await this.authService.login({ email, - password - }) + password, + }); const token = this.JWTService.generateToken({ userId: user.id, email: user.email, - role: user.role - }) + role: user.role, + }); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 24 * 60 * 60 * 1000, // 24h in ms + }); + + return res.status(200).json({ user }); + }; - return res.status(200).json({ user, token }) - } + signOut = async (req: Request, res: Response) => { + res.clearCookie("token"); + return res.status(200).json(); + }; } diff --git a/src/lib/create-server.ts b/src/lib/create-server.ts index 97ad5b3..201e0dc 100644 --- a/src/lib/create-server.ts +++ b/src/lib/create-server.ts @@ -1,5 +1,6 @@ import express from "express" import cors from "cors" +import cookieParser from "cookie-parser" import { AuthRepository } from "../repositories/auth.repository.js" import { AuthService } from "../services/auth.service.js" import { JWTService } from "../services/jwt.service.js" @@ -15,8 +16,9 @@ import teamsRouter from "../routes/teams.routes.js" const app = express() +app.use(cors({ origin: "*", credentials: true })) +app.use(cookieParser()) app.use(express.json()) -app.use(cors({ origin: "*" })) const authRepository = new AuthRepository() const authService = new AuthService(authRepository) @@ -31,6 +33,7 @@ const taskController = new TasksController(taskService) app.post("/auth/register", authController.signUp) app.post("/auth/login", authController.signIn) +app.post("/auth/logout", authController.signOut) app.get("/tasks", authenticate, authorize(["ADMIN"]), taskController.readTasks) app.get( diff --git a/src/middlewares/authenticate.middleware.ts b/src/middlewares/authenticate.middleware.ts index 93b2889..0721924 100644 --- a/src/middlewares/authenticate.middleware.ts +++ b/src/middlewares/authenticate.middleware.ts @@ -1,43 +1,43 @@ -import { Request, Response, NextFunction } from 'express' -import { JWTService } from '../services/jwt.service.js' +import { Request, Response, NextFunction } from "express"; +import { JWTService } from "../services/jwt.service.js"; export const authenticate = async ( req: Request, res: Response, - next: NextFunction + next: NextFunction, ) => { try { - const authHeader = req.headers.authorization + let token = req.cookies?.token; - if (!authHeader) { - return res.status(401).json({ message: 'Token not provided' }) - } - - const parts = authHeader.split(' ') + if (!token) { + const authHeader = req.headers.authorization; - if (parts.length !== 2 || parts[0] !== 'Bearer') { - return res.status(401).json({ message: 'Invalid token format' }) + if (authHeader && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + } } - const token = parts[1] + if (!token) { + return res.status(401).json({ message: "Token not provided" }); + } try { - const jwtService = new JWTService() - const payload = jwtService.verifyToken(token) + const jwtService = new JWTService(); + const payload = jwtService.verifyToken(token); req.user = { id: payload.userId, email: payload.email, - role: payload.role - } + role: payload.role, + }; - next() + next(); } catch (error) { const message = - error instanceof Error ? error.message : 'Token invalid or expired' - return res.status(401).json({ message }) + error instanceof Error ? error.message : "Token invalid or expired"; + return res.status(401).json({ message }); } } catch (error) { - return res.status(500).json({ message: 'Server internal error' }) + return res.status(500).json({ message: "Server internal error" }); } -} +}; diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index f28d770..bde8693 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -3,6 +3,7 @@ import { AuthService } from '../services/auth.service.js' import { AuthRepository } from '../repositories/auth.repository.js' import { AuthController } from '../controllers/auth.controller.js' import { JWTService } from '../services/jwt.service.js' +import { authenticate } from '../middlewares/authenticate.middleware.js' const router = Router() @@ -13,5 +14,9 @@ const authController = new AuthController(authService, jwtService) router.post('/auth/register', authController.signUp) router.post('/auth/login', authController.signIn) +router.post('/auth/logout', authController.signOut) +router.get('/auth/me', authenticate, async (req, res) => { + res.json({ user: req.user }) +}) export default router diff --git a/src/server.ts b/src/server.ts index ec4b8a0..7d8ef42 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,24 +1,27 @@ -import "dotenv/config" -import express from "express" -import cors from "cors" -import authRoutes from "./routes/auth.routes.js" -import teamsRoutes from "./routes/teams.routes.js" -import tasksRoutes from "./routes/tasks.routes.js" -import { errorHandler } from "./middlewares/error.handler.middleware.js" +import "dotenv/config"; +import express from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import authRoutes from "./routes/auth.routes.js"; +import teamsRoutes from "./routes/teams.routes.js"; +import tasksRoutes from "./routes/tasks.routes.js"; +import { errorHandler } from "./middlewares/error.handler.middleware.js"; -const app = express() -const port = process.env.PORT || 3000 +const app = express(); +const port = process.env.PORT || 3000; -app.use(cors({ origin: "*" })) +app.use(cors({ origin: "*", credentials: true })); -app.use(express.json()) +app.use(cookieParser()); -app.use(authRoutes) -app.use(teamsRoutes) -app.use(tasksRoutes) +app.use(express.json()); -app.use(errorHandler) +app.use(authRoutes); +app.use(teamsRoutes); +app.use(tasksRoutes); + +app.use(errorHandler); app.listen(port, () => { - console.log(`app listening on http://localhost:${port}`) -}) + console.log(`app listening on http://localhost:${port}`); +});