sitcod3 commited on
Commit
a2361c3
·
verified ·
1 Parent(s): dcdc25d

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

Browse files
Files changed (1) hide show
  1. src//server//middleware//auth.ts +144 -0
src//server//middleware//auth.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-X (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-E is unlocked directly or via the Full Bridge combo. */
64
+ const MODULE_GRANTS = new Set([
65
+ "modul-e",
66
+ "combo-full-bridge",
67
+ ]);
68
+
69
+ export async function hasModulELicense(userId: string): Promise<boolean> {
70
+ const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
71
+ if (!serviceKey) {
72
+ console.warn("[auth] SUPABASE_SERVICE_ROLE_KEY not set — cannot check licenses.");
73
+ return false;
74
+ }
75
+ const params = new URLSearchParams({
76
+ user_id: `eq.${userId}`,
77
+ status: "eq.active",
78
+ select: "product",
79
+ });
80
+ const res = await fetch(`${CENTRAL_SUPABASE_URL}/rest/v1/licenses?${params}`, {
81
+ headers: {
82
+ apikey: serviceKey,
83
+ Authorization: `Bearer ${serviceKey}`,
84
+ },
85
+ });
86
+ if (!res.ok) {
87
+ const body = await res.text().catch(() => "");
88
+ // Missing table (migrations not applied yet) => fail closed with a hint.
89
+ console.warn(`[auth] license lookup HTTP ${res.status}: ${body.slice(0, 200)}`);
90
+ return false;
91
+ }
92
+ const rows = (await res.json()) as { product?: string }[];
93
+ return (
94
+ Array.isArray(rows) && rows.some((r) => r.product && MODULE_GRANTS.has(r.product))
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Requires a valid Supabase JWT. No-op (with a one-time warning) while
100
+ * LICENSE_ENFORCEMENT is off so demo deployments keep working.
101
+ */
102
+ export function requireAuth(req: Request, res: Response, next: NextFunction) {
103
+ if (!enforcementOn()) return next();
104
+ void (async () => {
105
+ const token = bearerToken(req);
106
+ if (!token) {
107
+ return res.status(401).json({ detail: "Missing bearer token — sign in first." });
108
+ }
109
+ try {
110
+ const claims = await verifySupabaseJwt(token);
111
+ (req as Request & { auth?: AuthClaims }).auth = claims;
112
+ return next();
113
+ } catch {
114
+ return res.status(401).json({ detail: "Invalid or expired token." });
115
+ }
116
+ })();
117
+ }
118
+
119
+ /**
120
+ * Requires a valid JWT AND an active Modul-E license (direct or Full Bridge).
121
+ * No-op while LICENSE_ENFORCEMENT is off.
122
+ */
123
+ export function requireModulELicense(req: Request, res: Response, next: NextFunction) {
124
+ if (!enforcementOn()) return next();
125
+ void (async () => {
126
+ const token = bearerToken(req);
127
+ if (!token) {
128
+ return res.status(401).json({ detail: "Missing bearer token — sign in first." });
129
+ }
130
+ try {
131
+ const claims = await verifySupabaseJwt(token);
132
+ (req as Request & { auth?: AuthClaims }).auth = claims;
133
+ const ok = await hasModulELicense(claims.userId);
134
+ if (!ok) {
135
+ return res
136
+ .status(402)
137
+ .json({ detail: "No active Modul-E license — see /pricing." });
138
+ }
139
+ return next();
140
+ } catch {
141
+ return res.status(401).json({ detail: "Invalid or expired token." });
142
+ }
143
+ })();
144
+ }