feat: add forced password change page on first login

The backend sets mustChangePassword=true for all new accounts and
blocks API calls until the password is changed. The frontend was not
handling this flag - it logged the user in and redirected to the
dashboard where every API call silently failed with 403.

Add a /change-password page that is shown when mustChangePassword is
true. The login page now redirects there instead of home, and the
AuthGuard intercepts any direct navigation to force the change first.
This commit is contained in:
Siddharth Kumar Sah
2026-03-28 14:02:13 +08:00
parent 1c8a6f10c8
commit 01cd1d9f71
4 changed files with 185 additions and 10 deletions
+34 -5
View File
@@ -4,6 +4,7 @@ interface AuthState {
loading: boolean;
authEnabled: boolean;
isAuthenticated: boolean;
mustChangePassword: boolean;
}
export function useAuth(): AuthState {
@@ -11,6 +12,7 @@ export function useAuth(): AuthState {
loading: true,
authEnabled: false,
isAuthenticated: false,
mustChangePassword: false,
});
useEffect(() => {
@@ -21,14 +23,24 @@ export function useAuth(): AuthState {
const config = await configRes.json();
if (!config.authEnabled) {
setState({ loading: false, authEnabled: false, isAuthenticated: true });
setState({
loading: false,
authEnabled: false,
isAuthenticated: true,
mustChangePassword: false,
});
return;
}
// Auth is enabled — check if we have a valid session
const token = localStorage.getItem("stirling-token");
if (!token) {
setState({ loading: false, authEnabled: true, isAuthenticated: false });
setState({
loading: false,
authEnabled: true,
isAuthenticated: false,
mustChangePassword: false,
});
return;
}
@@ -37,14 +49,31 @@ export function useAuth(): AuthState {
});
if (sessionRes.ok) {
setState({ loading: false, authEnabled: true, isAuthenticated: true });
const session = await sessionRes.json();
const mustChange = session.user?.mustChangePassword === true;
setState({
loading: false,
authEnabled: true,
isAuthenticated: true,
mustChangePassword: mustChange,
});
} else {
localStorage.removeItem("stirling-token");
setState({ loading: false, authEnabled: true, isAuthenticated: false });
setState({
loading: false,
authEnabled: true,
isAuthenticated: false,
mustChangePassword: false,
});
}
} catch {
// Can't reach API — assume no auth needed (dev mode)
setState({ loading: false, authEnabled: false, isAuthenticated: true });
setState({
loading: false,
authEnabled: false,
isAuthenticated: true,
mustChangePassword: false,
});
}
}