Skip to content

Commit e859e23

Browse files
committed
Fix login/register registration policy UI
1 parent 8caeaf5 commit e859e23

6 files changed

Lines changed: 117 additions & 2 deletions

File tree

frontend/src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const clearCsrfToken = (): void => {
8282
export interface AuthStatusResponse {
8383
authEnabled?: boolean;
8484
enabled?: boolean;
85+
registrationEnabled?: boolean;
8586
authMode?: "local" | "hybrid" | "oidc_enforced";
8687
oidcEnabled?: boolean;
8788
oidcEnforced?: boolean;

frontend/src/context/AuthContext.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface AuthContextType {
2424
user: User | null;
2525
loading: boolean;
2626
authEnabled: boolean | null;
27+
registrationEnabled: boolean;
2728
authStatusError: string | null;
2829
authMode: 'local' | 'hybrid' | 'oidc_enforced';
2930
oidcEnabled: boolean;
@@ -48,6 +49,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
4849
const [user, setUser] = useState<User | null>(null);
4950
const [loading, setLoading] = useState(true);
5051
const [authEnabled, setAuthEnabled] = useState<boolean | null>(null);
52+
const [registrationEnabled, setRegistrationEnabled] = useState(false);
5153
const [authStatusError, setAuthStatusError] = useState<string | null>(null);
5254
const [authMode, setAuthMode] = useState<'local' | 'hybrid' | 'oidc_enforced'>('local');
5355
const [oidcEnabled, setOidcEnabled] = useState(false);
@@ -74,6 +76,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
7476
: true;
7577
setAuthEnabled(enabled);
7678
localStorage.setItem(AUTH_ENABLED_CACHE_KEY, String(enabled));
79+
setRegistrationEnabled(Boolean(statusResponse?.registrationEnabled));
7780
const nextAuthMode =
7881
statusResponse?.authMode === 'hybrid' || statusResponse?.authMode === 'oidc_enforced'
7982
? statusResponse.authMode
@@ -100,6 +103,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
100103
if (cachedAuthEnabled === "false") {
101104
setAuthStatusError(null);
102105
setAuthEnabled(false);
106+
setRegistrationEnabled(false);
103107
setAuthMode('local');
104108
setOidcEnabled(false);
105109
setOidcEnforced(false);
@@ -115,6 +119,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
115119
"Unable to reach the backend API. Check BACKEND_URL, FRONTEND_URL, and your reverse proxy configuration."
116120
);
117121
setAuthEnabled(null);
122+
setRegistrationEnabled(false);
118123
setAuthMode('local');
119124
setOidcEnabled(false);
120125
setOidcEnforced(false);
@@ -252,6 +257,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
252257
user,
253258
loading,
254259
authEnabled,
260+
registrationEnabled,
255261
authStatusError,
256262
authMode,
257263
oidcEnabled,

frontend/src/pages/Login.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const Login: React.FC = () => {
1919
login,
2020
logout,
2121
authEnabled,
22+
registrationEnabled,
2223
authStatusError,
2324
retryAuthStatus,
2425
oidcEnabled,
@@ -159,7 +160,7 @@ export const Login: React.FC = () => {
159160
? `Sign in with ${oidcProvider || 'OIDC'}`
160161
: 'Sign in to your account'}
161162
</h2>
162-
{!mustReset && !oidcEnforced ? (
163+
{!mustReset && !oidcEnforced && registrationEnabled ? (
163164
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
164165
Or{' '}
165166
<Link
@@ -169,6 +170,10 @@ export const Login: React.FC = () => {
169170
create a new account
170171
</Link>
171172
</p>
173+
) : !mustReset && !oidcEnforced ? (
174+
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
175+
Sign in with an existing account.
176+
</p>
172177
) : mustReset ? (
173178
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
174179
Your admin requires you to set a new password before using ExcaliDash.

frontend/src/pages/Register.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const Register: React.FC = () => {
1919
const {
2020
register,
2121
authEnabled,
22+
registrationEnabled,
2223
authStatusError,
2324
retryAuthStatus,
2425
oidcEnabled,
@@ -72,10 +73,24 @@ export const Register: React.FC = () => {
7273
navigate('/', { replace: true });
7374
return;
7475
}
76+
if (!bootstrapRequired && !registrationEnabled) {
77+
navigate('/login', { replace: true });
78+
return;
79+
}
7580
if (isAuthenticated) {
7681
navigate('/', { replace: true });
7782
}
78-
}, [authEnabled, authLoading, authOnboardingRequired, authStatusError, isAuthenticated, navigate, oidcEnforced]);
83+
}, [
84+
authEnabled,
85+
authLoading,
86+
authOnboardingRequired,
87+
authStatusError,
88+
bootstrapRequired,
89+
isAuthenticated,
90+
navigate,
91+
oidcEnforced,
92+
registrationEnabled,
93+
]);
7994

8095
if (authStatusError) {
8196
return <AuthStatusErrorPanel message={authStatusError} onRetry={retryAuthStatus} fullScreen />;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { MemoryRouter } from "react-router-dom";
3+
import { beforeEach, describe, expect, it, vi } from "vitest";
4+
import { Login } from "./Login";
5+
import { Register } from "./Register";
6+
7+
const mockUseAuth = vi.fn();
8+
const mockNavigate = vi.fn();
9+
10+
vi.mock("../context/AuthContext", () => ({
11+
useAuth: () => mockUseAuth(),
12+
}));
13+
14+
vi.mock("../api", () => ({
15+
startOidcSignIn: vi.fn(),
16+
api: {
17+
post: vi.fn(),
18+
},
19+
isAxiosError: vi.fn(() => false),
20+
}));
21+
22+
vi.mock("../components/Logo", () => ({
23+
Logo: () => <div data-testid="logo" />,
24+
}));
25+
26+
vi.mock("react-router-dom", async () => {
27+
const actual = await vi.importActual<typeof import("react-router-dom")>("react-router-dom");
28+
return {
29+
...actual,
30+
useNavigate: () => mockNavigate,
31+
};
32+
});
33+
34+
const baseAuthState = {
35+
login: vi.fn(),
36+
logout: vi.fn(),
37+
register: vi.fn(),
38+
authEnabled: true,
39+
registrationEnabled: true,
40+
authStatusError: null,
41+
retryAuthStatus: vi.fn(),
42+
oidcEnabled: false,
43+
oidcEnforced: false,
44+
oidcProvider: "OIDC",
45+
bootstrapRequired: false,
46+
authOnboardingRequired: false,
47+
isAuthenticated: false,
48+
loading: false,
49+
user: null,
50+
};
51+
52+
describe("auth page registration policy", () => {
53+
beforeEach(() => {
54+
vi.clearAllMocks();
55+
});
56+
57+
it("hides the register link on the login page when registration is disabled", () => {
58+
mockUseAuth.mockReturnValue({
59+
...baseAuthState,
60+
registrationEnabled: false,
61+
});
62+
63+
render(
64+
<MemoryRouter initialEntries={["/login"]}>
65+
<Login />
66+
</MemoryRouter>
67+
);
68+
69+
expect(screen.queryByRole("link", { name: /create a new account/i })).not.toBeInTheDocument();
70+
expect(screen.getByText(/sign in with an existing account/i)).toBeInTheDocument();
71+
});
72+
73+
it("redirects away from /register when registration is disabled outside bootstrap flow", () => {
74+
mockUseAuth.mockReturnValue({
75+
...baseAuthState,
76+
registrationEnabled: false,
77+
});
78+
79+
render(
80+
<MemoryRouter initialEntries={["/register"]}>
81+
<Register />
82+
</MemoryRouter>
83+
);
84+
85+
expect(mockNavigate).toHaveBeenCalledWith("/login", { replace: true });
86+
});
87+
});

frontend/src/pages/authStatusBlocking.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const baseAuthState = {
2727
logout: vi.fn(),
2828
register: vi.fn(),
2929
authEnabled: true,
30+
registrationEnabled: true,
3031
authStatusError: "Unable to reach the backend API. Check BACKEND_URL and reverse proxy settings, then retry.",
3132
retryAuthStatus: vi.fn(),
3233
oidcEnabled: false,

0 commit comments

Comments
 (0)