Skip to content

Commit 52ec455

Browse files
committed
wip full user management
1 parent 417a3f7 commit 52ec455

File tree

9 files changed

+671
-94
lines changed

9 files changed

+671
-94
lines changed

client/src/components/auth0.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export const useSecuredApi = () => {
481481
}
482482
};
483483

484-
const deleteJson = async (url: string) => {
484+
const deleteJson = async (url: string, data?: any) => {
485485
try {
486486
const accessToken = await getAccessTokenSilently({
487487
authorizationParams: {
@@ -496,6 +496,7 @@ export const useSecuredApi = () => {
496496
Authorization: `Bearer ${accessToken}`,
497497
"Content-Type": "application/json",
498498
},
499+
body: data ? JSON.stringify(data) : undefined,
499500
});
500501

501502
return await apiResponse.json();
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Modal: Assign one or many Auth0 user ids to an existing tester, or create a new tester and assign.
3+
*/
4+
import { useEffect, useState } from "react";
5+
import { Modal, ModalBody, ModalContent, ModalHeader } from "@heroui/modal";
6+
import { Button } from "@heroui/button";
7+
import { Input } from "@heroui/input";
8+
import { useTranslation } from "react-i18next";
9+
import { addToast } from "@heroui/toast";
10+
import { useSecuredApi } from "@/components/auth0";
11+
12+
export default function AssignTesterModal({
13+
isOpen,
14+
onClose,
15+
userIds,
16+
onSuccess,
17+
}: {
18+
isOpen: boolean;
19+
onClose: () => void;
20+
userIds: string[]; // one or more Auth0 IDs
21+
onSuccess?: () => void;
22+
}) {
23+
const { t } = useTranslation();
24+
const { getJson, postJson } = useSecuredApi();
25+
const [testers, setTesters] = useState<Array<{ uuid: string; name: string }>>([]);
26+
const [selectedUuid, setSelectedUuid] = useState<string | null>(null);
27+
const [createName, setCreateName] = useState("");
28+
const [isCreating, setIsCreating] = useState(false);
29+
const [isSubmitting, setIsSubmitting] = useState(false);
30+
31+
useEffect(() => {
32+
if (!isOpen) return;
33+
(async () => {
34+
try {
35+
const resp = await getJson(`${import.meta.env.API_BASE_URL}/testers`);
36+
const data = resp?.data || [];
37+
setTesters(data);
38+
} catch (err) {
39+
console.error(err);
40+
addToast({ title: t("error"), description: t("error-fetching-data"), variant: "solid" });
41+
}
42+
})();
43+
}, [isOpen]);
44+
45+
const assignToExisting = async () => {
46+
if (!selectedUuid) return;
47+
setIsSubmitting(true);
48+
try {
49+
const resp = await postJson(`${import.meta.env.API_BASE_URL}/tester/ids`, { uuid: selectedUuid, ids: userIds });
50+
if (resp.success) {
51+
addToast({ title: t("success"), description: t("assigned-successfully"), variant: "solid" });
52+
onSuccess?.();
53+
onClose();
54+
} else {
55+
addToast({ title: t("error"), description: resp.error || t("error-assigning-testers"), variant: "solid" });
56+
}
57+
} catch (err) {
58+
console.error(err);
59+
addToast({ title: t("error"), description: t("error-assigning-testers"), variant: "solid" });
60+
} finally {
61+
setIsSubmitting(false);
62+
}
63+
};
64+
65+
const createAndAssign = async () => {
66+
if (!createName.trim()) return;
67+
setIsCreating(true);
68+
try {
69+
const resp = await postJson(`${import.meta.env.API_BASE_URL}/tester`, { name: createName.trim(), ids: userIds });
70+
if (resp.success) {
71+
addToast({ title: t("success"), description: t("tester-created-and-assigned"), variant: "solid" });
72+
onSuccess?.();
73+
onClose();
74+
} else {
75+
addToast({ title: t("error"), description: resp.error || t("error-creating-tester"), variant: "solid" });
76+
}
77+
} catch (err) {
78+
console.error(err);
79+
addToast({ title: t("error"), description: t("error-creating-tester"), variant: "solid" });
80+
} finally {
81+
setIsCreating(false);
82+
}
83+
};
84+
85+
return (
86+
<Modal isOpen={isOpen} onClose={onClose} aria-labelledby="assign-tester-title">
87+
<ModalContent>
88+
<ModalHeader id="assign-tester-title">{t("assign-tester")}</ModalHeader>
89+
<ModalBody>
90+
<div className="mb-4">
91+
<label className="text-sm font-semibold">{t("select-tester")}</label>
92+
<select
93+
className="w-full p-2 border rounded mt-2"
94+
value={selectedUuid ?? ""}
95+
onChange={(e) => setSelectedUuid(e.target.value || null)}
96+
>
97+
<option value="">{t("choose-tester")}</option>
98+
{testers.map((tst) => (
99+
<option key={tst.uuid} value={tst.uuid}>{tst.name}</option>
100+
))}
101+
</select>
102+
<div className="text-sm text-muted-foreground mt-2">{t("or-create-new-tester")}</div>
103+
<Input value={createName} onChange={(e) => setCreateName(e.target.value)} placeholder={t("enter-tester-name")} className="mt-2" />
104+
</div>
105+
106+
<div className="flex justify-end gap-2 mt-4">
107+
<Button onPress={onClose} color="secondary">{t("cancel")}</Button>
108+
<Button color="primary" isLoading={isSubmitting} disabled={!selectedUuid} onPress={assignToExisting}>{t("assign")}</Button>
109+
<Button color="primary" isLoading={isCreating} disabled={!createName.trim()} onPress={createAndAssign}>{t("create-and-assign")}</Button>
110+
</div>
111+
</ModalBody>
112+
</ModalContent>
113+
</Modal>
114+
);
115+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Modal: Create a tester (without assigning any OAuth ids)
3+
*/
4+
import { FormEvent, useRef, useState } from "react";
5+
import { Modal, ModalBody, ModalContent, ModalHeader } from "@heroui/modal";
6+
import { Button } from "@heroui/button";
7+
import { Input } from "@heroui/input";
8+
import { useTranslation } from "react-i18next";
9+
import { addToast } from "@heroui/toast";
10+
import { useSecuredApi } from "@/components/auth0";
11+
12+
export default function CreateTesterModal({ isOpen, onClose, onSuccess }: { isOpen: boolean; onClose: () => void; onSuccess?: () => void; }) {
13+
const { t } = useTranslation();
14+
const { postJson } = useSecuredApi();
15+
const [name, setName] = useState("");
16+
const [isSubmitting, setIsSubmitting] = useState(false);
17+
const formRef = useRef<HTMLFormElement | null>(null);
18+
19+
const handleSubmit = async (e: FormEvent) => {
20+
e.preventDefault();
21+
if (!name.trim()) {
22+
addToast({ title: t('error'), description: t('please-enter-a-name'), variant: 'solid' });
23+
return;
24+
}
25+
setIsSubmitting(true);
26+
try {
27+
const resp = await postJson(`${import.meta.env.API_BASE_URL}/tester`, { name: name.trim(), ids: [] });
28+
if (resp.success) {
29+
addToast({ title: t('success'), description: t('tester-created'), variant: 'solid' });
30+
onSuccess?.();
31+
onClose();
32+
} else {
33+
addToast({ title: t('error'), description: resp.error || t('error-creating-tester'), variant: 'solid' });
34+
}
35+
} catch (err) {
36+
console.error(err);
37+
addToast({ title: t('error'), description: t('error-creating-tester'), variant: 'solid' });
38+
} finally {
39+
setIsSubmitting(false);
40+
}
41+
};
42+
43+
return (
44+
<Modal isOpen={isOpen} onClose={onClose} aria-labelledby="create-tester-title">
45+
<ModalContent>
46+
<ModalHeader id="create-tester-title">{t('create-tester')}</ModalHeader>
47+
<ModalBody>
48+
<form ref={formRef} onSubmit={handleSubmit}>
49+
<Input value={name} onChange={(e) => setName(e.target.value)} label={t('tester-name')} labelPlacement="outside" placeholder={t('enter-the-user-name')} />
50+
<div className="flex justify-end gap-2 mt-4">
51+
<Button color="secondary" onPress={onClose}>{t('cancel')}</Button>
52+
<Button color="primary" isLoading={isSubmitting} type="submit">{t('create')}</Button>
53+
</div>
54+
</form>
55+
</ModalBody>
56+
</ModalContent>
57+
</Modal>
58+
);
59+
}

client/src/locales/base/en-US.json

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"please-enter-feedback": "Please enter feedback",
123123
"error": "Error",
124124
"success": "Success",
125+
"info": "Info",
125126
"feedback-published-successfully": "Feedback published successfully",
126127
"refund-processed-successfully": "Refund processed successfully",
127128
"error-processing-refund": "Error processing refund",
@@ -136,6 +137,16 @@
136137
"please-enter-description": "Please enter desccription",
137138
"purchase-created-successfully": "Purchase created successfully",
138139
"error-creating-purchase": "Error creating purchase",
140+
"user-already-assigned": "This user is already assigned to a tester",
141+
"assign": "Assign",
142+
"assign-selected": "Assign selected",
143+
"unassign": "Unassign",
144+
"unassign-selected": "Unassign selected",
145+
"confirm-unassign-selected": "Are you sure you want to unassign the selected OAuth IDs?",
146+
"selected-unassigned": "Selected OAuth IDs unassigned",
147+
"no-tester-found-for-id": "No tester found for this OAuth ID",
148+
"oauth-id-removed-successfully": "OAuth ID removed successfully",
149+
"error-removing-oauth-id": "Error removing OAuth ID",
139150
"enter-order-number": "Enter order number",
140151
"enter-product-description": "Enter product description",
141152
"new-purchase": "New purchase",
@@ -270,5 +281,26 @@
270281
"no-management-token": "No management token",
271282
"confirm-delete-warning": "Are you sure you want to delete {{name}}?",
272283
"failed-loading-user-permissions": "Failed to load user permissions",
273-
"error-saving-permissions": "Error saving permissions"
284+
"error-saving-permissions": "Error saving permissions",
285+
"selected-count": "{{count}} selected",
286+
"assign-selected": "Assign selected",
287+
"unassign-selected": "Unassign selected",
288+
"assign": "Assign",
289+
"unassign": "Unassign",
290+
"no-tester-found-for-id": "No tester found for this ID",
291+
"oauth-id-removed-successfully": "OAuth ID removed successfully",
292+
"selected-unassigned": "Selected IDs were unassigned",
293+
"assign-tester": "Assign Tester",
294+
"select-tester": "Select a tester",
295+
"choose-tester": "Choose a tester",
296+
"or-create-new-tester": "or create a new tester",
297+
"enter-tester-name": "Enter the tester name",
298+
"create-and-assign": "Create and assign",
299+
"assigned-successfully": "Assigned successfully",
300+
"tester-created-and-assigned": "Tester created and assigned",
301+
"error-assigning-testers": "Error assigning testers",
302+
"error-creating-tester": "Error creating tester",
303+
"create-tester": "Create a tester",
304+
"tester-created": "Tester created",
305+
"confirm-unassign-selected": "Are you sure you want to unassign the selected IDs from their testers?"
274306
}

0 commit comments

Comments
 (0)