Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cors from "cors";
import express from "express";
import express, { NextFunction, Request, Response } from "express";
import { corsOptions } from "./middleware/cors";
import { errorHandler } from "./middleware/errorHandler";
import { requireAuth } from "./middleware/requireAuth";
Expand All @@ -17,9 +17,15 @@ export function createJobsApiApp() {
app.disable("x-powered-by");
app.use(express.json({ limit: "16kb" }));
app.use(securityHeaders);
app.use(cors(corsOptions));

//Confia no proxy reverso (nginx) para lidar com HTTPS e IPs reais dos clientes
// um wrapper para garantir se o CORS chega com erro 403
app.use((req: Request, res: Response, next: NextFunction) => {
cors(corsOptions)(req, res, (err) => {
if (err) return next(err);
next();
});
});

app.set("trust proxy", 1);

app.use("/api/auth", withSession, authRoutes);
Expand Down
6 changes: 4 additions & 2 deletions backend/src/lib/session.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { SessionOptions } from "iron-session";

const isProd = process.env.NODE_ENV === "production";

export const sessionOptions: SessionOptions = {
password: process.env.SESSION_SECRET!,
cookieName: "vagas_session",

cookieOptions: {
secure: process.env.NODE_ENV === "production",
secure: isProd,
httpOnly: true,
sameSite: "lax",
sameSite: isProd ? "none" : "lax",
},
};
17 changes: 12 additions & 5 deletions backend/src/middleware/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import cors from "cors";

const DEFAULT_ALLOWED_ORIGINS = [
"https://painel-vagas-lake.vercel.app",
"https://painel-vagas-m6hbzlqeh-bene-teslas-projects.vercel.app",
"https://jobsglobalscraper.ddns.net",
"http://jobsglobalscraper.ddns.net",
"http://localhost:5173",
Expand All @@ -22,11 +21,19 @@ export const corsOptions: cors.CorsOptions = {
const allowedOrigins = parseAllowedOrigins(
process.env.CORS_ALLOWED_ORIGINS,
);
if (!origin || allowedOrigins.has(origin)) return callback(null, true);
callback(new Error("Origin not allowed by CORS"));

if (!origin) return callback(null, true);

if (/^https:\/\/painel-vagas-[a-z0-9-]+\.vercel\.app$/.test(origin)) {
return callback(null, true);
}

if (allowedOrigins.has(origin)) return callback(null, true);

callback(new Error(`Origin not allowed by CORS: ${origin}`));
},
methods: ["GET", "POST", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
credentials: true,
maxAge: 86400,
};
2 changes: 1 addition & 1 deletion backend/src/middleware/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function errorHandler(
res: Response,
_next: NextFunction,
): void {
if (error.message === "Origin not allowed by CORS") {
if (error.message.startsWith("Origin not allowed by CORS")) {
res.status(403).json({ message: "Origem não permitida." });
return;
}
Expand Down
9 changes: 8 additions & 1 deletion backend/src/modules/auth/credentials.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export class CredentialsController {
if (!req.session.userId) {
return res.status(401).json({ error: "Não autenticado" });
}
return res.json({ userId: req.session.userId });

const user = await this.service.findById(req.session.userId);
if (!user) {
await req.session.destroy();
return res.status(401).json({ error: "Não autenticado" });
}

return res.json({ user });
}
}
62 changes: 19 additions & 43 deletions backend/src/modules/auth/credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ import {

const argonOptions = {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB de memória
timeCost: 3, // 3 iterações
parallelism: 4, // Usa 4 threads paralelas
memoryCost: 65536,
timeCost: 3,
parallelism: 4,
};

export class CredentialsService {
async findById(id: string): Promise<User | null> {
const user = await db.query.users.findFirst({
where: eq(users.id, id),
});
return user ?? null;
}

async register(
input: RegisterInput,
): Promise<{ user: User; session: Session }> {
Expand All @@ -30,44 +37,25 @@ export class CredentialsService {
const existingCredential = await db.query.credentials.findFirst({
where: eq(credentials.email, email),
});

if (existingCredential) {
throw new Error("Email já cadastrado");
}
if (existingCredential) throw new Error("Email já cadastrado");

const existingUser = await db.query.users.findFirst({
where: eq(users.email, email),
});

if (existingUser) {
throw new Error("Email já cadastrado");
}
if (existingUser) throw new Error("Email já cadastrado");

const passwordHash = await argon2.hash(password, argonOptions);
const username = await generateUsername(name ?? email.split("@")[0], db);

const [user] = await db
.insert(users)
.values({
email,
displayName: name,
username,
emailVerified: false,
})
.values({ email, displayName: name, username, emailVerified: false })
.returning();

await db.insert(credentials).values({
userId: user.id,
email,
passwordHash,
});

await db.insert(credentials).values({ userId: user.id, email, passwordHash });
await db.insert(userPreferences).values({ userId: user.id });

return {
user,
session: { userId: user.id },
};
return { user, session: { userId: user.id } };
}

async login(input: LoginInput): Promise<{ user: User; session: Session }> {
Expand All @@ -76,28 +64,16 @@ export class CredentialsService {
const credential = await db.query.credentials.findFirst({
where: eq(credentials.email, email),
});

if (!credential) {
throw new Error("Credenciais inválidas");
}
if (!credential) throw new Error("Credenciais inválidas");

const valid = await argon2.verify(credential.passwordHash, password);

if (!valid) {
throw new Error("Credenciais inválidas");
}
if (!valid) throw new Error("Credenciais inválidas");

const user = await db.query.users.findFirst({
where: eq(users.id, credential.userId),
});
if (!user) throw new Error("Usuário não encontrado");

if (!user) {
throw new Error("Usuário não encontrado");
}

return {
user,
session: { userId: user.id },
};
return { user, session: { userId: user.id } };
}
}
64 changes: 21 additions & 43 deletions backend/tests/integration/routes/auth.routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ vi.mock("../../../src/modules/auth/auth.service", () => ({
const mockCredentialsService = vi.hoisted(() => ({
register: vi.fn(),
login: vi.fn(),
findById: vi.fn(), // ← fix: adicionado
}));

vi.mock("../../../src/modules/auth/credentials.service", () => ({
Expand Down Expand Up @@ -109,6 +110,8 @@ describe("Integration - Auth Routes", () => {
session: { userId: fixtureUser.id },
});

mockCredentialsService.findById.mockResolvedValue(fixtureUser);

app = createJobsApiApp();
});

Expand Down Expand Up @@ -252,10 +255,7 @@ describe("Integration - Auth Routes", () => {
.get(`${BASE}/twitter/callback?code=abc&state=valid-state-abc123`)
.expect(400);

expect(res.body).toHaveProperty(
"error",
"Parâmetros de callback inválidos",
);
expect(res.body).toHaveProperty("error", "Parâmetros de callback inválidos");
});

it("retorna 400 quando code esta ausente (ZodError)", async () => {
Expand Down Expand Up @@ -288,10 +288,7 @@ describe("Integration - Auth Routes", () => {

describe("POST /register", () => {
it("cria usuario e retorna 201", async () => {
// CredentialsController usa req.session — injetado pelo withSession mock
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);

const res = await request(app)
.post(`${BASE}/register`)
Expand All @@ -303,9 +300,7 @@ describe("Integration - Auth Routes", () => {
});

it("chama register com email, password e name", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);

await request(app).post(`${BASE}/register`).send(registerPayload);

Expand All @@ -319,9 +314,7 @@ describe("Integration - Auth Routes", () => {
});

it("retorna 400 para email invalido (ZodError)", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);

const res = await request(app)
.post(`${BASE}/register`)
Expand All @@ -332,9 +325,7 @@ describe("Integration - Auth Routes", () => {
});

it("retorna 400 para senha curta (ZodError)", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);

await request(app)
.post(`${BASE}/register`)
Expand All @@ -343,9 +334,7 @@ describe("Integration - Auth Routes", () => {
});

it("retorna 409 quando email ja esta cadastrado", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);
mockCredentialsService.register.mockRejectedValueOnce(
new Error("Email já cadastrado"),
);
Expand All @@ -359,9 +348,7 @@ describe("Integration - Auth Routes", () => {
});

it("retorna 500 para erro inesperado no register", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);
mockCredentialsService.register.mockRejectedValueOnce(
new Error("db connection failed"),
);
Expand All @@ -377,9 +364,7 @@ describe("Integration - Auth Routes", () => {

describe("POST /login", () => {
it("autentica e retorna 200 com user e session", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);

const res = await request(app)
.post(`${BASE}/login`)
Expand All @@ -391,9 +376,7 @@ describe("Integration - Auth Routes", () => {
});

it("chama login com email e password corretos", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);

await request(app).post(`${BASE}/login`).send(loginPayload);

Expand All @@ -403,9 +386,7 @@ describe("Integration - Auth Routes", () => {
});

it("retorna 400 para email invalido (ZodError)", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);

await request(app)
.post(`${BASE}/login`)
Expand All @@ -414,9 +395,7 @@ describe("Integration - Auth Routes", () => {
});

it("retorna 400 para senha ausente (ZodError)", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);

await request(app)
.post(`${BASE}/login`)
Expand All @@ -425,9 +404,7 @@ describe("Integration - Auth Routes", () => {
});

it("retorna 401 para credenciais invalidas", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);
mockCredentialsService.login.mockRejectedValueOnce(
new Error("Credenciais inválidas"),
);
Expand All @@ -441,9 +418,7 @@ describe("Integration - Auth Routes", () => {
});

it("retorna 500 para erro inesperado no login", async () => {
vi.mocked(getIronSession).mockResolvedValue(
fixtureCredentialsSession as any,
);
vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any);
mockCredentialsService.login.mockRejectedValueOnce(
new Error("db timeout"),
);
Expand Down Expand Up @@ -486,15 +461,18 @@ describe("Integration - Auth Routes", () => {
// ── GET /me ───────────────────────────────────────────────────────────────

describe("GET /me", () => {
it("retorna userId quando autenticado", async () => {
it("retorna user completo quando autenticado", async () => {
vi.mocked(getIronSession).mockResolvedValue({
userId: "user-1",
save: vi.fn(),
destroy: vi.fn(),
} as any);

const res = await request(app).get(`${BASE}/me`).expect(200);

expect(res.body).toEqual({ userId: "user-1" });
expect(res.body).toHaveProperty("user");
expect(res.body.user).toHaveProperty("id", fixtureUser.id);
expect(res.body.user).toHaveProperty("email", fixtureUser.email);
});

it("retorna 401 quando nao autenticado", async () => {
Expand Down
Loading
Loading