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