修复账号禁用及队列场次状态规则
问题与需求:后台禁用员工后旧会话仍可继续访问;实时队列跨天误读昨日场次;项目暂停后需禁止取号但允许叫号。 修复思路:账号权限变更时撤销会话并同步前端登录态;实时查询统一按项目时区当天场次过滤;拆分取号与叫号的状态校验,并补充前后端及 PostgreSQL 回归测试。
This commit is contained in:
48
web/src/auth/AuthContext.test.tsx
Normal file
48
web/src/auth/AuthContext.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { api } from "../api";
|
||||
import { AuthProvider, useAuth } from "./AuthContext";
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function AuthProbe() {
|
||||
const { user } = useAuth();
|
||||
return (
|
||||
<>
|
||||
<span>{user ? user.username : "signed-out"}</span>
|
||||
<button type="button" onClick={() => void api.staffProjects().catch(() => undefined)}>
|
||||
load protected resource
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("AuthProvider session invalidation", () => {
|
||||
it("clears the current staff when a protected staff request returns 401", async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
user: { id: "staff-1", username: "staff01", display_name: "Staff", role: "STAFF" },
|
||||
projects: [],
|
||||
}))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
error: { code: "SESSION_INVALID", message: "Session expired" },
|
||||
}, 401));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<AuthProvider portal="staff"><AuthProbe /></AuthProvider>);
|
||||
|
||||
expect(await screen.findByText("staff01")).toBeVisible();
|
||||
fireEvent.click(screen.getByRole("button", { name: "load protected resource" }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText("signed-out")).toBeVisible());
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { ApiError, api } from "../api";
|
||||
import { AUTH_INVALID_EVENT, ApiError, api } from "../api";
|
||||
import type { AuthPayload, ProjectDto, UserDto } from "../types";
|
||||
|
||||
interface AuthContextValue {
|
||||
@@ -39,6 +39,19 @@ export function AuthProvider({ children, portal }: { children: ReactNode; portal
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleInvalidSession = (event: Event) => {
|
||||
const invalidPortal = (event as CustomEvent<{ portal?: AuthPortal }>).detail?.portal;
|
||||
if (invalidPortal !== portal) return;
|
||||
setUser(null);
|
||||
setProjects([]);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
};
|
||||
window.addEventListener(AUTH_INVALID_EVENT, handleInvalidSession);
|
||||
return () => window.removeEventListener(AUTH_INVALID_EVENT, handleInvalidSession);
|
||||
}, [portal]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [portal]);
|
||||
|
||||
Reference in New Issue
Block a user