sitcod3 commited on
Commit
b13f38d
·
verified ·
1 Parent(s): 907df84

Upload src/server/middleware/auth.ts with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/server/middleware/auth.ts +145 -0
src/server/middleware/auth.ts ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/server/middleware/auth.ts
2
+ //
3
+ // Supabase JWT verification + license gating for the paid API.
4
+ //
5
+ // The central Supabase project issues the session JWT; this backend verifies
6
+ // it against the project's PUBLIC JWKS endpoint (no shared database needed).
7
+ // License rows are read from the central project via its service-role REST
8
+ // API (server-side only).
9
+ //
10
+ // Enforcement is OFF by default (LICENSE_ENFORCEMENT != "true") so existing
11
+ // open/demo deployments keep working. Turn it on at go-live:
12
+ //
13
+ // SUPABASE_URL=https://ciytvulujiqemqljablq.supabase.co
14
+ // SUPABASE_SERVICE_ROLE_KEY=<service role key, server only>
15
+ // LICENSE_ENFORCEMENT=true
16
+ //
17
+ // Frontend equivalent lives in the FortyDN website repo (src/lib/productApi.ts).
18
+ // Ported from BRIDGE Modul-E (src/server/middleware/auth.ts) - same contract.
19
+
20
+ import type { Request, Response, NextFunction } from "express";
21
+ import { createRemoteJWKSet, jwtVerify } from "jose";
22
+
23
+ export const CENTRAL_SUPABASE_URL =
24
+ process.env.SUPABASE_URL || "https://ciytvulujiqemqljablq.supabase.co";
25
+
26
+ const JWKS_URL = new URL(`${CENTRAL_SUPABASE_URL}/auth/v1/.well-known/jwks.json`);
27
+
28
+ let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
29
+
30
+ function getJwks() {
31
+ if (!jwks) jwks = createRemoteJWKSet(JWKS_URL);
32
+ return jwks;
33
+ }
34
+
35
+ function enforcementOn(): boolean {
36
+ return process.env.LICENSE_ENFORCEMENT === "true";
37
+ }
38
+
39
+ export interface AuthClaims {
40
+ userId: string;
41
+ email?: string;
42
+ }
43
+
44
+ function bearerToken(req: Request): string | null {
45
+ const header = req.headers.authorization || "";
46
+ if (!header.startsWith("Bearer ")) return null;
47
+ const token = header.slice(7).trim();
48
+ return token.length > 0 ? token : null;
49
+ }
50
+
51
+ export async function verifySupabaseJwt(token: string): Promise<AuthClaims> {
52
+ const { payload } = await jwtVerify(token, getJwks(), {
53
+ audience: "authenticated",
54
+ });
55
+ const sub = payload.sub;
56
+ if (!sub) throw new Error("JWT has no sub claim");
57
+ return {
58
+ userId: sub,
59
+ email: typeof payload.email === "string" ? payload.email : undefined,
60
+ };
61
+ }
62
+
63
+ /** Modul-S is unlocked directly, via Validate+Send, or via Full Bridge. */
64
+ const MODULS_GRANTS = new Set([
65
+ "modul-s",
66
+ "combo-validate-send",
67
+ "combo-full-bridge",
68
+ ]);
69
+
70
+ export async function hasModulSLicense(userId: string): Promise<boolean> {
71
+ const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
72
+ if (!serviceKey) {
73
+ console.warn("[auth] SUPABASE_SERVICE_ROLE_KEY not set - cannot check licenses.");
74
+ return false;
75
+ }
76
+ const params = new URLSearchParams({
77
+ user_id: `eq.${userId}`,
78
+ status: "eq.active",
79
+ select: "product",
80
+ });
81
+ const res = await fetch(`${CENTRAL_SUPABASE_URL}/rest/v1/licenses?${params}`, {
82
+ headers: {
83
+ apikey: serviceKey,
84
+ Authorization: `Bearer ${serviceKey}`,
85
+ },
86
+ });
87
+ if (!res.ok) {
88
+ const body = await res.text().catch(() => "");
89
+ // Missing table (migrations not applied yet) => fail closed with a hint.
90
+ console.warn(`[auth] license lookup HTTP ${res.status}: ${body.slice(0, 200)}`);
91
+ return false;
92
+ }
93
+ const rows = (await res.json()) as { product?: string }[];
94
+ return (
95
+ Array.isArray(rows) && rows.some((r) => r.product && MODULS_GRANTS.has(r.product))
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Requires a valid Supabase JWT. No-op (with a one-time warning) while
101
+ * LICENSE_ENFORCEMENT is off so demo deployments keep working.
102
+ */
103
+ export function requireAuth(req: Request, res: Response, next: NextFunction) {
104
+ if (!enforcementOn()) return next();
105
+ void (async () => {
106
+ const token = bearerToken(req);
107
+ if (!token) {
108
+ return res.status(401).json({ detail: "Missing bearer token - sign in first." });
109
+ }
110
+ try {
111
+ const claims = await verifySupabaseJwt(token);
112
+ (req as Request & { auth?: AuthClaims }).auth = claims;
113
+ return next();
114
+ } catch {
115
+ return res.status(401).json({ detail: "Invalid or expired token." });
116
+ }
117
+ })();
118
+ }
119
+
120
+ /**
121
+ * Requires a valid JWT AND an active Modul-S license (direct, Validate+Send,
122
+ * or Full Bridge). No-op while LICENSE_ENFORCEMENT is off.
123
+ */
124
+ export function requireModulSLicense(req: Request, res: Response, next: NextFunction) {
125
+ if (!enforcementOn()) return next();
126
+ void (async () => {
127
+ const token = bearerToken(req);
128
+ if (!token) {
129
+ return res.status(401).json({ detail: "Missing bearer token - sign in first." });
130
+ }
131
+ try {
132
+ const claims = await verifySupabaseJwt(token);
133
+ (req as Request & { auth?: AuthClaims }).auth = claims;
134
+ const ok = await hasModulSLicense(claims.userId);
135
+ if (!ok) {
136
+ return res
137
+ .status(402)
138
+ .json({ detail: "No active Modul-S license - see /pricing." });
139
+ }
140
+ return next();
141
+ } catch {
142
+ return res.status(401).json({ detail: "Invalid or expired token." });
143
+ }
144
+ })();
145
+ }