1import { initializeApp, getApps } from "https://www.gstatic.com/firebasejs/10.13.0/firebase-app.js"; 2import { 3 getAuth, 4 signInWithPopup, 5 signInWithRedirect, 6 getRedirectResult, 7 GoogleAuthProvider, 8 onAuthStateChanged, 9 signOut, 10} from "https://www.gstatic.com/firebasejs/10.13.0/firebase-auth.js"; 11import { firebaseConfig, SCOPES, WORKSPACE_DOMAIN, WORKSPACE_ECOSYSTEM } from "./firebase-config.js"; 12 13// Singleton Firebase App and Auth instance 14export const app = getApps().length ? getApps()[0] : initializeApp(firebaseConfig); 15export const auth = getAuth(app); 16 17// Configure Google Auth Provider for 902 Youth Google Workspace Identity / SSO 18export const provider = new GoogleAuthProvider(); 19SCOPES.forEach((scope) => provider.addScope(scope)); 20 21// Pre-configure hosted domain to 902youth.org for Google Workspace Identity 22provider.setCustomParameters({ 23 hd: WORKSPACE_DOMAIN, 24 prompt: "select_account", 25}); 26 27let cachedAccessToken = sessionStorage.getItem("902_workspace_token") || null; 28let cachedUser = null; 29try { 30 const stored = sessionStorage.getItem("902_workspace_user"); 31 if (stored) cachedUser = JSON.parse(stored); 32} catch {} 33 34const authListeners = new Set(); 35 36export function notifyAuthListeners(user, token) { 37 authListeners.forEach((listener) => { 38 try { 39 listener(user, token); 40 } catch (err) { 41 console.error("Workspace auth listener error:", err); 42 } 43 }); 44 window.dispatchEvent(new CustomEvent("902-workspace-auth-changed", { detail: { user, token } })); 45} 46 47/** 48 * Initialize Google Workspace auth state listener and restore cached session. 49 */ 50export function initWorkspaceIdentity(callback) { 51 if (callback) authListeners.add(callback); 52 53 // Trigger immediately if cached 54 if (cachedUser && cachedAccessToken) { 55 if (callback) callback(cachedUser, cachedAccessToken); 56 } 57 58 // Check for redirect result on page load (mobile or popup-blocked flow) 59 getRedirectResult(auth).then(async (result) => { 60 if (result && result.user) { 61 const credential = GoogleAuthProvider.credentialFromResult(result); 62 const accessToken = credential?.accessToken || (await result.user.getIdToken()); 63 const isWorkspace = (result.user.email || "").toLowerCase().endsWith("@" + WORKSPACE_DOMAIN); 64 cachedUser = { 65 uid: result.user.uid, 66 email: result.user.email, 67 displayName: result.user.displayName || result.user.email.split("@")[0], 68 photoURL: result.user.photoURL, 69 isWorkspace, 70 organization: WORKSPACE_ECOSYSTEM.organization, 71 domain: WORKSPACE_DOMAIN, 72 }; 73 cachedAccessToken = accessToken; 74 sessionStorage.setItem("902_workspace_token", accessToken); 75 sessionStorage.setItem("902_workspace_user", JSON.stringify(cachedUser)); 76 if (isWorkspace || cachedUser.email.toLowerCase() === "[email protected]") { 77 sessionStorage.setItem("websiteAdminToken", accessToken); 78 } 79 notifyAuthListeners(cachedUser, cachedAccessToken); 80 } 81 }).catch((e) => { 82 console.warn("Redirect sign-in check notice:", e); 83 }); 84 85 onAuthStateChanged(auth, async (firebaseUser) => { 86 if (firebaseUser) { 87 const isWorkspace = (firebaseUser.email || "").toLowerCase().endsWith("@" + WORKSPACE_DOMAIN); 88 cachedUser = { 89 uid: firebaseUser.uid, 90 email: firebaseUser.email, 91 displayName: firebaseUser.displayName || firebaseUser.email.split("@")[0], 92 photoURL: firebaseUser.photoURL, 93 isWorkspace, 94 organization: WORKSPACE_ECOSYSTEM.organization, 95 domain: WORKSPACE_DOMAIN, 96 }; 97 sessionStorage.setItem("902_workspace_user", JSON.stringify(cachedUser)); 98 99 // Attempt to retrieve fresh ID token if access token wasn't just set 100 try { 101 const idToken = await firebaseUser.getIdToken(); 102 if (!cachedAccessToken) { 103 cachedAccessToken = idToken; 104 sessionStorage.setItem("902_workspace_token", idToken); 105 } 106 } catch (e) { 107 console.warn("Failed getting idToken:", e); 108 } 109 110 notifyAuthListeners(cachedUser, cachedAccessToken); 111 } else { 112 if (!sessionStorage.getItem("902_workspace_token")) { 113 cachedUser = null; 114 cachedAccessToken = null; 115 sessionStorage.removeItem("902_workspace_user"); 116 notifyAuthListeners(null, null); 117 } 118 } 119 }); 120 121 return () => { 122 if (callback) authListeners.delete(callback); 123 }; 124} 125 126/**
127 * Perform interactive Google Workspace SSO Sign In. 128 */ 129export async function signInWithGoogleWorkspace() { 130 try { 131 const result = await signInWithPopup(auth, provider); 132 const credential = GoogleAuthProvider.credentialFromResult(result); 133 const accessToken = credential?.accessToken || (await result.user.getIdToken()); 134 135 const isWorkspace = (result.user.email || "").toLowerCase().endsWith("@" + WORKSPACE_DOMAIN); 136 const user = { 137 uid: result.user.uid, 138 email: result.user.email, 139 displayName: result.user.displayName || result.user.email.split("@")[0], 140 photoURL: result.user.photoURL, 141 isWorkspace, 142 organization: WORKSPACE_ECOSYSTEM.organization, 143 domain: WORKSPACE_DOMAIN, 144 }; 145 146 cachedAccessToken = accessToken; 147 cachedUser = user; 148 sessionStorage.setItem("902_workspace_token", accessToken); 149 sessionStorage.setItem("902_workspace_user", JSON.stringify(user)); 150 // Also update legacy websiteAdminToken if authorized staff 151 if (isWorkspace || user.email.toLowerCase() === "[email protected]") { 152 sessionStorage.setItem("websiteAdminToken", accessToken); 153 } 154 155 notifyAuthListeners(user, accessToken); 156 return { user, accessToken }; 157 } catch (error) { 158 if (error.code === "auth/popup-blocked" || error.code === "auth/popup-closed-by-user") { 159 console.info("Popup blocked or closed, falling back to redirect flow..."); 160 await signInWithRedirect(auth, provider); 161 return; 162 } 163 console.error("Google Workspace SSO failed:", error); 164 throw error; 165 } 166} 167 168/** 169 * Sign out of Google Workspace SSO. 170 */ 171export async function signOutGoogleWorkspace() { 172 try { 173 await signOut(auth); 174 } catch (e) { 175 console.warn("Firebase signOut error:", e); 176 } 177 cachedAccessToken = null; 178 cachedUser = null; 179 sessionStorage.removeItem("902_workspace_token"); 180 sessionStorage.removeItem("902_workspace_user"); 181 notifyAuthListeners(null, null); 182} 183 184export function getWorkspaceAccessToken() { 185 return cachedAccessToken || sessionStorage.getItem("902_workspace_token") || null; 186} 187 188export function getWorkspaceUser() { 189 return cachedUser; 190} 191 192/** 193 * Call the backend /api/auth/session to verify identity claims, roles, and SSO permissions. 194 */ 195export async function fetchWorkspaceSession() { 196 const token = getWorkspaceAccessToken(); 197 if (!token) return { ok: true, authenticated: false, user: null }; 198 199 try { 200 const res = await fetch("/api/auth/session", { 201 headers: { Authorization: "Bearer " + token }, 202 }); 203 if (!res.ok) return { ok: true, authenticated: false, user: null }; 204 return await res.json(); 205 } catch (err) { 206 console.warn("fetchWorkspaceSession error:", err); 207 return { ok: false, error: err.message }; 208 } 209} 210 211export { WORKSPACE_DOMAIN, WORKSPACE_ECOSYSTEM };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.