normalize auth provider

This commit is contained in:
Igor Loskutov
2025-09-03 07:10:20 -04:00
parent 97f6db5556
commit 08b82c76ce
14 changed files with 80 additions and 470 deletions

View File

@@ -1,15 +1,14 @@
"use client";
import { createContext, useContext, useEffect } from "react";
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 {
assertExtendedToken,
assertExtendedTokenAndUserId,
CustomSession,
} from "./types";
import { assertExtendedTokenAndUserId, CustomSession } from "./types";
import { Session } from "next-auth";
import { SessionAutoRefresh } from "./SessionAutoRefresh";
type AuthContextType =
type AuthContextType = (
| { status: "loading" }
| { status: "unauthenticated"; error?: string }
| {
@@ -17,25 +16,41 @@ type AuthContextType =
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 } = useNextAuthSession();
const { data: session, status, update } = useNextAuthSession();
const customSession = session ? assertExtendedTokenAndUserId(session) : null;
const contextValue: AuthContextType =
status === "loading"
? { status: "loading" as const }
const contextValue: AuthContextType = {
...(status === "loading"
? { status }
: status === "authenticated" && customSession?.accessToken
? {
status: "authenticated" as const,
status,
accessToken: customSession.accessToken,
accessTokenExpires: customSession.accessTokenExpires,
user: customSession.user,
}
: { status: "unauthenticated" as const };
: status === "authenticated" && !customSession?.accessToken
? (() => {
console.warn(
"illegal state: authenticated but have no session/or access token. ignoring",
);
return { status: "unauthenticated" as const };
})()
: { status: "unauthenticated" as const }),
update,
signIn,
signOut,
};
// not useEffect, we need it ASAP
configureApiAuth(
@@ -43,7 +58,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
);
return (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
<AuthContext.Provider value={contextValue}>
<SessionAutoRefresh>{children}</SessionAutoRefresh>
</AuthContext.Provider>
);
}

View File

@@ -1,5 +1,5 @@
/**
* This is a custom hook that automatically refreshes the session when the access token is about to expire.
* This is a custom provider that automatically refreshes the session when the access token is about to expire.
* When communicating with the reflector API, we need to ensure that the access token is always valid.
*
* We could have implemented that as an interceptor on the API client, but not everything is using the
@@ -7,31 +7,29 @@
*/
"use client";
import { useSession } from "next-auth/react";
import { useEffect } from "react";
import { assertExtendedToken, CustomSession } from "./types";
import { useAuth } from "./AuthProvider";
export function SessionAutoRefresh({
children,
refreshInterval = 20 /* seconds */,
}) {
const { data: session, update } = useSession();
const accessTokenExpires = session
? assertExtendedToken(session).accessTokenExpires
: null;
const auth = useAuth();
const accessTokenExpires =
auth.status === "authenticated" ? auth.accessTokenExpires : null;
useEffect(() => {
const interval = setInterval(() => {
if (accessTokenExpires) {
const timeLeft = accessTokenExpires - Date.now();
if (timeLeft < refreshInterval * 1000) {
update();
auth.update();
}
}
}, refreshInterval * 1000);
return () => clearInterval(interval);
}, [accessTokenExpires, refreshInterval, update]);
}, [accessTokenExpires, refreshInterval, auth.update]);
return children;
}

View File

@@ -1,11 +0,0 @@
"use client";
import { SessionProvider as SessionProviderNextAuth } from "next-auth/react";
import { SessionAutoRefresh } from "./SessionAutoRefresh";
export default function SessionProvider({ children }) {
return (
<SessionProviderNextAuth>
<SessionAutoRefresh>{children}</SessionAutoRefresh>
</SessionProviderNextAuth>
);
}

View File

@@ -1,11 +1,7 @@
import { AuthOptions } from "next-auth";
import AuthentikProvider from "next-auth/providers/authentik";
import { JWT } from "next-auth/jwt";
import {
JWTWithAccessToken,
CustomSession,
assertExtendedToken,
} from "./types";
import type { JWT } from "next-auth/jwt";
import { JWTWithAccessToken, CustomSession } from "./types";
import {
assertExists,
assertExistsAndNonEmptyString,

View File

@@ -1,5 +1,5 @@
import { Session } from "next-auth";
import { JWT } from "next-auth/jwt";
import type { Session } from "next-auth";
import type { JWT } from "next-auth/jwt";
import { parseMaybeNonEmptyString } from "./utils";
export interface JWTWithAccessToken extends JWT {

View File

@@ -1,15 +0,0 @@
"use client";
import { useSession as useNextAuthSession } from "next-auth/react";
import { CustomSession } from "./types";
export default function useSessionAccessToken() {
const { data: session } = useNextAuthSession();
const customSession = session as CustomSession;
return {
accessToken: customSession?.accessToken ?? null,
accessTokenExpires: customSession?.accessTokenExpires ?? null,
error: customSession?.error,
};
}

View File

@@ -1,8 +0,0 @@
"use client";
import { useSession as useNextAuthSession } from "next-auth/react";
export default function useSessionStatus() {
const { status } = useNextAuthSession();
return status;
}