Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/asubap/website/llms.txt

Use this file to discover all available pages before exploring further.

Authentication in the BAP Beta Tau frontend is centralised in src/context/auth/authProvider.tsx. It wraps the entire application in <AuthProvider>, which manages the Supabase session, resolves it to an application role via the backend, and exposes everything downstream through the useAuth() hook. Sponsor users follow a passcode-based path; members use Google OAuth. Both paths converge at the same AuthContext.

Supabase Client

Path: src/context/auth/supabaseClient.ts The Supabase client is a singleton created with the anon key, configured to persist sessions in localStorage.
import { createClient } from "@supabase/supabase-js";

const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY;

export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    persistSession: true,
    storage: localStorage,
  },
});
Storing sessions in localStorage means Supabase tokens are accessible to any JavaScript running on the page. The Cookie Migration plan documents a path to HttpOnly cookie-based auth that eliminates this XSS risk.

RoleType

The RoleType union is the central type representing what a logged-in user is within the application.
// src/context/auth/authProvider.tsx

type SponsorRole = {
  type: "sponsor";
  companyName: string;
};

export type RoleType = string | SponsorRole | null;
ValueWho it represents
"general-member"Standard inducted member
"e-board"Executive board member (admin-level access)
"admin"System administrator (treated like e-board)
"pledge"Pledge/candidate member
"alumni"Alumni member
{ type: "sponsor", companyName: string }Company sponsor user
nullNot yet resolved, or unauthenticated

AuthContext Shape

interface AuthContextType {
  session: Session | null;           // Raw Supabase session object
  role: RoleType;                    // Resolved application role
  loading: boolean;                  // True while auth is initializing
  authError: string | null;          // Error from /users or Supabase
  isAuthenticated: boolean;          // session && role && !authError
  setSession: (user: Session | null) => void;
  setRole: (role: RoleType) => void;
  setAuthError: (error: string | null) => void;
}
isAuthenticated is a derived boolean — it is true only when all three conditions hold:
  • session is non-null (Supabase session exists)
  • role is non-null (backend confirmed the user’s role)
  • authError is null (no auth errors)

AuthProvider

Path: src/context/auth/authProvider.tsx AuthProvider wraps the entire React tree in App.tsx and drives the full auth lifecycle.

Initialization Flow

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
  const [session, setSession] = useState<Session | null>(null);
  const [role, setRole] = useState<RoleType>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [authError, setAuthError] = useState<string | null>(null);
  // ...
};
On mount, initializeAuth runs:
1

Get existing session

supabase.auth.getSession() checks localStorage for a persisted Supabase session. If found, setSession(session) is called immediately.
2

Resolve role from backend

If session?.user?.email exists, fetchUserRole(token, email) posts to POST /users with Authorization: Bearer <token>. A successful response calls setRole(data).
3

Handle auth errors

If /users returns a non-2xx response, setRole(null) and setAuthError(errorMessage) are called. The session is not cleared — AuthHome displays the error and handles sign-out.

Auth State Listener

const { data: authListener } = supabase.auth.onAuthStateChange(
  (_, newSession) => {
    setSession(newSession);
    if (newSession?.user?.email) {
      fetchUserRole(newSession.access_token, newSession.user.email);
    } else {
      setRole(null);
      setLoading(false);
    }
  }
);
This listener fires on every auth event — sign-in, sign-out, token refresh, and OAuth callback. It ensures role stays in sync with the Supabase session at all times.

Periodic Role Validation

Every 30 seconds, AuthProvider silently re-validates the session by calling fetchUserRole again:
const roleValidationInterval = setInterval(async () => {
  const { data: { session: currentSession } } = await supabase.auth.getSession();
  if (currentSession?.user?.email) {
    await fetchUserRole(currentSession.access_token, currentSession.user.email);
  }
}, 30000);
This ensures that if an e-board member archives a logged-in user, they will be detected and shown an error within 30 seconds — without any user action required.

