mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-04 02:33:47 +10:00
Fix authentication recovery, account imports, application tracking, resume editing and exports, sharing, API contracts, provider selection, and private local attachments. Preserve authored content during PDF pagination. Update guides, generated OpenAPI output, and translation catalogs to match verified behavior and documented constraints. Validation: 1,019 tests passed; 12 database/OAuth integration tests skipped. Ten affected package typechecks, production build, Biome, and package boundaries passed.
101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
import type { Locale } from "@reactive-resume/utils/locale";
|
|
import type { User } from "better-auth";
|
|
import { ORPCError, os } from "@orpc/server";
|
|
import { eq } from "drizzle-orm";
|
|
import { auth, verifyOAuthToken } from "@reactive-resume/auth/config";
|
|
import { db } from "@reactive-resume/db/client";
|
|
import { user } from "@reactive-resume/db/schema";
|
|
|
|
interface ORPCContext {
|
|
locale: Locale;
|
|
reqHeaders: Headers;
|
|
resHeaders?: Headers;
|
|
trustedClient?: string;
|
|
}
|
|
|
|
async function getUserFromBearerToken(headers: Headers): Promise<User | null> {
|
|
try {
|
|
const authHeader = headers.get("authorization");
|
|
if (!authHeader?.startsWith("Bearer ")) return null;
|
|
|
|
const payload = await verifyOAuthToken(authHeader.slice(7));
|
|
if (!payload?.sub) return null;
|
|
|
|
const [userResult] = await db.select().from(user).where(eq(user.id, payload.sub)).limit(1);
|
|
return userResult ?? null;
|
|
} catch (error) {
|
|
console.warn("Bearer token verification failed:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getUserFromHeaders(headers: Headers): Promise<User | null> {
|
|
try {
|
|
const result = await auth.api.getSession({ headers });
|
|
if (!result?.user) return null;
|
|
|
|
return result.user;
|
|
} catch (error) {
|
|
console.warn("Session verification failed:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getUserFromApiKey(apiKey: string): Promise<User | null> {
|
|
try {
|
|
const result = await auth.api.verifyApiKey({ body: { key: apiKey } });
|
|
if (!result.key || !result.valid) return null;
|
|
|
|
const [userResult] = await db.select().from(user).where(eq(user.id, result.key.referenceId)).limit(1);
|
|
if (!userResult) return null;
|
|
|
|
return userResult;
|
|
} catch (error) {
|
|
console.warn("API key verification failed:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the authenticated user from the same headers oRPC uses (`x-api-key`,
|
|
* `Authorization: Bearer`, or session cookies). Tries each auth method in
|
|
* priority order and returns the first valid identity. Used directly by
|
|
* oRPC's `publicProcedure` and by callers outside oRPC handlers (e.g. MCP
|
|
* tools) where `context.user` is not in scope.
|
|
*/
|
|
export async function resolveUserFromRequestHeaders(headers: Headers): Promise<User | null> {
|
|
const apiKey = headers.get("x-api-key");
|
|
if (apiKey) {
|
|
const apiKeyUser = await getUserFromApiKey(apiKey);
|
|
if (apiKeyUser) return apiKeyUser;
|
|
}
|
|
const bearerUser = await getUserFromBearerToken(headers);
|
|
if (bearerUser) return bearerUser;
|
|
|
|
return getUserFromHeaders(headers);
|
|
}
|
|
|
|
const base = os.$context<ORPCContext>();
|
|
|
|
export const publicProcedure = base.use(async ({ context, next }) => {
|
|
const user = await resolveUserFromRequestHeaders(context.reqHeaders);
|
|
|
|
return next({
|
|
context: {
|
|
...context,
|
|
user,
|
|
},
|
|
});
|
|
});
|
|
|
|
export const protectedProcedure = publicProcedure.use(({ context, next }) => {
|
|
if (!context.user) throw new ORPCError("UNAUTHORIZED");
|
|
|
|
return next({
|
|
context: {
|
|
...context,
|
|
user: context.user,
|
|
},
|
|
});
|
|
});
|