feat(client): 首次登录改密守卫与错误文案(F35,F38)
- 登录响应支持 mustChangePassword 并强制跳转改密页 - 新增改密页面与全局路由守卫 - 完善 AUTH_002/SYS_429/VALID_00x 等错误提示
这个提交包含在:
@@ -10,6 +10,7 @@ import {
|
||||
} from "react";
|
||||
import { configureHttpAuth } from "../api/http";
|
||||
import type { AuthState, MeResponse } from "../api/types";
|
||||
import { changePassword as changePasswordApi } from "../api/auth";
|
||||
import {
|
||||
fetchCurrentEmployee,
|
||||
loginWithPassword,
|
||||
@@ -19,7 +20,8 @@ import {
|
||||
import { resolvePermissionsForRoles } from "./permissions";
|
||||
|
||||
type AuthContextValue = AuthState & {
|
||||
login: (identifier: string, password: string) => Promise<void>;
|
||||
login: (identifier: string, password: string) => Promise<{ mustChangePassword: boolean }>;
|
||||
changePassword: (oldPassword: string, newPassword: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
bootstrapByRefresh: () => Promise<void>;
|
||||
clearSession: () => void;
|
||||
@@ -31,14 +33,35 @@ const EMPTY_STATE: AuthState = {
|
||||
accessToken: null,
|
||||
me: null,
|
||||
permissions: [],
|
||||
mustChangePassword: false,
|
||||
ready: false,
|
||||
};
|
||||
|
||||
function buildStateWithUser(accessToken: string, me: MeResponse): AuthState {
|
||||
const MUST_CHANGE_PASSWORD_STORAGE_KEY = "hyb-oa.must_change_password";
|
||||
|
||||
function readMustChangePasswordFromStorage() {
|
||||
return typeof window !== "undefined" && window.localStorage.getItem(MUST_CHANGE_PASSWORD_STORAGE_KEY) === "1";
|
||||
}
|
||||
|
||||
function writeMustChangePasswordToStorage(value: boolean) {
|
||||
if (typeof window === "undefined") return;
|
||||
if (value) {
|
||||
window.localStorage.setItem(MUST_CHANGE_PASSWORD_STORAGE_KEY, "1");
|
||||
} else {
|
||||
window.localStorage.removeItem(MUST_CHANGE_PASSWORD_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
function buildStateWithUser(
|
||||
accessToken: string,
|
||||
me: MeResponse,
|
||||
mustChangePassword: boolean,
|
||||
): AuthState {
|
||||
return {
|
||||
accessToken,
|
||||
me,
|
||||
permissions: resolvePermissionsForRoles(me.roleCodes ?? []),
|
||||
mustChangePassword,
|
||||
ready: true,
|
||||
};
|
||||
}
|
||||
@@ -53,25 +76,32 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
||||
|
||||
const clearSession = useCallback(() => {
|
||||
tokenRef.current = null;
|
||||
writeMustChangePasswordToStorage(false);
|
||||
setState((previous) => ({
|
||||
...previous,
|
||||
accessToken: null,
|
||||
me: null,
|
||||
permissions: [],
|
||||
mustChangePassword: false,
|
||||
ready: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const commitAuthenticatedUser = useCallback((accessToken: string, me: MeResponse) => {
|
||||
tokenRef.current = accessToken;
|
||||
setState(buildStateWithUser(accessToken, me));
|
||||
}, []);
|
||||
const commitAuthenticatedUser = useCallback(
|
||||
(accessToken: string, me: MeResponse, mustChangePassword: boolean) => {
|
||||
tokenRef.current = accessToken;
|
||||
writeMustChangePasswordToStorage(mustChangePassword);
|
||||
setState(buildStateWithUser(accessToken, me, mustChangePassword));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const bootstrapByRefresh = useCallback(async () => {
|
||||
try {
|
||||
const refreshed = await refreshSession();
|
||||
const me = await fetchCurrentEmployee(refreshed.accessToken);
|
||||
commitAuthenticatedUser(refreshed.accessToken, me);
|
||||
const mustChangePassword = readMustChangePasswordFromStorage();
|
||||
commitAuthenticatedUser(refreshed.accessToken, me, mustChangePassword);
|
||||
} catch {
|
||||
clearSession();
|
||||
}
|
||||
@@ -80,8 +110,19 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
||||
const login = useCallback(
|
||||
async (identifier: string, password: string) => {
|
||||
const result = await loginWithPassword({ identifier, password });
|
||||
const mustChangePassword = Boolean(result.mustChangePassword);
|
||||
const me = await fetchCurrentEmployee(result.accessToken);
|
||||
commitAuthenticatedUser(result.accessToken, me);
|
||||
commitAuthenticatedUser(result.accessToken, me, mustChangePassword);
|
||||
return { mustChangePassword };
|
||||
},
|
||||
[commitAuthenticatedUser],
|
||||
);
|
||||
|
||||
const changePassword = useCallback(
|
||||
async (oldPassword: string, newPassword: string) => {
|
||||
const result = await changePasswordApi({ oldPassword, newPassword });
|
||||
const me = await fetchCurrentEmployee(result.accessToken);
|
||||
commitAuthenticatedUser(result.accessToken, me, false);
|
||||
},
|
||||
[commitAuthenticatedUser],
|
||||
);
|
||||
@@ -124,11 +165,12 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
||||
() => ({
|
||||
...state,
|
||||
login,
|
||||
changePassword,
|
||||
logout,
|
||||
bootstrapByRefresh,
|
||||
clearSession,
|
||||
}),
|
||||
[bootstrapByRefresh, clearSession, login, logout, state],
|
||||
[bootstrapByRefresh, changePassword, clearSession, login, logout, state],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
|
||||
在新工单中引用
屏蔽一个用户