Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement admin edit user privilege #115

Merged
merged 10 commits into from
Sep 27, 2024
135 changes: 113 additions & 22 deletions frontend/components/admin-user-management/admin-edit-user-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Label } from "../ui/label";
import { updateUser } from "@/lib/update-user";
import { useAuth } from "@/app/auth/auth-context";
import { useToast } from "@/components/hooks/use-toast";
import { Switch } from "../ui/switch";
import { updateUserPrivilege } from "@/lib/update-user-privilege";
import { User } from "@/lib/schemas/user-schema";

interface AdminEditUserModalProps extends React.HTMLProps<HTMLDivElement> {
Expand All @@ -33,6 +35,7 @@ const AdminEditUserModal: React.FC<AdminEditUserModalProps> = ({
id?: string;
username?: string;
email?: string;
isAdmin?: boolean;
}
| undefined
>();
Expand All @@ -49,6 +52,19 @@ const AdminEditUserModal: React.FC<AdminEditUserModalProps> = ({
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();

updateInfo()
.then(() => updatePrivilege())
.then(() => {
toast({
title: "Success",
description: "User updated successfully!",
});
closeModal();
})
.catch(() => null);
};

const updateInfo = async () => {
if (!auth?.token) {
// Will not reach this point as button is disabled
// when token is missing
Expand All @@ -75,68 +91,129 @@ const AdminEditUserModal: React.FC<AdminEditUserModalProps> = ({
editingUser?.username,
editingUser?.email
);
if (!response.ok) {
toast({
title: "Unknown Error",
description: "An unexpected error has occurred",
});
}
switch (response.status) {
case 200:
toast({
title: "Success",
description: "User updated successfully!",
});
break;
props.onUserUpdate();
return;
case 400:
// In theory, they should never be able to send out a request
// with missing fields due to disabled submission button
toast({
title: "Missing Fields",
description: "Please fill in at least 1 field",
});
return;
break;
case 401:
toast({
title: "Access denied",
description: "Invalid session",
});
return;
break;
case 403:
toast({
title: "Access denied",
description: "Only admins can update other user",
});
return;
break;
case 404:
toast({
title: "User not found",
description: "User with specified ID not found",
});
return;
break;
case 409:
toast({
title: "Duplicated Username or Email",
description: "The username or email you entered is already in use",
});
return;
break;
case 500:
toast({
title: "Server Error",
description: "The server encountered an error",
});
return;
break;
default:
toast({
title: "Unknown Error",
description: "An unexpected error has occured",
});
return;
break;
}
throw new Error("Update user info request failed");
};

const updatePrivilege = async () => {
if (!auth?.token) {
// Will not reach this point as button is disabled
// when token is missing
toast({
title: "Access denied",
description: "No authentication token found",
});
return;
}

if (!editingUser?.id) {
// Will not reach this point as button is disabled
// when editing user's id is missing
toast({
title: "Invalid selection",
description: "No user selected",
});
return;
}

// Remove old states, update UI and close modal
props.onUserUpdate();
closeModal();
if (editingUser?.isAdmin == null) {
// Will not reach this point as button is disabled
// when field is missing
toast({
title: "Invalid selection",
description: "No user selected",
});
return;
}

const response = await updateUserPrivilege(
auth.token,
editingUser?.id,
editingUser?.isAdmin
);

switch (response.status) {
case 200:
props.onUserUpdate();
return;
case 400:
// In theory, they should never be able to send out a request
// with missing fields due to disabled submission button
break;
case 401:
toast({
title: "Access denied",
description: "Invalid session",
});
break;
case 403:
toast({
title: "Access denied",
description: "Only admins can update other user",
});
break;
case 404:
toast({
title: "User not found",
description: "User with specified ID not found",
});
break;
default:
toast({
title: "Unknown Error",
description: "An unexpected error has occured",
});
break;
}
throw new Error("Update user privilege request failed");
};

return (
Expand Down Expand Up @@ -177,14 +254,28 @@ const AdminEditUserModal: React.FC<AdminEditUserModalProps> = ({
required
/>
</div>
<div className="grid gap-2 mt-5">
<div className="flex items-center">
<Label htmlFor="isAdmin">Admin</Label>
</div>
<Switch
id="isAdmin"
checked={editingUser?.isAdmin}
onCheckedChange={(e) =>
setEditingUser({ ...editingUser, isAdmin: e })
}
required
/>
</div>
</form>
<DialogFooter>
<Button
onClick={handleSubmit}
disabled={
!auth?.token ||
!editingUser?.id ||
(!editingUser?.email && !editingUser?.username)
(!editingUser?.email && !editingUser?.username) ||
editingUser?.isAdmin == null
}
>
Save changes
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/ui/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-500 data-[state=unchecked]:bg-gray-500",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
"pointer-events-none block h-6 w-6 rounded-full bg-white shadow-lg ring-0 transition-transform duration-200 ease-in-out data-[state=checked]:translate-x-6 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
Expand Down
17 changes: 17 additions & 0 deletions frontend/lib/update-user-privilege.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const updateUserPrivilege = async (
jwtToken: string,
id: string,
isAdmin: boolean
) => {
const body = { isAdmin };

const response = await fetch(`http://localhost:3001/users/${id}/privilege`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${jwtToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
return response;
};
3 changes: 2 additions & 1 deletion user-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
- Required: `userId` path parameter

- Body

- At least one of the following fields is required: `username` (string), `email` (string), `password` (string), `skillLevel` (string)

```json
Expand Down Expand Up @@ -173,7 +174,7 @@

- HTTP Method: `PATCH`

- Endpoint: http://localhost:3001/users/{userId}
- Endpoint: http://localhost:3001/users/{userId}/privilege

- Parameters

Expand Down