mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccaae128b7 | ||
|
|
63bcb73637 | ||
|
|
531a7fdbcc | ||
|
|
920f2898e1 | ||
|
|
dba316c14f | ||
|
|
daf044a7e4 | ||
|
|
81512b7805 |
@@ -3381,7 +3381,15 @@
|
||||
"unexpectedError": "Unexpected error: {{message}}",
|
||||
"accountCreatedSuccess": "Account created successfully! You can now sign in.",
|
||||
"passwordChangedSuccess": "Password changed successfully! Please sign in with your new password.",
|
||||
"credentialsUpdated": "Your credentials have been updated. Please sign in again."
|
||||
"credentialsUpdated": "Your credentials have been updated. Please sign in again.",
|
||||
"backendLoadingTitle": "Backend starting up",
|
||||
"backendLoadingMessage": "Backend is currently loading and/or not started. Please wait a few moments and try again."
|
||||
},
|
||||
"backendStartup": {
|
||||
"title": "Backend Starting",
|
||||
"loadingTitle": "Backend starting up",
|
||||
"loadingMessage": "The backend is currently starting up. This usually takes a few moments.",
|
||||
"takingLonger": "This is taking longer than expected. The backend should be ready soon."
|
||||
},
|
||||
"signup": {
|
||||
"title": "Create an account",
|
||||
|
||||
@@ -8,6 +8,7 @@ import Login from "@app/routes/Login";
|
||||
import Signup from "@app/routes/Signup";
|
||||
import AuthCallback from "@app/routes/AuthCallback";
|
||||
import InviteAccept from "@app/routes/InviteAccept";
|
||||
import BackendStartup from "@app/routes/BackendStartup";
|
||||
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
|
||||
|
||||
// Import global styles
|
||||
@@ -30,6 +31,7 @@ export default function App() {
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route path="/invite/:token" element={<InviteAccept />} />
|
||||
<Route path="/backend-startup" element={<BackendStartup />} />
|
||||
|
||||
{/* Main app routes - Landing handles auth logic */}
|
||||
<Route path="/*" element={<Landing />} />
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDocumentMeta } from '@app/hooks/useDocumentMeta';
|
||||
import AuthLayout from '@app/routes/authShared/AuthLayout';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
|
||||
export default function BackendStartup() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [pollAttempt, setPollAttempt] = useState(0);
|
||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||
const baseUrl = window.location.origin + BASE_PATH;
|
||||
|
||||
// Set document meta
|
||||
useDocumentMeta({
|
||||
title: `${t('backendStartup.title', 'Backend Starting')} - Stirling PDF`,
|
||||
description: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'),
|
||||
ogTitle: `${t('backendStartup.title', 'Backend Starting')} - Stirling PDF`,
|
||||
ogDescription: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'),
|
||||
ogImage: `${baseUrl}/og_images/home.png`,
|
||||
ogUrl: `${window.location.origin}${window.location.pathname}`
|
||||
});
|
||||
|
||||
// Poll backend to check if it's ready
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
let isMounted = true;
|
||||
|
||||
const checkBackendStatus = async () => {
|
||||
try {
|
||||
// Use a universal endpoint that works in both proprietary and non-proprietary builds
|
||||
// Try /api/v1/info/status first (works in both builds if metrics enabled)
|
||||
const response = await fetch(`${BASE_PATH}/api/v1/info/status`, {
|
||||
method: 'GET',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Backend is ready if we get valid status back
|
||||
if (data && data.status === 'UP') {
|
||||
console.log('[BackendStartup] Backend is ready, redirecting');
|
||||
const redirectTo = sessionStorage.getItem('backendStartupRedirect') || '/';
|
||||
sessionStorage.removeItem('backendStartupRedirect');
|
||||
|
||||
// Small delay to ensure backend is fully ready
|
||||
setTimeout(() => {
|
||||
if (isMounted) {
|
||||
navigate(redirectTo, { replace: true });
|
||||
}
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If status endpoint fails with 403 (metrics disabled), try proprietary endpoint
|
||||
if (response.status === 403) {
|
||||
console.debug('[BackendStartup] Metrics disabled, trying proprietary endpoint');
|
||||
const proprietaryResponse = await fetch(`${BASE_PATH}/api/v1/proprietary/ui-data/login`, {
|
||||
method: 'GET',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
if (proprietaryResponse.ok) {
|
||||
const data = await proprietaryResponse.json();
|
||||
if (data && data.enableLogin !== null) {
|
||||
console.log('[BackendStartup] Backend is ready (proprietary check), redirecting');
|
||||
const redirectTo = sessionStorage.getItem('backendStartupRedirect') || '/';
|
||||
sessionStorage.removeItem('backendStartupRedirect');
|
||||
|
||||
setTimeout(() => {
|
||||
if (isMounted) {
|
||||
navigate(redirectTo, { replace: true });
|
||||
}
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backend not ready yet, continue polling
|
||||
if (response.status === 404 || response.status === 503) {
|
||||
setPollAttempt(prev => prev + 1);
|
||||
setStatusMessage(`Backend not ready (${response.status})`);
|
||||
} else {
|
||||
setPollAttempt(prev => prev + 1);
|
||||
setStatusMessage(response.statusText || null);
|
||||
}
|
||||
|
||||
// Schedule next poll (exponential backoff, max 5 seconds)
|
||||
const delay = Math.min(1000 * Math.pow(1.5, pollAttempt), 5000);
|
||||
timeoutId = setTimeout(checkBackendStatus, delay);
|
||||
} catch (err) {
|
||||
// Network error or backend not responding
|
||||
if (!isMounted) return;
|
||||
|
||||
console.debug('[BackendStartup] Poll attempt failed:', err);
|
||||
setPollAttempt(prev => prev + 1);
|
||||
setStatusMessage(err instanceof Error ? err.message : null);
|
||||
|
||||
// Continue polling with backoff
|
||||
const delay = Math.min(1000 * Math.pow(1.5, pollAttempt), 5000);
|
||||
timeoutId = setTimeout(checkBackendStatus, delay);
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling after a short delay to avoid race conditions
|
||||
timeoutId = setTimeout(checkBackendStatus, 500);
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
}, [navigate, pollAttempt, BASE_PATH]);
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
{/* Header without title prop for custom styling */}
|
||||
<div className="login-header">
|
||||
<div className="login-header-logos">
|
||||
<img src={`${BASE_PATH}/branding/StirlingPDFLogoBlackText.svg`} alt="Stirling PDF" className="login-logo-text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backend loading message */}
|
||||
<div
|
||||
className="auth-section"
|
||||
style={{
|
||||
padding: '1.5rem',
|
||||
marginTop: '1rem',
|
||||
borderRadius: '0.75rem',
|
||||
backgroundColor: 'rgba(37, 99, 235, 0.08)',
|
||||
border: '1px solid rgba(37, 99, 235, 0.2)',
|
||||
}}
|
||||
>
|
||||
<p style={{ fontSize: '1.125rem', fontWeight: 600, marginBottom: '0.5rem', textAlign: 'center' }}>
|
||||
{t('backendStartup.loadingTitle', 'Backend starting up')}
|
||||
</p>
|
||||
<p style={{ margin: 0, textAlign: 'center', color: 'rgba(15, 23, 42, 0.8)' }}>
|
||||
{t('backendStartup.loadingMessage', 'The backend is currently starting up. This usually takes a few moments.')}
|
||||
</p>
|
||||
|
||||
{/* Loading indicator */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '1rem' }}>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
border: '3px solid rgba(37, 99, 235, 0.2)',
|
||||
borderTop: '3px solid rgba(37, 99, 235, 0.8)',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
{statusMessage && (
|
||||
<p style={{ marginTop: '0.75rem', fontSize: '0.875rem', textAlign: 'center', color: 'rgba(15, 23, 42, 0.6)' }}>
|
||||
{statusMessage}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{pollAttempt > 10 && (
|
||||
<p style={{ marginTop: '1rem', fontSize: '0.875rem', textAlign: 'center', color: 'rgba(15, 23, 42, 0.7)' }}>
|
||||
{t('backendStartup.takingLonger', 'This is taking longer than expected. The backend should be ready soon.')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add spinner animation */}
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import HomePage from '@app/pages/HomePage'
|
||||
// Login component is used via routing, not directly imported
|
||||
import FirstLoginModal from '@app/components/shared/FirstLoginModal'
|
||||
import { accountService } from '@app/services/accountService'
|
||||
import { BASE_PATH } from '@app/constants/app'
|
||||
|
||||
/**
|
||||
* Landing component - Smart router based on authentication status
|
||||
@@ -16,11 +17,12 @@ import { accountService } from '@app/services/accountService'
|
||||
*/
|
||||
export default function Landing() {
|
||||
const { session, loading: authLoading, refreshSession } = useAuth();
|
||||
const { config, loading: configLoading } = useAppConfig();
|
||||
const { config, loading: configLoading, error: configError } = useAppConfig();
|
||||
const location = useLocation();
|
||||
const [isFirstLogin, setIsFirstLogin] = useState(false);
|
||||
const [checkingFirstLogin, setCheckingFirstLogin] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
const [backendCheckFailed, setBackendCheckFailed] = useState(false);
|
||||
|
||||
const loading = authLoading || configLoading;
|
||||
|
||||
@@ -54,13 +56,64 @@ export default function Landing() {
|
||||
// The auth system will automatically redirect to login when session is null
|
||||
}
|
||||
|
||||
// If AppConfig failed to load, check if backend is down
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
// Only check if config has an error and we haven't already redirected
|
||||
if (configError && !backendCheckFailed && !configLoading) {
|
||||
console.debug('[Landing] Config error detected, checking backend status');
|
||||
|
||||
const checkBackend = async () => {
|
||||
try {
|
||||
const response = await fetch(`${BASE_PATH}/api/v1/info/status`, {
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
// If backend responds, it's up - continue normally
|
||||
if (response.ok) {
|
||||
console.debug('[Landing] Backend is up despite config error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Backend returned error - redirect to startup page
|
||||
console.debug('[Landing] Backend not ready, redirecting to startup page');
|
||||
setBackendCheckFailed(true);
|
||||
sessionStorage.setItem('backendStartupRedirect', location.pathname + location.search);
|
||||
} catch (err) {
|
||||
// Network error - backend is down
|
||||
if (!isMounted) return;
|
||||
|
||||
console.debug('[Landing] Backend unavailable (network error), redirecting to startup page');
|
||||
setBackendCheckFailed(true);
|
||||
sessionStorage.setItem('backendStartupRedirect', location.pathname + location.search);
|
||||
}
|
||||
};
|
||||
|
||||
checkBackend();
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [configError, configLoading, location, backendCheckFailed]);
|
||||
|
||||
console.log('[Landing] State:', {
|
||||
pathname: location.pathname,
|
||||
loading,
|
||||
hasSession: !!session,
|
||||
loginEnabled: config?.enableLogin,
|
||||
configError,
|
||||
backendCheckFailed,
|
||||
});
|
||||
|
||||
// Redirect to backend startup if backend check failed
|
||||
if (backendCheckFailed) {
|
||||
return <Navigate to="/backend-startup" replace />;
|
||||
}
|
||||
|
||||
// Show loading while checking auth and config
|
||||
if (loading || checkingFirstLogin) {
|
||||
return (
|
||||
@@ -76,8 +129,13 @@ export default function Landing() {
|
||||
}
|
||||
|
||||
// If login is disabled, show app directly (anonymous mode)
|
||||
if (config?.enableLogin === false) {
|
||||
console.debug('[Landing] Login disabled - showing app in anonymous mode');
|
||||
// Check both the config AND sessionStorage flag (in case config is wrong)
|
||||
const loginActuallyDisabled = sessionStorage.getItem('loginActuallyDisabled') === 'true';
|
||||
if (config?.enableLogin === false || loginActuallyDisabled) {
|
||||
console.debug('[Landing] Login disabled - showing app in anonymous mode', {
|
||||
configSays: config?.enableLogin,
|
||||
actuallyDisabled: loginActuallyDisabled
|
||||
});
|
||||
return <HomePage />;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { springAuth } from '@app/auth/springAuthClient';
|
||||
import { useAuth } from '@app/auth/UseSession';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDocumentMeta } from '@app/hooks/useDocumentMeta';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import AuthLayout from '@app/routes/authShared/AuthLayout';
|
||||
|
||||
// Import login components
|
||||
@@ -19,6 +20,7 @@ export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { session, loading } = useAuth();
|
||||
const { config, loading: configLoading } = useAppConfig();
|
||||
const { t } = useTranslation();
|
||||
const [isSigningIn, setIsSigningIn] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -29,37 +31,169 @@ export default function Login() {
|
||||
const [enabledProviders, setEnabledProviders] = useState<string[]>([]);
|
||||
const [hasSSOProviders, setHasSSOProviders] = useState(false);
|
||||
const [_enableLogin, setEnableLogin] = useState<boolean | null>(null);
|
||||
const [backendCheckFailed, setBackendCheckFailed] = useState(false);
|
||||
const [hasRedirected, setHasRedirected] = useState(false);
|
||||
|
||||
// Check AppConfig first - if login is disabled, redirect to home immediately
|
||||
useEffect(() => {
|
||||
if (!configLoading && config?.enableLogin === false && !hasRedirected) {
|
||||
console.debug('[Login] Config loaded, login disabled - redirecting to home');
|
||||
setHasRedirected(true);
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
}, [config, configLoading, navigate, hasRedirected]);
|
||||
|
||||
// Fetch enabled SSO providers and login config from backend
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
// Don't fetch if config is still loading or if we know login is disabled
|
||||
if (configLoading) {
|
||||
console.debug('[Login] Waiting for config to load before fetching providers');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config?.enableLogin === false) {
|
||||
console.debug('[Login] Login disabled per config, skipping provider fetch');
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchProviders = async () => {
|
||||
try {
|
||||
const response = await fetch(`${BASE_PATH}/api/v1/proprietary/ui-data/login`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const response = await fetch(`${BASE_PATH}/api/v1/proprietary/ui-data/login`, {
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
// Check if login is disabled - if so, redirect to home
|
||||
if (data.enableLogin === false) {
|
||||
console.debug('[Login] Login disabled, redirecting to home');
|
||||
navigate('/');
|
||||
if (!isMounted) return;
|
||||
|
||||
if (!response.ok) {
|
||||
// 404 likely means security is disabled (non-proprietary backend)
|
||||
// Check the general status endpoint to see if backend is up
|
||||
if (response.status === 404) {
|
||||
console.debug('[Login] Proprietary endpoint not found - checking if security is disabled (this is expected behavior when security is disabled)');
|
||||
|
||||
try {
|
||||
const statusResponse = await fetch(`${BASE_PATH}/api/v1/info/status`, {
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
if (statusResponse.ok) {
|
||||
// Backend is up, but security is disabled
|
||||
// This means the config is wrong - login is actually disabled
|
||||
console.debug('[Login] Backend is up but security disabled - redirecting to home');
|
||||
|
||||
// Set a flag in sessionStorage so Landing knows login is actually disabled
|
||||
sessionStorage.setItem('loginActuallyDisabled', 'true');
|
||||
|
||||
navigate('/', { replace: true });
|
||||
return;
|
||||
}
|
||||
} catch (statusErr) {
|
||||
console.debug('[Login] Status check failed, backend may be starting up');
|
||||
}
|
||||
|
||||
// Backend is not responding properly - redirect to startup page
|
||||
console.debug('[Login] Backend starting up - redirecting to backend startup page');
|
||||
setBackendCheckFailed(true);
|
||||
sessionStorage.setItem('backendStartupRedirect', window.location.pathname + window.location.search);
|
||||
|
||||
setTimeout(() => {
|
||||
if (isMounted) {
|
||||
navigate('/backend-startup', { replace: true });
|
||||
}
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
setEnableLogin(data.enableLogin ?? true);
|
||||
// 503 means backend is starting up
|
||||
if (response.status === 503) {
|
||||
console.warn('[Login] Backend unavailable (503) - redirecting to backend startup');
|
||||
setBackendCheckFailed(true);
|
||||
sessionStorage.setItem('backendStartupRedirect', window.location.pathname + window.location.search);
|
||||
|
||||
// Extract provider IDs from the providerList map
|
||||
// The keys are like "/oauth2/authorization/google" - extract the last part
|
||||
const providerIds = Object.keys(data.providerList || {})
|
||||
.map(key => key.split('/').pop())
|
||||
.filter((id): id is string => id !== undefined);
|
||||
setEnabledProviders(providerIds);
|
||||
setTimeout(() => {
|
||||
if (isMounted) {
|
||||
navigate('/backend-startup', { replace: true });
|
||||
}
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || `Failed to fetch login configuration (${response.status})`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
if (!data || data.enableLogin === null) {
|
||||
console.warn('[Login] Login config returned empty or null data - checking backend status');
|
||||
|
||||
// Check if backend is actually up before assuming it's starting
|
||||
try {
|
||||
const statusResponse = await fetch(`${BASE_PATH}/api/v1/info/status`, {
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
if (statusResponse.ok) {
|
||||
// Backend is up but returning invalid data - redirect to home as fallback
|
||||
console.debug('[Login] Backend up but invalid login data - redirecting to home');
|
||||
navigate('/', { replace: true });
|
||||
return;
|
||||
}
|
||||
} catch (statusErr) {
|
||||
console.debug('[Login] Status check failed');
|
||||
}
|
||||
|
||||
// Backend is not responding - redirect to startup page
|
||||
setBackendCheckFailed(true);
|
||||
sessionStorage.setItem('backendStartupRedirect', window.location.pathname + window.location.search);
|
||||
|
||||
setTimeout(() => {
|
||||
if (isMounted) {
|
||||
navigate('/backend-startup', { replace: true });
|
||||
}
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if login is disabled - if so, redirect to home
|
||||
if (data.enableLogin === false) {
|
||||
console.debug('[Login] Login disabled, redirecting to home');
|
||||
navigate('/', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
setEnableLogin(data.enableLogin ?? true);
|
||||
|
||||
// Extract provider IDs from the providerList map
|
||||
// The keys are like "/oauth2/authorization/google" - extract the last part
|
||||
const providerIds = Object.keys(data.providerList || {})
|
||||
.map(key => key.split('/').pop())
|
||||
.filter((id): id is string => id !== undefined);
|
||||
setEnabledProviders(providerIds);
|
||||
} catch (err) {
|
||||
if (!isMounted) return;
|
||||
|
||||
console.error('[Login] Failed to fetch enabled providers:', err);
|
||||
setBackendCheckFailed(true);
|
||||
sessionStorage.setItem('backendStartupRedirect', window.location.pathname + window.location.search);
|
||||
|
||||
setTimeout(() => {
|
||||
if (isMounted) {
|
||||
navigate('/backend-startup', { replace: true });
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProviders();
|
||||
}, [navigate]);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [navigate, BASE_PATH, config, configLoading]);
|
||||
|
||||
// Update hasSSOProviders and showEmailForm when enabledProviders changes
|
||||
useEffect(() => {
|
||||
@@ -124,6 +258,23 @@ export default function Login() {
|
||||
return <LoggedInState />;
|
||||
}
|
||||
|
||||
// Show loading while checking backend, config, or redirecting
|
||||
// Also show loading if we know login is disabled (we're about to redirect)
|
||||
if (loading || configLoading || backendCheckFailed || hasRedirected || (!configLoading && config?.enableLogin === false)) {
|
||||
return (
|
||||
<AuthLayout>
|
||||
<div style={{ minHeight: '300px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-3"></div>
|
||||
<div className="text-gray-600">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc') => {
|
||||
try {
|
||||
setIsSigningIn(true);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
|
||||
interface LoginHeaderProps {
|
||||
title: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function LoginHeader({ title, subtitle }: LoginHeaderProps) {
|
||||
<div className="login-header-logos">
|
||||
<img src={`${BASE_PATH}/branding/StirlingPDFLogoBlackText.svg`} alt="Stirling PDF" className="login-logo-text" />
|
||||
</div>
|
||||
<h1 className="login-title">{title}</h1>
|
||||
{title && <h1 className="login-title">{title}</h1>}
|
||||
{subtitle && (
|
||||
<p className="login-subtitle">{subtitle}</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user