- 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.
110 lines
3.2 KiB
TypeScript
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();
|
|
});
|