agentify-help/auth.ts
2026-05-02 13:07:28 +00:00

60 lines
1.8 KiB
TypeScript

/**
* Minimal partner token auth for agentify-help standalone app.
* Replaces server/partner-api-routes.ts validatePartnerToken.
*/
import { pool } from "./db.js";
import crypto from "crypto";
function hashToken(token: string): string {
return crypto.createHash("sha256").update(token).digest("hex");
}
export interface PartnerTokenContext {
id: number;
partner_name: string;
permissions: string[];
area_codes: string[] | null;
}
export async function validatePartnerToken(
authHeader: string | undefined
): Promise<PartnerTokenContext | null> {
if (!authHeader?.startsWith("Bearer ws_")) return null;
const token = authHeader.slice(7);
if (!token.startsWith("ws_")) return null;
const hash = hashToken(token);
const result = await pool.query(
`SELECT id, partner_name, permissions, area_codes
FROM partner_api_tokens
WHERE token_hash = $1 AND active = true AND revoked_at IS NULL
LIMIT 1`,
[hash]
);
if (!result.rows.length) return null;
const r = result.rows[0];
pool.query(
`UPDATE partner_api_tokens SET last_used_at = NOW(), use_count = use_count + 1 WHERE id = $1`,
[r.id]
).catch(() => {});
return {
id: r.id,
partner_name: r.partner_name,
permissions: (r.permissions as string[]) || [],
area_codes: r.area_codes || null,
};
}
export function requirePermission(permission: string) {
return async (req: any, res: any, next: any) => {
const ctx = await validatePartnerToken(req.headers.authorization).catch(() => null);
if (!ctx) return res.status(401).json({ error: "Unauthorized" });
if (!ctx.permissions.includes(permission) && !ctx.permissions.includes("geo.seed")) {
return res.status(403).json({ error: "Forbidden", required: permission });
}
(req as any).partner = ctx;
next();
};
}