useAuth Hook

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
};
Calling useAuth() outside <AuthProvider> throws at runtime. Consume the hook in any component that needs auth state:
import { useAuth } from "../../context/auth/authProvider";

const MyComponent = () => {
  const { session, role, loading, authError, isAuthenticated, setSession, setRole, setAuthError } = useAuth();

  if (loading) return <LoadingSpinner />;
  if (!isAuthenticated) return <Navigate to="/login" />;

  return <div>Welcome, {session?.user?.email}</div>;
};

Return values

PropertyTypeDescription
sessionSession | nullRaw Supabase session; contains access_token, user, etc.
roleRoleTypeResolved application role
loadingbooleantrue during initial auth resolution
authErrorstring | nullError message from /users or Supabase
isAuthenticatedbooleansession && role && !authError
setSession(session) => voidUsed by SponsorAuth to inject a session
setRole(role) => voidUsed by SponsorAuth to set "sponsor" role
setAuthError(error) => voidUsed to clear errors programmatically

Member Authentication — GoogleLogin

Path: src/components/auth/GoogleLogin.tsx Members authenticate via Google OAuth using supabase.auth.signInWithOAuth. After OAuth completes, Supabase redirects to /login, where AuthProvider’s onAuthStateChange listener fires and calls fetchUserRole.
const handleGoogleLogin = async () => {
  const { error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: getRedirectUrl(), // Always redirects to /login
    },
  });
};
The redirect URL is environment-aware:
const baseUrl =
  import.meta.env.VITE_ENV_STATE === "development"
    ? "http://localhost:5173"
    : "http://asubap.com";

Path: src/components/auth/SponsorAuth.tsx Sponsors do not use OAuth. Instead, they select their company from a dropdown and enter a passcode. Internally, the company name is slugified to form a synthetic Supabase email:
const sponsorEmail = companyName.replace(/\s+/g, "-").toLowerCase();
// Example: "Acme Corp" → "acme-corp@example.com"

const { data, error: authError } = await supabase.auth.signInWithPassword({
  email: `${sponsorEmail}@example.com`,
  password: passcode,
});
On success:
setSession(data.session);
setRole("sponsor");
localStorage.setItem("sponsorName", companyName);
The company names in the dropdown are fetched from GET /sponsors/names — a public endpoint that requires no auth token.

Full Auth Flow Diagram

1

App mount

AuthProvider initializes, loading = true. supabase.auth.getSession() checks localStorage.
2

No session

loading = false. User visits a public page or is redirected to /login.
3

Login

Members: supabase.auth.signInWithOAuth → OAuth redirect → onAuthStateChange fires.
Sponsors: supabase.auth.signInWithPassword → direct → setSession + setRole.
4

Role resolution

fetchUserRole(token, email) calls POST /users. Response sets role in context.
5

Route access

ProtectedRoute reads session, loading, role from useAuth(). Renders <Outlet /> or redirects.
6

Periodic validation

Every 30 seconds, role is re-validated. Archived users get authError set; they are shown an error on /auth/Home.
7

Sign out

supabase.auth.signOut() clears the Supabase session. onAuthStateChange fires with null session → role = null, loading = false.

Role-Based Access Patterns

// Check for e-board admin
const isAdmin = role === "e-board";

// Check for sponsor (handles both string and object forms)
const isSponsor =
  role === "sponsor" ||
  (typeof role === "object" && role !== null && role.type === "sponsor");

// Check for general member
const isMember = role === "general-member" || role === "admin";

// Get company name (sponsor only)
const companyName =
  typeof role === "object" && role !== null && role.type === "sponsor"
    ? role.companyName
    : null;
The ProtectedRoute.hasPermission() function is the single source of truth for route-level access control. For component-level conditional rendering, consume role from useAuth() directly and apply the patterns above.

Build docs developers (and LLMs) love