Skip to content

Commit 748d0eb

Browse files
committed
STNDP-178 Disable updating settings for non-admin users (#113)
1 parent 824f1d9 commit 748d0eb

File tree

10 files changed

+521
-225
lines changed

10 files changed

+521
-225
lines changed

apps/api/src/boards/boards.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CreateBoardDto } from './dto/create-board.dto';
1515
import { Board } from 'src/libs/db/schema';
1616
import { AuthenticatedRequest, AuthGuard } from 'src/auth/guards/auth.guard';
1717
import { BoardAccessGuard } from './guards/board-access.guard';
18+
import { AdminRoleGuard } from './guards/admin-role.guard';
1819
import { UsersService } from 'src/auth/users/users.service';
1920
import { UpdateBoardDto } from 'src/boards/dto/update-board.dto';
2021

@@ -74,7 +75,7 @@ export class BoardsController {
7475
}
7576
}
7677

77-
@UseGuards(BoardAccessGuard)
78+
@UseGuards(BoardAccessGuard, AdminRoleGuard)
7879
@Patch(':boardId')
7980
async update(
8081
@Param('boardId', ParseIntPipe) boardId: number,

apps/api/src/boards/invitation/invitation.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { AuthGuard, AuthenticatedRequest } from '../../auth/guards/auth.guard';
1616
import { PermissiveAuth } from '../../auth/decorators/permissive-auth.decorator';
1717
import { BoardAccessGuard } from '../guards/board-access.guard';
18+
import { AdminRoleGuard } from '../guards/admin-role.guard';
1819
import { InvitationService } from './invitation.service';
1920
import { RegenerateInvitationDto } from './dto/regenerate-invitation.dto';
2021

@@ -57,6 +58,7 @@ export class InvitationController {
5758

5859
// POST /boards/:boardId/invitation/regenerate - Generate new invitation (deactivate existing)
5960
@Post('regenerate')
61+
@UseGuards(AdminRoleGuard)
6062
async regenerate(
6163
@Req() req: AuthenticatedRequest,
6264
@Param('boardId', ParseIntPipe) boardId: number,

apps/api/src/boards/standup-forms/standup-forms.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { StandupFormsService } from './standup-forms.service';
1313
import { CreateStandupFormDto } from './dto/create-standup-form.dto';
1414
import { AuthGuard } from 'src/auth/guards/auth.guard';
1515
import { BoardAccessGuard } from '../guards/board-access.guard';
16+
import { AdminRoleGuard } from '../guards/admin-role.guard';
1617

1718
@Controller('boards/:boardId/standup-forms')
1819
@UseGuards(AuthGuard, BoardAccessGuard)
@@ -21,6 +22,7 @@ export class StandupFormsController {
2122

2223
// POST /boards/:boardId/standup-forms
2324
@Post()
25+
@UseGuards(AdminRoleGuard)
2426
async create(
2527
@Param('boardId', ParseIntPipe) boardId: number,
2628
@Body() createStandupFormDto: CreateStandupFormDto,

apps/web/app/routes/board-settings-collaborators-route/collaborators-setting.tsx

Lines changed: 141 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
AlertDialog,
1010
Tooltip,
1111
useThemeContext,
12+
Callout,
1213
} from "@radix-ui/themes";
1314
import {
1415
Await,
@@ -85,10 +86,10 @@ function CollaboratorsTable({
8586

8687
const { appearance } = useThemeContext();
8788

88-
// Determine if the table is editable (current user is admin)
89-
const isEditable = useMemo(() => {
89+
// Check if current user is an admin
90+
const isCurrentUserAdmin = useMemo(() => {
91+
if (!currentUser) return false;
9092
return (
91-
currentUser &&
9293
collaborators.find((c: Collaborator) => c.userId === currentUser.id)
9394
?.role === "admin"
9495
);
@@ -129,14 +130,13 @@ function CollaboratorsTable({
129130

130131
useEffect(() => {
131132
// Only update draft state when safe to do so:
132-
// - If table is not editable (user can't make changes anyway)
133-
// - If table is editable but user has no pending changes
134-
// Note: When isEditable && hasEdited, we preserve user's draft changes
135-
if (!isEditable || !hasEdited) {
133+
// - Sync with server data when there are no pending edits
134+
// - Preserve draft when there are pending edits (admin only can edit)
135+
if (!hasEdited) {
136136
collaboratorsRef.current = collaborators;
137137
setDraftCollaborators(collaborators);
138138
}
139-
}, [collaborators, isEditable, hasEdited]);
139+
}, [collaborators, hasEdited]);
140140

141141
const changeRole = useCallback(
142142
(collaborator: Collaborator, newRole: "admin" | "collaborator") => {
@@ -257,6 +257,17 @@ function CollaboratorsTable({
257257

258258
return (
259259
<>
260+
{!isCurrentUserAdmin && (
261+
<Callout.Root size="1" variant="soft" className="py-2!" mt="5">
262+
<Callout.Icon>
263+
<InfoCircledIcon />
264+
</Callout.Icon>
265+
<Callout.Text>
266+
You will need admin privileges to update this setting.
267+
</Callout.Text>
268+
</Callout.Root>
269+
)}
270+
260271
<Table.Root mt="5">
261272
<Table.Header>
262273
<Table.Row>
@@ -286,9 +297,7 @@ function CollaboratorsTable({
286297
</Flex>
287298
</Tooltip>
288299
</Table.ColumnHeaderCell>
289-
{isEditable && (
290-
<Table.ColumnHeaderCell width="auto"></Table.ColumnHeaderCell>
291-
)}
300+
<Table.ColumnHeaderCell width="auto"></Table.ColumnHeaderCell>
292301
</Table.Row>
293302
</Table.Header>
294303

@@ -318,8 +327,6 @@ function CollaboratorsTable({
318327
draftCollaborators.filter((c) => c.role === "admin").length ===
319328
1;
320329

321-
// Get current user's role on this board (for reference, actual editability is determined by isEditable)
322-
323330
return (
324331
<Table.Row key={collaborator.userId}>
325332
<Table.RowHeaderCell
@@ -346,147 +353,140 @@ function CollaboratorsTable({
346353
>
347354
{role}
348355
</Table.Cell>
349-
{isEditable && (
350-
<Table.Cell className="align-middle!">
351-
<DropdownMenu.Root>
352-
<DropdownMenu.Trigger>
353-
<Button variant="soft" highContrast>
354-
Edit
355-
<DropdownMenu.TriggerIcon />
356-
</Button>
357-
</DropdownMenu.Trigger>
358-
<DropdownMenu.Content>
359-
<DropdownMenu.Group>
360-
<DropdownMenu.Label>
361-
Select roles
362-
</DropdownMenu.Label>
363-
<DropdownMenu.CheckboxItem
364-
checked={role === "admin" && !hasRemoved}
365-
onCheckedChange={() =>
366-
changeRole(collaborator, "admin")
367-
}
368-
>
369-
Admin
370-
</DropdownMenu.CheckboxItem>
371-
<DropdownMenu.CheckboxItem
372-
checked={role === "collaborator" && !hasRemoved}
373-
onCheckedChange={() =>
374-
changeRole(collaborator, "collaborator")
375-
}
376-
disabled={isOnlyActiveAdmin}
377-
>
378-
Collaborator
379-
</DropdownMenu.CheckboxItem>
380-
</DropdownMenu.Group>
381-
<DropdownMenu.Separator />
382-
383-
<DropdownMenu.Item
384-
onSelect={() => {
385-
removeCollaborator(collaborator);
386-
}}
387-
disabled={isCurrentUser || isOnlyActiveAdmin}
356+
<Table.Cell className="align-middle!">
357+
<DropdownMenu.Root>
358+
<DropdownMenu.Trigger disabled={!isCurrentUserAdmin}>
359+
<Button variant="soft" highContrast>
360+
Edit
361+
<DropdownMenu.TriggerIcon />
362+
</Button>
363+
</DropdownMenu.Trigger>
364+
<DropdownMenu.Content>
365+
<DropdownMenu.Group>
366+
<DropdownMenu.Label>Select roles</DropdownMenu.Label>
367+
<DropdownMenu.CheckboxItem
368+
checked={role === "admin" && !hasRemoved}
369+
onCheckedChange={() =>
370+
changeRole(collaborator, "admin")
371+
}
388372
>
389-
Remove access
390-
</DropdownMenu.Item>
391-
</DropdownMenu.Content>
392-
</DropdownMenu.Root>
393-
</Table.Cell>
394-
)}
373+
Admin
374+
</DropdownMenu.CheckboxItem>
375+
<DropdownMenu.CheckboxItem
376+
checked={role === "collaborator" && !hasRemoved}
377+
onCheckedChange={() =>
378+
changeRole(collaborator, "collaborator")
379+
}
380+
disabled={isOnlyActiveAdmin}
381+
>
382+
Collaborator
383+
</DropdownMenu.CheckboxItem>
384+
</DropdownMenu.Group>
385+
<DropdownMenu.Separator />
386+
387+
<DropdownMenu.Item
388+
onSelect={() => {
389+
removeCollaborator(collaborator);
390+
}}
391+
disabled={isCurrentUser || isOnlyActiveAdmin}
392+
>
393+
Remove access
394+
</DropdownMenu.Item>
395+
</DropdownMenu.Content>
396+
</DropdownMenu.Root>
397+
</Table.Cell>
395398
</Table.Row>
396399
);
397400
})}
398401
</Table.Body>
399402
</Table.Root>
400403
<Flex justify="end" mt="5" gap="3" align="center">
401-
{isEditable && (
404+
{hasEdited && (
402405
<>
403-
{hasEdited && (
404-
<>
405-
<Text size="2" color="gray" className="italic">
406-
Pending changes
407-
</Text>
408-
<Button
409-
variant="outline"
410-
size="2"
411-
onClick={() => {
412-
collaboratorsRef.current = collaborators;
413-
setDraftCollaborators(collaborators);
414-
}}
406+
<Text size="2" color="gray" className="italic">
407+
Pending changes
408+
</Text>
409+
<Button
410+
variant="outline"
411+
size="2"
412+
onClick={() => {
413+
collaboratorsRef.current = collaborators;
414+
setDraftCollaborators(collaborators);
415+
}}
416+
disabled={!isCurrentUserAdmin}
417+
>
418+
Cancel
419+
</Button>
420+
</>
421+
)}
422+
423+
<AlertDialog.Root>
424+
<AlertDialog.Trigger>
425+
<Button
426+
highContrast
427+
size="2"
428+
loading={isSubmitting}
429+
disabled={!isCurrentUserAdmin || !hasEdited || isSubmitting}
430+
>
431+
Save
432+
</Button>
433+
</AlertDialog.Trigger>
434+
<AlertDialog.Content maxWidth="450px">
435+
<AlertDialog.Title>Save changes?</AlertDialog.Title>
436+
<AlertDialog.Description size="2">
437+
<Text>Are you sure you want to save these changes?</Text>
438+
<br />
439+
<br />
440+
{warnings.length > 0 && (
441+
<div
442+
className={`prose prose-sm ${
443+
appearance === "dark" ? "prose-invert" : ""
444+
}`}
415445
>
446+
<ul style={{ marginTop: "0", paddingLeft: "16px" }}>
447+
{warnings.map((warning, index) => (
448+
<li key={index} style={{ marginBottom: "4px" }}>
449+
<Text size="2">{warning}</Text>
450+
</li>
451+
))}
452+
</ul>
453+
</div>
454+
)}
455+
<br />
456+
</AlertDialog.Description>
457+
458+
<Flex gap="3" mt="4" justify="end">
459+
<AlertDialog.Cancel>
460+
<Button variant="soft" color="gray">
416461
Cancel
417462
</Button>
418-
</>
419-
)}
420-
421-
<AlertDialog.Root>
422-
<AlertDialog.Trigger>
423-
<Button
424-
highContrast
425-
size="2"
426-
loading={isSubmitting}
427-
disabled={!hasEdited || isSubmitting}
428-
>
463+
</AlertDialog.Cancel>
464+
<AlertDialog.Action
465+
onClick={() => {
466+
if (!boardId) return;
467+
468+
fetcher.submit(
469+
{
470+
collaborators: draftCollaborators.map((c) => ({
471+
userId: c.userId,
472+
role: c.role,
473+
})),
474+
},
475+
{
476+
encType: "application/json",
477+
method: "POST",
478+
action: `/boards/${boardId}/collaborators/update`,
479+
}
480+
);
481+
}}
482+
>
483+
<Button variant="solid" highContrast loading={isSubmitting}>
429484
Save
430485
</Button>
431-
</AlertDialog.Trigger>
432-
<AlertDialog.Content maxWidth="450px">
433-
<AlertDialog.Title>Save changes?</AlertDialog.Title>
434-
<AlertDialog.Description size="2">
435-
<Text>Are you sure you want to save these changes?</Text>
436-
<br />
437-
<br />
438-
{warnings.length > 0 && (
439-
<div
440-
className={`prose prose-sm ${
441-
appearance === "dark" ? "prose-invert" : ""
442-
}`}
443-
>
444-
<ul style={{ marginTop: "0", paddingLeft: "16px" }}>
445-
{warnings.map((warning, index) => (
446-
<li key={index} style={{ marginBottom: "4px" }}>
447-
<Text size="2">{warning}</Text>
448-
</li>
449-
))}
450-
</ul>
451-
</div>
452-
)}
453-
<br />
454-
</AlertDialog.Description>
455-
456-
<Flex gap="3" mt="4" justify="end">
457-
<AlertDialog.Cancel>
458-
<Button variant="soft" color="gray">
459-
Cancel
460-
</Button>
461-
</AlertDialog.Cancel>
462-
<AlertDialog.Action
463-
onClick={() => {
464-
if (!boardId) return;
465-
466-
fetcher.submit(
467-
{
468-
collaborators: draftCollaborators.map((c) => ({
469-
userId: c.userId,
470-
role: c.role,
471-
})),
472-
},
473-
{
474-
encType: "application/json",
475-
method: "POST",
476-
action: `/boards/${boardId}/collaborators/update`,
477-
}
478-
);
479-
}}
480-
>
481-
<Button variant="solid" highContrast loading={isSubmitting}>
482-
Save
483-
</Button>
484-
</AlertDialog.Action>
485-
</Flex>
486-
</AlertDialog.Content>
487-
</AlertDialog.Root>
488-
</>
489-
)}
486+
</AlertDialog.Action>
487+
</Flex>
488+
</AlertDialog.Content>
489+
</AlertDialog.Root>
490490
</Flex>
491491
</>
492492
);

0 commit comments

Comments
 (0)