Skip to content

Commit 25d0e64

Browse files
authored
Merge pull request #80 from anonfedora/feature/auth-delegations-improvements
feat(gateway): implement refresh tokens, auth validation, delegations
2 parents ba54b5a + 5dcca1d commit 25d0e64

16 files changed

Lines changed: 1154 additions & 46 deletions

File tree

apps/backend/gateway/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"dependencies": {
1717
"@delego/types": "workspace:*",
1818
"@delego/utils": "workspace:*",
19+
"ajv": "^8.20.0",
20+
"ajv-formats": "^3.0.1",
1921
"bcryptjs": "^3.0.3",
2022
"ioredis": "^5.11.1",
2123
"jsonwebtoken": "^9.0.3",
@@ -34,4 +36,3 @@
3436
"typescript": "^5.7.0"
3537
}
3638
}
37-

apps/backend/gateway/routes/auth.ts

Lines changed: 118 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,136 @@
11
import type { IncomingMessage, ServerResponse } from "node:http";
22
import { json } from "@delego/utils";
3-
import { registerUser, loginUser } from "../src/auth/authService.js";
3+
import { registerUser, loginUser, refreshAccessToken } from "../src/auth/authService.js";
4+
import { validateSchema, RegisterSchema, LoginSchema } from "../src/validation.js";
5+
import { readJsonBody, InvalidJsonError, BodyTooLargeError } from "../src/request.js";
46

5-
async function readJsonBody(req: IncomingMessage): Promise<any> {
6-
return new Promise((resolve, reject) => {
7-
let body = "";
8-
req.on("data", (chunk) => {
9-
body += chunk;
10-
});
11-
req.on("end", () => {
12-
try {
13-
resolve(body ? JSON.parse(body) : {});
14-
} catch (err) {
15-
reject(new Error("Invalid JSON body"));
7+
function parseCookies(req: IncomingMessage): Record<string, string> {
8+
const list: Record<string, string> = {};
9+
const cookieHeader = req.headers.cookie;
10+
if (cookieHeader) {
11+
cookieHeader.split(";").forEach((cookie) => {
12+
const parts = cookie.split("=");
13+
if (parts.length >= 2) {
14+
const key = parts.shift()?.trim() ?? "";
15+
const value = decodeURIComponent(parts.join("=").trim());
16+
if (key) {
17+
list[key] = value;
18+
}
1619
}
1720
});
18-
req.on("error", (err) => {
19-
reject(err);
20-
});
21-
});
21+
}
22+
return list;
23+
}
24+
25+
function setRefreshTokenCookie(res: ServerResponse, refreshToken: string): void {
26+
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
27+
const cookie = [
28+
`refresh_token=${refreshToken}`,
29+
`Expires=${expires.toUTCString()}`,
30+
"HttpOnly",
31+
"Secure",
32+
"SameSite=Strict",
33+
"Path=/",
34+
].join("; ");
35+
res.setHeader("Set-Cookie", cookie);
2236
}
2337

2438
export async function registerHandler(req: IncomingMessage, res: ServerResponse): Promise<void> {
2539
try {
26-
const { email, password, displayName } = await readJsonBody(req);
27-
const result = await registerUser(email, password, displayName);
28-
json(res, 201, { data: result, error: null });
29-
} catch (err: any) {
30-
json(res, 400, {
31-
data: null,
32-
error: { code: "BAD_REQUEST", message: err.message },
40+
const body = await readJsonBody(req);
41+
const validation = validateSchema(RegisterSchema, body);
42+
if (!validation.valid) {
43+
json(res, 400, {
44+
data: null,
45+
error: { code: "VALIDATION_ERROR", message: "Invalid request body", details: validation.errors },
46+
});
47+
return;
48+
}
49+
50+
const result = await registerUser(body.email, body.password, body.displayName);
51+
setRefreshTokenCookie(res, result.refreshToken);
52+
json(res, 201, {
53+
data: {
54+
user: result.user,
55+
accessToken: result.accessToken,
56+
expiresIn: result.expiresIn,
57+
},
58+
error: null,
3359
});
60+
} catch (err: any) {
61+
if (err instanceof InvalidJsonError || err instanceof BodyTooLargeError) {
62+
json(res, 400, {
63+
data: null,
64+
error: { code: "VALIDATION_ERROR", message: err.message },
65+
});
66+
} else {
67+
json(res, 400, {
68+
data: null,
69+
error: { code: "BAD_REQUEST", message: err.message },
70+
});
71+
}
3472
}
3573
}
3674

3775
export async function loginHandler(req: IncomingMessage, res: ServerResponse): Promise<void> {
3876
try {
39-
const { email, password } = await readJsonBody(req);
40-
const result = await loginUser(email, password);
41-
json(res, 200, { data: result, error: null });
77+
const body = await readJsonBody(req);
78+
const validation = validateSchema(LoginSchema, body);
79+
if (!validation.valid) {
80+
json(res, 400, {
81+
data: null,
82+
error: { code: "VALIDATION_ERROR", message: "Invalid request body", details: validation.errors },
83+
});
84+
return;
85+
}
86+
87+
const result = await loginUser(body.email, body.password);
88+
setRefreshTokenCookie(res, result.refreshToken);
89+
json(res, 200, {
90+
data: {
91+
user: result.user,
92+
accessToken: result.accessToken,
93+
expiresIn: result.expiresIn,
94+
},
95+
error: null,
96+
});
97+
} catch (err: any) {
98+
if (err instanceof InvalidJsonError || err instanceof BodyTooLargeError) {
99+
json(res, 400, {
100+
data: null,
101+
error: { code: "VALIDATION_ERROR", message: err.message },
102+
});
103+
} else {
104+
json(res, 401, {
105+
data: null,
106+
error: { code: "UNAUTHORIZED", message: err.message },
107+
});
108+
}
109+
}
110+
}
111+
112+
export async function refreshHandler(req: IncomingMessage, res: ServerResponse): Promise<void> {
113+
try {
114+
const cookies = parseCookies(req);
115+
const refreshToken = cookies.refresh_token;
116+
117+
if (!refreshToken) {
118+
json(res, 401, {
119+
data: null,
120+
error: { code: "UNAUTHORIZED", message: "Refresh token missing" },
121+
});
122+
return;
123+
}
124+
125+
const result = await refreshAccessToken(refreshToken);
126+
setRefreshTokenCookie(res, result.refreshToken);
127+
json(res, 200, {
128+
data: {
129+
accessToken: result.accessToken,
130+
expiresIn: result.expiresIn,
131+
},
132+
error: null,
133+
});
42134
} catch (err: any) {
43135
json(res, 401, {
44136
data: null,

0 commit comments

Comments
 (0)