PageSourceSearch

https://902youth.org/src/google-identity.js

js 902youth.org collected 2026-10-05 07:09:11 UTC 7,466 bytes, 211 lines download raw bytes

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.