This commit is contained in:
Igor Loskutov
2025-09-02 14:44:10 -04:00
parent 5ffc312d4a
commit 31c44ac0bb
11 changed files with 52 additions and 78 deletions

View File

@@ -19,7 +19,7 @@ import {
parseAsStringLiteral,
} from "nuqs";
import { LuX } from "react-icons/lu";
import useSessionUser from "../../lib/useSessionUser";
import useSessionUser from "../../lib/useUserId";
import type { components } from "../../reflector-api";
type Room = components["schemas"]["Room"];
@@ -43,6 +43,7 @@ import TranscriptCards from "./_components/TranscriptCards";
import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog";
import { formatLocalDate } from "../../lib/time";
import { RECORD_A_MEETING_URL } from "../../api/urls";
import { useUserName } from "../../lib/useUserName";
const SEARCH_FORM_QUERY_INPUT_NAME = "query" as const;
@@ -255,7 +256,7 @@ export default function TranscriptBrowser() {
const totalPages = getTotalPages(totalResults, pageSize);
const userName = useSessionUser().name;
const userName = useUserName();
const [deletionLoading, setDeletionLoading] = useState(false);
const cancelRef = React.useRef(null);
const [transcriptToDeleteId, setTranscriptToDeleteId] =

View File

@@ -19,11 +19,10 @@ import {
} from "@chakra-ui/react";
import { LuShare2 } from "react-icons/lu";
import { useTranscriptUpdate } from "../../lib/apiHooks";
import useSessionUser from "../../lib/useSessionUser";
import { CustomSession } from "../../lib/types";
import ShareLink from "./shareLink";
import ShareCopy from "./shareCopy";
import ShareZulip from "./shareZulip";
import useUserId from "../../lib/useUserId";
type ShareAndPrivacyProps = {
finalSummaryRef: any;
@@ -86,7 +85,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
}
};
const userId = useSessionUser().id;
const userId = useUserId();
useEffect(() => {
setIsOwner(!!(requireLogin && userId === props.transcriptResponse.user_id));

View File

@@ -1,16 +0,0 @@
"use client";
import { useEffect } from "react";
import { configureApiAuth } from "./apiClient";
import useSessionAccessToken from "./useSessionAccessToken";
// TODO should be context
export function ApiAuthProvider({ children }: { children: React.ReactNode }) {
const { accessToken } = useSessionAccessToken();
useEffect(() => {
configureApiAuth(accessToken);
}, [accessToken]);
return <>{children}</>;
}

View File

@@ -3,7 +3,7 @@
import { createContext, useContext, useEffect } from "react";
import { useSession as useNextAuthSession } from "next-auth/react";
import { configureApiAuth } from "./apiClient";
import { CustomSession } from "./types";
import { assertExtendedToken, CustomSession } from "./types";
type AuthContextType =
| { status: "loading" }
@@ -12,26 +12,24 @@ type AuthContextType =
status: "authenticated";
accessToken: string;
accessTokenExpires: number;
user: CustomSession["user"];
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const { data: session, status } = useNextAuthSession();
const customSession = session as CustomSession;
if (session) {
debugger;
}
const customSession = session ? assertExtendedToken(session) : null;
const contextValue: AuthContextType =
status === "loading"
? { status: "loading" as const }
: customSession?.accessToken
: status === "authenticated" && customSession?.accessToken
? {
status: "authenticated" as const,
accessToken: customSession.accessToken,
accessTokenExpires: customSession.accessTokenExpires,
user: customSession.user,
}
: { status: "unauthenticated" as const };

View File

@@ -9,15 +9,16 @@
import { useSession } from "next-auth/react";
import { useEffect } from "react";
import { CustomSession } from "./types";
import { assertExtendedToken, CustomSession } from "./types";
export function SessionAutoRefresh({
children,
refreshInterval = 20 /* seconds */,
}) {
const { data: session, update } = useSession();
const customSession = session as CustomSession;
const accessTokenExpires = customSession?.accessTokenExpires;
const accessTokenExpires = session
? assertExtendedToken(session).accessTokenExpires
: null;
useEffect(() => {
const interval = setInterval(() => {

View File

@@ -11,9 +11,9 @@ import {
import createFetchClient from "openapi-react-query";
// Create the base openapi-fetch client with a default URL
// The actual URL will be set via middleware in ApiAuthProvider
// The actual URL will be set via middleware in AuthProvider
export const client = createClient<paths>({
baseUrl: "http://192.0.2.1:1250",
baseUrl: "http://127.0.0.1:1250",
});
export const $api = createFetchClient<paths>(client);

View File

@@ -7,6 +7,7 @@ import {
assertExtendedToken,
} from "./types";
import {
assertExists,
assertExistsAndNonEmptyString,
parseMaybeNonEmptyString,
} from "./utils";
@@ -48,23 +49,24 @@ export const authOptions: AuthOptions = {
},
callbacks: {
async jwt({ token, account, user }) {
const extendedToken = assertExtendedToken(token);
const KEY = `token:${token.sub}`;
if (account && user) {
// called only on first login
// XXX account.expires_in used in example is not defined for authentik backend, but expires_at is
const expiresAt = (account.expires_at as number) - PRETIMEOUT;
const expiresAtS = assertExists(account.expires_at) - PRETIMEOUT;
const expiresAtMs = expiresAtS * 1000;
if (!account.access_token) {
tokenCache.delete(KEY);
} else {
const jwtToken: JWTWithAccessToken = {
...extendedToken,
...token,
accessToken: account.access_token,
accessTokenExpires: expiresAt * 1000,
accessTokenExpires: expiresAtMs,
refreshToken: account.refresh_token || "",
};
// Store in memory cache
tokenCache.set(`token:${jwtToken.sub}`, {
tokenCache.set(KEY, {
token: jwtToken,
timestamp: Date.now(),
});
@@ -72,8 +74,9 @@ export const authOptions: AuthOptions = {
}
}
if (Date.now() < extendedToken.accessTokenExpires) {
return token;
const currentToken = tokenCache.get(KEY);
if (currentToken && Date.now() < currentToken.token.accessTokenExpires) {
return currentToken.token;
}
// access token has expired, try to update it

View File

@@ -13,11 +13,6 @@ export interface CustomSession extends Session {
accessToken: string;
accessTokenExpires: number;
error?: string;
user: {
id?: string;
name?: string | null;
email?: string | null;
};
}
// assumption that JWT is JWTWithAccessToken - not ideal, TODO find a reason we have to do that

View File

@@ -1,33 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { useSession as useNextAuthSession } from "next-auth/react";
import { Session } from "next-auth";
// user type with id, name, email
export interface User {
id?: string | null;
name?: string | null;
email?: string | null;
}
export default function useSessionUser() {
const { data: session } = useNextAuthSession();
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
if (!session?.user) {
setUser(null);
return;
}
if (JSON.stringify(session.user) !== JSON.stringify(user)) {
setUser(session.user);
}
}, [session]);
return {
id: user?.id,
name: user?.name,
email: user?.email,
};
}

19
www/app/lib/useUserId.ts Normal file
View File

@@ -0,0 +1,19 @@
"use client";
import { useState, useEffect } from "react";
import { useSession as useNextAuthSession } from "next-auth/react";
import { Session } from "next-auth";
import { useAuth } from "./AuthProvider";
const assertUserId = <T>(u: T): T & { id: string } => {
if (typeof (u as { id: string }).id !== "string")
throw new Error("Expected user.id to be a string");
return u as T & { id: string };
};
// the current assumption in useSessionUser is that "useNextAuthSession" also returns user.id, although useNextAuthSession documentation states it doesn't
// the hook is to isolate the potential impact and to document this behaviour
export default function useUserId() {
const auth = useAuth();
return auth.status === "authenticated" ? assertUserId(auth.user) : null;
}

View File

@@ -0,0 +1,7 @@
import { useAuth } from "./AuthProvider";
export const useUserName = (): string | null | undefined => {
const auth = useAuth();
if (auth.status !== "authenticated") return undefined;
return auth.user?.name || null;
};