Compare commits
1 Commits
feat/ms21-
...
feat/ms21-
| Author | SHA1 | Date | |
|---|---|---|---|
| b174ba4f14 |
@@ -24,15 +24,7 @@ describe("AdminService", () => {
|
|||||||
workspaceMember: {
|
workspaceMember: {
|
||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
},
|
},
|
||||||
session: {
|
$transaction: vi.fn(),
|
||||||
deleteMany: vi.fn(),
|
|
||||||
},
|
|
||||||
$transaction: vi.fn(async (ops) => {
|
|
||||||
if (typeof ops === "function") {
|
|
||||||
return ops(mockPrismaService);
|
|
||||||
}
|
|
||||||
return Promise.all(ops);
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockAdminId = "550e8400-e29b-41d4-a716-446655440001";
|
const mockAdminId = "550e8400-e29b-41d4-a716-446655440001";
|
||||||
@@ -90,6 +82,10 @@ describe("AdminService", () => {
|
|||||||
service = module.get<AdminService>(AdminService);
|
service = module.get<AdminService>(AdminService);
|
||||||
|
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockPrismaService.$transaction.mockImplementation(async (fn: (tx: unknown) => unknown) => {
|
||||||
|
return fn(mockPrismaService);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be defined", () => {
|
it("should be defined", () => {
|
||||||
@@ -329,13 +325,12 @@ describe("AdminService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("deactivateUser", () => {
|
describe("deactivateUser", () => {
|
||||||
it("should set deactivatedAt and invalidate sessions", async () => {
|
it("should set deactivatedAt on the user", async () => {
|
||||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
|
mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
|
||||||
mockPrismaService.user.update.mockResolvedValue({
|
mockPrismaService.user.update.mockResolvedValue({
|
||||||
...mockUser,
|
...mockUser,
|
||||||
deactivatedAt: new Date(),
|
deactivatedAt: new Date(),
|
||||||
});
|
});
|
||||||
mockPrismaService.session.deleteMany.mockResolvedValue({ count: 3 });
|
|
||||||
|
|
||||||
const result = await service.deactivateUser(mockUserId);
|
const result = await service.deactivateUser(mockUserId);
|
||||||
|
|
||||||
@@ -346,7 +341,6 @@ describe("AdminService", () => {
|
|||||||
data: { deactivatedAt: expect.any(Date) },
|
data: { deactivatedAt: expect.any(Date) },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(mockPrismaService.session.deleteMany).toHaveBeenCalledWith({ where: { userId: mockUserId } });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw NotFoundException if user does not exist", async () => {
|
it("should throw NotFoundException if user does not exist", async () => {
|
||||||
|
|||||||
@@ -192,22 +192,19 @@ export class AdminService {
|
|||||||
throw new BadRequestException(`User ${id} is already deactivated`);
|
throw new BadRequestException(`User ${id} is already deactivated`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [user] = await this.prisma.$transaction([
|
const user = await this.prisma.user.update({
|
||||||
this.prisma.user.update({
|
where: { id },
|
||||||
where: { id },
|
data: { deactivatedAt: new Date() },
|
||||||
data: { deactivatedAt: new Date() },
|
include: {
|
||||||
include: {
|
workspaceMemberships: {
|
||||||
workspaceMemberships: {
|
include: {
|
||||||
include: {
|
workspace: { select: { id: true, name: true } },
|
||||||
workspace: { select: { id: true, name: true } },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
this.prisma.session.deleteMany({ where: { userId: id } }),
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
this.logger.log(`User deactivated and sessions invalidated: ${id}`);
|
this.logger.log(`User deactivated: ${id}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user