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

@@ -9,35 +9,24 @@ import { useRouter } from "next/navigation";
import useCreateTranscript from "../createTranscript";
import SelectSearch from "react-select-search";
import { supportedLanguages } from "../../../supportedLanguages";
import useSessionStatus from "../../../lib/useSessionStatus";
import { featureEnabled } from "../../../domainContext";
import { signIn } from "next-auth/react";
import {
Flex,
Box,
Spinner,
Heading,
Button,
Card,
Center,
Link,
CardBody,
Stack,
Text,
Icon,
Grid,
IconButton,
Spacer,
Menu,
Tooltip,
Input,
} from "@chakra-ui/react";
import { useAuth } from "../../../lib/AuthProvider";
const TranscriptCreate = () => {
const isClient = typeof window !== "undefined";
const router = useRouter();
const status = useSessionStatus();
const isAuthenticated = status === "authenticated";
const isLoading = status === "loading";
const auth = useAuth();
const isAuthenticated = auth.status === "authenticated";
const isLoading = auth.status === "loading";
const requireLogin = featureEnabled("requireLogin");
const [name, setName] = useState<string>("");
@@ -145,7 +134,7 @@ const TranscriptCreate = () => {
{isLoading ? (
<Spinner />
) : requireLogin && !isAuthenticated ? (
<Button onClick={() => signIn("authentik")}>Log in</Button>
<Button onClick={() => auth.signIn("authentik")}>Log in</Button>
) : (
<Flex
rounded="xl"

View File

@@ -1,8 +1,7 @@
import { useContext, useEffect, useState } from "react";
import { DomainContext } from "../../domainContext";
import { useTranscriptGet } from "../../lib/apiHooks";
import { useSession } from "next-auth/react";
import { assertExtendedToken } from "../../lib/types";
import { useAuth } from "../../lib/AuthProvider";
export type Mp3Response = {
media: HTMLMediaElement | null;
@@ -21,11 +20,9 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
);
const [audioDeleted, setAudioDeleted] = useState<boolean | null>(null);
const { api_url } = useContext(DomainContext);
const { data: session } = useSession();
const sessionExtended =
session === null ? null : assertExtendedToken(session);
const auth = useAuth();
const accessTokenInfo =
sessionExtended === null ? null : sessionExtended.accessToken;
auth.status === "authenticated" ? auth.accessToken : null;
const {
data: transcript,

View File

@@ -1,10 +1,11 @@
"use client";
import { signOut, signIn } from "next-auth/react";
import useSessionStatus from "../lib/useSessionStatus";
import { Spinner, Link } from "@chakra-ui/react";
import { useAuth } from "../lib/AuthProvider";
export default function UserInfo() {
const status = useSessionStatus();
const auth = useAuth();
const status = auth.status;
const isLoading = status === "loading";
const isAuthenticated = status === "authenticated";
return isLoading ? (
@@ -13,7 +14,7 @@ export default function UserInfo() {
<Link
href="/"
className="font-light px-2"
onClick={() => signIn("authentik")}
onClick={() => auth.signIn("authentik")}
>
Log in
</Link>
@@ -21,7 +22,7 @@ export default function UserInfo() {
<Link
href="#"
className="font-light px-2"
onClick={() => signOut({ callbackUrl: "/" })}
onClick={() => auth.signOut({ callbackUrl: "/" })}
>
Log out
</Link>

View File

@@ -21,13 +21,13 @@ import { toaster } from "../components/ui/toaster";
import useRoomMeeting from "./useRoomMeeting";
import { useRouter } from "next/navigation";
import { notFound } from "next/navigation";
import useSessionStatus from "../lib/useSessionStatus";
import { useRecordingConsent } from "../recordingConsentContext";
import { useMeetingAudioConsent } from "../lib/apiHooks";
import type { components } from "../reflector-api";
type Meeting = components["schemas"]["Meeting"];
import { FaBars } from "react-icons/fa6";
import { useAuth } from "../lib/AuthProvider";
export type RoomDetails = {
params: {
@@ -260,7 +260,7 @@ export default function Room(details: RoomDetails) {
const roomName = details.params.roomName;
const meeting = useRoomMeeting(roomName);
const router = useRouter();
const status = useSessionStatus();
const status = useAuth().status;
const isAuthenticated = status === "authenticated";
const isLoading = status === "loading" || meeting.loading;

View File

@@ -1,7 +1,6 @@
import "./styles/globals.scss";
import { Metadata, Viewport } from "next";
import { Poppins } from "next/font/google";
import SessionProvider from "./lib/SessionProvider";
import { ErrorProvider } from "./(errors)/errorContext";
import ErrorMessage from "./(errors)/errorMessage";
import { DomainContextProvider } from "./domainContext";
@@ -74,18 +73,16 @@ export default async function RootLayout({
return (
<html lang="en" className={poppins.className} suppressHydrationWarning>
<body className={"h-[100svh] w-[100svw] overflow-x-hidden relative"}>
<SessionProvider>
<DomainContextProvider config={config}>
<RecordingConsentProvider>
<ErrorBoundary fallback={<p>"something went really wrong"</p>}>
<ErrorProvider>
<ErrorMessage />
<Providers>{children}</Providers>
</ErrorProvider>
</ErrorBoundary>
</RecordingConsentProvider>
</DomainContextProvider>
</SessionProvider>
<DomainContextProvider config={config}>
<RecordingConsentProvider>
<ErrorBoundary fallback={<p>"something went really wrong"</p>}>
<ErrorProvider>
<ErrorMessage />
<Providers>{children}</Providers>
</ErrorProvider>
</ErrorBoundary>
</RecordingConsentProvider>
</DomainContextProvider>
</body>
</html>
);

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;
}

View File

@@ -9,19 +9,22 @@ import { NuqsAdapter } from "nuqs/adapters/next/app";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./lib/queryClient";
import { AuthProvider } from "./lib/AuthProvider";
import { SessionProvider as SessionProviderNextAuth } from "next-auth/react";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<NuqsAdapter>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<ChakraProvider value={system}>
<WherebyProvider>
{children}
<Toaster />
</WherebyProvider>
</ChakraProvider>
</AuthProvider>
<SessionProviderNextAuth>
<AuthProvider>
<ChakraProvider value={system}>
<WherebyProvider>
{children}
<Toaster />
</WherebyProvider>
</ChakraProvider>
</AuthProvider>
</SessionProviderNextAuth>
</QueryClientProvider>
</NuqsAdapter>
);