mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
fix auth
This commit is contained in:
@@ -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] =
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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}</>;
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
19
www/app/lib/useUserId.ts
Normal 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;
|
||||
}
|
||||
7
www/app/lib/useUserName.ts
Normal file
7
www/app/lib/useUserName.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user