fix impersonation issues
This commit is contained in:
@@ -69,6 +69,10 @@ export const clearCsrfToken = (): void => {
|
||||
export interface AuthStatusResponse {
|
||||
authEnabled?: boolean;
|
||||
enabled?: boolean;
|
||||
authMode?: "local" | "hybrid" | "oidc_enforced";
|
||||
oidcEnabled?: boolean;
|
||||
oidcEnforced?: boolean;
|
||||
oidcProvider?: string;
|
||||
bootstrapRequired?: boolean;
|
||||
authOnboardingRequired?: boolean;
|
||||
authOnboardingMode?: "migration" | "fresh";
|
||||
@@ -92,6 +96,13 @@ export const authStatus = async (): Promise<AuthStatusResponse> => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const startOidcSignIn = (returnTo?: string): void => {
|
||||
const fallbackPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
const requestedPath = typeof returnTo === "string" && returnTo.startsWith("/") ? returnTo : fallbackPath;
|
||||
const safeReturnTo = requestedPath.startsWith("/") ? requestedPath : "/";
|
||||
window.location.href = `/api/auth/oidc/start?returnTo=${encodeURIComponent(safeReturnTo)}`;
|
||||
};
|
||||
|
||||
export const authMe = async (): Promise<{ user: AuthUser }> => {
|
||||
const response = await axios.get<{ user: AuthUser }>(`${API_URL}/auth/me`, {
|
||||
withCredentials: true,
|
||||
@@ -204,6 +215,16 @@ const getAuthEnabledStatus = async (): Promise<boolean | null> => {
|
||||
};
|
||||
|
||||
const redirectToLogin = async () => {
|
||||
try {
|
||||
const status = await authStatus();
|
||||
if (status?.oidcEnforced) {
|
||||
startOidcSignIn();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Best-effort status probe; fall through to legacy behavior.
|
||||
}
|
||||
|
||||
const authEnabled = await getAuthEnabledStatus();
|
||||
if (authEnabled === false) return;
|
||||
if (window.location.pathname !== '/login') {
|
||||
|
||||
@@ -22,6 +22,13 @@ type ImpersonationTargetsResponse = {
|
||||
users: ImpersonationTarget[];
|
||||
};
|
||||
|
||||
type AuthStatusResponse = {
|
||||
authenticated?: boolean;
|
||||
user?: {
|
||||
impersonatorId?: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type ImpersonateResponse = {
|
||||
user: {
|
||||
id: string;
|
||||
@@ -46,6 +53,11 @@ export const ImpersonationBanner: React.FC = () => {
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const clearLocalImpersonation = () => {
|
||||
localStorage.removeItem(IMPERSONATION_KEY);
|
||||
setImpersonation(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!authEnabled) {
|
||||
setImpersonation(null);
|
||||
@@ -54,6 +66,20 @@ export const ImpersonationBanner: React.FC = () => {
|
||||
|
||||
const sync = () => setImpersonation(readImpersonationState());
|
||||
sync();
|
||||
|
||||
const verifyServerImpersonationState = async () => {
|
||||
try {
|
||||
const response = await api.get<AuthStatusResponse>('/auth/status');
|
||||
const serverImpersonating = Boolean(response.data?.authenticated && response.data?.user?.impersonatorId);
|
||||
if (!serverImpersonating && readImpersonationState()) {
|
||||
clearLocalImpersonation();
|
||||
}
|
||||
} catch {
|
||||
// Ignore probe failures; retry on next render/event.
|
||||
}
|
||||
};
|
||||
|
||||
void verifyServerImpersonationState();
|
||||
window.addEventListener('storage', sync);
|
||||
return () => window.removeEventListener('storage', sync);
|
||||
}, [authEnabled]);
|
||||
@@ -116,6 +142,14 @@ export const ImpersonationBanner: React.FC = () => {
|
||||
let message = 'Failed to stop impersonation';
|
||||
if (isAxiosError(err)) {
|
||||
message = err.response?.data?.message || err.response?.data?.error || message;
|
||||
if (
|
||||
err.response?.status === 409 &&
|
||||
/not currently impersonating/i.test(message)
|
||||
) {
|
||||
clearLocalImpersonation();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
setError(message);
|
||||
setBusy(false);
|
||||
@@ -159,36 +193,38 @@ export const ImpersonationBanner: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-2xl border-2 border-amber-200 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-3 sm:p-4 shadow-[2px_2px_0px_0px_rgba(0,0,0,0.18)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.12)]">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-amber-900 dark:text-amber-200">
|
||||
<LogIn size={16} />
|
||||
<span className="text-sm font-bold uppercase tracking-wide">Impersonating:</span>
|
||||
<div className="sticky top-0 z-[45] -mt-2 mb-6 rounded-xl border border-red-200 dark:border-red-800/50 bg-red-50/80 dark:bg-red-950/30 backdrop-blur-md px-3 py-2 shadow-sm transition-all duration-200">
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5 text-red-700 dark:text-red-400 flex-shrink-0">
|
||||
<LogIn size={14} strokeWidth={2.5} />
|
||||
<span className="text-[10px] font-black uppercase tracking-wider">Impersonating</span>
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-semibold text-amber-900 dark:text-amber-200 truncate">
|
||||
{impersonation.target.name} ({impersonation.target.email})
|
||||
</div>
|
||||
<div className="text-xs text-amber-800/90 dark:text-amber-200/80 truncate">
|
||||
Acting as this account. Stop to return to {impersonation.impersonator.email}.
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-sm font-bold text-red-900 dark:text-red-100 truncate">
|
||||
{impersonation.target.name}
|
||||
</span>
|
||||
<span className="hidden sm:inline text-xs font-medium text-red-800/60 dark:text-red-200/40 truncate">
|
||||
{impersonation.target.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 lg:flex-shrink-0 lg:justify-end">
|
||||
<label className="text-xs font-bold uppercase tracking-wide text-amber-900 dark:text-amber-200">
|
||||
Switch user:
|
||||
</label>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<div className="hidden lg:flex items-center gap-1.5 text-[10px] font-black uppercase tracking-wider text-red-700/60 dark:text-red-400/40">
|
||||
Switch:
|
||||
</div>
|
||||
<select
|
||||
value={impersonation.target.id}
|
||||
onChange={(e) => {
|
||||
void switchTarget(e.target.value);
|
||||
}}
|
||||
disabled={busy || loadingTargets || options.length === 0}
|
||||
className="min-w-[220px] max-w-[320px] px-3 py-2 rounded-xl border-2 border-amber-300 dark:border-amber-700 bg-white dark:bg-neutral-900 text-sm font-semibold text-slate-900 dark:text-neutral-100 outline-none disabled:opacity-70"
|
||||
className="h-8 min-w-[140px] max-w-[200px] px-2 rounded-lg border border-red-200 dark:border-red-800/50 bg-white/50 dark:bg-neutral-900/50 text-xs font-bold text-red-900 dark:text-red-100 outline-none hover:border-red-300 dark:hover:border-red-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{options.map((target) => (
|
||||
<option key={target.id} value={target.id}>
|
||||
{target.name} ({target.email})
|
||||
{target.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -196,28 +232,28 @@ export const ImpersonationBanner: React.FC = () => {
|
||||
type="button"
|
||||
onClick={stop}
|
||||
disabled={busy}
|
||||
className="inline-flex items-center justify-center gap-2 px-3 py-2 rounded-xl border-2 border-amber-300 dark:border-amber-700 bg-white dark:bg-neutral-900 text-sm font-bold text-amber-900 dark:text-amber-200 hover:bg-amber-100 dark:hover:bg-amber-900/30 transition-all disabled:opacity-70"
|
||||
className="h-8 flex items-center justify-center gap-1.5 px-3 rounded-lg bg-red-600 dark:bg-red-600/80 text-[11px] font-black uppercase tracking-wider text-white hover:bg-red-700 dark:hover:bg-red-500 transition-all disabled:opacity-50 shadow-sm shadow-red-900/10"
|
||||
>
|
||||
<XCircle size={15} />
|
||||
Stop
|
||||
<XCircle size={14} strokeWidth={2.5} />
|
||||
<span className="hidden sm:inline">Stop</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(loadingTargets || error) && (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs font-medium text-amber-900 dark:text-amber-200">
|
||||
<div className="mt-1.5 pt-1.5 border-t border-red-200/50 dark:border-red-800/20 flex items-center gap-3 text-[10px] font-bold text-red-800 dark:text-red-300">
|
||||
{loadingTargets ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<RefreshCw size={12} className="animate-spin" />
|
||||
Loading users...
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<RefreshCw size={10} className="animate-spin" />
|
||||
Syncing targets...
|
||||
</span>
|
||||
) : null}
|
||||
{error ? <span>{error}</span> : null}
|
||||
{error ? <span className="truncate">{error}</span> : null}
|
||||
{error ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadTargets()}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-amber-300 dark:border-amber-700 px-2 py-1"
|
||||
className="px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-900/40 border border-red-200 dark:border-red-700/50 hover:bg-red-200 transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { startOidcSignIn } from '../api';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
@@ -12,11 +13,24 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||
isAuthenticated,
|
||||
loading,
|
||||
authEnabled,
|
||||
oidcEnforced,
|
||||
bootstrapRequired,
|
||||
authOnboardingRequired,
|
||||
user,
|
||||
} = useAuth();
|
||||
|
||||
const OidcRedirect: React.FC<{ returnTo: string }> = ({ returnTo }) => {
|
||||
useEffect(() => {
|
||||
startOidcSignIn(returnTo);
|
||||
}, [returnTo]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-gray-600 dark:text-gray-400">Redirecting to sign-in...</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading || authEnabled === null) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
@@ -39,6 +53,10 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||
if (bootstrapRequired) {
|
||||
return <Navigate to="/register" replace />;
|
||||
}
|
||||
if (oidcEnforced) {
|
||||
const returnTo = `${location.pathname}${location.search}${location.hash}`;
|
||||
return <OidcRedirect returnTo={returnTo} />;
|
||||
}
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
authEnabled: boolean | null;
|
||||
authMode: 'local' | 'hybrid' | 'oidc_enforced';
|
||||
oidcEnabled: boolean;
|
||||
oidcEnforced: boolean;
|
||||
oidcProvider: string | null;
|
||||
bootstrapRequired: boolean;
|
||||
authOnboardingRequired: boolean;
|
||||
authOnboardingMode: 'migration' | 'fresh' | null;
|
||||
@@ -42,6 +46,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [authEnabled, setAuthEnabled] = useState<boolean | null>(null);
|
||||
const [authMode, setAuthMode] = useState<'local' | 'hybrid' | 'oidc_enforced'>('local');
|
||||
const [oidcEnabled, setOidcEnabled] = useState(false);
|
||||
const [oidcEnforced, setOidcEnforced] = useState(false);
|
||||
const [oidcProvider, setOidcProvider] = useState<string | null>(null);
|
||||
const [bootstrapRequired, setBootstrapRequired] = useState(false);
|
||||
const [authOnboardingRequired, setAuthOnboardingRequired] = useState(false);
|
||||
const [authOnboardingMode, setAuthOnboardingMode] = useState<'migration' | 'fresh' | null>(null);
|
||||
@@ -60,6 +68,14 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
: true;
|
||||
setAuthEnabled(enabled);
|
||||
localStorage.setItem(AUTH_ENABLED_CACHE_KEY, String(enabled));
|
||||
const nextAuthMode =
|
||||
statusResponse?.authMode === 'hybrid' || statusResponse?.authMode === 'oidc_enforced'
|
||||
? statusResponse.authMode
|
||||
: 'local';
|
||||
setAuthMode(nextAuthMode);
|
||||
setOidcEnabled(Boolean(statusResponse?.oidcEnabled));
|
||||
setOidcEnforced(Boolean(statusResponse?.oidcEnforced));
|
||||
setOidcProvider(typeof statusResponse?.oidcProvider === 'string' ? statusResponse.oidcProvider : null);
|
||||
setBootstrapRequired(Boolean(statusResponse?.bootstrapRequired));
|
||||
setAuthOnboardingRequired(Boolean(statusResponse?.authOnboardingRequired));
|
||||
setAuthOnboardingMode(
|
||||
@@ -77,6 +93,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
const cachedAuthEnabled = localStorage.getItem(AUTH_ENABLED_CACHE_KEY);
|
||||
if (cachedAuthEnabled === "false") {
|
||||
setAuthEnabled(false);
|
||||
setAuthMode('local');
|
||||
setOidcEnabled(false);
|
||||
setOidcEnforced(false);
|
||||
setOidcProvider(null);
|
||||
setBootstrapRequired(false);
|
||||
setAuthOnboardingRequired(false);
|
||||
setAuthOnboardingMode(null);
|
||||
@@ -85,6 +105,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
return;
|
||||
}
|
||||
setAuthEnabled(true);
|
||||
setAuthMode('local');
|
||||
setOidcEnabled(false);
|
||||
setOidcEnforced(false);
|
||||
setOidcProvider(null);
|
||||
setBootstrapRequired(false);
|
||||
setAuthOnboardingRequired(false);
|
||||
setAuthOnboardingMode(null);
|
||||
@@ -192,6 +216,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
user,
|
||||
loading,
|
||||
authEnabled,
|
||||
authMode,
|
||||
oidcEnabled,
|
||||
oidcEnforced,
|
||||
oidcProvider,
|
||||
bootstrapRequired,
|
||||
authOnboardingRequired,
|
||||
authOnboardingMode,
|
||||
|
||||
@@ -16,6 +16,9 @@ export const Login: React.FC = () => {
|
||||
login,
|
||||
logout,
|
||||
authEnabled,
|
||||
oidcEnabled,
|
||||
oidcEnforced,
|
||||
oidcProvider,
|
||||
bootstrapRequired,
|
||||
authOnboardingRequired,
|
||||
isAuthenticated,
|
||||
@@ -25,8 +28,16 @@ export const Login: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryMustReset = searchParams.get('mustReset') === '1';
|
||||
const oidcErrorCode = searchParams.get('oidcError');
|
||||
const oidcErrorMessage = searchParams.get('oidcErrorMessage');
|
||||
const oidcReturnTo = searchParams.get('returnTo') || '/';
|
||||
const mustReset = Boolean(user?.mustResetPassword) || queryMustReset;
|
||||
|
||||
useEffect(() => {
|
||||
if (!oidcErrorCode) return;
|
||||
setError(oidcErrorMessage || 'OIDC sign-in failed');
|
||||
}, [oidcErrorCode, oidcErrorMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading || authEnabled === null) return;
|
||||
if (authOnboardingRequired) {
|
||||
@@ -41,11 +52,28 @@ export const Login: React.FC = () => {
|
||||
navigate('/register', { replace: true });
|
||||
return;
|
||||
}
|
||||
if (oidcEnforced && !mustReset) {
|
||||
if (!oidcErrorCode) {
|
||||
api.startOidcSignIn(oidcReturnTo);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isAuthenticated) {
|
||||
if (mustReset) return;
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
}, [authEnabled, authLoading, authOnboardingRequired, bootstrapRequired, isAuthenticated, mustReset, navigate]);
|
||||
}, [
|
||||
authEnabled,
|
||||
authLoading,
|
||||
authOnboardingRequired,
|
||||
bootstrapRequired,
|
||||
isAuthenticated,
|
||||
mustReset,
|
||||
navigate,
|
||||
oidcEnforced,
|
||||
oidcErrorCode,
|
||||
oidcReturnTo,
|
||||
]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -114,9 +142,13 @@ export const Login: React.FC = () => {
|
||||
<div className="text-center">
|
||||
<Logo className="mx-auto h-12 w-auto" />
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||
{mustReset ? 'Reset your password' : 'Sign in to your account'}
|
||||
{mustReset
|
||||
? 'Reset your password'
|
||||
: oidcEnforced
|
||||
? `Sign in with ${oidcProvider || 'OIDC'}`
|
||||
: 'Sign in to your account'}
|
||||
</h2>
|
||||
{!mustReset ? (
|
||||
{!mustReset && !oidcEnforced ? (
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Or{' '}
|
||||
<Link
|
||||
@@ -126,10 +158,14 @@ export const Login: React.FC = () => {
|
||||
create a new account
|
||||
</Link>
|
||||
</p>
|
||||
) : (
|
||||
) : mustReset ? (
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Your admin requires you to set a new password before using ExcaliDash.
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
You will be redirected to {oidcProvider || 'your identity provider'}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={mustReset ? handleMustReset : handleSubmit}>
|
||||
@@ -138,8 +174,19 @@ export const Login: React.FC = () => {
|
||||
<div className="text-sm text-red-800 dark:text-red-200">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
{!mustReset ? (
|
||||
{oidcEnforced && !mustReset ? (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => api.startOidcSignIn(oidcReturnTo)}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Continue with {oidcProvider || 'OIDC'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
{!mustReset ? (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
@@ -174,7 +221,7 @@ export const Login: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="newPassword" className="sr-only">
|
||||
@@ -211,10 +258,11 @@ export const Login: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!mustReset && (
|
||||
{!mustReset && !oidcEnforced && (
|
||||
<div className="flex justify-end">
|
||||
<Link
|
||||
to="/reset-password"
|
||||
@@ -225,15 +273,31 @@ export const Login: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{mustReset ? (loading ? 'Updating...' : 'Set new password') : (loading ? 'Signing in...' : 'Sign in')}
|
||||
</button>
|
||||
</div>
|
||||
{(!oidcEnforced || mustReset) && (
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{mustReset
|
||||
? (loading ? 'Updating...' : 'Set new password')
|
||||
: (loading ? 'Signing in...' : 'Sign in')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!mustReset && oidcEnabled && !oidcEnforced && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => api.startOidcSignIn('/')}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-gray-300 dark:border-gray-700 text-sm font-medium rounded-md text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Continue with {oidcProvider || 'OIDC'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mustReset && (
|
||||
<div className="text-center">
|
||||
|
||||
@@ -12,6 +12,7 @@ export const Register: React.FC = () => {
|
||||
const {
|
||||
register,
|
||||
authEnabled,
|
||||
oidcEnforced,
|
||||
bootstrapRequired,
|
||||
authOnboardingRequired,
|
||||
isAuthenticated,
|
||||
@@ -25,6 +26,10 @@ export const Register: React.FC = () => {
|
||||
navigate('/auth-setup', { replace: true });
|
||||
return;
|
||||
}
|
||||
if (oidcEnforced) {
|
||||
navigate('/login', { replace: true });
|
||||
return;
|
||||
}
|
||||
if (!authEnabled) {
|
||||
navigate('/', { replace: true });
|
||||
return;
|
||||
@@ -32,7 +37,7 @@ export const Register: React.FC = () => {
|
||||
if (isAuthenticated) {
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
}, [authEnabled, authLoading, authOnboardingRequired, isAuthenticated, navigate]);
|
||||
}, [authEnabled, authLoading, authOnboardingRequired, isAuthenticated, navigate, oidcEnforced]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user