SSO styling changes (#5671)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## 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.

---------

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
This commit is contained in:
Anthony Stirling 2026-02-06 22:00:42 +00:00 committed by GitHub
parent d135e25d02
commit 00a9174939
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 169 additions and 49 deletions

View File

@ -4,7 +4,7 @@ import { authService, UserInfo } from '@app/services/authService';
import { buildOAuthCallbackHtml } from '@app/utils/oauthCallbackHtml';
import { BASE_PATH } from '@app/constants/app';
import { STIRLING_SAAS_URL } from '@app/constants/connection';
import '@app/routes/authShared/auth.css';
import '@app/components/SetupWizard/desktopOAuth.css';
type KnownProviderId = 'google' | 'github' | 'keycloak' | 'azure' | 'apple' | 'oidc';
export type OAuthProviderId = KnownProviderId | string;
@ -95,8 +95,9 @@ export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
return null;
}
// Desktop always uses its own styling classes (independent of web)
return (
<div className="oauth-container-vertical">
<div className="oauth-container-vertical-desktop">
{providers
.filter((providerConfigEntry) => providerConfigEntry && providerConfigEntry.id)
.map((providerEntry) => {
@ -114,15 +115,19 @@ export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
key={providerEntry.id}
onClick={() => handleOAuthLogin(providerEntry)}
disabled={isDisabled || oauthLoading}
className="oauth-button-vertical"
className="oauth-button-vertical-desktop"
title={label}
>
<img
src={`${BASE_PATH}/Login/${iconConfig?.file || GENERIC_PROVIDER_ICON}`}
alt={label}
className="oauth-icon-tiny"
/>
{label}
<span className="oauth-button-left-desktop">
<span className="oauth-icon-wrapper-desktop">
<img
src={`${BASE_PATH}/Login/${iconConfig?.file || GENERIC_PROVIDER_ICON}`}
alt={label}
className="oauth-icon-tiny-desktop"
/>
</span>
<span className="oauth-button-text-desktop">{label}</span>
</span>
</button>
);
})}

View File

@ -13,6 +13,7 @@ import '@app/routes/authShared/auth.css';
interface SelfHostedLoginScreenProps {
serverUrl: string;
enabledOAuthProviders?: SSOProviderConfig[];
loginMethod?: string;
onLogin: (username: string, password: string) => Promise<void>;
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
mfaCode: string;
@ -25,6 +26,7 @@ interface SelfHostedLoginScreenProps {
export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
serverUrl,
enabledOAuthProviders,
loginMethod = 'all',
onLogin,
onOAuthSuccess,
mfaCode,
@ -38,6 +40,9 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
const [password, setPassword] = useState('');
const [validationError, setValidationError] = useState<string | null>(null);
// Check if username/password authentication is allowed
const isUserPassAllowed = loginMethod === 'all' || loginMethod === 'normal';
const handleSubmit = async () => {
// Validation
if (!username.trim()) {
@ -69,7 +74,7 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
<>
<LoginHeader
title={t('setup.selfhosted.title', 'Sign in to Server')}
subtitle={t('setup.selfhosted.subtitle', 'Enter your server credentials')}
subtitle={isUserPassAllowed ? t('setup.selfhosted.subtitle', 'Enter your server credentials') : undefined}
/>
<ErrorMessage error={displayError} />
@ -90,36 +95,42 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
providers={enabledOAuthProviders}
/>
<DividerWithText
text={t('setup.login.orContinueWith', 'Or continue with email')}
respondsToDarkMode={false}
opacity={0.4}
/>
{/* Only show divider if username/password auth is also allowed */}
{isUserPassAllowed && (
<DividerWithText
text={t('setup.login.orContinueWith', 'Or continue with email')}
respondsToDarkMode={false}
opacity={0.4}
/>
)}
</>
)}
<EmailPasswordForm
email={username}
password={password}
setEmail={(value) => {
setUsername(value);
setValidationError(null);
}}
setPassword={(value) => {
setPassword(value);
setValidationError(null);
}}
mfaCode={mfaCode}
setMfaCode={(value) => {
setMfaCode(value);
setValidationError(null);
}}
showMfaField={requiresMfa || Boolean(mfaCode)}
requiresMfa={requiresMfa}
onSubmit={handleSubmit}
isSubmitting={loading}
submitButtonText={t('setup.login.submit', 'Login')}
/>
{/* Only show email/password form if username/password auth is allowed */}
{isUserPassAllowed && (
<EmailPasswordForm
email={username}
password={password}
setEmail={(value) => {
setUsername(value);
setValidationError(null);
}}
setPassword={(value) => {
setPassword(value);
setValidationError(null);
}}
mfaCode={mfaCode}
setMfaCode={(value) => {
setMfaCode(value);
setValidationError(null);
}}
showMfaField={requiresMfa || Boolean(mfaCode)}
requiresMfa={requiresMfa}
onSubmit={handleSubmit}
isSubmitting={loading}
submitButtonText={t('setup.login.submit', 'Login')}
/>
)}
</>
);
};

