mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, useContext } from "react";
|
|
import { useSession as useNextAuthSession } from "next-auth/react";
|
|
import { signOut, signIn } from "next-auth/react";
|
|
import { configureApiAuth } from "./apiClient";
|
|
import { assertCustomSession, CustomSession } from "./types";
|
|
import { Session } from "next-auth";
|
|
import { SessionAutoRefresh } from "./SessionAutoRefresh";
|
|
import { REFRESH_ACCESS_TOKEN_ERROR } from "./auth";
|
|
import { assertExists } from "./utils";
|
|
|
|
type AuthContextType = (
|
|
| { status: "loading" }
|
|
| { status: "refreshing"; user: CustomSession["user"] }
|
|
| { status: "unauthenticated"; error?: string }
|
|
| {
|
|
status: "authenticated";
|
|
accessToken: string;
|
|
accessTokenExpires: number;
|
|
user: CustomSession["user"];
|
|
}
|
|
) & {
|
|
update: () => Promise<Session | null>;
|
|
signIn: typeof signIn;
|
|
signOut: typeof signOut;
|
|
};
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const { data: session, status, update } = useNextAuthSession();
|
|
const customSession = session ? assertCustomSession(session) : null;
|
|
|
|
const contextValue: AuthContextType = {
|
|
...(() => {
|
|
switch (status) {
|
|
case "loading": {
|
|
const sessionIsHere = !!customSession;
|
|
switch (sessionIsHere) {
|
|
case false: {
|
|
return { status };
|
|
}
|
|
case true: {
|
|
return {
|
|
status: "refreshing" as const,
|
|
user: assertExists(customSession).user,
|
|
};
|
|
}
|
|
default: {
|
|
const _: never = sessionIsHere;
|
|
throw new Error("unreachable");
|
|
}
|
|
}
|
|
}
|
|
case "authenticated": {
|
|
if (customSession?.error === REFRESH_ACCESS_TOKEN_ERROR) {
|
|
// token had expired but next auth still returns "authenticated" so show user unauthenticated state
|
|
return {
|
|
status: "unauthenticated" as const,
|
|
};
|
|
} else if (customSession?.accessToken) {
|
|
return {
|
|
status,
|
|
accessToken: customSession.accessToken,
|
|
accessTokenExpires: customSession.accessTokenExpires,
|
|
user: customSession.user,
|
|
};
|
|
} else {
|
|
console.warn(
|
|
"illegal state: authenticated but have no session/or access token. ignoring",
|
|
);
|
|
return { status: "unauthenticated" as const };
|
|
}
|
|
}
|
|
case "unauthenticated": {
|
|
return { status: "unauthenticated" as const };
|
|
}
|
|
default: {
|
|
const _: never = status;
|
|
throw new Error("unreachable");
|
|
}
|
|
}
|
|
})(),
|
|
update,
|
|
signIn,
|
|
signOut,
|
|
};
|
|
|
|
// not useEffect, we need it ASAP
|
|
// apparently, still no guarantee this code runs before mutations are fired
|
|
configureApiAuth(
|
|
contextValue.status === "authenticated"
|
|
? contextValue.accessToken
|
|
: contextValue.status === "loading"
|
|
? undefined
|
|
: null,
|
|
);
|
|
|
|
return (
|
|
<AuthContext.Provider value={contextValue}>
|
|
<SessionAutoRefresh>{children}</SessionAutoRefresh>
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
if (context === undefined) {
|
|
throw new Error("useAuth must be used within an AuthProvider");
|
|
}
|
|
return context;
|
|
}
|