Files
growqr-backend/src/auth/clerk.ts
NinjasPyajamas 2d471c61b4 feat: introduce workflow job management and agent orchestration
- Added workflow job actor to manage job application workflows.
- Implemented agent catalog for various workflow agents.
- Created service agents for interview, roleplay, and Q-Score functionalities.
- Enhanced user authentication to automatically create users if they do not exist.
- Updated configuration to support new LLM provider and API keys.
- Introduced new routes for agent and workflow management.
- Refactored Docker management to improve Gitea admin user creation and token generation.
- Removed deprecated Anthropics SDK integration.
2026-05-21 23:17:26 +05:30

110 lines
3.2 KiB
TypeScript

import { createClerkClient, verifyToken } from "@clerk/backend";
import { createMiddleware } from "hono/factory";
import { HTTPException } from "hono/http-exception";
import { config } from "../config.js";
import { db } from "../db/client.js";
import { users } from "../db/schema.js";
import { eq } from "drizzle-orm";
export type AuthContext = {
Variables: {
userId: string;
userEmail: string;
};
};
export const clerk = config.clerkSecretKey
? createClerkClient({ secretKey: config.clerkSecretKey })
: null;
// Verifies a Clerk session JWT from the Authorization header.
// Falls back to allowing the configured SERVICE_TOKEN for actor → backend calls.
//
// Bootstraps a row in `users` on first sight so downstream code can FK to it.
export const requireUser = createMiddleware<AuthContext>(async (c, next) => {
const auth = c.req.header("authorization") ?? "";
const token = auth.replace(/^Bearer\s+/i, "").trim();
// Service-to-service path (Grow Agent actor calling backend).
// Header `x-growqr-user` is REQUIRED so we can scope the call.
if (
token &&
config.serviceToken &&
token === config.serviceToken &&
c.req.header("x-growqr-user")
) {
const userId = c.req.header("x-growqr-user")!;
let row = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (!row) {
const inserted = await db
.insert(users)
.values({
id: userId,
email: `${userId}@service.local`,
displayName: userId,
})
.onConflictDoNothing()
.returning();
row = inserted[0];
}
if (!row) {
throw new HTTPException(401, { message: "service token references unknown user" });
}
c.set("userId", row.id);
c.set("userEmail", row.email);
return next();
}
if (!token) {
throw new HTTPException(401, { message: "missing bearer token" });
}
if (!clerk) {
throw new HTTPException(500, { message: "Clerk not configured" });
}
let payload: Awaited<ReturnType<typeof verifyToken>>;
try {
payload = await verifyToken(token, {
secretKey: config.clerkSecretKey,
});
} catch {
throw new HTTPException(401, { message: "invalid clerk token" });
}
const userId = payload.sub;
if (!userId) {
throw new HTTPException(401, { message: "clerk token missing subject" });
}
// Lazy-mirror Clerk user → users table.
let row = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (!row) {
const clerkUser = await clerk.users.getUser(userId);
const email =
clerkUser.primaryEmailAddress?.emailAddress ??
clerkUser.emailAddresses[0]?.emailAddress ??
`${userId}@unknown.local`;
const displayName =
[clerkUser.firstName, clerkUser.lastName].filter(Boolean).join(" ") ||
clerkUser.username ||
null;
const inserted = await db
.insert(users)
.values({ id: userId, email, displayName })
.onConflictDoUpdate({
target: users.id,
set: { email, displayName, updatedAt: new Date() },
})
.returning();
row = inserted[0];
if (!row) {
throw new HTTPException(500, { message: "failed to upsert user" });
}
}
c.set("userId", row.id);
c.set("userEmail", row.email);
return next();
});