Skip to content

Commit 2128fca

Browse files
committed
feat: core team
1 parent 84084cb commit 2128fca

17 files changed

Lines changed: 927 additions & 289 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"react": "19.2.3",
7878
"react-day-picker": "9.13.2",
7979
"react-dom": "19.2.3",
80+
"react-easy-crop": "^5.5.7",
8081
"react-hook-form": "7.67.0",
8182
"react-markdown": "10.1.0",
8283
"react-pdf": "10.3.0",

pnpm-lock.yaml

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/api/admin/faculty/[id]/route.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type NextRequest, NextResponse } from "next/server";
22
import { z } from "zod";
3-
import { permissionProtected, type RouteContext } from "~/auth/routes-wrapper";
3+
import { adminProtected, type RouteContext } from "~/auth/routes-wrapper";
44
import {
55
deleteFacultyMember,
66
findFacultyMemberById,
@@ -21,8 +21,7 @@ const updateFacultyRequestSchema = updateFacultySchema.extend({
2121
socialLinks: updateFacultySchema.shape.socialLinks.optional(),
2222
});
2323

24-
export const PUT = permissionProtected<FacultyMemberParams>(
25-
["team:view_all"],
24+
export const PUT = adminProtected<FacultyMemberParams>(
2625
async (
2726
request: NextRequest,
2827
{ params }: RouteContext<FacultyMemberParams>,
@@ -73,8 +72,7 @@ export const PUT = permissionProtected<FacultyMemberParams>(
7372
},
7473
);
7574

76-
export const DELETE = permissionProtected<FacultyMemberParams>(
77-
["team:view_all"],
75+
export const DELETE = adminProtected<FacultyMemberParams>(
7876
async (
7977
_request: NextRequest,
8078
{ params }: RouteContext<FacultyMemberParams>,

src/app/api/admin/faculty/[id]/toggle/route.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type NextRequest, NextResponse } from "next/server";
2-
import { permissionProtected, type RouteContext } from "~/auth/routes-wrapper";
2+
import { adminProtected, type RouteContext } from "~/auth/routes-wrapper";
33
import {
44
normalizeFacultyMember,
55
toggleFacultyMemberStatus,
@@ -9,8 +9,7 @@ type FacultyMemberParams = {
99
id: string;
1010
};
1111

12-
export const PATCH = permissionProtected<FacultyMemberParams>(
13-
["team:view_all"],
12+
export const PATCH = adminProtected<FacultyMemberParams>(
1413
async (
1514
_request: NextRequest,
1615
{ params }: RouteContext<FacultyMemberParams>,

src/app/api/admin/faculty/route.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type NextRequest, NextResponse } from "next/server";
22
import { z } from "zod";
3-
import { permissionProtected } from "~/auth/routes-wrapper";
3+
import { adminProtected } from "~/auth/routes-wrapper";
44
import {
55
createFacultyMember,
66
listAllFacultyMembers,
@@ -9,8 +9,7 @@ import {
99
import { AppError } from "~/lib/errors/app-error";
1010
import { createFacultySchema } from "~/lib/validation/faculty";
1111

12-
export const GET = permissionProtected(
13-
["team:view_all"],
12+
export const GET = adminProtected(
1413
async (_request: NextRequest) => {
1514
try {
1615
const faculty = await listAllFacultyMembers();
@@ -27,8 +26,7 @@ export const GET = permissionProtected(
2726
},
2827
);
2928

30-
export const POST = permissionProtected(
31-
["team:view_all"],
29+
export const POST = adminProtected(
3230
async (request: NextRequest) => {
3331
const requestStart = Date.now();
3432

src/app/api/admin/teams/[id]/route.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { type NextRequest, NextResponse } from "next/server";
22
import { z } from "zod";
3-
import { permissionProtected, type RouteContext } from "~/auth/routes-wrapper";
3+
import {
4+
type DashboardUser,
5+
permissionProtected,
6+
type RouteContext,
7+
} from "~/auth/routes-wrapper";
48
import {
59
deleteTeamMember,
610
findTeamMemberById,
@@ -11,6 +15,11 @@ import {
1115
deleteCloudinaryAsset,
1216
extractCloudinaryPublicId,
1317
} from "~/lib/cloudinary/server";
18+
import {
19+
getAllowedCommittees,
20+
type TeamCommittee,
21+
} from "~/lib/constants/team-committees";
22+
import { isAdmin } from "~/lib/auth/permissions";
1423
import { updateTeamMemberSchema } from "~/lib/validation/team-member";
1524

1625
type TeamMemberParams = {
@@ -21,13 +30,35 @@ const updateTeamMemberRequestSchema = updateTeamMemberSchema.extend({
2130
socialLinks: updateTeamMemberSchema.shape.socialLinks.optional(),
2231
});
2332

33+
function getUserPermissionKeys(user: NonNullable<DashboardUser>): string[] {
34+
return user.roles.flatMap((r) => r.permissions).map((p) => p.key);
35+
}
36+
function canManageCommittee(
37+
user: NonNullable<DashboardUser>,
38+
committee: string,
39+
): boolean {
40+
const allowed = getAllowedCommittees(
41+
getUserPermissionKeys(user),
42+
isAdmin(user),
43+
);
44+
return allowed.includes(committee as TeamCommittee);
45+
}
46+
2447
export const PUT = permissionProtected<TeamMemberParams>(
25-
["team:view_all"],
26-
async (request: NextRequest, { params }: RouteContext<TeamMemberParams>) => {
48+
["core:manage"],
49+
async (request: NextRequest, { params }: RouteContext<TeamMemberParams>, user) => {
2750
try {
2851
const { id } = await params;
2952
const existing = await findTeamMemberById(id);
3053

54+
// Verify the user can manage this member's committee
55+
if (!canManageCommittee(user, existing.committee)) {
56+
return NextResponse.json(
57+
{ message: "You do not have permission to manage this committee" },
58+
{ status: 403 },
59+
);
60+
}
61+
3162
const body = await request.json();
3263
const parsed = updateTeamMemberRequestSchema.safeParse(body);
3364

@@ -38,6 +69,17 @@ export const PUT = permissionProtected<TeamMemberParams>(
3869
);
3970
}
4071

72+
if (
73+
parsed.data.committee &&
74+
parsed.data.committee !== existing.committee &&
75+
!canManageCommittee(user, parsed.data.committee)
76+
) {
77+
return NextResponse.json(
78+
{ message: "You do not have permission to move members to that committee" },
79+
{ status: 403 },
80+
);
81+
}
82+
4183
const nextPhoto = parsed.data.photo;
4284
const hasPhotoReplacement = !!nextPhoto && nextPhoto !== existing.photo;
4385

@@ -71,10 +113,21 @@ export const PUT = permissionProtected<TeamMemberParams>(
71113
);
72114

73115
export const DELETE = permissionProtected<TeamMemberParams>(
74-
["team:view_all"],
75-
async (_request: NextRequest, { params }: RouteContext<TeamMemberParams>) => {
116+
["core:manage"],
117+
async (_request: NextRequest, { params }: RouteContext<TeamMemberParams>, user) => {
76118
try {
77119
const { id } = await params;
120+
121+
// Fetch member first to check committee
122+
const existing = await findTeamMemberById(id);
123+
124+
if (!canManageCommittee(user, existing.committee)) {
125+
return NextResponse.json(
126+
{ message: "You do not have permission to manage this committee" },
127+
{ status: 403 },
128+
);
129+
}
130+
78131
const member = await deleteTeamMember(id);
79132

80133
const cloudinaryId =

src/app/api/admin/teams/[id]/toggle/route.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,42 @@
11
import { type NextRequest, NextResponse } from "next/server";
2-
import { permissionProtected, type RouteContext } from "~/auth/routes-wrapper";
32
import {
3+
type DashboardUser,
4+
permissionProtected,
5+
type RouteContext,
6+
} from "~/auth/routes-wrapper";
7+
import {
8+
findTeamMemberById,
49
normalizeTeamMember,
510
toggleTeamMemberStatus,
611
} from "~/db/services/team-member-services";
12+
import {
13+
getAllowedCommittees,
14+
type TeamCommittee,
15+
} from "~/lib/constants/team-committees";
16+
import { isAdmin } from "~/lib/auth/permissions";
717

818
type TeamMemberParams = {
919
id: string;
1020
};
1121

1222
export const PATCH = permissionProtected<TeamMemberParams>(
13-
["team:view_all"],
14-
async (_request: NextRequest, { params }: RouteContext<TeamMemberParams>) => {
23+
["core:manage"],
24+
async (_request: NextRequest, { params }: RouteContext<TeamMemberParams>, user) => {
1525
try {
1626
const { id } = await params;
27+
28+
// Verify the user can manage this member's committee
29+
const existing = await findTeamMemberById(id);
30+
const permKeys = user.roles.flatMap((r) => r.permissions).map((p) => p.key);
31+
const allowed = getAllowedCommittees(permKeys, isAdmin(user));
32+
33+
if (!allowed.includes(existing.committee as TeamCommittee)) {
34+
return NextResponse.json(
35+
{ message: "You do not have permission to manage this committee" },
36+
{ status: 403 },
37+
);
38+
}
39+
1740
const member = await toggleTeamMemberStatus(id);
1841

1942
return NextResponse.json({

src/app/api/admin/teams/route.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
11
import { type NextRequest, NextResponse } from "next/server";
22
import { z } from "zod";
3-
import { permissionProtected } from "~/auth/routes-wrapper";
3+
import {
4+
type DashboardUser,
5+
permissionProtected,
6+
} from "~/auth/routes-wrapper";
47
import {
58
createTeamMember,
69
listAllTeamMembers,
710
normalizeTeamMember,
811
} from "~/db/services/team-member-services";
12+
import {
13+
getAllowedCommittees,
14+
type TeamCommittee,
15+
} from "~/lib/constants/team-committees";
16+
import { isAdmin } from "~/lib/auth/permissions";
917
import { createTeamMemberSchema } from "~/lib/validation/team-member";
1018

19+
function getUserPermissionKeys(user: NonNullable<DashboardUser>): string[] {
20+
return user.roles.flatMap((r) => r.permissions).map((p) => p.key);
21+
}
22+
1123
export const GET = permissionProtected(
12-
["team:view_all"],
13-
async (_request: NextRequest) => {
24+
["core:manage"],
25+
async (_request: NextRequest, _ctx, user) => {
1426
try {
1527
const members = await listAllTeamMembers();
28+
29+
const allowed = getAllowedCommittees(
30+
getUserPermissionKeys(user),
31+
isAdmin(user),
32+
);
33+
34+
const filtered = members.filter((m) =>
35+
allowed.includes(m.committee as TeamCommittee),
36+
);
37+
1638
return NextResponse.json({
17-
members: members.map(normalizeTeamMember),
39+
members: filtered.map(normalizeTeamMember),
1840
});
1941
} catch (error) {
2042
console.error("Error fetching team members:", error);
@@ -27,12 +49,25 @@ export const GET = permissionProtected(
2749
);
2850

2951
export const POST = permissionProtected(
30-
["team:view_all"],
31-
async (request: NextRequest) => {
52+
["core:manage"],
53+
async (request: NextRequest, _ctx, user) => {
3254
const requestStart = Date.now();
3355

3456
try {
3557
const body = await request.json();
58+
59+
const allowed = getAllowedCommittees(
60+
getUserPermissionKeys(user),
61+
isAdmin(user),
62+
);
63+
64+
if (body?.committee && !allowed.includes(body.committee as TeamCommittee)) {
65+
return NextResponse.json(
66+
{ message: "You do not have permission to add members to this committee" },
67+
{ status: 403 },
68+
);
69+
}
70+
3671
console.info("[API][admin/teams][POST] Request received", {
3772
hasPhoto: Boolean(body?.photo),
3873
committee: body?.committee,

0 commit comments

Comments
 (0)