mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-22 21:29:05 +00:00
self-review-fix
This commit is contained in:
@@ -4,6 +4,7 @@ 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();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ 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
|
||||
export const client = createClient<paths>({
|
||||
baseUrl: "http://127.0.0.1:1250",
|
||||
baseUrl: "http://192.0.2.1:1250",
|
||||
});
|
||||
|
||||
export const $api = createFetchClient<paths>(client);
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { AuthOptions } from "next-auth";
|
||||
import AuthentikProvider from "next-auth/providers/authentik";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
import { JWTWithAccessToken, CustomSession } from "./types";
|
||||
import {
|
||||
JWTWithAccessToken,
|
||||
CustomSession,
|
||||
assertExtendedToken,
|
||||
} from "./types";
|
||||
import {
|
||||
assertExistsAndNonEmptyString,
|
||||
parseMaybeNonEmptyString,
|
||||
} from "./utils";
|
||||
|
||||
const PRETIMEOUT = 60; // seconds before token expires to refresh it
|
||||
|
||||
@@ -15,11 +23,18 @@ const TOKEN_CACHE_TTL = 60 * 60 * 24 * 30 * 1000; // 30 days in milliseconds
|
||||
// Simple lock mechanism to prevent concurrent token refreshes
|
||||
const refreshLocks = new Map<string, Promise<JWTWithAccessToken>>();
|
||||
|
||||
const CLIENT_ID = assertExistsAndNonEmptyString(
|
||||
process.env.AUTHENTIK_CLIENT_ID,
|
||||
);
|
||||
const CLIENT_SECRET = assertExistsAndNonEmptyString(
|
||||
process.env.AUTHENTIK_CLIENT_SECRET,
|
||||
);
|
||||
|
||||
export const authOptions: AuthOptions = {
|
||||
providers: [
|
||||
AuthentikProvider({
|
||||
clientId: process.env.AUTHENTIK_CLIENT_ID as string,
|
||||
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET as string,
|
||||
clientId: CLIENT_ID,
|
||||
clientSecret: CLIENT_SECRET,
|
||||
issuer: process.env.AUTHENTIK_ISSUER,
|
||||
authorization: {
|
||||
params: {
|
||||
@@ -33,23 +48,28 @@ export const authOptions: AuthOptions = {
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, account, user }) {
|
||||
const extendedToken = token as JWTWithAccessToken;
|
||||
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 jwtToken: JWTWithAccessToken = {
|
||||
...extendedToken,
|
||||
accessToken: account.access_token || "",
|
||||
accessTokenExpires: expiresAt * 1000,
|
||||
refreshToken: account.refresh_token || "",
|
||||
};
|
||||
// Store in memory cache
|
||||
tokenCache.set(`token:${jwtToken.sub}`, {
|
||||
token: jwtToken,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return jwtToken;
|
||||
if (!account.access_token) {
|
||||
tokenCache.delete(KEY);
|
||||
} else {
|
||||
const jwtToken: JWTWithAccessToken = {
|
||||
...extendedToken,
|
||||
accessToken: account.access_token,
|
||||
accessTokenExpires: expiresAt * 1000,
|
||||
refreshToken: account.refresh_token || "",
|
||||
};
|
||||
// Store in memory cache
|
||||
tokenCache.set(`token:${jwtToken.sub}`, {
|
||||
token: jwtToken,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return jwtToken;
|
||||
}
|
||||
}
|
||||
|
||||
if (Date.now() < extendedToken.accessTokenExpires) {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Application-wide constants
|
||||
export const RECORD_A_MEETING_URL = "/transcripts/new" as const;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Session } from "next-auth";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
import { parseMaybeNonEmptyString } from "./utils";
|
||||
|
||||
export interface JWTWithAccessToken extends JWT {
|
||||
accessToken: string;
|
||||
@@ -18,3 +19,28 @@ export interface CustomSession extends Session {
|
||||
email?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
// assumption that JWT is JWTWithAccessToken - not ideal, TODO find a reason we have to do that
|
||||
export const assertExtendedToken = <T>(
|
||||
t: T,
|
||||
): T & {
|
||||
accessTokenExpires: number;
|
||||
accessToken: string;
|
||||
} => {
|
||||
if (
|
||||
typeof (t as { accessTokenExpires: any }).accessTokenExpires === "number" &&
|
||||
!isNaN((t as { accessTokenExpires: any }).accessTokenExpires) &&
|
||||
typeof (
|
||||
t as {
|
||||
accessToken: any;
|
||||
}
|
||||
).accessToken === "string" &&
|
||||
parseMaybeNonEmptyString((t as { accessToken: any }).accessToken) !== null
|
||||
) {
|
||||
return t as T & {
|
||||
accessTokenExpires: number;
|
||||
accessToken: string;
|
||||
};
|
||||
}
|
||||
throw new Error("Token is not extended with access token");
|
||||
};
|
||||
|
||||
@@ -10,13 +10,16 @@ import { isAuthConfigured } from "./apiClient";
|
||||
* Prevents race conditions where React Query fires requests before the token is set.
|
||||
*/
|
||||
export default function useAuthReady() {
|
||||
const { status, isAuthenticated } = useSessionStatus();
|
||||
const status = useSessionStatus();
|
||||
const isAuthenticated = status === "authenticated";
|
||||
const [authReady, setAuthReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let ready_ = false;
|
||||
// Check if both session is authenticated and token is configured
|
||||
const checkAuthReady = () => {
|
||||
const ready = isAuthenticated && isAuthConfigured();
|
||||
ready_ = ready;
|
||||
setAuthReady(ready);
|
||||
};
|
||||
|
||||
@@ -27,7 +30,14 @@ export default function useAuthReady() {
|
||||
const interval = setInterval(checkAuthReady, 100);
|
||||
|
||||
// Stop checking after 2 seconds (auth should be ready by then)
|
||||
const timeout = setTimeout(() => clearInterval(interval), 2000);
|
||||
const timeout = setTimeout(() => {
|
||||
if (ready_) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
} else {
|
||||
console.warn("Auth not ready after 2 seconds");
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSession as useNextAuthSession } from "next-auth/react";
|
||||
import { Session } from "next-auth";
|
||||
|
||||
export default function useSessionStatus() {
|
||||
const { status: naStatus } = useNextAuthSession();
|
||||
const [status, setStatus] = useState<typeof naStatus>("loading");
|
||||
|
||||
useEffect(() => {
|
||||
if (naStatus !== "loading" && naStatus !== status) {
|
||||
setStatus(naStatus);
|
||||
}
|
||||
}, [naStatus]);
|
||||
|
||||
return {
|
||||
status,
|
||||
isLoading: status === "loading",
|
||||
isAuthenticated: status === "authenticated",
|
||||
};
|
||||
const { status } = useNextAuthSession();
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -137,9 +137,28 @@ export function extractDomain(url) {
|
||||
}
|
||||
}
|
||||
|
||||
export function assertExists<T>(value: T | null | undefined, err?: string): T {
|
||||
export type NonEmptyString = string & { __brand: "NonEmptyString" };
|
||||
export const parseMaybeNonEmptyString = (
|
||||
s: string,
|
||||
trim = true,
|
||||
): NonEmptyString | null => {
|
||||
s = trim ? s.trim() : s;
|
||||
return s.length > 0 ? (s as NonEmptyString) : null;
|
||||
};
|
||||
export const parseNonEmptyString = (s: string, trim = true): NonEmptyString =>
|
||||
assertExists(parseMaybeNonEmptyString(s, trim), "Expected non-empty string");
|
||||
|
||||
export const assertExists = <T>(
|
||||
value: T | null | undefined,
|
||||
err?: string,
|
||||
): T => {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error(`Assertion failed: ${err ?? "value is null or undefined"}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export const assertExistsAndNonEmptyString = (
|
||||
value: string | null | undefined,
|
||||
): NonEmptyString =>
|
||||
parseNonEmptyString(assertExists(value, "Expected non-empty string"));
|
||||
|
||||
Reference in New Issue
Block a user