mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-24 08:54:05 +10:00
62f8270b3e
commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 13:13:38 2026 +0200
chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies
commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 13:08:04 2026 +0200
fix: remove timestamp conflict guard
commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 12:11:51 2026 +0200
chore(release): v5.1.5
commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:58:35 2026 +0200
revert: compose.yml
commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:58:08 2026 +0200
refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086
commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:10:41 2026 +0200
refactor(pdf): simplify sidebar section filtering and update summary feature logic
commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:53:37 2026 +0200
chore: update pnpm-lock.yaml and turbo.json
commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:38:30 2026 +0200
fix(polyfill): add tested polyfill for Map Upsert methods
commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:23:29 2026 +0200
Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration
# Conflicts:
# packages/api/src/services/agent-url.ts
# packages/runtime-externals/package.json
commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:22:12 2026 +0200
chore: preserve branch changes before main sync
commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Mon May 18 07:50:28 2026 +0200
chore: lot of fixes for monorepo migration
commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 13:57:17 2026 +0200
chore: update knip version and refine web app routing with new SEO endpoints
commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 12:10:06 2026 +0200
refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint
commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:54:29 2026 +0200
chore: update dependencies and enhance PWA metadata in web app
commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:12:35 2026 +0200
docs: revise manifest-only pwa testing scope
commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:11:33 2026 +0200
docs: add manifest-only pwa design
commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:05:04 2026 +0200
chore(dev): simplify server proxy config
commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:04:50 2026 +0200
docs: add unsafe oauth redirect plan
commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:04:34 2026 +0200
feat(auth): add unsafe oauth redirect flag
commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:55:02 2026 +0200
docs: design unsafe oauth redirect flag
commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:22:04 2026 +0200
chore: update translation source paths
commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:09:25 2026 +0200
refactor(arch): react spa + hono migration
commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 01:10:47 2026 +0200
docs: add docker nightly tagging design
commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Thu May 14 20:05:44 2026 +0200
feat: migrate to hono spa server
213 lines
6.2 KiB
TypeScript
213 lines
6.2 KiB
TypeScript
import crypto from "node:crypto";
|
|
import { eq } from "drizzle-orm";
|
|
import { auth } from "@reactive-resume/auth/config";
|
|
import { db } from "@reactive-resume/db/client";
|
|
import { oauthClient, verification } from "@reactive-resume/db/schema";
|
|
import { env } from "@reactive-resume/env/server";
|
|
import { generateId } from "@reactive-resume/utils/string";
|
|
import { isAllowedOAuthRedirectUri } from "@reactive-resume/utils/url-security.node";
|
|
|
|
const oauthAuthorizeSanitizedParams = [
|
|
"prompt",
|
|
"redirect_uri",
|
|
"client_id",
|
|
"code_challenge",
|
|
"code_challenge_method",
|
|
"response_type",
|
|
"scope",
|
|
"state",
|
|
"resource",
|
|
] as const;
|
|
|
|
function sanitizeOAuthAuthorizeRequest(request: Request): Request {
|
|
if (request.method !== "GET") return request;
|
|
|
|
const url = new URL(request.url);
|
|
if (!url.pathname.endsWith("/oauth2/authorize")) return request;
|
|
|
|
const sanitizeValue = (value: string) =>
|
|
value
|
|
.replace(/[\r\n\t]+/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
const sanitizeParam = (key: string) => {
|
|
const value = url.searchParams.get(key);
|
|
if (!value) return;
|
|
url.searchParams.set(key, sanitizeValue(value));
|
|
};
|
|
|
|
for (const key of oauthAuthorizeSanitizedParams) sanitizeParam(key);
|
|
|
|
const redirectUri = url.searchParams.get("redirect_uri");
|
|
if (redirectUri && !URL.canParse(redirectUri)) {
|
|
try {
|
|
const decodedRedirectUri = decodeURIComponent(redirectUri);
|
|
if (URL.canParse(decodedRedirectUri)) {
|
|
url.searchParams.set("redirect_uri", decodedRedirectUri);
|
|
}
|
|
} catch {
|
|
// Ignore malformed encoded values and let Better Auth validation handle them.
|
|
}
|
|
}
|
|
|
|
if (url.toString() === request.url) return request;
|
|
return new Request(url.toString(), request);
|
|
}
|
|
|
|
async function defaultPublicClientRegistration(request: Request): Promise<Request> {
|
|
if (request.method !== "POST") return request;
|
|
|
|
const url = new URL(request.url);
|
|
if (!url.pathname.endsWith("/oauth2/register")) return request;
|
|
|
|
const cloned = request.clone();
|
|
let body: Record<string, unknown>;
|
|
|
|
try {
|
|
body = await cloned.json();
|
|
} catch {
|
|
return request;
|
|
}
|
|
|
|
if (!request.headers.get("authorization")) {
|
|
body.token_endpoint_auth_method = "none";
|
|
}
|
|
|
|
return new Request(url.toString(), {
|
|
method: request.method,
|
|
headers: request.headers,
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
async function validateDynamicClientRegistrationRequest(request: Request): Promise<Response | undefined> {
|
|
if (request.method !== "POST") return;
|
|
|
|
const url = new URL(request.url);
|
|
if (!url.pathname.endsWith("/oauth2/register")) return;
|
|
|
|
const cloned = request.clone();
|
|
let body: Record<string, unknown>;
|
|
|
|
try {
|
|
body = await cloned.json();
|
|
} catch {
|
|
return Response.json({ message: "Invalid registration payload" }, { status: 400 });
|
|
}
|
|
|
|
const oauthTrustedOrigins = [new URL(env.APP_URL).origin.toLowerCase()];
|
|
|
|
const redirectUris = Array.isArray(body.redirect_uris) ? body.redirect_uris : [];
|
|
for (const redirectUri of redirectUris) {
|
|
if (
|
|
typeof redirectUri !== "string" ||
|
|
!isAllowedOAuthRedirectUri(redirectUri, oauthTrustedOrigins, {
|
|
allowUnsafe: env.FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI,
|
|
})
|
|
) {
|
|
return Response.json(
|
|
{ error: "invalid_redirect_uri", error_description: "redirect_uri is not allowed" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function handleAuth(request: Request) {
|
|
const registrationValidationError = await validateDynamicClientRegistrationRequest(request);
|
|
if (registrationValidationError) return registrationValidationError;
|
|
|
|
const sanitizedRequest = sanitizeOAuthAuthorizeRequest(request);
|
|
const finalRequest = await defaultPublicClientRegistration(sanitizedRequest);
|
|
|
|
return auth.handler(finalRequest);
|
|
}
|
|
|
|
function generateCode() {
|
|
return crypto.randomBytes(32).toString("base64url");
|
|
}
|
|
|
|
function hashCode(code: string) {
|
|
return crypto.createHash("sha256").update(code).digest("base64url");
|
|
}
|
|
|
|
export async function handleOAuth(request: Request) {
|
|
const session = await auth.api.getSession({ headers: request.headers });
|
|
const url = new URL(request.url);
|
|
|
|
if (session?.user) {
|
|
const clientId = url.searchParams.get("client_id");
|
|
const redirectUri = url.searchParams.get("redirect_uri");
|
|
const state = url.searchParams.get("state");
|
|
const scope = url.searchParams.get("scope");
|
|
const codeChallenge = url.searchParams.get("code_challenge");
|
|
const codeChallengeMethod = url.searchParams.get("code_challenge_method");
|
|
|
|
if (!clientId || !redirectUri) {
|
|
return Response.json({ error: "missing client_id or redirect_uri" }, { status: 400 });
|
|
}
|
|
|
|
const [client] = await db.select().from(oauthClient).where(eq(oauthClient.clientId, clientId)).limit(1);
|
|
|
|
if (!client) {
|
|
return Response.json({ error: "invalid client" }, { status: 400 });
|
|
}
|
|
|
|
if (!client.redirectUris.includes(redirectUri)) {
|
|
return Response.json({ error: "invalid redirect_uri" }, { status: 400 });
|
|
}
|
|
|
|
const code = generateCode();
|
|
const hashedCode = hashCode(code);
|
|
const now = new Date();
|
|
const expiresAt = new Date(now.getTime() + 600_000);
|
|
|
|
await db.insert(verification).values({
|
|
id: generateId(),
|
|
identifier: hashedCode,
|
|
value: JSON.stringify({
|
|
type: "authorization_code",
|
|
query: {
|
|
response_type: "code",
|
|
client_id: clientId,
|
|
redirect_uri: redirectUri,
|
|
scope,
|
|
state,
|
|
code_challenge: codeChallenge,
|
|
code_challenge_method: codeChallengeMethod,
|
|
},
|
|
userId: session.user.id,
|
|
sessionId: session.session.id,
|
|
authTime: new Date(session.session.createdAt).getTime(),
|
|
}),
|
|
expiresAt,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
|
|
const callbackUrl = new URL(redirectUri);
|
|
callbackUrl.searchParams.set("code", code);
|
|
if (state) callbackUrl.searchParams.set("state", state);
|
|
callbackUrl.searchParams.set("iss", `${env.APP_URL}/api/auth`);
|
|
|
|
return new Response(null, {
|
|
status: 302,
|
|
headers: { Location: callbackUrl.toString() },
|
|
});
|
|
}
|
|
|
|
const loginUrl = new URL("/auth/login", env.APP_URL);
|
|
const oauthParams = new URLSearchParams();
|
|
for (const [key, value] of url.searchParams) {
|
|
if (!["exp", "sig"].includes(key)) {
|
|
oauthParams.set(key, value);
|
|
}
|
|
}
|
|
loginUrl.searchParams.set("callbackURL", `/api/auth/oauth?${oauthParams.toString()}`);
|
|
|
|
return new Response(null, {
|
|
status: 302,
|
|
headers: { Location: `${loginUrl.pathname}${loginUrl.search}` },
|
|
});
|
|
}
|