Compare commits
1 Commits
14dc2d598e
...
feat/ms21-
| Author | SHA1 | Date | |
|---|---|---|---|
| b174ba4f14 |
@@ -42,6 +42,7 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { fetchUserWorkspaces } from "@/lib/api/workspaces";
|
||||||
import {
|
import {
|
||||||
deactivateUser,
|
deactivateUser,
|
||||||
fetchAdminUsers,
|
fetchAdminUsers,
|
||||||
@@ -105,6 +106,8 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
const [editError, setEditError] = useState<string | null>(null);
|
const [editError, setEditError] = useState<string | null>(null);
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [isAdmin, setIsAdmin] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const loadUsers = useCallback(async (showLoadingState: boolean): Promise<void> => {
|
const loadUsers = useCallback(async (showLoadingState: boolean): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (showLoadingState) {
|
if (showLoadingState) {
|
||||||
@@ -129,6 +132,20 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
void loadUsers(true);
|
void loadUsers(true);
|
||||||
}, [loadUsers]);
|
}, [loadUsers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUserWorkspaces()
|
||||||
|
.then((workspaces) => {
|
||||||
|
const adminRoles: WorkspaceMemberRole[] = [
|
||||||
|
WorkspaceMemberRole.OWNER,
|
||||||
|
WorkspaceMemberRole.ADMIN,
|
||||||
|
];
|
||||||
|
setIsAdmin(workspaces.some((ws) => adminRoles.includes(ws.role)));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsAdmin(true); // fail open
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
function resetInviteForm(): void {
|
function resetInviteForm(): void {
|
||||||
setInviteForm(INITIAL_INVITE_FORM);
|
setInviteForm(INITIAL_INVITE_FORM);
|
||||||
setInviteError(null);
|
setInviteError(null);
|
||||||
@@ -212,6 +229,17 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAdmin === false) {
|
||||||
|
return (
|
||||||
|
<div className="p-8 max-w-2xl">
|
||||||
|
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-center">
|
||||||
|
<p className="text-lg font-semibold text-red-700">Access Denied</p>
|
||||||
|
<p className="mt-2 text-sm text-red-600">You need Admin or Owner role to manage users.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto p-6 space-y-6">
|
<div className="max-w-6xl mx-auto p-6 space-y-6">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
||||||
import * as client from "./client";
|
|
||||||
import { fetchAdminUsers, inviteUser, updateUser, deactivateUser } from "./admin";
|
|
||||||
|
|
||||||
vi.mock("./client");
|
|
||||||
|
|
||||||
beforeEach((): void => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fetchAdminUsers", (): void => {
|
|
||||||
it("calls admin/users endpoint without params when none provided", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiGet).mockResolvedValueOnce({ data: [], meta: {} } as never);
|
|
||||||
await fetchAdminUsers();
|
|
||||||
expect(client.apiGet).toHaveBeenCalledWith("/api/admin/users");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("appends page and limit params when provided", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiGet).mockResolvedValueOnce({ data: [], meta: {} } as never);
|
|
||||||
await fetchAdminUsers(2, 50);
|
|
||||||
expect(client.apiGet).toHaveBeenCalledWith("/api/admin/users?page=2&limit=50");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws on API error", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiGet).mockRejectedValueOnce(new Error("Network error"));
|
|
||||||
await expect(fetchAdminUsers()).rejects.toThrow("Network error");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("inviteUser", (): void => {
|
|
||||||
it("posts to invite endpoint", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiPost).mockResolvedValueOnce({ id: "inv-1" } as never);
|
|
||||||
await inviteUser({
|
|
||||||
email: "a@b.com",
|
|
||||||
name: "Alice",
|
|
||||||
workspaceId: "ws-1",
|
|
||||||
role: "MEMBER" as never,
|
|
||||||
});
|
|
||||||
expect(client.apiPost).toHaveBeenCalledWith(
|
|
||||||
"/api/admin/users/invite",
|
|
||||||
expect.objectContaining({ email: "a@b.com" })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("updateUser", (): void => {
|
|
||||||
it("patches correct endpoint with dto", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiPatch).mockResolvedValueOnce({ id: "u1", name: "Bob" } as never);
|
|
||||||
await updateUser("u1", { name: "Bob" });
|
|
||||||
expect(client.apiPatch).toHaveBeenCalledWith("/api/admin/users/u1", { name: "Bob" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("deactivateUser", (): void => {
|
|
||||||
it("deletes correct endpoint", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiDelete).mockResolvedValueOnce({} as never);
|
|
||||||
await deactivateUser("u1");
|
|
||||||
expect(client.apiDelete).toHaveBeenCalledWith("/api/admin/users/u1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
||||||
import * as client from "./client";
|
|
||||||
import { fetchTeams, createTeam, fetchTeamMembers } from "./teams";
|
|
||||||
|
|
||||||
vi.mock("./client");
|
|
||||||
|
|
||||||
const localStorageMock = {
|
|
||||||
getItem: vi.fn().mockReturnValue("ws-1"),
|
|
||||||
setItem: vi.fn(),
|
|
||||||
clear: vi.fn(),
|
|
||||||
removeItem: vi.fn(),
|
|
||||||
length: 0,
|
|
||||||
key: vi.fn(),
|
|
||||||
};
|
|
||||||
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
|
||||||
|
|
||||||
beforeEach((): void => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
localStorageMock.getItem.mockReturnValue("ws-1");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fetchTeams", (): void => {
|
|
||||||
it("calls teams endpoint for active workspace", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
|
||||||
await fetchTeams();
|
|
||||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/teams", "ws-1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if no workspace id in localStorage", async (): Promise<void> => {
|
|
||||||
localStorageMock.getItem.mockReturnValue(null);
|
|
||||||
await expect(fetchTeams()).rejects.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("createTeam", (): void => {
|
|
||||||
it("posts to teams endpoint", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiPost).mockResolvedValueOnce({ id: "t1", name: "Dev" } as never);
|
|
||||||
await createTeam({ name: "Dev" });
|
|
||||||
expect(client.apiPost).toHaveBeenCalledWith(
|
|
||||||
"/api/workspaces/ws-1/teams",
|
|
||||||
expect.objectContaining({ name: "Dev" }),
|
|
||||||
"ws-1"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fetchTeamMembers", (): void => {
|
|
||||||
it("calls members endpoint for team", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
|
||||||
await fetchTeamMembers("t-1");
|
|
||||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/teams/t-1/members", "ws-1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
||||||
import * as client from "./client";
|
|
||||||
import {
|
|
||||||
fetchUserWorkspaces,
|
|
||||||
fetchWorkspaceMembers,
|
|
||||||
addWorkspaceMember,
|
|
||||||
updateWorkspaceMemberRole,
|
|
||||||
removeWorkspaceMember,
|
|
||||||
} from "./workspaces";
|
|
||||||
|
|
||||||
vi.mock("./client");
|
|
||||||
|
|
||||||
beforeEach((): void => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fetchUserWorkspaces", (): void => {
|
|
||||||
it("calls correct endpoint", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
|
||||||
await fetchUserWorkspaces();
|
|
||||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fetchWorkspaceMembers", (): void => {
|
|
||||||
it("calls correct endpoint with workspace id", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
|
||||||
await fetchWorkspaceMembers("ws-1");
|
|
||||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/members");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws on error", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiGet).mockRejectedValueOnce(new Error("Forbidden"));
|
|
||||||
await expect(fetchWorkspaceMembers("ws-1")).rejects.toThrow("Forbidden");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("addWorkspaceMember", (): void => {
|
|
||||||
it("posts to correct endpoint", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiPost).mockResolvedValueOnce({} as never);
|
|
||||||
await addWorkspaceMember("ws-1", { userId: "u1", role: "MEMBER" as never });
|
|
||||||
expect(client.apiPost).toHaveBeenCalledWith("/api/workspaces/ws-1/members", {
|
|
||||||
userId: "u1",
|
|
||||||
role: "MEMBER",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("updateWorkspaceMemberRole", (): void => {
|
|
||||||
it("patches correct endpoint", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiPatch).mockResolvedValueOnce({} as never);
|
|
||||||
await updateWorkspaceMemberRole("ws-1", "u1", { role: "ADMIN" as never });
|
|
||||||
expect(client.apiPatch).toHaveBeenCalledWith("/api/workspaces/ws-1/members/u1", {
|
|
||||||
role: "ADMIN",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("removeWorkspaceMember", (): void => {
|
|
||||||
it("calls delete on correct endpoint", async (): Promise<void> => {
|
|
||||||
vi.mocked(client.apiDelete).mockResolvedValueOnce(undefined as never);
|
|
||||||
await removeWorkspaceMember("ws-1", "u1");
|
|
||||||
expect(client.apiDelete).toHaveBeenCalledWith("/api/workspaces/ws-1/members/u1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user