修复账号禁用及队列场次状态规则

问题与需求:后台禁用员工后旧会话仍可继续访问;实时队列跨天误读昨日场次;项目暂停后需禁止取号但允许叫号。

修复思路:账号权限变更时撤销会话并同步前端登录态;实时查询统一按项目时区当天场次过滤;拆分取号与叫号的状态校验,并补充前后端及 PostgreSQL 回归测试。
This commit is contained in:
2026-07-30 12:02:05 +08:00
parent 0e1ac400dc
commit 17d296263c
14 changed files with 752 additions and 54 deletions

View 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());
});
});

View File

@@ -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]);