Skip to content

Commit dbc66f9

Browse files
committed
refactored code
1 parent 2c1ea9c commit dbc66f9

5 files changed

Lines changed: 125 additions & 204 deletions

File tree

backend/scripts/demo_user_flow.ts

Lines changed: 9 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,20 @@
1-
// Demo script: simulates user sign-in, session, proxy call, stats fetch
2-
// Usage: deno run -A backend/scripts/demo_user_flow.ts
3-
// Reads backend/.env or prompts for BASE_URL/APP_BASE_URL and USER_JWT.
1+
import { loadCombinedEnv, normalizeBackendBase, getJwt, headersWithJwt } from './lib/env_auth.ts';
42

5-
type EnvMap = Record<string, string>;
6-
const parseDotenv = (txt: string): EnvMap => {
7-
const out: EnvMap = {};
8-
const lines = txt.split(/\r?\n/);
9-
for (let raw of lines) {
10-
if (!raw) continue;
11-
raw = raw.replace(/^\s*/, '');
12-
if (!raw || raw.startsWith('#')) continue;
13-
if (raw.startsWith('export ')) raw = raw.slice(7);
14-
const i = raw.indexOf('=');
15-
if (i < 0) continue;
16-
const key = raw.slice(0, i).trim();
17-
let val = raw.slice(i + 1);
18-
val = val.replace(/^\s+/, '');
19-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
20-
val = val.slice(1, -1)
21-
.replace(/\\n/g, '\n')
22-
.replace(/\\r/g, '\r')
23-
.replace(/\\t/g, '\t')
24-
.replace(/\\"/g, '"')
25-
.replace(/\\'/g, '\'')
26-
.replace(/\\\\/g, '\\');
27-
} else {
28-
val = val.replace(/\s+$/, '');
29-
}
30-
out[key] = val;
31-
}
32-
return out;
33-
};
34-
const loadEnvFile = async (path: string): Promise<EnvMap> => {
35-
try { return parseDotenv(await Deno.readTextFile(path)); } catch { return {}; }
36-
};
37-
const envFiles = [ './backend/.env', './.env' ];
38-
const envFromFiles: EnvMap = (await Promise.all(envFiles.map(loadEnvFile))).reduce((a,b)=>({...a,...b}),{});
39-
const get = (k: string) => Deno.env.get(k) ?? envFromFiles[k];
40-
41-
let BASE = (get('BASE_URL') || '').replace(/\/$/, '');
42-
const appBase = (get('APP_BASE_URL') || '').replace(/\/$/, '');
43-
if (BASE.endsWith('/openai')) BASE = BASE.slice(0, -'/openai'.length);
44-
if (!BASE && appBase) BASE = appBase.replace(/\/openai$/, '');
45-
let USER_JWT = get('USER_JWT');
46-
let SUPABASE_URL = get('SUPABASE_URL');
47-
let SUPABASE_ANON_KEY = get('SUPABASE_ANON_KEY');
48-
let EMAIL = get('EMAIL');
49-
let PASSWORD = get('PASSWORD');
3+
const env = await loadCombinedEnv();
4+
let BASE = normalizeBackendBase(env);
505
if (!BASE) BASE = (prompt('Backend base URL (e.g., https://<ref>.functions.supabase.co):') ?? '').replace(/\/$/, '');
51-
if (!USER_JWT) {
52-
// Try to login interactively
53-
if (!SUPABASE_URL) SUPABASE_URL = prompt('Supabase URL (https://<ref>.supabase.co):') ?? '';
54-
if (!SUPABASE_ANON_KEY) SUPABASE_ANON_KEY = prompt('Supabase anon key:') ?? '';
55-
if (!EMAIL) EMAIL = prompt('Email:') ?? '';
56-
if (!PASSWORD) PASSWORD = prompt('Password (input hidden not supported):') ?? '';
57-
}
58-
if (!USER_JWT && SUPABASE_URL && SUPABASE_ANON_KEY && EMAIL && PASSWORD) {
59-
const endpoint = `${SUPABASE_URL.replace(/\/$/, '')}/auth/v1/token?grant_type=password`;
60-
const authRes = await fetch(endpoint, {
61-
method: 'POST',
62-
headers: { 'Content-Type': 'application/json', apikey: SUPABASE_ANON_KEY },
63-
body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
64-
});
65-
if (authRes.ok) {
66-
const data = await authRes.json();
67-
USER_JWT = data?.access_token ?? '';
68-
}
69-
}
70-
if (!BASE || !USER_JWT) {
71-
console.error('Missing inputs. Need BASE_URL (or APP_BASE_URL) and USER_JWT.');
72-
Deno.exit(1);
73-
}
6+
let USER_JWT = await getJwt(env, 'user');
7+
if (!BASE || !USER_JWT) { console.error('Missing inputs. Need BASE and USER_JWT.'); Deno.exit(1); }
748

