Skip to content

Commit 3331554

Browse files
committed
feat: finalize Users and Permissions
1 parent 503ff98 commit 3331554

File tree

5 files changed

+92
-12
lines changed

5 files changed

+92
-12
lines changed

client/get-openapi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from "fs";
22

33
import swaggerJsdoc from "swagger-jsdoc";
44

5-
export const API_VERSION = "1.6.0";
5+
export const API_VERSION = "1.7.0";
66

77
const options = {
88
encoding: "utf8",

client/public/openapi.json

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.0.0",
33
"info": {
44
"title": "Feedback Flow API",
5-
"version": "1.6.0"
5+
"version": "1.7.0"
66
},
77
"paths": {
88
"/api/testers": {
@@ -92,7 +92,7 @@
9292
},
9393
"post": {
9494
"summary": "Get testers filtered by OAuth IDs (supports pagination)",
95-
"description": "Returns a list of testers filtered by OAuth IDs provided in the request body. Requires admin permission.",
95+
"description": "Returns a list of testers filtered by OAuth IDs provided in the request body. Requires admin permission. If limit is not provided, all matching testers are returned.",
9696
"tags": [
9797
"Testers"
9898
],
@@ -320,6 +320,48 @@
320320
"description": "ID already exists in the database"
321321
}
322322
}
323+
},
324+
"delete": {
325+
"summary": "Remove ID from existing tester",
326+
"description": "Removes an ID from a tester. Requires admin permission.",
327+
"tags": [
328+
"Testers"
329+
],
330+
"requestBody": {
331+
"required": true,
332+
"content": {
333+
"application/json": {
334+
"schema": {
335+
"type": "object",
336+
"properties": {
337+
"uuid": {
338+
"type": "string"
339+
},
340+
"name": {
341+
"type": "string"
342+
},
343+
"id": {
344+
"type": "string"
345+
}
346+
},
347+
"required": [
348+
"id"
349+
]
350+
}
351+
}
352+
}
353+
},
354+
"responses": {
355+
"200": {
356+
"description": "ID successfully removed from tester"
357+
},
358+
"400": {
359+
"description": "Invalid request or missing required fields"
360+
},
361+
"403": {
362+
"description": "Unauthorized request"
363+
}
364+
}
323365
}
324366
},
325367
"/api/purchase/{purchaseId}": {

client/src/locales/base/fr-FR.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,13 @@
271271
"failed-loading-user-permissions": "Échec du chargement des autorisations de l'utilisateur",
272272
"error-saving-permissions": "Erreur lors de l'enregistrement des permissions",
273273
"tester": "Testeur",
274-
"assign": "Attribuer",
275-
"assign-selected": "Attribuer la sélection",
276-
"assign-tester": "Attribuer un testeur",
277-
"assigned-successfully": "Attribué avec succès",
274+
"assign": "Assigner",
275+
"assign-selected": "Assigner la sélection",
276+
"assign-tester": "Assigner un testeur",
277+
"assigned-successfully": "Assigné avec succès",
278278
"choose-tester": "Choisissez un testeur",
279279
"confirm-unassign-selected": "Êtes-vous sûr de vouloir annuler l'assignation des identifiants sélectionnés à leurs testeurs ?",
280-
"create-and-assign": "Créer et attribuer",
280+
"create-and-assign": "Créer et assigner",
281281
"create-tester": "Créer un testeur",
282282
"enter-tester-name": "Entrez le nom du testeur",
283283
"error-assigning-testers": "Erreur lors de l'assignation des testeurs",

client/src/pages/users-and-permissions.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ import { Checkbox } from "@heroui/checkbox";
3434
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@heroui/table";
3535
import { addToast } from "@heroui/toast";
3636
import ConfirmDeleteModal from "@/components/modals/confirm-delete-modal";
37+
import { useAuth0 } from "@auth0/auth0-react";
3738
// import { Toast } from "@heroui/toast"; // Not using Toast API directly, using message state
3839

3940
export default function UsersAndPermissionsPage() {
41+
const { user } = useAuth0();
42+
const currentUserId = (user?.sub || "").toString().trim();
4043
const { getAuth0ManagementToken, listAuth0Users, getUserPermissions, addPermissionToUser, removePermissionFromUser, deleteAuth0User, postJson, deleteJson } = useSecuredApi();
4144
const postJsonRef = useRef(postJson);
4245
useEffect(() => { postJsonRef.current = postJson; }, [postJson]);
@@ -299,6 +302,8 @@ export default function UsersAndPermissionsPage() {
299302
};
300303

301304
const toggleSelectUser = (userId: string) => {
305+
// Prevent selecting the current logged-in user to avoid accidental self-unassign/delete
306+
if (userId === currentUserId) return;
302307
setSelectedUserIds((prev) => ({ ...prev, [userId]: !prev[userId] }));
303308
};
304309

@@ -334,6 +339,11 @@ export default function UsersAndPermissionsPage() {
334339
};
335340

336341
const unassignId = async (id: string) => {
342+
// Prevent self-unassign to avoid breaking the current user's session
343+
if (id === currentUserId) {
344+
addToast({ title: t('error'), description: t('cannot-unassign-self'), variant: 'solid' });
345+
return;
346+
}
337347
try {
338348
// find the tester object for id via map
339349
const userObj = users.find(u => u.user_id === id) ?? usersWithTester.find(u => u.user_id === id);
@@ -368,6 +378,8 @@ export default function UsersAndPermissionsPage() {
368378

369379
const doUnassignSelected = async (ids: string[]) => {
370380
if (ids.length === 0) return;
381+
// Never unassign the current logged-in user
382+
ids = ids.filter(id => id !== currentUserId);
371383
// for simplification, call delete for each id in parallel
372384
try {
373385
await Promise.all(ids.map(async (id) => {
@@ -431,6 +443,11 @@ export default function UsersAndPermissionsPage() {
431443
};
432444

433445
const deleteUser = async (userId: string) => {
446+
// Prevent deleting the current logged-in user from the UI
447+
if (userId === currentUserId) {
448+
addToast({ title: t('error'), description: t('cannot-delete-self'), variant: 'solid' });
449+
return;
450+
}
434451
try {
435452
if (!token) throw new Error('No management token');
436453
const mgmtToken = token.access_token;
@@ -549,10 +566,10 @@ export default function UsersAndPermissionsPage() {
549566
<TableHeader>
550567
<TableColumn>
551568
<Checkbox isSelected={Object.keys(selectedUserIds).length > 0 && Object.keys(selectedUserIds).every(k => selectedUserIds[k])} onValueChange={(v) => {
552-
// select/unselect all
569+
// select/unselect all (skip the current user)
553570
if (v) {
554571
const map: Record<string, boolean> = {};
555-
(usersWithTester.length ? usersWithTester : users).forEach(u => { if (u.user_id) map[u.user_id] = true });
572+
(usersWithTester.length ? usersWithTester : users).forEach(u => { if (u.user_id && u.user_id !== currentUserId) map[u.user_id] = true });
556573
setSelectedUserIds(map);
557574
} else {
558575
setSelectedUserIds({});
@@ -579,10 +596,12 @@ export default function UsersAndPermissionsPage() {
579596
{!assignedName && u.user_id && (
580597
<Button color="secondary" onPress={() => u.user_id && openAssignModalForOne(u.user_id)}>{t('assign')}</Button>
581598
)}
582-
{assignedName && u.user_id && (
599+
{assignedName && u.user_id && u.user_id !== currentUserId && (
583600
<Button color="warning" onPress={() => unassignId(u.user_id)}>{t('unassign')}</Button>
584601
)}
585-
<Button className="ml-2" color="danger" onPress={() => { setConfirmDeleteUser(u); setConfirmDeleteOpen(true); }} disabled={deletingUserId === u.user_id} isLoading={deletingUserId === u.user_id}>{t('delete')}</Button>
602+
{u.user_id !== currentUserId && (
603+
<Button className="ml-2" color="danger" onPress={() => { setConfirmDeleteUser(u); setConfirmDeleteOpen(true); }} disabled={deletingUserId === u.user_id} isLoading={deletingUserId === u.user_id}>{t('delete')}</Button>
604+
)}
586605
</TableCell>
587606
</TableRow>
588607
)

cloudflare-worker/src/routes/testers/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,25 @@ export const setupTesterRoutes = (router: Router, env: Env) => {
366366
// Check for collisions in the DB
367367
const existing = await db.idMappings.existsMultiple(idArray);
368368
if (existing.length > 0) {
369+
// Determine owners of the existing IDs
370+
const owners = await Promise.all(existing.map((id) => db.idMappings.getTesterUuid(id)));
371+
const uniqueOwners = Array.from(new Set(owners.filter(Boolean)));
372+
// If all conflicting ids are already bound to the same tester we are adding to,
373+
// return a 409 error with a specific message about duplicate ID on the same tester.
374+
if (uniqueOwners.length === 1 && uniqueOwners[0] === tester.uuid) {
375+
return new Response(
376+
JSON.stringify({ success: false, error: "ID already exists in the database", existing }),
377+
{
378+
status: 409,
379+
headers: {
380+
...router.corsHeaders,
381+
"Content-Type": "application/json",
382+
},
383+
},
384+
);
385+
}
386+
387+
// Otherwise, at least one ID already exists for another tester
369388
return new Response(
370389
JSON.stringify({ success: false, error: "Some IDs already exist in the database", existing }),
371390
{

0 commit comments

Comments
 (0)