mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Fix any type usage in the saas/ folder (#5934)
# Description of Changes Ages ago I made #4835 to try and fix all the `any` type usage in the system but never got it finished, and there were just too many to review and ensure it still worked. There's even more now. My new tactic is to fix folder by folder. This fixes the `any` typing in the `saas/` folder, and also enables `no-unnecessary-type-assertion`, which really helps reduce pointless `as` casts that AI generates when the type is already known. I hope to expand both of these to the rest of the folders soon, but one folder is better than none.
This commit is contained in:
@@ -59,6 +59,20 @@ export default defineConfig(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Folders that have been cleaned up and are now conformant - stricter rules enforced here
|
||||||
|
{
|
||||||
|
files: ['src/saas/**/*.{js,mjs,jsx,ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
// Config for browser scripts
|
// Config for browser scripts
|
||||||
{
|
{
|
||||||
files: srcGlobs,
|
files: srcGlobs,
|
||||||
|
|||||||
Vendored
+6
@@ -12,6 +12,12 @@ declare module 'assets/material-symbols-icons.json' {
|
|||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__STIRLING_PDF_BASE_URL__?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'axios' {
|
declare module 'axios' {
|
||||||
export interface AxiosRequestConfig<_D = unknown> {
|
export interface AxiosRequestConfig<_D = unknown> {
|
||||||
suppressErrorToast?: boolean;
|
suppressErrorToast?: boolean;
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setSubscription(subscriptionInfo)
|
setSubscription(subscriptionInfo)
|
||||||
|
|
||||||
console.debug('[Auth Debug] Credits fetched successfully:', credits)
|
console.debug('[Auth Debug] Credits fetched successfully:', credits)
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.debug('[Auth Debug] Failed to fetch credits:', error)
|
console.debug('[Auth Debug] Failed to fetch credits:', error)
|
||||||
// Don't set error state for credit fetching failures to avoid disrupting auth flow
|
// Don't set error state for credit fetching failures to avoid disrupting auth flow
|
||||||
// Credits might not be available in all deployments
|
// Credits might not be available in all deployments
|
||||||
@@ -149,7 +149,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsPro(isProUser)
|
setIsPro(isProUser)
|
||||||
console.debug('[Auth Debug] Pro status fetched:', isProUser)
|
console.debug('[Auth Debug] Pro status fetched:', isProUser)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.debug('[Auth Debug] Failed to fetch pro status:', error)
|
console.debug('[Auth Debug] Failed to fetch pro status:', error)
|
||||||
setIsPro(false) // Default to false if there's an error
|
setIsPro(false) // Default to false if there's an error
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
} else {
|
} else {
|
||||||
setTrialStatus(null)
|
setTrialStatus(null)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.debug('[Auth Debug] Failed to fetch trial status:', error)
|
console.debug('[Auth Debug] Failed to fetch trial status:', error)
|
||||||
setTrialStatus(null)
|
setTrialStatus(null)
|
||||||
}
|
}
|
||||||
@@ -243,7 +243,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setProfilePictureUrl(data.signedUrl)
|
setProfilePictureUrl(data.signedUrl)
|
||||||
console.debug('[Auth Debug] Profile picture URL fetched successfully')
|
console.debug('[Auth Debug] Profile picture URL fetched successfully')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.debug('[Auth Debug] Failed to fetch profile picture:', error)
|
console.debug('[Auth Debug] Failed to fetch profile picture:', error)
|
||||||
setProfilePictureUrl(null)
|
setProfilePictureUrl(null)
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const metadata = await getProfilePictureMetadata(currentSession.user.id)
|
const metadata = await getProfilePictureMetadata(currentSession.user.id)
|
||||||
setProfilePictureMetadata(metadata)
|
setProfilePictureMetadata(metadata)
|
||||||
console.debug('[Auth Debug] Profile picture metadata fetched:', metadata)
|
console.debug('[Auth Debug] Profile picture metadata fetched:', metadata)
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.debug('[Auth Debug] Failed to fetch profile picture metadata:', error)
|
console.debug('[Auth Debug] Failed to fetch profile picture metadata:', error)
|
||||||
setProfilePictureMetadata(null)
|
setProfilePictureMetadata(null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const signInAnonymously = async () => {
|
|||||||
// Account linking functions
|
// Account linking functions
|
||||||
export const linkEmailIdentity = async (email: string, password?: string) => {
|
export const linkEmailIdentity = async (email: string, password?: string) => {
|
||||||
try {
|
try {
|
||||||
const updateData: any = { email }
|
const updateData: { email: string; password?: string } = { email }
|
||||||
if (password) {
|
if (password) {
|
||||||
updateData.password = password
|
updateData.password = password
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ export const linkOAuthIdentity = async (provider: 'google' | 'github' | 'apple'
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if user is anonymous
|
// Helper function to check if user is anonymous
|
||||||
export const isUserAnonymous = (user: any) => {
|
export const isUserAnonymous = (user: { is_anonymous?: boolean }) => {
|
||||||
return user?.is_anonymous === true
|
return user?.is_anonymous === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ export function ManageBillingButton({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.functions.invoke('manage-billing', {
|
const { data, error } = await supabase.functions.invoke<{ url: string; error?: string }>('manage-billing', {
|
||||||
body: {
|
body: {
|
||||||
name: 'Functions',
|
name: 'Functions',
|
||||||
return_url: returnUrl},
|
return_url: returnUrl},
|
||||||
})
|
})
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
if (!data || 'error' in data) throw new Error((data as any)?.error ?? 'No portal URL');
|
if (!data || 'error' in data) throw new Error(data?.error ?? 'No portal URL');
|
||||||
window.location.href = (data as any).url;
|
window.location.href = data.url;
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setErr(e.message ?? 'Could not open billing portal');
|
setErr(e instanceof Error ? e.message : 'Could not open billing portal');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface StripeCheckoutProps {
|
|||||||
currency?: string;
|
currency?: string;
|
||||||
isTrialConversion?: boolean;
|
isTrialConversion?: boolean;
|
||||||
// Proprietary-specific props (for compatibility)
|
// Proprietary-specific props (for compatibility)
|
||||||
planGroup?: any;
|
planGroup?: unknown;
|
||||||
minimumSeats?: number;
|
minimumSeats?: number;
|
||||||
onLicenseActivated?: (licenseInfo: {licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}) => void;
|
onLicenseActivated?: (licenseInfo: {licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}) => void;
|
||||||
hostedCheckoutSuccess?: {
|
hostedCheckoutSuccess?: {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function createScale(domain: [number, number], range: [number, number]) {
|
|||||||
* @param wait The wait time in milliseconds
|
* @param wait The wait time in milliseconds
|
||||||
* @returns Debounced function
|
* @returns Debounced function
|
||||||
*/
|
*/
|
||||||
export function debounce<T extends (...args: any[]) => any>(
|
export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||||
func: T,
|
func: T,
|
||||||
wait: number
|
wait: number
|
||||||
): (...args: Parameters<T>) => void {
|
): (...args: Parameters<T>) => void {
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ const Overview: React.FC<OverviewProps> = ({ onLogoutClick }) => {
|
|||||||
|
|
||||||
// Clear success message after 3 seconds
|
// Clear success message after 3 seconds
|
||||||
setTimeout(() => setSuccess(null), 3000);
|
setTimeout(() => setSuccess(null), 3000);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
setProfileError(error.message || 'Failed to switch to custom picture');
|
setProfileError(error instanceof Error ? error.message : 'Failed to switch to custom picture');
|
||||||
} finally {
|
} finally {
|
||||||
setProfileUploading(false);
|
setProfileUploading(false);
|
||||||
}
|
}
|
||||||
@@ -181,8 +181,8 @@ const Overview: React.FC<OverviewProps> = ({ onLogoutClick }) => {
|
|||||||
setSuccess('Account upgraded successfully! You can now sign in with your email.');
|
setSuccess('Account upgraded successfully! You can now sign in with your email.');
|
||||||
setEmail('');
|
setEmail('');
|
||||||
setPassword('');
|
setPassword('');
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setUpgradeError(err?.message || 'Failed to upgrade account');
|
setUpgradeError(err instanceof Error ? err.message : 'Failed to upgrade account');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -404,7 +404,7 @@ const Overview: React.FC<OverviewProps> = ({ onLogoutClick }) => {
|
|||||||
style={{ width: 16, height: 16 }}
|
style={{ width: 16, height: 16 }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onClick={() => handleOAuthUpgrade(provider.id as any)}
|
onClick={() => handleOAuthUpgrade(provider.id as 'github' | 'google' | 'apple' | 'azure')}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{provider.label}
|
{provider.label}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ const PasswordSecurity: React.FC = () => {
|
|||||||
setOpened(false);
|
setOpened(false);
|
||||||
setDidUpdate(false);
|
setDidUpdate(false);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setError(e?.message || 'Failed to change password');
|
setError(e instanceof Error ? e.message : 'Failed to change password');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -1,8 +1,11 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { isAxiosError } from "axios";
|
||||||
import apiClient from "@app/services/apiClient";
|
import apiClient from "@app/services/apiClient";
|
||||||
import { useAuth } from "@app/auth/UseSession";
|
import { useAuth } from "@app/auth/UseSession";
|
||||||
import { isUserAnonymous } from "@app/auth/supabase";
|
import { isUserAnonymous } from "@app/auth/supabase";
|
||||||
|
|
||||||
|
type ApiKeyResponse = string | { apiKey?: string };
|
||||||
|
|
||||||
export function useApiKey() {
|
export function useApiKey() {
|
||||||
const { session, loading, user } = useAuth();
|
const { session, loading, user } = useAuth();
|
||||||
const isAnonymous = Boolean(user && isUserAnonymous(user));
|
const isAnonymous = Boolean(user && isUserAnonymous(user));
|
||||||
@@ -17,24 +20,21 @@ export function useApiKey() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
// Backend is POST for get and update
|
// Backend is POST for get and update
|
||||||
const res = await apiClient.post("/api/v1/user/get-api-key");
|
const res = await apiClient.post<ApiKeyResponse>("/api/v1/user/get-api-key");
|
||||||
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
|
const value = typeof res.data === "string" ? res.data : res.data.apiKey;
|
||||||
if (typeof value === "string") setApiKey(value);
|
if (typeof value === "string") setApiKey(value);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
// If not found, try to create one by calling update endpoint
|
// If not found, try to create one by calling update endpoint
|
||||||
if (e?.response?.status === 404) {
|
if (isAxiosError(e) && e.response?.status === 404) {
|
||||||
try {
|
try {
|
||||||
const createRes = await apiClient.post("/api/v1/user/update-api-key");
|
const createRes = await apiClient.post<ApiKeyResponse>("/api/v1/user/update-api-key");
|
||||||
const created =
|
const created = typeof createRes.data === "string" ? createRes.data : createRes.data.apiKey;
|
||||||
typeof createRes.data === "string"
|
|
||||||
? createRes.data
|
|
||||||
: createRes.data?.apiKey;
|
|
||||||
if (typeof created === "string") setApiKey(created);
|
if (typeof created === "string") setApiKey(created);
|
||||||
} catch (createErr: any) {
|
} catch (createErr: unknown) {
|
||||||
setError(createErr);
|
setError(createErr instanceof Error ? createErr : new Error(String(createErr)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setError(e);
|
setError(e instanceof Error ? e : new Error(String(e)));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -46,11 +46,11 @@ export function useApiKey() {
|
|||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.post("/api/v1/user/update-api-key");
|
const res = await apiClient.post<ApiKeyResponse>("/api/v1/user/update-api-key");
|
||||||
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
|
const value = typeof res.data === "string" ? res.data : res.data.apiKey;
|
||||||
if (typeof value === "string") setApiKey(value);
|
if (typeof value === "string") setApiKey(value);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setError(e);
|
setError(e instanceof Error ? e : new Error(String(e)));
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -6,10 +6,10 @@ import { isUserAnonymous } from "@app/auth/supabase";
|
|||||||
|
|
||||||
function coerceNumber(value: unknown, fallback = 0): number {
|
function coerceNumber(value: unknown, fallback = 0): number {
|
||||||
const n = typeof value === "string" ? Number(value) : (value as number);
|
const n = typeof value === "string" ? Number(value) : (value as number);
|
||||||
return Number.isFinite(n) ? (n as number) : fallback;
|
return Number.isFinite(n) ? n : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCredits(raw: any): ApiCredits {
|
function normalizeCredits(raw: Record<string, unknown>): ApiCredits {
|
||||||
// Accept a variety of possible backend keys to be resilient
|
// Accept a variety of possible backend keys to be resilient
|
||||||
return {
|
return {
|
||||||
weeklyCreditsRemaining: coerceNumber(
|
weeklyCreditsRemaining: coerceNumber(
|
||||||
@@ -28,9 +28,9 @@ function normalizeCredits(raw: any): ApiCredits {
|
|||||||
raw?.totalAvailableCredits ?? raw?.totalRemaining ?? raw?.available_total
|
raw?.totalAvailableCredits ?? raw?.totalRemaining ?? raw?.available_total
|
||||||
),
|
),
|
||||||
weeklyResetDate:
|
weeklyResetDate:
|
||||||
(raw?.weeklyResetDate ?? raw?.weeklyReset ?? raw?.reset_date) || "",
|
String(raw?.weeklyResetDate ?? raw?.weeklyReset ?? raw?.reset_date ?? ""),
|
||||||
lastApiUsage:
|
lastApiUsage:
|
||||||
(raw?.lastApiUsage ?? raw?.lastApiUse ?? raw?.last_used_at) || "",
|
String(raw?.lastApiUsage ?? raw?.lastApiUse ?? raw?.last_used_at ?? ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export function useCredits() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get("/api/v1/credits");
|
const res = await apiClient.get<Record<string, unknown>>("/api/v1/credits");
|
||||||
const normalized = normalizeCredits(res.data);
|
const normalized = normalizeCredits(res.data);
|
||||||
// If backend returns an "empty" payload, keep data null so the UI stays in loading/skeleton
|
// If backend returns an "empty" payload, keep data null so the UI stays in loading/skeleton
|
||||||
const isEmpty = !normalized.weeklyCreditsAllocated &&
|
const isEmpty = !normalized.weeklyCreditsAllocated &&
|
||||||
@@ -57,8 +57,8 @@ export function useCredits() {
|
|||||||
!normalized.weeklyResetDate &&
|
!normalized.weeklyResetDate &&
|
||||||
!normalized.lastApiUsage;
|
!normalized.lastApiUsage;
|
||||||
setData(isEmpty ? null : normalized);
|
setData(isEmpty ? null : normalized);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setError(e);
|
setError(e instanceof Error ? e : new Error(String(e)));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setHasAttempted(true);
|
setHasAttempted(true);
|
||||||
|
|||||||
+3
-3
@@ -7,9 +7,9 @@ import PlanCard from '@app/components/shared/config/configSections/plan/PlanCard
|
|||||||
interface AvailablePlansSectionProps {
|
interface AvailablePlansSectionProps {
|
||||||
plans: PlanTier[];
|
plans: PlanTier[];
|
||||||
currentPlan?: PlanTier;
|
currentPlan?: PlanTier;
|
||||||
currentLicenseInfo?: any;
|
currentLicenseInfo?: unknown;
|
||||||
onUpgradeClick: (plan: PlanTier | any) => void; // Accept PlanTierGroup for compatibility
|
onUpgradeClick: (plan: PlanTier) => void;
|
||||||
onManageClick?: (plan: PlanTier | any) => void; // Accept PlanTierGroup for compatibility
|
onManageClick?: (plan: PlanTier) => void;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
onCurrencyChange?: (currency: string) => void;
|
onCurrencyChange?: (currency: string) => void;
|
||||||
currencyOptions?: Array<{ value: string; label: string }>;
|
currencyOptions?: Array<{ value: string; label: string }>;
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import { PlanTier } from '@app/hooks/usePlans';
|
|||||||
|
|
||||||
interface PlanCardProps {
|
interface PlanCardProps {
|
||||||
plan?: PlanTier;
|
plan?: PlanTier;
|
||||||
planGroup?: any; // For proprietary PlanTierGroup compatibility
|
planGroup?: { monthly?: PlanTier; yearly?: PlanTier }; // For proprietary PlanTierGroup compatibility
|
||||||
isCurrentPlan?: boolean;
|
isCurrentPlan?: boolean;
|
||||||
isCurrentTier?: boolean;
|
isCurrentTier?: boolean;
|
||||||
isDowngrade?: boolean;
|
isDowngrade?: boolean;
|
||||||
isUserProOrAbove?: boolean;
|
isUserProOrAbove?: boolean;
|
||||||
currentLicenseInfo?: any;
|
currentLicenseInfo?: unknown;
|
||||||
currentTier?: string | null; // Accept null for proprietary compatibility
|
currentTier?: string | null; // Accept null for proprietary compatibility
|
||||||
onUpgradeClick?: (plan: any) => void; // Accept PlanTierGroup or PlanTier
|
onUpgradeClick?: (plan: PlanTier) => void;
|
||||||
onManageClick?: (plan: any) => void;
|
onManageClick?: (plan: PlanTier) => void;
|
||||||
loginEnabled?: boolean;
|
loginEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ const PlanCard: React.FC<PlanCardProps> = ({
|
|||||||
loginEnabled: _loginEnabled
|
loginEnabled: _loginEnabled
|
||||||
}) => {
|
}) => {
|
||||||
// Use plan from props, or extract from planGroup if proprietary is using it
|
// Use plan from props, or extract from planGroup if proprietary is using it
|
||||||
const plan = propPlan || (planGroup as any)?.monthly || (planGroup as any)?.yearly;
|
const plan = propPlan || planGroup?.monthly || planGroup?.yearly;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!plan) return null; // Safety check
|
if (!plan) return null; // Safety check
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ export * from '@core/constants/app';
|
|||||||
// Get base URL with fallback (for use outside React components)
|
// Get base URL with fallback (for use outside React components)
|
||||||
export const getBaseUrl = (): string => {
|
export const getBaseUrl = (): string => {
|
||||||
// Try to get from window object if set by app config
|
// Try to get from window object if set by app config
|
||||||
const baseUrl = (window as any).__STIRLING_PDF_BASE_URL__ || window.location.origin;
|
return window.__STIRLING_PDF_BASE_URL__ || window.location.origin;
|
||||||
return baseUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to set base URL (to be called when app config loads)
|
// Helper to set base URL (to be called when app config loads)
|
||||||
export const setBaseUrl = (url: string): void => {
|
export const setBaseUrl = (url: string): void => {
|
||||||
(window as any).__STIRLING_PDF_BASE_URL__ = url;
|
window.__STIRLING_PDF_BASE_URL__ = url;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { isAxiosError } from 'axios';
|
||||||
import apiClient from '@app/services/apiClient';
|
import apiClient from '@app/services/apiClient';
|
||||||
import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability';
|
import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability';
|
||||||
|
|
||||||
@@ -153,9 +154,9 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
|||||||
setEndpointStatus(fullStatus.status);
|
setEndpointStatus(fullStatus.status);
|
||||||
setEndpointDetails(prev => ({ ...prev, ...fullStatus.details }));
|
setEndpointDetails(prev => ({ ...prev, ...fullStatus.details }));
|
||||||
globalFetchedSets.add(endpointsKey);
|
globalFetchedSets.add(endpointsKey);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
// On 401 (auth error), use optimistic fallback instead of disabling
|
// On 401 (auth error), use optimistic fallback instead of disabling
|
||||||
if (err.response?.status === 401) {
|
if (isAxiosError(err) && err.response?.status === 401) {
|
||||||
console.warn('[useEndpointConfig] 401 error - using optimistic fallback');
|
console.warn('[useEndpointConfig] 401 error - using optimistic fallback');
|
||||||
const optimisticStatus = endpoints.reduce(
|
const optimisticStatus = endpoints.reduce(
|
||||||
(acc, endpoint) => {
|
(acc, endpoint) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { withBasePath } from '@app/constants/app'
|
|||||||
interface CallbackState {
|
interface CallbackState {
|
||||||
status: 'processing' | 'success' | 'error'
|
status: 'processing' | 'success' | 'error'
|
||||||
message: string
|
message: string
|
||||||
details?: Record<string, any>
|
details?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AuthCallback() {
|
export default function AuthCallback() {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function Login() {
|
|||||||
const redirectTo = absoluteWithBasePath('/auth/callback')
|
const redirectTo = absoluteWithBasePath('/auth/callback')
|
||||||
console.log(`[Login] Signing in with ${provider}`)
|
console.log(`[Login] Signing in with ${provider}`)
|
||||||
|
|
||||||
const oauthOptions: any = { redirectTo }
|
const oauthOptions: { redirectTo: string; queryParams?: Record<string, string> } = { redirectTo }
|
||||||
if (provider === 'apple') {
|
if (provider === 'apple') {
|
||||||
oauthOptions.queryParams = { scope: 'email name' }
|
oauthOptions.queryParams = { scope: 'email name' }
|
||||||
} else if (provider === 'azure') {
|
} else if (provider === 'azure') {
|
||||||
@@ -175,12 +175,9 @@ export default function Login() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
console.log('[Login] Signing in anonymously')
|
console.log('[Login] Signing in anonymously')
|
||||||
|
|
||||||
const { data, error } = await signInAnonymously()
|
const { data } = await signInAnonymously()
|
||||||
|
|
||||||
if (error) {
|
if (data.user) {
|
||||||
console.error('[Login] Anonymous sign in error:', error)
|
|
||||||
setError((error as any)?.message || 'Unknown error')
|
|
||||||
} else if (data.user) {
|
|
||||||
console.log('[Login] Anonymous sign in successful, refreshing session...')
|
console.log('[Login] Anonymous sign in successful, refreshing session...')
|
||||||
|
|
||||||
// Refresh session to ensure backend endpoints are properly synchronized
|
// Refresh session to ensure backend endpoints are properly synchronized
|
||||||
|
|||||||
@@ -62,12 +62,9 @@ export default function Signup() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
console.log('[Signup] Initiating anonymous sign-in...')
|
console.log('[Signup] Initiating anonymous sign-in...')
|
||||||
const { data, error } = await signInAnonymously()
|
const { data } = await signInAnonymously()
|
||||||
|
|
||||||
if (error) {
|
if (data.user) {
|
||||||
console.error('[Signup] Anonymous sign-in error:', error)
|
|
||||||
setError(`Failed to create guest account: ${(error as any)?.message || 'Unknown error'}`)
|
|
||||||
} else if (data.user) {
|
|
||||||
console.log('[Signup] Anonymous sign-in successful, refreshing session...')
|
console.log('[Signup] Anonymous sign-in successful, refreshing session...')
|
||||||
|
|
||||||
// Refresh session to ensure backend endpoints are properly synchronized
|
// Refresh session to ensure backend endpoints are properly synchronized
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
|||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => onProviderClick(p.id as any)}
|
onClick={() => onProviderClick(p.id as 'github' | 'google')}
|
||||||
disabled={isSubmitting || p.isDisabled}
|
disabled={isSubmitting || p.isDisabled}
|
||||||
className="oauth-button-icon"
|
className="oauth-button-icon"
|
||||||
aria-label={`${t('login.signInWith', 'Sign in with')} ${p.label}`}
|
aria-label={`${t('login.signInWith', 'Sign in with')} ${p.label}`}
|
||||||
@@ -55,7 +55,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
|||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => onProviderClick(p.id as any)}
|
onClick={() => onProviderClick(p.id as 'github' | 'google')}
|
||||||
disabled={isSubmitting || p.isDisabled}
|
disabled={isSubmitting || p.isDisabled}
|
||||||
className="oauth-button-grid"
|
className="oauth-button-grid"
|
||||||
aria-label={`${t('login.signInWith', 'Sign in with')} ${p.label}`}
|
aria-label={`${t('login.signInWith', 'Sign in with')} ${p.label}`}
|
||||||
@@ -74,7 +74,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
|||||||
{oauthProviders.map((p) => (
|
{oauthProviders.map((p) => (
|
||||||
<button
|
<button
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onClick={() => onProviderClick(p.id as any)}
|
onClick={() => onProviderClick(p.id as 'github' | 'google')}
|
||||||
disabled={isSubmitting || p.isDisabled}
|
disabled={isSubmitting || p.isDisabled}
|
||||||
className="oauth-button-fullwidth"
|
className="oauth-button-fullwidth"
|
||||||
title={p.label}
|
title={p.label}
|
||||||
@@ -92,7 +92,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
|||||||
{oauthProviders.map((p) => (
|
{oauthProviders.map((p) => (
|
||||||
<button
|
<button
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onClick={() => onProviderClick(p.id as any)}
|
onClick={() => onProviderClick(p.id as 'github' | 'google')}
|
||||||
disabled={isSubmitting || p.isDisabled}
|
disabled={isSubmitting || p.isDisabled}
|
||||||
className="oauth-button-vertical"
|
className="oauth-button-vertical"
|
||||||
title={p.label}
|
title={p.label}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import type { Session, AuthError } from '@supabase/supabase-js';
|
||||||
import { supabase } from '@app/auth/supabase';
|
import { supabase } from '@app/auth/supabase';
|
||||||
|
|
||||||
// Mock supabase
|
// Mock supabase
|
||||||
@@ -20,13 +21,13 @@ describe('apiClient', () => {
|
|||||||
|
|
||||||
it('should add JWT token to request headers when session exists', async () => {
|
it('should add JWT token to request headers when session exists', async () => {
|
||||||
const mockToken = 'test-jwt-token-12345';
|
const mockToken = 'test-jwt-token-12345';
|
||||||
const mockSession: any = {
|
const mockSession = {
|
||||||
access_token: mockToken,
|
access_token: mockToken,
|
||||||
refresh_token: 'refresh-token',
|
refresh_token: 'refresh-token',
|
||||||
expires_in: 3600,
|
expires_in: 3600,
|
||||||
token_type: 'bearer',
|
token_type: 'bearer',
|
||||||
user: { id: 'user-123' },
|
user: { id: 'user-123' },
|
||||||
};
|
} as unknown as Session;
|
||||||
|
|
||||||
// Mock getSession to return a session with token
|
// Mock getSession to return a session with token
|
||||||
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
||||||
@@ -99,21 +100,21 @@ describe('apiClient', () => {
|
|||||||
const oldToken = 'old-token';
|
const oldToken = 'old-token';
|
||||||
const newToken = 'new-refreshed-token';
|
const newToken = 'new-refreshed-token';
|
||||||
|
|
||||||
const oldSession: any = {
|
const oldSession = {
|
||||||
access_token: oldToken,
|
access_token: oldToken,
|
||||||
refresh_token: 'refresh-token',
|
refresh_token: 'refresh-token',
|
||||||
expires_in: 3600,
|
expires_in: 3600,
|
||||||
token_type: 'bearer',
|
token_type: 'bearer',
|
||||||
user: { id: 'user-123' },
|
user: { id: 'user-123' },
|
||||||
};
|
} as unknown as Session;
|
||||||
|
|
||||||
const newSession: any = {
|
const newSession = {
|
||||||
access_token: newToken,
|
access_token: newToken,
|
||||||
refresh_token: 'new-refresh-token',
|
refresh_token: 'new-refresh-token',
|
||||||
expires_in: 3600,
|
expires_in: 3600,
|
||||||
token_type: 'bearer',
|
token_type: 'bearer',
|
||||||
user: { id: 'user-123' },
|
user: { id: 'user-123' },
|
||||||
};
|
} as unknown as Session;
|
||||||
|
|
||||||
// Mock initial session for first request
|
// Mock initial session for first request
|
||||||
let getSessionCallCount = 0;
|
let getSessionCallCount = 0;
|
||||||
@@ -129,8 +130,8 @@ describe('apiClient', () => {
|
|||||||
// Mock refresh to return new session
|
// Mock refresh to return new session
|
||||||
vi.mocked(supabase.auth.refreshSession).mockResolvedValue({
|
vi.mocked(supabase.auth.refreshSession).mockResolvedValue({
|
||||||
data: { user: null, session: newSession },
|
data: { user: null, session: newSession },
|
||||||
error: null as any,
|
error: null,
|
||||||
} as any);
|
});
|
||||||
|
|
||||||
// Import apiClient after mocking
|
// Import apiClient after mocking
|
||||||
const { default: apiClient } = await import('@app/services/apiClient');
|
const { default: apiClient } = await import('@app/services/apiClient');
|
||||||
@@ -143,12 +144,10 @@ describe('apiClient', () => {
|
|||||||
if (requestCount === 1) {
|
if (requestCount === 1) {
|
||||||
// Verify first request has old token
|
// Verify first request has old token
|
||||||
expect(config.headers.Authorization).toBe(`Bearer ${oldToken}`);
|
expect(config.headers.Authorization).toBe(`Bearer ${oldToken}`);
|
||||||
const error: any = new Error('Unauthorized');
|
const error = Object.assign(new Error('Unauthorized'), {
|
||||||
error.response = {
|
response: { status: 401, data: { error: 'Unauthorized' } },
|
||||||
status: 401,
|
config,
|
||||||
data: { error: 'Unauthorized' },
|
});
|
||||||
};
|
|
||||||
error.config = config;
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,35 +186,32 @@ describe('apiClient', () => {
|
|||||||
|
|
||||||
// Mock initial session
|
// Mock initial session
|
||||||
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
||||||
data: { session: oldSession },
|
data: { session: oldSession as unknown as Session },
|
||||||
error: null,
|
error: null,
|
||||||
} as any);
|
});
|
||||||
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
vi.mocked(supabase.auth.getSession).mockResolvedValue({
|
||||||
data: { session: oldSession },
|
data: { session: oldSession as unknown as Session },
|
||||||
error: null,
|
error: null,
|
||||||
} as any);
|
});
|
||||||
|
|
||||||
// Mock refresh to fail
|
// Mock refresh to fail
|
||||||
vi.mocked(supabase.auth.refreshSession).mockResolvedValue({
|
vi.mocked(supabase.auth.refreshSession).mockResolvedValue({
|
||||||
data: { user: null, session: null },
|
data: { user: null, session: null },
|
||||||
error: { name: 'AuthError', message: 'Refresh failed', status: 400, code: 'auth_error', __isAuthError: true } as any,
|
error: { name: 'AuthError', message: 'Refresh failed', status: 400, code: 'auth_error' } as unknown as AuthError,
|
||||||
} as any);
|
});
|
||||||
|
|
||||||
// Import apiClient after mocking
|
// Import apiClient after mocking
|
||||||
const { default: apiClient } = await import('@app/services/apiClient');
|
const { default: apiClient } = await import('@app/services/apiClient');
|
||||||
|
|
||||||
// Mock window.location for redirect test
|
// Mock window.location for redirect test
|
||||||
delete (window as any).location;
|
Object.defineProperty(window, 'location', { writable: true, value: { href: '' } });
|
||||||
window.location = { href: '' } as any;
|
|
||||||
|
|
||||||
const mockAdapter = vi.fn((config) => {
|
const mockAdapter = vi.fn((config) => {
|
||||||
// Always return 401 to trigger refresh
|
// Always return 401 to trigger refresh
|
||||||
const error: any = new Error('Unauthorized');
|
const error = Object.assign(new Error('Unauthorized'), {
|
||||||
error.response = {
|
response: { status: 401, data: { error: 'Unauthorized' } },
|
||||||
status: 401,
|
config,
|
||||||
data: { error: 'Unauthorized' },
|
});
|
||||||
};
|
|
||||||
error.config = config;
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface AdminSettingsData {
|
|||||||
disabledUsers: number;
|
disabledUsers: number;
|
||||||
currentUsername?: string;
|
currentUsername?: string;
|
||||||
roleDetails?: Record<string, string>;
|
roleDetails?: Record<string, string>;
|
||||||
teams?: any[];
|
teams?: Array<{ id: number; name: string; [key: string]: unknown }>;
|
||||||
maxPaidUsers?: number;
|
maxPaidUsers?: number;
|
||||||
// License information
|
// License information
|
||||||
maxAllowedUsers: number;
|
maxAllowedUsers: number;
|
||||||
@@ -138,7 +138,7 @@ export const userManagementService = {
|
|||||||
}
|
}
|
||||||
await apiClient.post('/api/v1/user/admin/saveUser', formData, {
|
await apiClient.post('/api/v1/user/admin/saveUser', formData, {
|
||||||
suppressErrorToast: true, // Component will handle error display
|
suppressErrorToast: true, // Component will handle error display
|
||||||
} as any);
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,7 +153,7 @@ export const userManagementService = {
|
|||||||
}
|
}
|
||||||
await apiClient.post('/api/v1/user/admin/changeRole', formData, {
|
await apiClient.post('/api/v1/user/admin/changeRole', formData, {
|
||||||
suppressErrorToast: true,
|
suppressErrorToast: true,
|
||||||
} as any);
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,7 +164,7 @@ export const userManagementService = {
|
|||||||
formData.append('enabled', enabled.toString());
|
formData.append('enabled', enabled.toString());
|
||||||
await apiClient.post(`/api/v1/user/admin/changeUserEnabled/${username}`, formData, {
|
await apiClient.post(`/api/v1/user/admin/changeUserEnabled/${username}`, formData, {
|
||||||
suppressErrorToast: true,
|
suppressErrorToast: true,
|
||||||
} as any);
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,7 +208,7 @@ export const userManagementService = {
|
|||||||
formData,
|
formData,
|
||||||
{
|
{
|
||||||
suppressErrorToast: true, // Component will handle error display
|
suppressErrorToast: true, // Component will handle error display
|
||||||
} as any
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -239,7 +239,7 @@ export const userManagementService = {
|
|||||||
formData,
|
formData,
|
||||||
{
|
{
|
||||||
suppressErrorToast: true,
|
suppressErrorToast: true,
|
||||||
} as any
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -259,7 +259,7 @@ export const userManagementService = {
|
|||||||
async revokeInviteLink(inviteId: number): Promise<void> {
|
async revokeInviteLink(inviteId: number): Promise<void> {
|
||||||
await apiClient.delete(`/api/v1/invite/revoke/${inviteId}`, {
|
await apiClient.delete(`/api/v1/invite/revoke/${inviteId}`, {
|
||||||
suppressErrorToast: true,
|
suppressErrorToast: true,
|
||||||
} as any);
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -89,15 +89,16 @@ for (let i = 0; i < 32; i++) {
|
|||||||
Object.defineProperty(globalThis, 'crypto', {
|
Object.defineProperty(globalThis, 'crypto', {
|
||||||
value: {
|
value: {
|
||||||
subtle: {
|
subtle: {
|
||||||
digest: vi.fn().mockImplementation(async (_algorithm: string, _data: any) => {
|
digest: vi.fn().mockImplementation(async (_algorithm: string, _data: BufferSource) => {
|
||||||
// Always return the mock hash buffer regardless of input
|
// Always return the mock hash buffer regardless of input
|
||||||
return mockHashBuffer.slice();
|
return mockHashBuffer.slice();
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
getRandomValues: vi.fn().mockImplementation((array: any) => {
|
getRandomValues: vi.fn().mockImplementation(<T extends ArrayBufferView>(array: T): T => {
|
||||||
// Mock getRandomValues if needed
|
// Mock getRandomValues if needed
|
||||||
for (let i = 0; i < array.length; i++) {
|
const view = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
||||||
array[i] = Math.floor(Math.random() * 256);
|
for (let i = 0; i < view.length; i++) {
|
||||||
|
view[i] = Math.floor(Math.random() * 256);
|
||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user