mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
OAuth Provider Buttons (#5103)
PR to allow other OAuth providers to be displayed on the login screen. Will show a generic icon when the provider is not in the known set of providers. <img width="424" height="205" alt="47ab288dadbc889fd84cc83c9ded0829" src="https://github.com/user-attachments/assets/2877eb3d-2ade-406f-a2bf-dc404793e30f" /> --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ludy <Ludy87@users.noreply.github.com> Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Co-authored-by: Ethan <ethan@MacBook-Pro.local> Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
This commit is contained in:
parent
65a3eeca76
commit
f902e8aca9
5
frontend/public/Login/authentik.svg
Normal file
5
frontend/public/Login/authentik.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.9 KiB |
5
frontend/public/Login/cloudron.svg
Normal file
5
frontend/public/Login/cloudron.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.5 KiB |
1
frontend/public/Login/keycloak.svg
Normal file
1
frontend/public/Login/keycloak.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.4 KiB |
6
frontend/public/Login/oidc.svg
Normal file
6
frontend/public/Login/oidc.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
@ -1,6 +1,40 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock localStorage for tests
|
||||
class LocalStorageMock implements Storage {
|
||||
private store: Record<string, string> = {};
|
||||
|
||||
get length(): number {
|
||||
return Object.keys(this.store).length;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
getItem(key: string): string | null {
|
||||
return this.store[key] ?? null;
|
||||
}
|
||||
|
||||
key(index: number): string | null {
|
||||
return Object.keys(this.store)[index] ?? null;
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
delete this.store[key];
|
||||
}
|
||||
|
||||
setItem(key: string, value: string): void {
|
||||
this.store[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: new LocalStorageMock(),
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Mock i18next for tests
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
|
||||
@ -107,6 +107,7 @@ export default function Login() {
|
||||
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);
|
||||
@ -225,16 +226,25 @@ export default function Login() {
|
||||
);
|
||||
}
|
||||
|
||||
const signInWithProvider = async (provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc') => {
|
||||
// Known OAuth providers that have dedicated backend support
|
||||
const KNOWN_OAUTH_PROVIDERS = ['github', 'google', 'apple', 'azure', 'keycloak', 'oidc'] as const;
|
||||
type KnownOAuthProvider = typeof KNOWN_OAUTH_PROVIDERS[number];
|
||||
|
||||
const signInWithProvider = async (provider: string) => {
|
||||
try {
|
||||
setIsSigningIn(true);
|
||||
setError(null);
|
||||
|
||||
console.log(`[Login] Signing in with ${provider}`);
|
||||
// Map unknown providers to 'oidc' for the backend redirect
|
||||
const backendProvider: KnownOAuthProvider = KNOWN_OAUTH_PROVIDERS.includes(provider as KnownOAuthProvider)
|
||||
? (provider as KnownOAuthProvider)
|
||||
: 'oidc';
|
||||
|
||||
console.log(`[Login] Signing in with ${provider} (backend: ${backendProvider})`);
|
||||
|
||||
// Redirect to Spring OAuth2 endpoint
|
||||
const { error } = await springAuth.signInWithOAuth({
|
||||
provider,
|
||||
provider: backendProvider,
|
||||
options: { redirectTo: `${BASE_PATH}/auth/callback` }
|
||||
});
|
||||
|
||||
|
||||
@ -6,19 +6,23 @@ import { BASE_PATH } from '@app/constants/app';
|
||||
export const DEBUG_SHOW_ALL_PROVIDERS = false;
|
||||
|
||||
// OAuth provider configuration - maps provider ID to display info
|
||||
export const oauthProviderConfig = {
|
||||
// Known providers get custom icons; unknown providers use generic SSO icon
|
||||
export const oauthProviderConfig: Record<string, { label: string; file: string }> = {
|
||||
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' }
|
||||
keycloak: { label: 'Keycloak', file: 'keycloak.svg' },
|
||||
cloudron: { label: 'Cloudron', file: 'cloudron.svg' },
|
||||
authentik: { label: 'Authentik', file: 'authentik.svg' },
|
||||
oidc: { label: 'OIDC', file: 'oidc.svg' }
|
||||
};
|
||||
|
||||
// Generic fallback for unknown providers
|
||||
const GENERIC_PROVIDER_ICON = 'oidc.svg';
|
||||
|
||||
interface OAuthButtonsProps {
|
||||
onProviderClick: (provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc') => void
|
||||
onProviderClick: (provider: string) => void
|
||||
isSubmitting: boolean
|
||||
layout?: 'vertical' | 'grid' | 'icons'
|
||||
enabledProviders?: string[] // List of enabled provider IDs from backend
|
||||
@ -32,13 +36,22 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
||||
? Object.keys(oauthProviderConfig)
|
||||
: enabledProviders;
|
||||
|
||||
// Filter to only show enabled providers from backend
|
||||
const providers = providersToShow
|
||||
.filter(id => id in oauthProviderConfig)
|
||||
.map(id => ({
|
||||
// Build provider list - use provider ID to determine icon and label
|
||||
const providers = providersToShow.map(id => {
|
||||
if (id in oauthProviderConfig) {
|
||||
// Known provider - use predefined icon and label
|
||||
return {
|
||||
id,
|
||||
...oauthProviderConfig[id]
|
||||
};
|
||||
}
|
||||
// Unknown provider - use generic icon and capitalize ID for label
|
||||
return {
|
||||
id,
|
||||
...oauthProviderConfig[id as keyof typeof oauthProviderConfig]
|
||||
}));
|
||||
label: id.charAt(0).toUpperCase() + id.slice(1),
|
||||
file: GENERIC_PROVIDER_ICON
|
||||
};
|
||||
});
|
||||
|
||||
// If no providers are enabled, don't render anything
|
||||
if (providers.length === 0) {
|
||||
@ -51,7 +64,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
||||
{providers.map((p) => (
|
||||
<div key={p.id} title={`${t('login.signInWith', 'Sign in with')} ${p.label}`}>
|
||||
<button
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
onClick={() => onProviderClick(p.id)}
|
||||
disabled={isSubmitting}
|
||||
className="oauth-button-icon"
|
||||
aria-label={`${t('login.signInWith', 'Sign in with')} ${p.label}`}
|
||||
@ -70,7 +83,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
||||
{providers.map((p) => (
|
||||
<div key={p.id} title={`${t('login.signInWith', 'Sign in with')} ${p.label}`}>
|
||||
<button
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
onClick={() => onProviderClick(p.id)}
|
||||
disabled={isSubmitting}
|
||||
className="oauth-button-grid"
|
||||
aria-label={`${t('login.signInWith', 'Sign in with')} ${p.label}`}
|
||||
@ -88,7 +101,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = '
|
||||
{providers.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => onProviderClick(p.id as any)}
|
||||
onClick={() => onProviderClick(p.id)}
|
||||
disabled={isSubmitting}
|
||||
className="oauth-button-vertical"
|
||||
title={p.label}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user