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,
|
parseAsStringLiteral,
|
||||||
} from "nuqs";
|
} from "nuqs";
|
||||||
import { LuX } from "react-icons/lu";
|
import { LuX } from "react-icons/lu";
|
||||||
import useSessionUser from "../../lib/useSessionUser";
|
import useSessionUser from "../../lib/useUserId";
|
||||||
import type { components } from "../../reflector-api";
|
import type { components } from "../../reflector-api";
|
||||||
|
|
||||||
type Room = components["schemas"]["Room"];
|
type Room = components["schemas"]["Room"];
|
||||||
@@ -43,6 +43,7 @@ import TranscriptCards from "./_components/TranscriptCards";
|
|||||||
import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog";
|
import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog";
|
||||||
import { formatLocalDate } from "../../lib/time";
|
import { formatLocalDate } from "../../lib/time";
|
||||||
import { RECORD_A_MEETING_URL } from "../../api/urls";
|
import { RECORD_A_MEETING_URL } from "../../api/urls";
|
||||||
|
import { useUserName } from "../../lib/useUserName";
|
||||||
|
|
||||||
const SEARCH_FORM_QUERY_INPUT_NAME = "query" as const;
|
const SEARCH_FORM_QUERY_INPUT_NAME = "query" as const;
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ export default function TranscriptBrowser() {
|
|||||||
|
|
||||||
const totalPages = getTotalPages(totalResults, pageSize);
|
const totalPages = getTotalPages(totalResults, pageSize);
|
||||||
|
|
||||||
const userName = useSessionUser().name;
|
const userName = useUserName();
|
||||||
const [deletionLoading, setDeletionLoading] = useState(false);
|
const [deletionLoading, setDeletionLoading] = useState(false);
|
||||||
const cancelRef = React.useRef(null);
|
const cancelRef = React.useRef(null);
|
||||||
const [transcriptToDeleteId, setTranscriptToDeleteId] =
|
const [transcriptToDeleteId, setTranscriptToDeleteId] =
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { LuShare2 } from "react-icons/lu";
|
import { LuShare2 } from "react-icons/lu";
|
||||||
import { useTranscriptUpdate } from "../../lib/apiHooks";
|
import { useTranscriptUpdate } from "../../lib/apiHooks";
|
||||||
import useSessionUser from "../../lib/useSessionUser";
|
|
||||||
import { CustomSession } from "../../lib/types";
|
|
||||||
import ShareLink from "./shareLink";
|
import ShareLink from "./shareLink";
|
||||||
import ShareCopy from "./shareCopy";
|
import ShareCopy from "./shareCopy";
|
||||||
import ShareZulip from "./shareZulip";
|
import ShareZulip from "./shareZulip";
|
||||||
|
import useUserId from "../../lib/useUserId";
|
||||||
|
|
||||||
type ShareAndPrivacyProps = {
|
type ShareAndPrivacyProps = {
|
||||||
finalSummaryRef: any;
|
finalSummaryRef: any;
|
||||||
@@ -86,7 +85,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const userId = useSessionUser().id;
|
const userId = useUserId();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsOwner(!!(requireLogin && userId === props.transcriptResponse.user_id));
|
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 { createContext, useContext, useEffect } from "react";
|
||||||
import { useSession as useNextAuthSession } from "next-auth/react";
|
import { useSession as useNextAuthSession } from "next-auth/react";
|
||||||
import { configureApiAuth } from "./apiClient";
|
import { configureApiAuth } from "./apiClient";
|
||||||
import { CustomSession } from "./types";
|
import { assertExtendedToken, CustomSession } from "./types";
|
||||||
|
|
||||||
type AuthContextType =
|
type AuthContextType =
|
||||||
| { status: "loading" }
|
| { status: "loading" }
|
||||||
@@ -12,26 +12,24 @@ type AuthContextType =
|
|||||||
status: "authenticated";
|
status: "authenticated";
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessTokenExpires: number;
|
accessTokenExpires: number;
|
||||||
|
user: CustomSession["user"];
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { data: session, status } = useNextAuthSession();
|
const { data: session, status } = useNextAuthSession();
|
||||||
const customSession = session as CustomSession;
|
const customSession = session ? assertExtendedToken(session) : null;
|
||||||
|
|
||||||
if (session) {
|
|
||||||
debugger;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextValue: AuthContextType =
|
const contextValue: AuthContextType =
|
||||||
status === "loading"
|
status === "loading"
|
||||||
? { status: "loading" as const }
|
? { status: "loading" as const }
|
||||||
: customSession?.accessToken
|
: status === "authenticated" && customSession?.accessToken
|
||||||
? {
|
? {
|
||||||
status: "authenticated" as const,
|
status: "authenticated" as const,
|
||||||
accessToken: customSession.accessToken,
|
accessToken: customSession.accessToken,
|
||||||
accessTokenExpires: customSession.accessTokenExpires,
|
accessTokenExpires: customSession.accessTokenExpires,
|
||||||
|
user: customSession.user,
|
||||||
}
|
}
|
||||||
: { status: "unauthenticated" as const };
|
: { status: "unauthenticated" as const };
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,16 @@
|
|||||||
|
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { CustomSession } from "./types";
|
import { assertExtendedToken, CustomSession } from "./types";
|
||||||
|
|
||||||
export function SessionAutoRefresh({
|
export function SessionAutoRefresh({
|
||||||
children,
|
children,
|
||||||
refreshInterval = 20 /* seconds */,
|
refreshInterval = 20 /* seconds */,
|
||||||
}) {
|
}) {
|
||||||
const { data: session, update } = useSession();
|
const { data: session, update } = useSession();
|
||||||
const customSession = session as CustomSession;
|
const accessTokenExpires = session
|
||||||
const accessTokenExpires = customSession?.accessTokenExpires;
|
? assertExtendedToken(session).accessTokenExpires
|
||||||
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import {
|
|||||||
import createFetchClient from "openapi-react-query";
|
import createFetchClient from "openapi-react-query";
|
||||||
|
|
||||||
// Create the base openapi-fetch client with a default URL
|
// 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>({
|
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);
|
export const $api = createFetchClient<paths>(client);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
assertExtendedToken,
|
assertExtendedToken,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
|
assertExists,
|
||||||
assertExistsAndNonEmptyString,
|
assertExistsAndNonEmptyString,
|
||||||
parseMaybeNonEmptyString,
|
parseMaybeNonEmptyString,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
@@ -48,23 +49,24 @@ export const authOptions: AuthOptions = {
|
|||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account, user }) {
|
async jwt({ token, account, user }) {
|
||||||
const extendedToken = assertExtendedToken(token);
|
|
||||||
const KEY = `token:${token.sub}`;
|
const KEY = `token:${token.sub}`;
|
||||||
|
|
||||||
if (account && user) {
|
if (account && user) {
|
||||||
// called only on first login
|
// called only on first login
|
||||||
// XXX account.expires_in used in example is not defined for authentik backend, but expires_at is
|
// 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) {
|
if (!account.access_token) {
|
||||||
tokenCache.delete(KEY);
|
tokenCache.delete(KEY);
|
||||||
} else {
|
} else {
|
||||||
const jwtToken: JWTWithAccessToken = {
|
const jwtToken: JWTWithAccessToken = {
|
||||||
...extendedToken,
|
...token,
|
||||||
accessToken: account.access_token,
|
accessToken: account.access_token,
|
||||||
accessTokenExpires: expiresAt * 1000,
|
accessTokenExpires: expiresAtMs,
|
||||||
refreshToken: account.refresh_token || "",
|
refreshToken: account.refresh_token || "",
|
||||||
};
|
};
|
||||||
// Store in memory cache
|
// Store in memory cache
|
||||||
tokenCache.set(`token:${jwtToken.sub}`, {
|
tokenCache.set(KEY, {
|
||||||
token: jwtToken,
|
token: jwtToken,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -72,8 +74,9 @@ export const authOptions: AuthOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() < extendedToken.accessTokenExpires) {
|
const currentToken = tokenCache.get(KEY);
|
||||||
return token;
|
if (currentToken && Date.now() < currentToken.token.accessTokenExpires) {
|
||||||
|
return currentToken.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// access token has expired, try to update it
|
// access token has expired, try to update it
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ export interface CustomSession extends Session {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessTokenExpires: number;
|
accessTokenExpires: number;
|
||||||
error?: string;
|
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
|
// 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