draupnir4all-web

git clone git://archive.git.mtrnord.blog/MTRNord/draupnir4all-web.git
Log | Files | Refs | README | LICENSE

session-context.tsx (13163B)


      1 "use client";
      2 
      3 import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
      4 import { generateOpenIDToken, login as realLogin, logout as realLogout } from "@/lib/clientSideAuth";
      5 import { User } from "@/lib/auth";
      6 
      7 type SessionContextType = {
      8     user?: User;
      9     isLoading: boolean;
     10     isAuthenticated: boolean;
     11     discoveryStatus: "idle" | "loading" | "success" | "error";
     12     homeserverUrl?: string;
     13     login: (matrixId: string, password: string) => Promise<void>;
     14     register: (matrixId: string, password: string) => Promise<void>;
     15     logout: () => Promise<void>;
     16     discoverHomeserver: (matrixId: string) => Promise<void>;
     17     fetchWithTokenRefresh: (url: string, options?: RequestInit) => Promise<Response>;
     18     refreshOpenIDToken: () => Promise<void>;
     19 };
     20 
     21 const SessionContext = createContext<SessionContextType | undefined>(undefined);
     22 
     23 const refreshOpenIDToken = async (homeserverUrl: string, accessToken: string, user: User, setUser: (user: User) => void, setOpenIDExpiration: (expiration: number) => void, logout: () => Promise<void>) => {
     24     try {
     25         const openidToken = await generateOpenIDToken(homeserverUrl, user.matrixId, accessToken);
     26 
     27         // Update the session cookie with the new OpenID token
     28         const response = await fetch("/api/session", {
     29             method: "POST",
     30             headers: { "Content-Type": "application/json" },
     31             body: JSON.stringify({ openidToken, homeserverUrl, matrixId: user.matrixId, openidExpiration: Math.floor(Date.now() / 1000) + openidToken.expires_in }),
     32         });
     33         setOpenIDExpiration(openidToken.expires_in);
     34 
     35         if (!response.ok) {
     36             console.error("Failed to update session with new OpenID token:", response.status, response.statusText, response.body);
     37             throw new Error("Failed to update session with new OpenID token.", {
     38                 cause: response.body
     39             });
     40         }
     41 
     42         const userData = await response.json();
     43         setUser(userData);
     44     } catch (error) {
     45         console.error("Failed to refresh OpenID token:", error);
     46         await logout();
     47 
     48         throw error;
     49     }
     50 };
     51 
     52 export function SessionProvider({ children }: { children: ReactNode }) {
     53     const [user, setUser] = useState<User | undefined>();
     54     const [isLoading, setIsLoading] = useState(false);
     55     const [homeserverUrl, setHomeserverUrl] = useState<string | undefined>();
     56     const [discoveryStatus, setDiscoveryStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
     57     const [accessToken, setAccessToken] = useState<string | undefined>();
     58     const [openidExpiration, setOpenIDExpiration] = useState<number | undefined>()
     59 
     60     const logout = useCallback(async () => {
     61         try {
     62             await fetch("/api/session", { method: "DELETE" });
     63 
     64             if (homeserverUrl && accessToken) {
     65                 await realLogout(homeserverUrl, accessToken);
     66             }
     67 
     68             setUser(undefined);
     69             setAccessToken(undefined);
     70             setOpenIDExpiration(undefined);
     71 
     72             // Clear sessionStorage
     73             sessionStorage.removeItem("accessToken");
     74             sessionStorage.removeItem("openidExpiration");
     75         } catch (error) {
     76             console.error("Logout failed:", error);
     77         }
     78     }, [accessToken, homeserverUrl]);
     79 
     80     // Restore session from cookies on initial load
     81     useEffect(() => {
     82         if (homeserverUrl && accessToken && user) {
     83             return;
     84         }
     85         const fetchSession = async () => {
     86             console.log("Restoring session...");
     87             try {
     88                 if (openidExpiration) {
     89                     const now = Date.now();
     90                     const refreshTime = openidExpiration - 60 * 1000; // Refresh 1 minute before expiration
     91 
     92                     if (refreshTime > now) {
     93                         const timeout = setTimeout(() => {
     94                             console.log("Refreshing OpenID token...");
     95                             if (!homeserverUrl || !accessToken || !user) {
     96                                 throw new Error("Missing required information to refresh OpenID token.");
     97                             }
     98                             refreshOpenIDToken(homeserverUrl, accessToken, user, setUser, setOpenIDExpiration, logout).catch((err) => {
     99                                 console.error("Failed to refresh OpenID token:", err);
    100                             });
    101                         }, refreshTime - now);
    102 
    103                         return () => clearTimeout(timeout);
    104                     }
    105                 }
    106                 const response = await fetch("/api/session");
    107                 if (response.ok) {
    108                     const userData: User = await response.json();
    109                     setUser(userData);
    110                     setAccessToken(userData.token);
    111                     setHomeserverUrl(userData.homeserverUrl);
    112                 }
    113             } catch (error) {
    114                 console.error("Failed to restore session:", error);
    115             } finally {
    116                 setIsLoading(false);
    117             }
    118         };
    119 
    120         fetchSession();
    121     }, [accessToken, homeserverUrl, logout, openidExpiration, user]);
    122 
    123 
    124 
    125     // Automatically refresh OpenID token before it expires
    126     useEffect(() => {
    127         if (openidExpiration) {
    128             const now = Date.now();
    129             const refreshTime = openidExpiration - 60 * 1000; // Refresh 1 minute before expiration
    130 
    131             if (refreshTime > now) {
    132                 const timeout = setTimeout(() => {
    133                     if (!homeserverUrl || !accessToken || !user) {
    134                         throw new Error("Missing required information to refresh OpenID token.");
    135                     }
    136                     refreshOpenIDToken(homeserverUrl, accessToken, user, setUser, setOpenIDExpiration, logout).catch((err) => {
    137                         console.error("Failed to refresh OpenID token:", err);
    138                     });
    139                 }, refreshTime - now);
    140 
    141                 return () => clearTimeout(timeout);
    142             }
    143         }
    144     }, [accessToken, homeserverUrl, logout, openidExpiration, user]);
    145 
    146     const discoverHomeserver = async (matrixId: string) => {
    147         setDiscoveryStatus("loading");
    148         if (!matrixId.includes(":")) {
    149             setDiscoveryStatus("error");
    150             throw new Error("Invalid Matrix ID format. Please include a domain (e.g., @user:domain).");
    151         }
    152 
    153         const serverName = matrixId.split(":").pop();
    154 
    155         try {
    156             const url = new URL("/.well-known/matrix/client", `https://${serverName}`);
    157             const response = await fetch(url);
    158             if (response.ok) {
    159                 // Check if the response is empty
    160                 if (!response.body) {
    161                     setHomeserverUrl(`https://${serverName}`);
    162                     setDiscoveryStatus("success");
    163                     return;
    164                 }
    165 
    166                 const data = await response.json();
    167                 if (data["m.homeserver"]?.base_url) {
    168                     // Check if the base_url is a valid URL by checking /_matrix/client/versions on the endpoint
    169                     const url = new URL("/_matrix/client/versions", data["m.homeserver"].base_url)
    170                     const versionsResponse = await fetch(url);
    171                     console.log("Versions response:", versionsResponse.status);
    172                     if (versionsResponse.ok) {
    173                         setHomeserverUrl(data["m.homeserver"].base_url);
    174                         setDiscoveryStatus("success");
    175                         return;
    176                     } else {
    177                         setDiscoveryStatus("error");
    178                         throw new Error("Homeserver did not respond successfully to versions request.");
    179                     }
    180                 }
    181             }
    182             setHomeserverUrl(`https://${serverName}`);
    183             setDiscoveryStatus("success");
    184         } catch (error: unknown) {
    185             console.warn("Failed to discover homeserver:", error);
    186             setHomeserverUrl(`https://${serverName}`);
    187             setDiscoveryStatus("success");
    188         }
    189     };
    190 
    191     const login = async (matrixId: string, password: string) => {
    192         setIsLoading(true);
    193         if (!homeserverUrl) {
    194             throw new Error("Homeserver URL is not set. Please discover your homeserver first.");
    195         }
    196 
    197         try {
    198             // Step 1: Login to Matrix and get access token
    199             const accessToken = await realLogin(homeserverUrl, matrixId, password);
    200             setAccessToken(accessToken);
    201 
    202             // Step 2: Generate OpenID token
    203             const { access_token: openidToken, expires_in } = await generateOpenIDToken(homeserverUrl, matrixId, accessToken);
    204             setOpenIDExpiration(expires_in);
    205 
    206             // Step 3: Send OpenID token to the server for validation and session creation
    207             const response = await fetch("/api/session", {
    208                 method: "POST",
    209                 headers: { "Content-Type": "application/json" },
    210                 body: JSON.stringify({ openidToken, homeserverUrl, matrixId, openidExpiration: Math.floor(Date.now() / 1000) + expires_in }),
    211             });
    212 
    213             if (!response.ok) {
    214                 throw new Error("Failed to create session on the server.");
    215             }
    216 
    217             const userData = await response.json();
    218             setUser(userData);
    219 
    220             // Persist access token and expiration in sessionStorage
    221             sessionStorage.setItem("accessToken", accessToken);
    222             sessionStorage.setItem("openidExpiration", expires_in.toString());
    223         } catch (error) {
    224             console.error("Login failed:", error);
    225             throw error;
    226         } finally {
    227             setIsLoading(false);
    228         }
    229     };
    230 
    231     const register = async (matrixId: string, password: string) => {
    232         setIsLoading(true);
    233         if (!homeserverUrl) {
    234             throw new Error("Homeserver URL is not set. Please discover your homeserver first.");
    235         }
    236 
    237         try {
    238 
    239             // Step 1: Login to Matrix and get access token
    240             const accessToken = await realLogin(homeserverUrl, matrixId, password);
    241             setAccessToken(accessToken);
    242 
    243             // Step 2: Generate OpenID token
    244             const { access_token: openidToken, expires_in } = await generateOpenIDToken(homeserverUrl, matrixId, accessToken);
    245             setOpenIDExpiration(expires_in);
    246 
    247             // Step 3: Send OpenID token to the server for validation and session creation
    248             const response = await fetch("/api/session", {
    249                 method: "POST",
    250                 headers: { "Content-Type": "application/json" },
    251                 body: JSON.stringify({ openidToken, homeserverUrl, matrixId, openidExpiration: Math.floor(Date.now() / 1000) + expires_in }),
    252             });
    253 
    254             if (!response.ok) {
    255                 throw new Error("Failed to create session on the server.");
    256             }
    257             const userData = await response.json();
    258             setUser(userData);
    259 
    260             // Persist access token and expiration in sessionStorage
    261             sessionStorage.setItem("accessToken", accessToken);
    262             sessionStorage.setItem("openidExpiration", expires_in.toString());
    263         } catch (error) {
    264             console.error("Login failed:", error);
    265             throw error;
    266         } finally {
    267             setIsLoading(false);
    268         }
    269     };
    270 
    271     const refreshOpenIDTokenHook = async () => {
    272         if (!homeserverUrl || !accessToken || !user) {
    273             throw new Error("Missing required information to refresh OpenID token.");
    274         }
    275         console.log("Refreshing OpenID token...");
    276 
    277         await refreshOpenIDToken(homeserverUrl, accessToken, user, setUser, setOpenIDExpiration, logout);
    278     }
    279 
    280     const fetchWithTokenRefresh = async (url: string, options: RequestInit = {}) => {
    281         const response = await fetch(url, options);
    282 
    283         if (response.status === 401) {
    284             // Token expired, refresh it
    285             if (!homeserverUrl || !accessToken || !user) {
    286                 throw new Error("Missing required information to refresh OpenID token.");
    287             }
    288             await refreshOpenIDToken(homeserverUrl, accessToken, user, setUser, setOpenIDExpiration, logout);
    289 
    290             // Retry the request
    291             return fetch(url, options);
    292         }
    293 
    294         return response;
    295     };
    296 
    297     return (
    298         <SessionContext.Provider
    299             value={{
    300                 user,
    301                 isLoading,
    302                 homeserverUrl,
    303                 discoveryStatus,
    304                 isAuthenticated: !!user,
    305                 login,
    306                 register,
    307                 logout,
    308                 discoverHomeserver,
    309                 fetchWithTokenRefresh,
    310                 refreshOpenIDToken: refreshOpenIDTokenHook,
    311             }}
    312         >
    313             {children}
    314         </SessionContext.Provider>
    315     );
    316 }
    317 
    318 export function useSession() {
    319     const context = useContext(SessionContext);
    320     if (context === undefined) {
    321         throw new Error("useSession must be used within a SessionProvider");
    322     }
    323     return context;
    324 }