Skip to content

Commit 49f5924

Browse files
authored
Only show leader of group if unauthenticated (#3055)
1 parent cc60ca0 commit 49f5924

File tree

7 files changed

+231
-10
lines changed

7 files changed

+231
-10
lines changed

apps/rpc/src/modules/authorization-service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { GroupId, GroupRoleType, UserId } from "@dotkomonline/types"
33
import { minutesToMilliseconds } from "date-fns"
44
import { LRUCache } from "lru-cache"
55

6+
export const HOVEDSTYRET_GROUP_SLUG = "hs"
7+
68
export const COMMITTEE_AFFILIATIONS = [
79
"appkom",
810
"arrkom",
@@ -13,7 +15,7 @@ export const COMMITTEE_AFFILIATIONS = [
1315
"dotkom",
1416
"fagkom",
1517
"feminit",
16-
"hs",
18+
HOVEDSTYRET_GROUP_SLUG,
1719
"online-il",
1820
"fond",
1921
"prokom",
@@ -24,7 +26,7 @@ export const COMMITTEE_AFFILIATIONS = [
2426
"velkom",
2527
] as const satisfies GroupId[]
2628

27-
export const ADMIN_AFFILIATIONS = ["dotkom", "hs"] as const satisfies GroupId[]
29+
export const ADMIN_AFFILIATIONS = ["dotkom", HOVEDSTYRET_GROUP_SLUG] as const satisfies GroupId[]
2830

2931
export interface AuthorizationService {
3032
/**
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { DBHandle } from "@dotkomonline/db"
2+
import { getGroupRepository } from "../group-repository"
3+
4+
describe("GroupRepository.findManyGroupMemberships", () => {
5+
const groupRepository = getGroupRepository()
6+
7+
const createHandle = () => {
8+
const findMany = vi.fn().mockResolvedValue([])
9+
const handle = {
10+
groupMembership: {
11+
findMany,
12+
},
13+
} as unknown as DBHandle
14+
15+
return { handle, findMany }
16+
}
17+
18+
it("queries all memberships for a group when userId is not provided", async () => {
19+
const { handle, findMany } = createHandle()
20+
21+
await groupRepository.findManyGroupMemberships(handle, "dotkom")
22+
23+
expect(findMany).toHaveBeenCalledWith({
24+
where: {
25+
groupId: "dotkom",
26+
},
27+
include: {
28+
roles: {
29+
include: {
30+
role: true,
31+
},
32+
},
33+
},
34+
})
35+
})
36+
37+
it("filters memberships by user when userId is provided", async () => {
38+
const { handle, findMany } = createHandle()
39+
40+
await groupRepository.findManyGroupMemberships(handle, "dotkom", "user-1")
41+
42+
expect(findMany).toHaveBeenCalledWith({
43+
where: {
44+
groupId: "dotkom",
45+
userId: "user-1",
46+
},
47+
include: {
48+
roles: {
49+
include: {
50+
role: true,
51+
},
52+
},
53+
},
54+
})
55+
})
56+
})
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { DBHandle } from "@dotkomonline/db"
2+
import type { TRPCContext } from "../../../trpc"
3+
import { groupRouter } from "../group-router"
4+
5+
describe("group router service forwarding", () => {
6+
it("routes anonymous non-Hovedstyret getMembers to getLeaders", async () => {
7+
const txHandle = {} as DBHandle
8+
const groupService = {
9+
getLeaders: vi.fn().mockResolvedValue([]),
10+
getMembers: vi.fn(),
11+
getMember: vi.fn(),
12+
}
13+
14+
const ctx = {
15+
principal: null,
16+
prisma: {
17+
$transaction: vi.fn(async (fn: (handle: DBHandle) => Promise<unknown>) => await fn(txHandle)),
18+
},
19+
groupService,
20+
addAuthorizationGuard: vi.fn(),
21+
} as unknown as TRPCContext
22+
23+
const caller = groupRouter.createCaller(ctx)
24+
25+
await caller.getMembers("dotkom")
26+
27+
expect(groupService.getLeaders).toHaveBeenCalledWith(txHandle, "dotkom")
28+
expect(groupService.getMembers).not.toHaveBeenCalled()
29+
})
30+
31+
it("routes anonymous Hovedstyret getMembers to getMembers", async () => {
32+
const txHandle = {} as DBHandle
33+
const groupService = {
34+
getLeaders: vi.fn(),
35+
getMembers: vi.fn().mockResolvedValue(new Map()),
36+
getMember: vi.fn(),
37+
}
38+
39+
const ctx = {
40+
principal: null,
41+
prisma: {
42+
$transaction: vi.fn(async (fn: (handle: DBHandle) => Promise<unknown>) => await fn(txHandle)),
43+
},
44+
groupService,
45+
addAuthorizationGuard: vi.fn(),
46+
} as unknown as TRPCContext
47+
48+
const caller = groupRouter.createCaller(ctx)
49+
50+
await caller.getMembers("hs")
51+
52+
expect(groupService.getMembers).toHaveBeenCalledWith(txHandle, "hs")
53+
expect(groupService.getLeaders).not.toHaveBeenCalled()
54+
})
55+
56+
it("passes through group and user ids to getMember", async () => {
57+
const txHandle = {} as DBHandle
58+
const groupService = {
59+
getMembers: vi.fn(),
60+
getMember: vi.fn().mockResolvedValue({}),
61+
}
62+
63+
const ctx = {
64+
principal: {
65+
subject: "user-1",
66+
affiliations: new Map(),
67+
},
68+
prisma: {
69+
$transaction: vi.fn(async (fn: (handle: DBHandle) => Promise<unknown>) => await fn(txHandle)),
70+
},
71+
groupService,
72+
addAuthorizationGuard: vi.fn(),
73+
} as unknown as TRPCContext
74+
75+
const caller = groupRouter.createCaller(ctx)
76+
77+
await caller.getMember({ groupId: "dotkom", userId: "user-1" })
78+
79+
expect(groupService.getMember).toHaveBeenCalledWith(txHandle, "dotkom", "user-1")
80+
})
81+
})

apps/rpc/src/modules/group/__test__/group-service.spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { randomUUID } from "node:crypto"
21
import type { S3Client } from "@aws-sdk/client-s3"
32
import type { Group } from "@dotkomonline/types"
43
import { PrismaClient } from "@prisma/client"
54
import type { ManagementClient } from "auth0"
5+
import { randomUUID } from "node:crypto"
66
import { getFeideGroupsRepository } from "src/modules/feide/feide-groups-repository"
77
import { getMembershipService } from "src/modules/user/membership-service"
88
import { getUserRepository } from "src/modules/user/user-repository"
99
import { getUserService } from "src/modules/user/user-service"
1010
import { mockDeep } from "vitest-mock-extended"
11-
import { NotFoundError } from "../../../error"
1211
import { getGroupRepository } from "../group-repository"
1312
import { getGroupService } from "../group-service"
1413

@@ -52,16 +51,20 @@ describe("GroupService", () => {
5251
showLeaderAsContact: false,
5352
recruitmentMethod: "AUTUMN_APPLICATION",
5453
}
55-
const id = randomUUID()
54+
vi.spyOn(groupRepository, "findBySlug")
55+
.mockResolvedValueOnce(null)
56+
.mockResolvedValueOnce({ ...group })
5657
vi.spyOn(groupRepository, "create").mockResolvedValueOnce({ ...group })
58+
vi.spyOn(groupRepository, "createGroupRoles").mockResolvedValueOnce([])
59+
5760
const created = await groupService.create(db, group)
58-
expect(created).toEqual({ id, ...group })
59-
expect(groupRepository.create).toHaveBeenCalledWith(db, group)
61+
expect(created).toEqual(group)
62+
expect(groupRepository.create).toHaveBeenCalledWith(db, "dotkom", group)
6063
})
6164

6265
it("does not find non-existent committees", async () => {
6366
const id = randomUUID()
6467
vi.spyOn(groupRepository, "findBySlug").mockResolvedValueOnce(null)
65-
await expect(async () => groupService.findBySlug(db, id)).rejects.toThrowError(NotFoundError)
68+
await expect(groupService.findBySlug(db, id)).resolves.toBeNull()
6669
})
6770
})

apps/rpc/src/modules/group/group-repository.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import type { DBHandle } from "@dotkomonline/db"
22
import {
33
type Group,
44
type GroupId,
5+
type GroupMember,
6+
GroupMemberSchema,
57
type GroupMembership,
68
type GroupMembershipId,
79
GroupMembershipSchema,
810
type GroupMembershipWrite,
911
type GroupRole,
1012
type GroupRoleId,
1113
GroupRoleSchema,
14+
type GroupRoleType,
1215
type GroupRoleWrite,
1316
GroupSchema,
1417
type GroupWrite,
@@ -42,6 +45,7 @@ export interface GroupRepository {
4245
groupRoleIds: Set<GroupRoleId>
4346
): Promise<GroupMembership>
4447
findGroupMembershipById(handle: DBHandle, groupMembershipId: GroupMembershipId): Promise<GroupMembership | null>
48+
findGroupMembersByRoleType(handle: DBHandle, groupSlug: GroupId, roleType: GroupRoleType): Promise<GroupMember[]>
4549
findManyGroupMemberships(handle: DBHandle, groupSlug: GroupId, userId?: UserId): Promise<GroupMembership[]>
4650

4751
createGroupRoles(handle: DBHandle, groupRolesData: GroupRoleWrite[]): Promise<GroupRole[]>
@@ -257,9 +261,63 @@ export function getGroupRepository(): GroupRepository {
257261
: null
258262
},
259263

264+
async findGroupMembersByRoleType(handle, groupSlug, roleType) {
265+
const users = await handle.user.findMany({
266+
where: {
267+
groupMemberships: {
268+
some: {
269+
groupId: groupSlug,
270+
roles: {
271+
some: {
272+
role: {
273+
type: roleType,
274+
},
275+
},
276+
},
277+
},
278+
},
279+
},
280+
include: {
281+
memberships: true,
282+
groupMemberships: {
283+
where: {
284+
groupId: groupSlug,
285+
roles: {
286+
some: {
287+
role: {
288+
type: roleType,
289+
},
290+
},
291+
},
292+
},
293+
include: {
294+
roles: {
295+
include: {
296+
role: true,
297+
},
298+
},
299+
},
300+
},
301+
},
302+
})
303+
304+
const groupMembers = users.map(({ groupMemberships, ...user }) => ({
305+
...user,
306+
groupMembership: groupMemberships.map(({ roles, ...membership }) => ({
307+
...membership,
308+
roles: roles.map((role) => role.role),
309+
})),
310+
}))
311+
312+
return parseOrReport(GroupMemberSchema.array(), groupMembers)
313+
},
314+
260315
async findManyGroupMemberships(handle, groupSlug, userId) {
261316
const memberships = await handle.groupMembership.findMany({
262-
where: { groupId: groupSlug, ...(userId ? { userId } : {}) },
317+
where: {
318+
groupId: groupSlug,
319+
...(userId ? { userId } : {}),
320+
},
263321
include: {
264322
roles: {
265323
include: {

apps/rpc/src/modules/group/group-router.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { z } from "zod"
1212
import { hasGroupRole, isAdministrator, isCommitteeMember, isGroupMember, or } from "../../authorization"
1313
import { withAuditLogEntry, withAuthentication, withAuthorization, withDatabaseTransaction } from "../../middlewares"
1414
import { procedure, t } from "../../trpc"
15+
import { HOVEDSTYRET_GROUP_SLUG } from "../authorization-service"
1516

1617
export type CreateGroupInput = inferProcedureInput<typeof createGroupProcedure>
1718
export type CreateGroupOutput = inferProcedureOutput<typeof createGroupProcedure>
@@ -133,7 +134,13 @@ export type GetMembersOutput = inferProcedureOutput<typeof getMembersProcedure>
133134
const getMembersProcedure = procedure
134135
.input(GroupSchema.shape.slug)
135136
.use(withDatabaseTransaction())
136-
.query(async ({ input, ctx }) => ctx.groupService.getMembers(ctx.handle, input))
137+
.query(async ({ input, ctx }) => {
138+
// We only show leaders of groups to unathenticated users, except for Hovedstyret who is public
139+
if (!ctx.principal && input !== HOVEDSTYRET_GROUP_SLUG) {
140+
return ctx.groupService.getLeaders(ctx.handle, input)
141+
}
142+
return ctx.groupService.getMembers(ctx.handle, input)
143+
})
137144

138145
export type GetMemberInput = inferProcedureInput<typeof getMemberProcedure>
139146
export type GetMemberOutput = inferProcedureOutput<typeof getMemberProcedure>

apps/rpc/src/modules/group/group-service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface GroupService {
5151

5252
getMember(handle: DBHandle, groupSlug: GroupId, userId: UserId): Promise<GroupMember>
5353
getMembers(handle: DBHandle, groupSlug: GroupId): Promise<Map<UserId, GroupMember>>
54+
getLeaders(handle: DBHandle, groupSlug: GroupId): Promise<Map<UserId, GroupMember>>
5455

5556
startMembership(
5657
handle: DBHandle,
@@ -154,6 +155,19 @@ export function getGroupService(
154155
return group
155156
},
156157

158+
async getLeaders(handle, groupSlug) {
159+
const leaders = await groupRepository.findGroupMembersByRoleType(handle, groupSlug, "LEADER")
160+
161+
if (leaders.length === 0) {
162+
throw new NotFoundError(`Leaders for Group(ID=${groupSlug}) not found`)
163+
}
164+
165+
return leaders.reduce((map, leader) => {
166+
map.set(leader.id, leader)
167+
return map
168+
}, new Map<UserId, GroupMember>())
169+
},
170+
157171
async findMany(handle) {
158172
return groupRepository.findMany(handle)
159173
},

0 commit comments

Comments
 (0)