View File

@ -73,6 +73,7 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
// Fetch OAuth providers and check if login is enabled
const enabledProviders: SSOProviderConfig[] = [];
let loginMethod = 'all'; // Default to 'all' (allows both SSO and username/password)
try {
console.log('[ServerSelection] Fetching login configuration...');
const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`);
@ -108,6 +109,10 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
return;
}
// Extract loginMethod from response
loginMethod = data.loginMethod || 'all';
console.log('[ServerSelection] Login method:', loginMethod);
// Extract provider IDs from authorization URLs
// Example: "/oauth2/authorization/google" → "google"
const providerEntries = Object.entries(data.providerList || {});
@ -149,11 +154,12 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
return;
}
// Connection successful - pass URL and OAuth providers
// Connection successful - pass URL, OAuth providers, and login method
console.log('[ServerSelection] ✅ Server selection complete, proceeding to login');
onSelect({
url,
enabledOAuthProviders: enabledProviders.length > 0 ? enabledProviders : undefined,
loginMethod,
});
} catch (error) {
console.error('[ServerSelection] ❌ Unexpected error during connection test:', error);

View File

@ -0,0 +1,96 @@
/* Desktop-specific OAuth button styles for self-hosted server connections */
/* These styles are isolated from the web SSO buttons to prevent conflicts */
.oauth-container-vertical-desktop {
display: flex;
flex-direction: column;
gap: 0.875rem; /* 14px */
align-items: stretch;
}
.oauth-button-vertical-desktop {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0.75rem 1rem; /* 12px 16px */
border: 1px solid #d1d5db;
border-radius: 0.75rem; /* 12px */
background-color: var(--auth-card-bg-light-only);
font-size: 1rem; /* 16px */
font-weight: 500;
color: var(--auth-text-primary-light-only);
cursor: pointer;
gap: 0.75rem; /* 12px */
font-family: inherit;
transition: background-color 0.2s ease;
}
.oauth-button-vertical-desktop:hover:not(:disabled) {
background-color: #f3f4f6;
}
.oauth-button-vertical-desktop:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.oauth-button-vertical-desktop:focus-visible {
outline: 2px solid var(--auth-border-focus-light-only);
outline-offset: 2px;
}
/* Fix Mantine Button internal spans */
.oauth-button-vertical-desktop .mantine-Button-inner {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
}
.oauth-button-vertical-desktop .mantine-Button-label {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.75rem;
overflow: visible;
}
.oauth-button-left-desktop {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
flex: 1 1 auto;
}
.oauth-button-text-desktop {
font-size: 1rem;
font-weight: 500;
color: inherit;
line-height: 1.2;
display: inline-flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.oauth-icon-wrapper-desktop {
width: 1.25rem; /* 20px */
height: 1.25rem; /* 20px */
border-radius: 0;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
}
.oauth-icon-tiny-desktop {
width: 1.25rem; /* 20px */
height: 1.25rem; /* 20px */
display: block;
flex-shrink: 0;
}

View File

@ -327,6 +327,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
<SelfHostedLoginScreen
serverUrl={serverConfig?.url || ''}
enabledOAuthProviders={serverConfig?.enabledOAuthProviders}
loginMethod={serverConfig?.loginMethod}
onLogin={handleSelfHostedLogin}
onOAuthSuccess={handleSelfHostedOAuthSuccess}
mfaCode={selfHostedMfaCode}

View File

@ -12,6 +12,7 @@ export interface SSOProviderConfig {
export interface ServerConfig {
url: string;
enabledOAuthProviders?: SSOProviderConfig[];
loginMethod?: string;
}
export interface ConnectionConfig {

View File

@ -193,24 +193,24 @@
align-items: center;
justify-content: space-between;
padding: 0.875rem 1.5rem; /* 14px 24px */
border: none;
border: 1px solid #e5e7eb;
border-radius: 999px;
background: #0f172a;
background-color: #ffffff;
font-size: 1rem; /* 16px */
font-weight: 600;
color: #f8fafc;
color: #1f2937;
cursor: pointer;
gap: 1rem; /* 16px */
font-family: inherit;
box-shadow: 0 0.4rem 1rem rgba(15, 23, 42, 0.25);
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.08);
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
}
.oauth-button-vertical:hover:not(:disabled) {
background: #111827;
box-shadow: 0 0.55rem 1.25rem rgba(15, 23, 42, 0.3);
background-color: #f9fafb;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.12);
transform: translateY(-1px);
color: #f8fafc;
border-color: #d1d5db;
}
.oauth-button-vertical-tinted {
@ -295,7 +295,7 @@
}
.oauth-button-vertical:focus-visible {
outline: 3px solid rgba(59, 130, 246, 0.6);
outline: 3px solid rgba(59, 130, 246, 0.5);
outline-offset: 2px;
}
@ -367,8 +367,8 @@
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
background: #f3f4f6;
border: 1px solid #e5e7eb;
}
.oauth-button-vertical-tinted .oauth-icon-wrapper {