759
console.log('Using BASE =', BASE);
7610
console.log('USER_JWT prefix =', USER_JWT.slice(0, 12) + '…');
7711

78-
const sessionHeaders: Record<string,string> = { Authorization: `Bearer ${USER_JWT}` };
79-
if (SUPABASE_ANON_KEY) sessionHeaders['apikey'] = SUPABASE_ANON_KEY;
80-
let sessionRes = await fetch(`${BASE}/session`, { method: 'POST', headers: sessionHeaders });
12+
let sessionRes = await fetch(`${BASE}/session`, { method: 'POST', headers: headersWithJwt(USER_JWT, env) });
8113
if (!sessionRes.ok) {
8214
const body = await sessionRes.text();
83-
// If Invalid JWT and we have login inputs, attempt one retry to refresh token
84-
if (sessionRes.status === 401 && body.includes('Invalid JWT') && SUPABASE_URL && SUPABASE_ANON_KEY && EMAIL && PASSWORD) {
85-
const endpoint = `${SUPABASE_URL.replace(/\/$/, '')}/auth/v1/token?grant_type=password`;
86-
const authRes = await fetch(endpoint, {
87-
method: 'POST',
88-
headers: { 'Content-Type': 'application/json', apikey: SUPABASE_ANON_KEY },
89-
body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
90-
});
91-
if (authRes.ok) {
92-
const data = await authRes.json();
93-
USER_JWT = data?.access_token ?? USER_JWT;
94-
const hdrs: Record<string,string> = { Authorization: `Bearer ${USER_JWT}` };
95-
if (SUPABASE_ANON_KEY) hdrs['apikey'] = SUPABASE_ANON_KEY;
96-
sessionRes = await fetch(`${BASE}/session`, { method: 'POST', headers: hdrs });
97-
}
15+
if (sessionRes.status === 401 && body.includes('Invalid JWT')) {
16+
USER_JWT = await getJwt(env, 'user');
17+
sessionRes = await fetch(`${BASE}/session`, { method: 'POST', headers: headersWithJwt(USER_JWT, env) });
9818
}
9919
if (!sessionRes.ok) {
10020
console.error('Session failed:', sessionRes.status, body);

backend/scripts/lib/env_auth.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
export type EnvMap = Record<string, string>;
2+
3+
const parseDotenv = (txt: string): EnvMap => {
4+
const out: EnvMap = {};
5+
const lines = txt.split(/\r?\n/);
6+
for (let raw of lines) {
7+
if (!raw) continue;
8+
raw = raw.replace(/^\s*/, '');
9+
if (!raw || raw.startsWith('#')) continue;
10+
if (raw.startsWith('export ')) raw = raw.slice(7);
11+
const i = raw.indexOf('=');
12+
if (i < 0) continue;
13+
const key = raw.slice(0, i).trim();
14+
let val = raw.slice(i + 1);
15+
val = val.replace(/^\s+/, '');
16+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
17+
val = val.slice(1, -1)
18+
.replace(/\\n/g, '\n')
19+
.replace(/\\r/g, '\r')
20+
.replace(/\\t/g, '\t')
21+
.replace(/\\"/g, '"')
22+
.replace(/\\'/g, '\'')
23+
.replace(/\\\\/g, '\\');
24+
} else {
25+
val = val.replace(/\s+$/, '');
26+
}
27+
out[key] = val;
28+
}
29+
return out;
30+
};
31+
32+
const loadEnvFile = async (path: string): Promise<EnvMap> => {
33+
try { return parseDotenv(await Deno.readTextFile(path)); } catch { return {}; }
34+
};
35+
36+
export const loadCombinedEnv = async (): Promise<EnvMap> => {
37+
const files = [ './backend/.env', './.env' ];
38+
const fileEnv = (await Promise.all(files.map(loadEnvFile))).reduce((a,b)=>({...a,...b}), {} as EnvMap);
39+
const env: EnvMap = { ...fileEnv };
40+
// Overlay Deno.env on top
41+
for (const [k, v] of Object.entries(Deno.env.toObject())) env[k] = v;
42+
return env;
43+
};
44+
45+
export const normalizeBackendBase = (env: EnvMap): string => {
46+
let base = (env['BASE_URL'] || '').replace(/\/$/, '');
47+
const appBase = (env['APP_BASE_URL'] || '').replace(/\/$/, '');
48+
if (base.endsWith('/openai')) base = base.slice(0, -'/openai'.length);
49+
if (!base && appBase) base = appBase.replace(/\/openai$/, '');
50+
return base;
51+
};
52+
53+
type Role = 'user' | 'admin';
54+
55+
export const getJwt = async (env: EnvMap, role: Role = 'user'): Promise<string> => {
56+
const keyName = role === 'admin' ? 'ADMIN_JWT' : 'USER_JWT';
57+
let jwt = env[keyName] || '';
58+
if (jwt) return jwt;
59+
60+
let supaUrl = env['SUPABASE_URL'] || '';
61+
let anon = env['SUPABASE_ANON_KEY'] || '';
62+
let email = (role === 'admin' ? (env['ADMIN_EMAIL'] || env['EMAIL']) : env['EMAIL']) || '';
63+
let password = (role === 'admin' ? (env['ADMIN_PASSWORD'] || env['PASSWORD']) : env['PASSWORD']) || '';
64+
65+
if (!supaUrl) supaUrl = prompt('Supabase URL (https://<ref>.supabase.co):') ?? '';
66+
if (!anon) anon = prompt('Supabase anon key:') ?? '';
67+
if (!email) email = prompt(role === 'admin' ? 'Admin email:' : 'Email:') ?? '';
68+
if (!password) password = prompt(role === 'admin' ? 'Admin password (input hidden not supported):' : 'Password (input hidden not supported):') ?? '';
69+
70+
if (!supaUrl || !anon || !email || !password) return '';
71+
const endpoint = `${supaUrl.replace(/\/$/, '')}/auth/v1/token?grant_type=password`;
72+
const res = await fetch(endpoint, {
73+
method: 'POST',
74+
headers: { 'Content-Type': 'application/json', apikey: anon },
75+
body: JSON.stringify({ email, password }),
76+
});
77+
if (!res.ok) return '';
78+
const data = await res.json();
79+
return data?.access_token ?? '';
80+
};
81+
82+
export const headersWithJwt = (jwt: string, env: EnvMap): Record<string,string> => {
83+
const h: Record<string,string> = { Authorization: `Bearer ${jwt}` };
84+
if (env['SUPABASE_ANON_KEY']) h['apikey'] = env['SUPABASE_ANON_KEY'];
85+
return h;
86+
};
87+

backend/scripts/test_admin.ts

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,16 @@
1-
// Test admin endpoints: /admin and /admin/users (auto-login admin if ADMIN_JWT missing)
1+
// Test admin endpoints using shared helpers
2+
import { loadCombinedEnv, normalizeBackendBase, getJwt, headersWithJwt } from './lib/env_auth.ts';
23

3-
type EnvMap = Record<string, string>;
4-
const parseDotenv = (t: string): EnvMap => { const o:EnvMap={}; for(let r of t.split(/\r?\n/)){ if(!r)continue; r=r.replace(/^\s*/,''); if(!r||r.startsWith('#'))continue; if(r.startsWith('export ')) r=r.slice(7); const i=r.indexOf('='); if(i<0)continue; const k=r.slice(0,i).trim(); let v=r.slice(i+1).replace(/^\s+/,''); if((v.startsWith('"')&&v.endsWith('"'))||(v.startsWith('\'')&&v.endsWith('\''))){ v=v.slice(1,-1).replace(/\\n/g,'\n').replace(/\\r/g,'\r').replace(/\\t/g,'\t').replace(/\\"/g,'"').replace(/\\'/g,'\'').replace(/\\\\/g,'\\'); } else v=v.replace(/\s+$/,''); o[k]=v;} return o; };
5-
const loadEnvFile=async(p:string)=>{try{return parseDotenv(await Deno.readTextFile(p))}catch{return {}}};
6-
const envFiles=['./backend/.env','./.env'];
7-
const envFromFiles: EnvMap = (await Promise.all(envFiles.map(loadEnvFile))).reduce((a,b)=>({...a,...b}),{} as EnvMap);
8-
const get=(k:string)=>Deno.env.get(k)??envFromFiles[k];
4+
const env = await loadCombinedEnv();
5+
let BASE = normalizeBackendBase(env);
6+
if (!BASE) BASE = (prompt('Backend base URL (e.g., https://<ref>.functions.supabase.co):') ?? '').replace(/\/$/, '');
7+
const adminJwt = await getJwt(env, 'admin');
8+
if (!BASE || !adminJwt) { console.error('Missing BASE or unable to obtain ADMIN_JWT'); Deno.exit(1); }
99

10-
let BASE=(get('BASE_URL')||'').replace(/\/$/,'');
11-
const appBase=(get('APP_BASE_URL')||'').replace(/\/$/,'');
12-
if(BASE.endsWith('/openai')) BASE=BASE.slice(0,-'/openai'.length);
13-
if(!BASE&&appBase) BASE=appBase.replace(/\/openai$/,'');
14-
let ADMIN_JWT=get('ADMIN_JWT');
15-
let SUPABASE_URL=get('SUPABASE_URL');
16-
let SUPABASE_ANON_KEY=get('SUPABASE_ANON_KEY');
17-
let ADMIN_EMAIL=get('ADMIN_EMAIL')||get('EMAIL');
18-
let ADMIN_PASSWORD=get('ADMIN_PASSWORD')||get('PASSWORD');
19-
20-
if(!BASE) BASE=(prompt('Backend base URL (e.g., https://<ref>.functions.supabase.co):')??'').replace(/\/$/,'');
21-
if(!ADMIN_JWT){ if(!SUPABASE_URL) SUPABASE_URL=prompt('Supabase URL:')??''; if(!SUPABASE_ANON_KEY) SUPABASE_ANON_KEY=prompt('Supabase anon key:')??''; if(!ADMIN_EMAIL) ADMIN_EMAIL=prompt('Admin email:')??''; if(!ADMIN_PASSWORD) ADMIN_PASSWORD=prompt('Admin password (input hidden not supported):')??''; }
22-
23-
const login=async()=>{ if(!SUPABASE_URL||!SUPABASE_ANON_KEY||!ADMIN_EMAIL||!ADMIN_PASSWORD) return false; const endpoint=`${SUPABASE_URL.replace(/\/$/,'')}/auth/v1/token?grant_type=password`; const r=await fetch(endpoint,{method:'POST',headers:{'Content-Type':'application/json',apikey:SUPABASE_ANON_KEY},body:JSON.stringify({email:ADMIN_EMAIL,password:ADMIN_PASSWORD})}); if(!r.ok) return false; const d=await r.json(); ADMIN_JWT=d?.access_token??''; return !!ADMIN_JWT; };
24-
25-
if(!BASE){ console.error('Missing BASE_URL/APP_BASE_URL'); Deno.exit(1);} if(!ADMIN_JWT && !(await login())){ console.error('Missing ADMIN_JWT and login failed.'); Deno.exit(1);}
26-
27-
const hdr=()=>{ const h:Record<string,string>={Authorization:`Bearer ${ADMIN_JWT}`}; if(SUPABASE_ANON_KEY) h['apikey']=SUPABASE_ANON_KEY; return h; };
28-
29-
let a=await fetch(`${BASE}/admin`,{headers:hdr()}); let aText=await a.text();
30-
if(!a.ok&&a.status===401&&aText.includes('Invalid JWT')&&await login()) { a=await fetch(`${BASE}/admin`,{headers:hdr()}); aText=await a.text(); }
10+
let a = await fetch(`${BASE}/admin`, { headers: headersWithJwt(adminJwt, env) });
11+
let aText = await a.text();
3112
console.log('admin status:', a.status); console.log('admin body (trunc):', aText.slice(0,400));
3213

33-
let u=await fetch(`${BASE}/admin/users?limit=5`,{headers:hdr()}); let uText=await u.text();
34-
if(!u.ok&&u.status===401&&uText.includes('Invalid JWT')&&await login()) { u=await fetch(`${BASE}/admin/users?limit=5`,{headers:hdr()}); uText=await u.text(); }
14+
let u = await fetch(`${BASE}/admin/users?limit=5`, { headers: headersWithJwt(adminJwt, env) });
15+
let uText = await u.text();
3516
console.log('admin users status:', u.status); console.log('admin users body (trunc):', uText.slice(0,400));
Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,16 @@
1-
// Test /dashboard and /stats: auto-login to fetch USER_JWT if needed
1+
// Test /dashboard and /stats using shared helpers
2+
import { loadCombinedEnv, normalizeBackendBase, getJwt, headersWithJwt } from './lib/env_auth.ts';
23

3-
type EnvMap = Record<string, string>;
4-
const parseDotenv = (t: string): EnvMap => { const o:EnvMap={}; for(let r of t.split(/\r?\n/)){ if(!r)continue; r=r.replace(/^\s*/,''); if(!r||r.startsWith('#'))continue; if(r.startsWith('export '))r=r.slice(7); const i=r.indexOf('='); if(i<0)continue; const k=r.slice(0,i).trim(); let v=r.slice(i+1).replace(/^\s+/,''); if((v.startsWith('"')&&v.endsWith('"'))||(v.startsWith('\'')&&v.endsWith('\''))){ v=v.slice(1,-1).replace(/\\n/g,'\n').replace(/\\r/g,'\r').replace(/\\t/g,'\t').replace(/\\"/g,'"').replace(/\\'/g,'\'').replace(/\\\\/g,'\\'); } else v=v.replace(/\s+$/,''); o[k]=v;} return o; };
5-
const loadEnvFile=async(p:string)=>{try{return parseDotenv(await Deno.readTextFile(p))}catch{return {}}};
6-
const envFiles=['./backend/.env','./.env'];
7-
const envFromFiles: EnvMap = (await Promise.all(envFiles.map(loadEnvFile))).reduce((a,b)=>({...a,...b}),{} as EnvMap);
8-
const get=(k:string)=>Deno.env.get(k)??envFromFiles[k];
4+
const env = await loadCombinedEnv();
5+
let BASE = normalizeBackendBase(env);
6+
if (!BASE) BASE = (prompt('Backend base URL (e.g., https://<ref>.functions.supabase.co):') ?? '').replace(/\/$/, '');
7+
const jwt = await getJwt(env, 'user');
8+
if (!BASE || !jwt) { console.error('Missing BASE or unable to obtain USER_JWT'); Deno.exit(1); }
99

10-
let BASE=(get('BASE_URL')||'').replace(/\/$/,'');
11-
const appBase=(get('APP_BASE_URL')||'').replace(/\/$/,'');
12-
if(BASE.endsWith('/openai')) BASE=BASE.slice(0,-'/openai'.length);
13-
if(!BASE&&appBase) BASE=appBase.replace(/\/openai$/,'');
14-
let USER_JWT=get('USER_JWT');
15-
let SUPABASE_URL=get('SUPABASE_URL');
16-
let SUPABASE_ANON_KEY=get('SUPABASE_ANON_KEY');
17-
let EMAIL=get('EMAIL');
18-
let PASSWORD=get('PASSWORD');
19-
20-
if(!BASE) BASE=(prompt('Backend base URL (e.g., https://<ref>.functions.supabase.co):')??'').replace(/\/$/,'');
21-
if(!USER_JWT){ if(!SUPABASE_URL) SUPABASE_URL=prompt('Supabase URL:')??''; if(!SUPABASE_ANON_KEY) SUPABASE_ANON_KEY=prompt('Supabase anon key:')??''; if(!EMAIL) EMAIL=prompt('Email:')??''; if(!PASSWORD) PASSWORD=prompt('Password (input hidden not supported):')??''; }
22-
23-
const login=async()=>{ if(!SUPABASE_URL||!SUPABASE_ANON_KEY||!EMAIL||!PASSWORD) return false; const endpoint=`${SUPABASE_URL.replace(/\/$/,'')}/auth/v1/token?grant_type=password`; const r=await fetch(endpoint,{method:'POST',headers:{'Content-Type':'application/json',apikey:SUPABASE_ANON_KEY},body:JSON.stringify({email:EMAIL,password:PASSWORD})}); if(!r.ok) return false; const d=await r.json(); USER_JWT=d?.access_token??''; return !!USER_JWT; };
24-
25-
if(!BASE){ console.error('Missing BASE_URL/APP_BASE_URL'); Deno.exit(1);} if(!USER_JWT && !(await login())){ console.error('Missing USER_JWT and login failed.'); Deno.exit(1);}
26-
27-
const hdr=()=>{ const h:Record<string,string>={Authorization:`Bearer ${USER_JWT}`}; if(SUPABASE_ANON_KEY) h['apikey']=SUPABASE_ANON_KEY; return h; };
28-
29-
let ds=await fetch(`${BASE}/dashboard`,{headers:hdr()}); let dsText=await ds.text();
30-
if(!ds.ok&&ds.status===401&&dsText.includes('Invalid JWT')&&await login()) { ds=await fetch(`${BASE}/dashboard`,{headers:hdr()}); dsText=await ds.text(); }
10+
let ds = await fetch(`${BASE}/dashboard`, { headers: headersWithJwt(jwt, env) });
11+
let dsText = await ds.text();
3112
console.log('dashboard status:', ds.status); console.log('dashboard body (trunc):', dsText.slice(0,400));
3213

33-
let st=await fetch(`${BASE}/stats`,{headers:hdr()}); let stText=await st.text();
34-
if(!st.ok&&st.status===401&&stText.includes('Invalid JWT')&&await login()) { st=await fetch(`${BASE}/stats`,{headers:hdr()}); stText=await st.text(); }
14+
let st = await fetch(`${BASE}/stats`, { headers: headersWithJwt(jwt, env) });
15+
let stText = await st.text();
3516
console.log('stats status:', st.status); console.log('stats body (trunc):', stText.slice(0,400));

0 commit comments

Comments
 (0)