1
0
forked from wrenn/wrenn
Files
wrenn-releases/frontend/src/lib/auth.svelte.ts
pptx704 e069b3e679 Add BYOC page, admin section, and is_byoc team visibility gating
- Frontend: BYOC hosts page (/dashboard/byoc) with register/delete flows,
  shimmer loading, pulsing online status, animated token reveal checkmark
- Frontend: Admin section (/admin/hosts) with platform + BYOC tabs, stat
  pills, skeleton loading, slide-in animations for new rows
- Frontend: AdminSidebar component with accent top bar and admin pill badge
- Frontend: BYOC nav item shown only when team.is_byoc is true (derived
  from teams store, not JWT); disabled for members
- Frontend: Admin shield button in Sidebar, visible only to platform admins
- Backend: is_admin in JWT claims + requireAdmin middleware (DB-validated)
- Backend: is_byoc added to teamResponse so frontend derives visibility
  from fresh team data rather than stale JWT fields
- Backend: SetBYOC admin endpoint (PUT /v1/admin/teams/{id}/byoc)
- Backend: Admin hosts list enriches BYOC entries with team_name
- Host agent: load .env file via godotenv on startup
2026-03-25 03:10:41 +06:00

129 lines
3.1 KiB
TypeScript

import { goto } from '$app/navigation';
const STORAGE_KEYS = {
token: 'wrenn_token',
userId: 'wrenn_user_id',
teamId: 'wrenn_team_id',
email: 'wrenn_email',
name: 'wrenn_name'
} as const;
function isTokenExpired(token: string): boolean {
try {
const payload = token.split('.')[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
const { exp } = JSON.parse(decoded);
return Date.now() / 1000 >= exp;
} catch {
return true;
}
}
function decodeJWTPayload(token: string): Record<string, unknown> {
try {
const payload = token.split('.')[1];
return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
} catch {
return {};
}
}
function createAuth() {
let token = $state<string | null>(null);
let userId = $state<string | null>(null);
let teamId = $state<string | null>(null);
let email = $state<string | null>(null);
let name = $state<string | null>(null);
let isAdmin = $state(false);
let role = $state<string>('member');
let initialized = $state(false);
// Initialize from localStorage synchronously at module load.
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(STORAGE_KEYS.token);
if (stored && !isTokenExpired(stored)) {
token = stored;
userId = localStorage.getItem(STORAGE_KEYS.userId);
teamId = localStorage.getItem(STORAGE_KEYS.teamId);
email = localStorage.getItem(STORAGE_KEYS.email);
name = localStorage.getItem(STORAGE_KEYS.name);
const payload = decodeJWTPayload(stored);
isAdmin = Boolean(payload.is_admin);
role = String(payload.role || 'member');
} else if (stored) {
// Expired — clean up.
for (const key of Object.values(STORAGE_KEYS)) {
localStorage.removeItem(key);
}
}
initialized = true;
}
const isAuthenticated = $derived(token !== null && !isTokenExpired(token));
return {
get token() {
return token;
},
get userId() {
return userId;
},
get teamId() {
return teamId;
},
get email() {
return email;
},
get name() {
return name;
},
get isAdmin() {
return isAdmin;
},
get role() {
return role;
},
get isAuthenticated() {
return isAuthenticated;
},
get initialized() {
return initialized;
},
login(data: { token: string; user_id: string; team_id: string; email: string; name: string }) {
token = data.token;
userId = data.user_id;
teamId = data.team_id;
email = data.email;
name = data.name;
const payload = decodeJWTPayload(data.token);
isAdmin = Boolean(payload.is_admin);
role = String(payload.role || 'member');
localStorage.setItem(STORAGE_KEYS.token, data.token);
localStorage.setItem(STORAGE_KEYS.userId, data.user_id);
localStorage.setItem(STORAGE_KEYS.teamId, data.team_id);
localStorage.setItem(STORAGE_KEYS.email, data.email);
localStorage.setItem(STORAGE_KEYS.name, data.name);
},
logout() {
token = null;
userId = null;
teamId = null;
email = null;
name = null;
isAdmin = false;
role = 'member';
for (const key of Object.values(STORAGE_KEYS)) {
localStorage.removeItem(key);
}
goto('/login');
}
};
}
export const auth = createAuth();