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:
James Brunton
2026-03-16 11:51:16 +00:00
committed by GitHub
parent 1722733802
commit dbff05814f
22 changed files with 123 additions and 112 deletions
+14
View File
@@ -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,
+6
View File
@@ -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;
+5 -5
View File
@@ -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)
} }
+2 -2
View File
@@ -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);
} }
@@ -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);
} }
@@ -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);
@@ -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
+2 -3
View File
@@ -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;
}; };
+3 -2
View File
@@ -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) => {
+1 -1
View File
@@ -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() {
+3 -6
View File
@@ -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
+2 -5
View File
@@ -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}
+24 -28
View File
@@ -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); });
}, },
/** /**
+5 -4
View File
@@ -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;
}), }),