mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-09-13 20:25:28 +02:00
Settings display demo and login fix (#4884)
# Description of Changes <img width="1569" height="980" alt="image" src="https://github.com/user-attachments/assets/dca1c227-ed84-4393-97a1-e3ce6eb1620b" /> <img width="1596" height="935" alt="image" src="https://github.com/user-attachments/assets/2003e1be-034a-4cbb-869e-6d5d912ab61d" /> <img width="1543" height="997" alt="image" src="https://github.com/user-attachments/assets/fe0c4f4b-eeee-4db4-a041-e554f350255a" /> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -3,7 +3,7 @@ import { Navigate, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '@app/auth/UseSession'
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext'
|
||||
import HomePage from '@app/pages/HomePage'
|
||||
import Login from '@app/routes/Login'
|
||||
// Login component is used via routing, not directly imported
|
||||
import FirstLoginModal from '@app/components/shared/FirstLoginModal'
|
||||
import { accountService } from '@app/services/accountService'
|
||||
|
||||
@@ -95,13 +95,7 @@ export default function Landing() {
|
||||
);
|
||||
}
|
||||
|
||||
// If we're at home route ("/"), show login directly (marketing/landing page)
|
||||
// Otherwise navigate to login (fixes URL mismatch for tool routes)
|
||||
const isHome = location.pathname === '/' || location.pathname === '';
|
||||
if (isHome) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
// For non-home routes without auth, navigate to login (preserves from location)
|
||||
// No session - redirect to login page
|
||||
// This ensures the URL always shows /login when not authenticated
|
||||
return <Navigate to="/login" replace state={{ from: location }} />;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import AuthLayout from '@app/routes/authShared/AuthLayout';
|
||||
import LoginHeader from '@app/routes/login/LoginHeader';
|
||||
import ErrorMessage from '@app/routes/login/ErrorMessage';
|
||||
import EmailPasswordForm from '@app/routes/login/EmailPasswordForm';
|
||||
import OAuthButtons from '@app/routes/login/OAuthButtons';
|
||||
import OAuthButtons, { DEBUG_SHOW_ALL_PROVIDERS, oauthProviderConfig } from '@app/routes/login/OAuthButtons';
|
||||
import DividerWithText from '@app/components/shared/DividerWithText';
|
||||
import LoggedInState from '@app/routes/login/LoggedInState';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
@@ -26,8 +26,55 @@ export default function Login() {
|
||||
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [enabledProviders, setEnabledProviders] = useState<string[]>([]);
|
||||
const [hasSSOProviders, setHasSSOProviders] = useState(false);
|
||||
const [_enableLogin, setEnableLogin] = useState<boolean | null>(null);
|
||||
|
||||
// Handle query params (email prefill and success messages)
|
||||
// Fetch enabled SSO providers and login config from backend
|
||||
useEffect(() => {
|
||||
const fetchProviders = async () => {
|
||||
try {
|
||||
const response = await fetch(`${BASE_PATH}/api/v1/proprietary/ui-data/login`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Check if login is disabled - if so, redirect to home
|
||||
if (data.enableLogin === false) {
|
||||
console.debug('[Login] Login disabled, redirecting to home');
|
||||
navigate('/');
|
||||
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) {
|
||||
console.error('[Login] Failed to fetch enabled providers:', err);
|
||||
}
|
||||
};
|
||||
fetchProviders();
|
||||
}, [navigate]);
|
||||
|
||||
// Update hasSSOProviders and showEmailForm when enabledProviders changes
|
||||
useEffect(() => {
|
||||
// In debug mode, check if any providers exist in the config
|
||||
const hasProviders = DEBUG_SHOW_ALL_PROVIDERS
|
||||
? Object.keys(oauthProviderConfig).length > 0
|
||||
: enabledProviders.length > 0;
|
||||
setHasSSOProviders(hasProviders);
|
||||
// If no SSO providers, show email form by default
|
||||
if (!hasProviders) {
|
||||
setShowEmailForm(true);
|
||||
}
|
||||
}, [enabledProviders]);
|
||||
|
||||
// Handle query params (email prefill, success messages, and session expiry)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const emailFromQuery = searchParams.get('email');
|
||||
@@ -35,6 +82,12 @@ export default function Login() {
|
||||
setEmail(emailFromQuery);
|
||||
}
|
||||
|
||||
// Check if session expired (401 redirect)
|
||||
const expired = searchParams.get('expired');
|
||||
if (expired === 'true') {
|
||||
setError(t('login.sessionExpired', 'Your session has expired. Please sign in again.'));
|
||||
}
|
||||
|
||||
const messageType = searchParams.get('messageType')
|
||||
if (messageType) {
|
||||
switch (messageType) {
|
||||
@@ -71,7 +124,7 @@ export default function Login() {
|
||||
return <LoggedInState />;
|
||||
}
|
||||
|
||||
const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure') => {
|
||||
const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc') => {
|
||||
try {
|
||||
setIsSigningIn(true);
|
||||
setError(null);
|
||||
@@ -129,9 +182,10 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPassword = () => {
|
||||
navigate('/auth/reset');
|
||||
};
|
||||
// Forgot password handler (currently unused, reserved for future implementation)
|
||||
// const handleForgotPassword = () => {
|
||||
// navigate('/auth/reset');
|
||||
// };
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
@@ -160,25 +214,31 @@ export default function Login() {
|
||||
onProviderClick={signInWithProvider}
|
||||
isSubmitting={isSigningIn}
|
||||
layout="vertical"
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
|
||||
{/* Divider between OAuth and Email */}
|
||||
<DividerWithText text={t('signup.or', 'or')} respondsToDarkMode={false} opacity={0.4} />
|
||||
{/* Divider between OAuth and Email - only show if SSO is available */}
|
||||
{hasSSOProviders && (
|
||||
<DividerWithText text={t('signup.or', 'or')} respondsToDarkMode={false} opacity={0.4} />
|
||||
)}
|
||||
|
||||
{/* Sign in with email button (primary color to match signup CTA) */}
|
||||
<div className="auth-section">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmailForm(true)}
|
||||
disabled={isSigningIn}
|
||||
className="w-full px-4 py-[0.75rem] rounded-[0.625rem] text-base font-semibold mb-2 cursor-pointer border-0 disabled:opacity-50 disabled:cursor-not-allowed auth-cta-button"
|
||||
>
|
||||
{t('login.useEmailInstead', 'Login with email')}
|
||||
</button>
|
||||
</div>
|
||||
{/* Sign in with email button - only show if SSO providers exist */}
|
||||
{hasSSOProviders && !showEmailForm && (
|
||||
<div className="auth-section">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmailForm(true)}
|
||||
disabled={isSigningIn}
|
||||
className="w-full px-4 py-[0.75rem] rounded-[0.625rem] text-base font-semibold mb-2 cursor-pointer border-0 disabled:opacity-50 disabled:cursor-not-allowed auth-cta-button"
|
||||
>
|
||||
{t('login.useEmailInstead', 'Login with email')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email form - show by default if no SSO, or when button clicked */}
|
||||
{showEmailForm && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<div style={{ marginTop: hasSSOProviders ? '1rem' : '0' }}>
|
||||
<EmailPasswordForm
|
||||
email={email}
|
||||
password={password}
|
||||
@@ -191,31 +251,6 @@ export default function Login() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEmailForm && (
|
||||
<div className="auth-section-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleForgotPassword}
|
||||
className="auth-link-black"
|
||||
>
|
||||
{t('login.forgotPassword', 'Forgot your password?')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider then signup link */}
|
||||
<DividerWithText text={t('signup.or', 'or')} respondsToDarkMode={false} opacity={0.4} />
|
||||
|
||||
<div style={{ textAlign: 'center', margin: '0.5rem 0 0.25rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/signup')}
|
||||
className="auth-link-black"
|
||||
>
|
||||
{t('signup.signUp', 'Sign up')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
}
|
||||
|
||||
.login-logo-text {
|
||||
height: 1.5rem; /* 24px */
|
||||
height: 2rem; /* 32px - increased from 24px */
|
||||
}
|
||||
|
||||
.login-title {
|
||||
|
||||
@@ -2,11 +2,13 @@ import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@app/auth/UseSession';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLogoPath } from '@app/hooks/useLogoPath';
|
||||
|
||||
export default function LoggedInState() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const logoPath = useLogoPath();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -34,7 +36,13 @@ export default function LoggedInState() {
|
||||
padding: '32px'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>✅</div>
|
||||
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'center' }}>
|
||||
<img
|
||||
src={logoPath}
|
||||
alt="Stirling PDF Logo"
|
||||
style={{ width: '64px', height: '64px', objectFit: 'contain' }}
|
||||
/>
|
||||
</div>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: 'bold', color: '#059669', marginBottom: '8px' }}>
|
||||
{t('login.youAreLoggedIn')}
|
||||
</h1>
|
||||
|
||||
@@ -10,7 +10,6 @@ export default function LoginHeader({ title, subtitle }: LoginHeaderProps) {
|
||||
return (
|
||||
<div className="login-header">
|
||||
<div className="login-header-logos">
|
||||
<img src={`${BASE_PATH}/logo192.png`} alt="Logo" className="login-logo-icon" />
|
||||
<img src={`${BASE_PATH}/branding/StirlingPDFLogoBlackText.svg`} alt="Stirling PDF" className="login-logo-text" />
|
||||
</div>
|
||||
<h1 className="login-title">{title}</h1>
|
||||
|
||||
@@ -1,30 +1,54 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
|
||||
// OAuth provider configuration
|
||||
const oauthProviders = [
|
||||
{ id: 'google', label: 'Google', file: 'google.svg', isDisabled: false },
|
||||
{ id: 'github', label: 'GitHub', file: 'github.svg', isDisabled: false },
|
||||
{ id: 'apple', label: 'Apple', file: 'apple.svg', isDisabled: true },
|
||||
{ id: 'azure', label: 'Microsoft', file: 'microsoft.svg', isDisabled: true }
|
||||
];
|
||||
// Debug flag to show all providers for UI testing
|
||||
// Set to true to see all SSO options regardless of backend configuration
|
||||
export const DEBUG_SHOW_ALL_PROVIDERS = false;
|
||||
|
||||
// OAuth provider configuration - maps provider ID to display info
|
||||
export const oauthProviderConfig = {
|
||||
google: { label: 'Google', file: 'google.svg' },
|
||||
github: { label: 'GitHub', file: 'github.svg' },
|
||||
apple: { label: 'Apple', file: 'apple.svg' },
|
||||
azure: { label: 'Microsoft', file: 'microsoft.svg' },
|
||||
// microsoft and azure are the same, keycloak and oidc need their own icons
|
||||
// These are commented out from debug view since they need proper icons or backend doesn't use them
|
||||
// keycloak: { label: 'Keycloak', file: 'keycloak.svg' },
|
||||
// oidc: { label: 'OIDC', file: 'oidc.svg' }
|
||||
};
|
||||
|
||||
interface OAuthButtonsProps {
|
||||
onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure') => void
|
||||
onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc') => void
|
||||
isSubmitting: boolean
|
||||
layout?: 'vertical' | 'grid' | 'icons'
|
||||
enabledProviders?: string[] // List of enabled provider IDs from backend
|
||||
}
|
||||
|
||||
export default function OAuthButtons({ onProviderClick, isSubmitting, layout = 'vertical' }: OAuthButtonsProps) {
|
||||
export default function OAuthButtons({ onProviderClick, isSubmitting, layout = 'vertical', enabledProviders = [] }: OAuthButtonsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Filter out disabled providers - don't show them at all
|
||||
const enabledProviders = oauthProviders.filter(p => !p.isDisabled);
|
||||
// Debug mode: show all providers for UI testing
|
||||
const providersToShow = DEBUG_SHOW_ALL_PROVIDERS
|
||||
? Object.keys(oauthProviderConfig)
|
||||
: enabledProviders;
|
||||
|
||||
// Filter to only show enabled providers from backend
|
||||
const providers = providersToShow
|
||||
.filter(id => id in oauthProviderConfig)
|
||||
.map(id => ({
|
||||
id,
|
||||
...oauthProviderConfig[id as keyof typeof oauthProviderConfig]
|
||||
}));
|
||||
|
||||
// If no providers are enabled, don't render anything
|
||||
if (providers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (layout === 'icons') {
|
||||
return (
|
||||
<div className="oauth-container-icons">
|
||||
{enabledProviders.map((p) => (
|
||||
{providers.map((p) => (
|
||||
<div key={p.id} title={`${t('login.signInWith', 'Sign in with')} ${p.label}`}>
|
||||
<button
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
@@ -43,7 +67,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
||||
if (layout === 'grid') {
|
||||
return (
|
||||
<div className="oauth-container-grid">
|
||||
{enabledProviders.map((p) => (
|
||||
{providers.map((p) => (
|
||||
<div key={p.id} title={`${t('login.signInWith', 'Sign in with')} ${p.label}`}>
|
||||
<button
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
@@ -61,7 +85,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
||||
|
||||
return (
|
||||
<div className="oauth-container-vertical">
|
||||
{enabledProviders.map((p) => (
|
||||
{providers.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
|
||||
Reference in New Issue
Block a user