Skip to content

Commit bd37122

Browse files
authored
Develop (#171)
2 parents 9f874b4 + 448dc83 commit bd37122

41 files changed

Lines changed: 1578 additions & 880 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/lib/errors.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { ZodError } from "zod";
2+
3+
export type ErrorCode =
4+
| "VALIDATION_ERROR"
5+
| "UNAUTHORIZED"
6+
| "FORBIDDEN"
7+
| "NOT_FOUND"
8+
| "CONFLICT"
9+
| "INTERNAL_ERROR";
10+
11+
export type ErrorDetails = Record<string, unknown>;
12+
13+
export class AppError extends Error {
14+
readonly code: ErrorCode;
15+
readonly statusCode: number;
16+
readonly details?: ErrorDetails;
17+
18+
constructor(
19+
code: ErrorCode,
20+
message: string,
21+
statusCode: number,
22+
details?: ErrorDetails,
23+
) {
24+
super(message);
25+
this.name = "AppError";
26+
this.code = code;
27+
this.statusCode = statusCode;
28+
this.details = details;
29+
}
30+
31+
static validation(
32+
message = "Dados inválidos",
33+
details?: ErrorDetails,
34+
): AppError {
35+
return new AppError("VALIDATION_ERROR", message, 400, details);
36+
}
37+
38+
static unauthorized(message = "Não autenticado."): AppError {
39+
return new AppError("UNAUTHORIZED", message, 401);
40+
}
41+
42+
static forbidden(message = "Acesso negado."): AppError {
43+
return new AppError("FORBIDDEN", message, 403);
44+
}
45+
46+
static notFound(message = "Recurso não encontrado."): AppError {
47+
return new AppError("NOT_FOUND", message, 404);
48+
}
49+
50+
static conflict(message: string, details?: ErrorDetails): AppError {
51+
return new AppError("CONFLICT", message, 409, details);
52+
}
53+
54+
static internal(
55+
message = "Erro interno.",
56+
details?: ErrorDetails,
57+
): AppError {
58+
return new AppError("INTERNAL_ERROR", message, 500, details);
59+
}
60+
61+
static fromZodError(
62+
error: ZodError,
63+
message = "Dados inválidos",
64+
): AppError {
65+
return AppError.validation(message, error.flatten().fieldErrors);
66+
}
67+
68+
toJSON(): { code: ErrorCode; message: string; details?: ErrorDetails } {
69+
const body: { code: ErrorCode; message: string; details?: ErrorDetails } = {
70+
code: this.code,
71+
message: this.message,
72+
};
73+
if (this.details !== undefined) {
74+
body.details = this.details;
75+
}
76+
return body;
77+
}
78+
}
79+
80+
export function isAppError(error: unknown): error is AppError {
81+
return error instanceof AppError;
82+
}
Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { NextFunction, Request, Response } from "express";
2+
import { ZodError } from "zod";
3+
import { AppError } from "../lib/errors";
4+
5+
function isProduction(): boolean {
6+
return process.env.NODE_ENV === "production";
7+
}
28

39
export function errorHandler(
410
error: Error,
@@ -7,8 +13,25 @@ export function errorHandler(
713
_next: NextFunction,
814
): void {
915
if (error.message.startsWith("Origin not allowed by CORS")) {
10-
res.status(403).json({ message: "Origem não permitida." });
16+
const appError = AppError.forbidden("Origem não permitida.");
17+
res.status(appError.statusCode).json(appError.toJSON());
18+
return;
19+
}
20+
21+
if (error instanceof AppError) {
22+
res.status(error.statusCode).json(error.toJSON());
1123
return;
1224
}
13-
res.status(500).json({ message: "Erro interno.", error: error.message });
25+
26+
if (error instanceof ZodError) {
27+
const appError = AppError.fromZodError(error);
28+
res.status(appError.statusCode).json(appError.toJSON());
29+
return;
30+
}
31+
32+
const details = isProduction()
33+
? undefined
34+
: { cause: error.message || "unknown" };
35+
const appError = AppError.internal("Erro interno.", details);
36+
res.status(appError.statusCode).json(appError.toJSON());
1437
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { NextFunction, Request, Response } from "express";
2+
import { AppError } from "../lib/errors";
23

34
export function requireAuth(req: Request, res: Response, next: NextFunction) {
45
if (!req.session?.userId) {
5-
return res.status(401).json({ message: "Não autenticado." });
6+
const error = AppError.unauthorized();
7+
return res.status(error.statusCode).json(error.toJSON());
68
}
79
next();
810
}

backend/src/middleware/validate.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { NextFunction, Request, RequestHandler, Response } from "express";
2+
import type { ZodType } from "zod";
3+
import { ZodError } from "zod";
4+
import { AppError } from "../lib/errors";
5+
6+
type ValidateSchemas = {
7+
body?: ZodType;
8+
query?: ZodType;
9+
params?: ZodType;
10+
};
11+
12+
export function validate(schemas: ValidateSchemas): RequestHandler {
13+
return (req: Request, _res: Response, next: NextFunction) => {
14+
try {
15+
if (schemas.body) {
16+
req.body = schemas.body.parse(req.body);
17+
}
18+
if (schemas.query) {
19+
const parsed = schemas.query.parse(req.query);
20+
Object.defineProperty(req, "query", {
21+
value: parsed,
22+
writable: true,
23+
configurable: true,
24+
enumerable: true,
25+
});
26+
}
27+
if (schemas.params) {
28+
const parsed = schemas.params.parse(req.params);
29+
Object.defineProperty(req, "params", {
30+
value: parsed,
31+
writable: true,
32+
configurable: true,
33+
enumerable: true,
34+
});
35+
}
36+
next();
37+
} catch (error) {
38+
if (error instanceof ZodError) {
39+
next(AppError.fromZodError(error));
40+
return;
41+
}
42+
next(error);
43+
}
44+
};
45+
}

backend/src/modules/auth/auth.controller.ts

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,21 @@
11
import { randomBytes } from "crypto";
22
import { Request, Response } from "express";
3-
import { z } from "zod";
4-
import {
5-
AuthCallbackParamsSchema,
6-
OAuthProviderSchema,
7-
} from "../types/auth.types.js";
8-
3+
import type { OAuthProvider } from "../types/auth.types.js";
4+
import { AuthCallbackParamsSchema } from "../types/auth.types.js";
95
import { AuthService } from "./auth.service.js";
106

117
export class AuthController {
128
constructor(private readonly authService: AuthService) {}
139

1410
async getUrl(req: Request, res: Response) {
15-
try {
16-
const provider = OAuthProviderSchema.parse(req.params.provider);
17-
18-
const state = randomBytes(16).toString("hex");
19-
20-
(req.session as any).oauth_state = state;
21-
await req.session.save();
11+
const provider = req.params.provider as OAuthProvider;
12+
const state = randomBytes(16).toString("hex");
2213

23-
const url = await this.authService.getAuthUrl(provider, state);
14+
(req.session as { oauth_state?: string }).oauth_state = state;
15+
await req.session.save();
2416

25-
return res.json({ url });
26-
} catch (error) {
27-
if (error instanceof z.ZodError) {
28-
return res.status(400).json({
29-
error: "Provider inválido",
30-
details: error.message,
31-
});
32-
}
33-
34-
return res.status(400).json({
35-
error: (error as Error).message,
36-
});
37-
}
17+
const url = await this.authService.getAuthUrl(provider, state);
18+
return res.json({ url });
3819
}
3920

4021
async callback(req: Request, res: Response) {
@@ -50,7 +31,7 @@ export class AuthController {
5031
callbackUrl,
5132
});
5233

53-
const oauthState = (req.session as any).oauth_state;
34+
const oauthState = (req.session as { oauth_state?: string }).oauth_state;
5435

5536
if (!oauthState) {
5637
return res.redirect(`${frontendUrl}/login?error=oauth_state_missing`);
@@ -60,7 +41,7 @@ export class AuthController {
6041
return res.redirect(`${frontendUrl}/login?error=oauth_state_invalid`);
6142
}
6243

63-
delete (req.session as any).oauth_state;
44+
delete (req.session as { oauth_state?: string }).oauth_state;
6445

6546
const result = await this.authService.handleCallback({
6647
...params,

backend/src/modules/auth/credentials.controller.ts

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,30 @@
11
import { Request, Response } from "express";
2-
import { z } from "zod";
3-
import { LoginSchema, RegisterSchema } from "../types/credentials.types";
2+
import { AppError } from "../../lib/errors";
43
import { CredentialsService } from "./credentials.service";
54

65
export class CredentialsController {
76
constructor(private readonly service: CredentialsService) {}
87

98
async register(req: Request, res: Response) {
10-
try {
11-
const input = RegisterSchema.parse(req.body);
12-
const { user, session: userSession } = await this.service.register(input);
9+
const { user, session: userSession } = await this.service.register(
10+
req.body,
11+
);
1312

14-
req.session.userId = userSession.userId;
15-
req.session.role = userSession.role;
16-
await req.session.save();
13+
req.session.userId = userSession.userId;
14+
req.session.role = userSession.role;
15+
await req.session.save();
1716

18-
return res.status(201).json({ user, session: userSession });
19-
} catch (error) {
20-
if (error instanceof z.ZodError) {
21-
return res.status(400).json({ error: error.format() });
22-
}
23-
const message = error instanceof Error ? error.message : "Erro interno";
24-
const status = message === "Email já cadastrado" ? 409 : 500;
25-
return res.status(status).json({ error: message });
26-
}
17+
return res.status(201).json({ user, session: userSession });
2718
}
2819

2920
async login(req: Request, res: Response) {
30-
try {
31-
const input = LoginSchema.parse(req.body);
32-
const { user, session: userSession } = await this.service.login(input);
21+
const { user, session: userSession } = await this.service.login(req.body);
3322

34-
req.session.userId = userSession.userId;
35-
req.session.role = userSession.role;
36-
await req.session.save();
23+
req.session.userId = userSession.userId;
24+
req.session.role = userSession.role;
25+
await req.session.save();
3726

38-
return res.json({ user, session: userSession });
39-
} catch (error) {
40-
if (error instanceof z.ZodError) {
41-
return res.status(400).json({ error: error.format() });
42-
}
43-
const message = error instanceof Error ? error.message : "Erro interno";
44-
const status = message === "Credenciais inválidas" ? 401 : 500;
45-
return res.status(status).json({ error: message });
46-
}
27+
return res.json({ user, session: userSession });
4728
}
4829

4930
async logout(req: Request, res: Response) {
@@ -53,13 +34,13 @@ export class CredentialsController {
5334

5435
async me(req: Request, res: Response) {
5536
if (!req.session.userId) {
56-
return res.status(401).json({ error: "Não autenticado" });
37+
throw AppError.unauthorized();
5738
}
5839

5940
const user = await this.service.findById(req.session.userId);
6041
if (!user) {
6142
await req.session.destroy();
62-
return res.status(401).json({ error: "Não autenticado" });
43+
throw AppError.unauthorized();
6344
}
6445

6546
return res.json({ user });

0 commit comments

Comments
 (0)