mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
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:
co-authored by
stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
parent
d135e25d02
commit
00a9174939
@@ -4,7 +4,7 @@ import { authService, UserInfo } from '@app/services/authService';
|
|||||||
import { buildOAuthCallbackHtml } from '@app/utils/oauthCallbackHtml';
|
import { buildOAuthCallbackHtml } from '@app/utils/oauthCallbackHtml';
|
||||||
import { BASE_PATH } from '@app/constants/app';
|
import { BASE_PATH } from '@app/constants/app';
|
||||||
import { STIRLING_SAAS_URL } from '@app/constants/connection';
|
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';
|
type KnownProviderId = 'google' | 'github' | 'keycloak' | 'azure' | 'apple' | 'oidc';
|
||||||
export type OAuthProviderId = KnownProviderId | string;
|
export type OAuthProviderId = KnownProviderId | string;
|
||||||
@@ -95,8 +95,9 @@ export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Desktop always uses its own styling classes (independent of web)
|
||||||
return (
|
return (
|
||||||
<div className="oauth-container-vertical">
|
<div className="oauth-container-vertical-desktop">
|
||||||
{providers
|
{providers
|
||||||
.filter((providerConfigEntry) => providerConfigEntry && providerConfigEntry.id)
|
.filter((providerConfigEntry) => providerConfigEntry && providerConfigEntry.id)
|
||||||
.map((providerEntry) => {
|
.map((providerEntry) => {
|
||||||
@@ -114,15 +115,19 @@ export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
|
|||||||
key={providerEntry.id}
|
key={providerEntry.id}
|
||||||
onClick={() => handleOAuthLogin(providerEntry)}
|
onClick={() => handleOAuthLogin(providerEntry)}
|
||||||
disabled={isDisabled || oauthLoading}
|
disabled={isDisabled || oauthLoading}
|
||||||
className="oauth-button-vertical"
|
className="oauth-button-vertical-desktop"
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
<img
|
<span className="oauth-button-left-desktop">
|
||||||
src={`${BASE_PATH}/Login/${iconConfig?.file || GENERIC_PROVIDER_ICON}`}
|
<span className="oauth-icon-wrapper-desktop">
|
||||||
alt={label}
|
<img
|
||||||
className="oauth-icon-tiny"
|
src={`${BASE_PATH}/Login/${iconConfig?.file || GENERIC_PROVIDER_ICON}`}
|
||||||
/>
|
alt={label}
|
||||||
{label}
|
className="oauth-icon-tiny-desktop"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="oauth-button-text-desktop">{label}</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import '@app/routes/authShared/auth.css';
|
|||||||
interface SelfHostedLoginScreenProps {
|
interface SelfHostedLoginScreenProps {
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
enabledOAuthProviders?: SSOProviderConfig[];
|
enabledOAuthProviders?: SSOProviderConfig[];
|
||||||
|
loginMethod?: string;
|
||||||
onLogin: (username: string, password: string) => Promise<void>;
|
onLogin: (username: string, password: string) => Promise<void>;
|
||||||
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
|
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
|
||||||
mfaCode: string;
|
mfaCode: string;
|
||||||
@@ -25,6 +26,7 @@ interface SelfHostedLoginScreenProps {
|
|||||||
export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
||||||
serverUrl,
|
serverUrl,
|
||||||
enabledOAuthProviders,
|
enabledOAuthProviders,
|
||||||
|
loginMethod = 'all',
|
||||||
onLogin,
|
onLogin,
|
||||||
onOAuthSuccess,
|
onOAuthSuccess,
|
||||||
mfaCode,
|
mfaCode,
|
||||||
@@ -38,6 +40,9 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [validationError, setValidationError] = useState<string | null>(null);
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Check if username/password authentication is allowed
|
||||||
|
const isUserPassAllowed = loginMethod === 'all' || loginMethod === 'normal';
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// Validation
|
// Validation
|
||||||
if (!username.trim()) {
|
if (!username.trim()) {
|
||||||
@@ -69,7 +74,7 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
|||||||
<>
|
<>
|
||||||
<LoginHeader
|
<LoginHeader
|
||||||
title={t('setup.selfhosted.title', 'Sign in to Server')}
|
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} />
|
<ErrorMessage error={displayError} />
|
||||||
@@ -90,36 +95,42 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
|||||||
providers={enabledOAuthProviders}
|
providers={enabledOAuthProviders}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DividerWithText
|
{/* Only show divider if username/password auth is also allowed */}
|
||||||
text={t('setup.login.orContinueWith', 'Or continue with email')}
|
{isUserPassAllowed && (
|
||||||
respondsToDarkMode={false}
|
<DividerWithText
|
||||||
opacity={0.4}
|
text={t('setup.login.orContinueWith', 'Or continue with email')}
|
||||||
/>
|
respondsToDarkMode={false}
|
||||||
|
opacity={0.4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<EmailPasswordForm
|
{/* Only show email/password form if username/password auth is allowed */}
|
||||||
email={username}
|
{isUserPassAllowed && (
|
||||||
password={password}
|
<EmailPasswordForm
|
||||||
setEmail={(value) => {
|
email={username}
|
||||||
setUsername(value);
|
password={password}
|
||||||
setValidationError(null);
|
setEmail={(value) => {
|
||||||
}}
|
setUsername(value);
|
||||||
setPassword={(value) => {
|
setValidationError(null);
|
||||||
setPassword(value);
|
}}
|
||||||
setValidationError(null);
|
setPassword={(value) => {
|
||||||
}}
|
setPassword(value);
|
||||||
mfaCode={mfaCode}
|
setValidationError(null);
|
||||||
setMfaCode={(value) => {
|
}}
|
||||||
setMfaCode(value);
|
mfaCode={mfaCode}
|
||||||
setValidationError(null);
|
setMfaCode={(value) => {
|
||||||
}}
|
setMfaCode(value);
|
||||||
showMfaField={requiresMfa || Boolean(mfaCode)}
|
setValidationError(null);
|
||||||
requiresMfa={requiresMfa}
|
}}
|
||||||
onSubmit={handleSubmit}
|
showMfaField={requiresMfa || Boolean(mfaCode)}
|
||||||
isSubmitting={loading}
|
requiresMfa={requiresMfa}
|
||||||
submitButtonText={t('setup.login.submit', 'Login')}
|
onSubmit={handleSubmit}
|
||||||
/>
|
isSubmitting={loading}
|
||||||
|
submitButtonText={t('setup.login.submit', 'Login')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
|
|||||||
|
|
||||||
// Fetch OAuth providers and check if login is enabled
|
// Fetch OAuth providers and check if login is enabled
|
||||||
const enabledProviders: SSOProviderConfig[] = [];
|
const enabledProviders: SSOProviderConfig[] = [];
|
||||||
|
let loginMethod = 'all'; // Default to 'all' (allows both SSO and username/password)
|
||||||
try {
|
try {
|
||||||
console.log('[ServerSelection] Fetching login configuration...');
|
console.log('[ServerSelection] Fetching login configuration...');
|
||||||
const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`);
|
const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`);
|
||||||
@@ -108,6 +109,10 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract loginMethod from response
|
||||||
|
loginMethod = data.loginMethod || 'all';
|
||||||
|
console.log('[ServerSelection] Login method:', loginMethod);
|
||||||
|
|
||||||
// Extract provider IDs from authorization URLs
|
// Extract provider IDs from authorization URLs
|
||||||
// Example: "/oauth2/authorization/google" → "google"
|
// Example: "/oauth2/authorization/google" → "google"
|
||||||
const providerEntries = Object.entries(data.providerList || {});
|
const providerEntries = Object.entries(data.providerList || {});
|
||||||
@@ -149,11 +154,12 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
|
|||||||
return;
|
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');
|
console.log('[ServerSelection] ✅ Server selection complete, proceeding to login');
|
||||||
onSelect({
|
onSelect({
|
||||||
url,
|
url,
|
||||||
enabledOAuthProviders: enabledProviders.length > 0 ? enabledProviders : undefined,
|
enabledOAuthProviders: enabledProviders.length > 0 ? enabledProviders : undefined,
|
||||||
|
loginMethod,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ServerSelection] ❌ Unexpected error during connection test:', error);
|
console.error('[ServerSelection] ❌ Unexpected error during connection test:', error);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -327,6 +327,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|||||||
<SelfHostedLoginScreen
|
<SelfHostedLoginScreen
|
||||||
serverUrl={serverConfig?.url || ''}
|
serverUrl={serverConfig?.url || ''}
|
||||||
enabledOAuthProviders={serverConfig?.enabledOAuthProviders}
|
enabledOAuthProviders={serverConfig?.enabledOAuthProviders}
|
||||||
|
loginMethod={serverConfig?.loginMethod}
|
||||||
onLogin={handleSelfHostedLogin}
|
onLogin={handleSelfHostedLogin}
|
||||||
onOAuthSuccess={handleSelfHostedOAuthSuccess}
|
onOAuthSuccess={handleSelfHostedOAuthSuccess}
|
||||||
mfaCode={selfHostedMfaCode}
|
mfaCode={selfHostedMfaCode}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface SSOProviderConfig {
|
|||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
url: string;
|
url: string;
|
||||||
enabledOAuthProviders?: SSOProviderConfig[];
|
enabledOAuthProviders?: SSOProviderConfig[];
|
||||||
|
loginMethod?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectionConfig {
|
export interface ConnectionConfig {
|
||||||
|
|||||||
@@ -193,24 +193,24 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.875rem 1.5rem; /* 14px 24px */
|
padding: 0.875rem 1.5rem; /* 14px 24px */
|
||||||
border: none;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #0f172a;
|
background-color: #ffffff;
|
||||||
font-size: 1rem; /* 16px */
|
font-size: 1rem; /* 16px */
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #f8fafc;
|
color: #1f2937;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
gap: 1rem; /* 16px */
|
gap: 1rem; /* 16px */
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
box-shadow: 0 0.4rem 1rem rgba(15, 23, 42, 0.25);
|
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;
|
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) {
|
.oauth-button-vertical:hover:not(:disabled) {
|
||||||
background: #111827;
|
background-color: #f9fafb;
|
||||||
box-shadow: 0 0.55rem 1.25rem rgba(15, 23, 42, 0.3);
|
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.12);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
color: #f8fafc;
|
border-color: #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oauth-button-vertical-tinted {
|
.oauth-button-vertical-tinted {
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.oauth-button-vertical:focus-visible {
|
.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;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,8 +367,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(255, 255, 255, 0.12);
|
background: #f3f4f6;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oauth-button-vertical-tinted .oauth-icon-wrapper {
|
.oauth-button-vertical-tinted .oauth-icon-wrapper {
|
||||||
|
|||||||
Reference in New Issue
Block a user