Skip to content

Commit 53b062c

Browse files
authored
Merge pull request #434 from trycompai/claudio/comp-71-add-enum-support-to-comments-data-schema
Add admin dashboard with organization management and member controls
2 parents e72798d + f24626f commit 53b062c

File tree

8 files changed

+1259
-5
lines changed

8 files changed

+1259
-5
lines changed

.cursor/rules/react-code.mdc

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
description:
3-
globs:
4-
alwaysApply: true
3+
globs: *.tsx
4+
alwaysApply: false
55
---
66
When writing React code follow these standards:
77

.windsurfrules

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"use server";
2+
3+
import { headers } from "next/headers";
4+
import { z } from "zod";
5+
import { auth } from "@/utils/auth";
6+
import { db } from "@comp/db";
7+
import type { Member, Organization, User } from "@comp/db/types";
8+
import { Role } from "@comp/db/types";
9+
import type { ActionResponse } from "@/types/actions";
10+
11+
// Define the detailed Organization type expected by the frontend
12+
interface OrganizationWithMembersAndUsers extends Organization {
13+
members: (Member & { user: User })[];
14+
}
15+
16+
// Define the Member type expected by the frontend
17+
interface MemberWithUser extends Member {
18+
user: User;
19+
}
20+
21+
// --- Fetch All Organizations --- Replaces fetchOrganizationsAction
22+
export async function fetchOrganizations(): Promise<
23+
ActionResponse<OrganizationWithMembersAndUsers[]>
24+
> {
25+
const session = await auth.api.getSession({ headers: await headers() });
26+
if (!session?.user?.email?.endsWith("@trycomp.ai")) {
27+
return { success: false, error: "Unauthorized: Admin access required" };
28+
}
29+
30+
try {
31+
const organizations = await db.organization.findMany({
32+
include: {
33+
members: {
34+
include: {
35+
user: true,
36+
},
37+
},
38+
},
39+
orderBy: {
40+
name: "asc",
41+
},
42+
});
43+
return { success: true, data: organizations };
44+
} catch (error) {
45+
console.error("Error fetching organizations:", error);
46+
return { success: false, error: "Failed to fetch organizations" };
47+
}
48+
}
49+
50+
// --- Fetch Admin Users (@trycomp.ai) ---
51+
export async function fetchAdminUsers(): Promise<ActionResponse<User[]>> {
52+
const session = await auth.api.getSession({ headers: await headers() });
53+
if (!session?.user?.email?.endsWith("@trycomp.ai")) {
54+
return { success: false, error: "Unauthorized: Admin access required" };
55+
}
56+
57+
try {
58+
const adminUsers = await db.user.findMany({
59+
where: {
60+
email: {
61+
endsWith: "@trycomp.ai",
62+
},
63+
},
64+
orderBy: {
65+
email: "asc",
66+
},
67+
});
68+
return { success: true, data: adminUsers };
69+
} catch (error) {
70+
console.error("Error fetching admin users:", error);
71+
return { success: false, error: "Failed to fetch admin users" };
72+
}
73+
}
74+
75+
// --- Add Member to Organization --- Renamed from addSelfToOrg
76+
const addMemberSchema = z.object({
77+
organizationId: z.string(),
78+
targetUserId: z.string(), // ID of the user to add
79+
});
80+
81+
export async function addMemberToOrg(
82+
input: z.infer<typeof addMemberSchema>,
83+
): Promise<ActionResponse<MemberWithUser>> {
84+
// Authorization check: Ensure the CALLER is an admin
85+
const session = await auth.api.getSession({ headers: await headers() });
86+
if (!session?.user?.email?.endsWith("@trycomp.ai")) {
87+
return {
88+
success: false,
89+
error: "Unauthorized: Caller is not an admin",
90+
};
91+
}
92+
93+
// Validate input
94+
const parseResult = addMemberSchema.safeParse(input);
95+
if (!parseResult.success) {
96+
return { success: false, error: "Invalid input" };
97+
}
98+
const { organizationId, targetUserId } = parseResult.data;
99+
100+
try {
101+
// Check if member already exists
102+
const existingMember = await db.member.findFirst({
103+
where: {
104+
organizationId: organizationId,
105+
userId: targetUserId, // Check for the target user
106+
},
107+
include: { user: true },
108+
});
109+
110+
if (existingMember) {
111+
return { success: true, data: existingMember }; // Already a member
112+
}
113+
114+
// Use Kinde server-side API to add the TARGET member
115+
// Note: Ensure Kinde API allows adding arbitrary users by an authorized admin
116+
// This might require specific Kinde setup or permissions.
117+
await auth.api.addMember({
118+
body: {
119+
userId: targetUserId, // Use the target user ID
120+
organizationId: organizationId,
121+
role: Role.admin, // Defaulting to admin for now
122+
},
123+
});
124+
125+
// Refetch the newly created member with user details
126+
const createdMember = await db.member.findFirst({
127+
where: {
128+
organizationId: organizationId,
129+
userId: targetUserId,
130+
},
131+
include: { user: true },
132+
});
133+
134+
if (!createdMember) {
135+
console.error(
136+
"Failed to retrieve member immediately after adding via Kinde API.",
137+
);
138+
return {
139+
success: false,
140+
error: "Failed to confirm membership creation.",
141+
};
142+
}
143+
144+
return { success: true, data: createdMember };
145+
} catch (error) {
146+
console.error("Error adding member to organization:", error);
147+
const errorMessage =
148+
error instanceof Error ? error.message : "Failed to add member";
149+
return { success: false, error: errorMessage };
150+
}
151+
}
152+
153+
// --- Remove Member from Organization ---
154+
const removeMemberSchema = z.object({
155+
organizationId: z.string(),
156+
targetUserId: z.string(), // ID of the user to remove
157+
});
158+
159+
export async function removeMember(
160+
input: z.infer<typeof removeMemberSchema>,
161+
): Promise<ActionResponse<{ removed: boolean }>> {
162+
// Authorization check: Ensure the CALLER is an admin
163+
const session = await auth.api.getSession({ headers: await headers() });
164+
if (!session?.user?.email?.endsWith("@trycomp.ai")) {
165+
return {
166+
success: false,
167+
error: "Unauthorized: Caller is not an admin",
168+
};
169+
}
170+
171+
// Validate input
172+
const parseResult = removeMemberSchema.safeParse(input);
173+
if (!parseResult.success) {
174+
return { success: false, error: "Invalid input" };
175+
}
176+
const { organizationId, targetUserId } = parseResult.data;
177+
178+
// Prevent admin from removing themselves via this action (they should use leaveOrganization)
179+
if (session.user?.id === targetUserId) {
180+
return {
181+
success: false,
182+
error: "Admin cannot remove self using this action. Use 'Leave Organization'.",
183+
};
184+
}
185+
186+
try {
187+
// Check if the target user is actually a member
188+
const targetMember = await db.member.findFirst({
189+
where: {
190+
organizationId: organizationId,
191+
userId: targetUserId,
192+
},
193+
});
194+
195+
if (!targetMember) {
196+
return {
197+
success: false,
198+
error: "Target user is not a member of this organization.",
199+
};
200+
}
201+
202+
// Prevent removing the organization owner
203+
if (targetMember.role === Role.owner) {
204+
return {
205+
success: false,
206+
error: "Cannot remove the organization owner.",
207+
};
208+
}
209+
210+
// Use Kinde server-side API to remove the TARGET member
211+
// Check Kinde documentation for the correct server-side method.
212+
// Assuming a hypothetical `auth.api.removeMember` exists.
213+
// If not, direct DB deletion is needed, but ensure cascading deletes or other relations are handled.
214+
215+
// Placeholder for Kinde remove member API call or DB operation
216+
// await auth.api.removeMember({ organizationId, userId: targetUserId });
217+
// OR direct DB deletion:
218+
await db.member.deleteMany({
219+
where: {
220+
organizationId: organizationId,
221+
userId: targetUserId,
222+
},
223+
});
224+
// Consider if associated sessions for the removed user should be invalidated:
225+
// await db.session.deleteMany({ where: { userId: targetUserId, organizationId: organizationId } });
226+
227+
return { success: true, data: { removed: true } };
228+
} catch (error) {
229+
console.error("Error removing member from organization:", error);
230+
const errorMessage =
231+
error instanceof Error ? error.message : "Failed to remove member";
232+
return { success: false, error: errorMessage };
233+
}
234+
}
235+
236+
// --- Fetch Organization Members --- Replaces fetchOrgMembersAction
237+
const fetchMembersSchema = z.object({
238+
organizationId: z.string(),
239+
});
240+
241+
export async function fetchOrgMembers(
242+
input: z.infer<typeof fetchMembersSchema>,
243+
): Promise<ActionResponse<MemberWithUser[]>> {
244+
const session = await auth.api.getSession({ headers: await headers() });
245+
if (!session?.user?.email?.endsWith("@trycomp.ai")) {
246+
return { success: false, error: "Unauthorized: Admin access required" };
247+
}
248+
249+
// Validate input
250+
const parseResult = fetchMembersSchema.safeParse(input);
251+
if (!parseResult.success) {
252+
return { success: false, error: "Invalid input" };
253+
}
254+
const { organizationId } = parseResult.data;
255+
256+
try {
257+
const members = await db.member.findMany({
258+
where: {
259+
organizationId: organizationId,
260+
},
261+
include: {
262+
user: true,
263+
},
264+
orderBy: {
265+
user: {
266+
name: "asc",
267+
},
268+
},
269+
});
270+
return { success: true, data: members };
271+
} catch (error) {
272+
console.error("Error fetching organization members:", error);
273+
return { success: false, error: "Failed to fetch members" };
274+
}
275+
}

0 commit comments

Comments
 (0)