feat(phase-39): role-based access control overhaul + forced password change
Aegis CI / lint-and-test (push) Has been cancelled

- Add must_change_password field to User model with migration b023

- Add POST /auth/change-password endpoint with password policy validation

- Add require_password_changed dependency to block requests until password is changed

- Add ChangePasswordModal with live password policy checklist (forced on first login)

- Show password policy in CreateUserModal and EditUserModal

- Fix backend permissions: tests, campaigns, templates, reports, evidence, worklogs

- red_tech/blue_tech: execute only, cannot create tests/campaigns/templates

- red_lead/blue_lead: create/edit tests/campaigns/templates, generate reports, no system access

- viewer: read-only everywhere, can generate reports

- Fix frontend role checks across TestDetailPage, TestDetailHeader, TeamTabs, TestsPage, CampaignsPage, CampaignDetailPage, Sidebar
This commit is contained in:
2026-02-18 10:37:02 +01:00
parent 8f764d8e39
commit a4a2adccee
24 changed files with 338 additions and 72 deletions
+17 -9
View File
@@ -12,6 +12,7 @@ import {
getMe,
} from "../api/auth";
import type { User } from "../types/models";
import ChangePasswordModal from "../components/ChangePasswordModal";
/* ── Context shape ────────────────────────────────────────────────── */
@@ -31,18 +32,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// On mount — try to hydrate the user from the existing HttpOnly cookie.
// If no valid cookie exists the /auth/me call will 401 and we stay
// unauthenticated — no localStorage involved.
useEffect(() => {
getMe()
.then(setUser)
.catch(() => setUser(null))
.finally(() => setIsLoading(false));
const refreshUser = useCallback(async () => {
try {
const me = await getMe();
setUser(me);
} catch {
setUser(null);
}
}, []);
useEffect(() => {
refreshUser().finally(() => setIsLoading(false));
}, [refreshUser]);
const login = useCallback(async (username: string, password: string) => {
// The backend sets the HttpOnly cookie automatically
await apiLogin(username, password);
const me = await getMe();
setUser(me);
@@ -53,6 +56,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setUser(null);
}, []);
const mustChangePassword = user?.must_change_password === true;
return (
<AuthContext.Provider
value={{
@@ -64,6 +69,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}}
>
{children}
{mustChangePassword && (
<ChangePasswordModal isForced onSuccess={refreshUser} />
)}
</AuthContext.Provider>
);
}