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 edit user feature for admins #114

Merged
merged 15 commits into from
Sep 26, 2024
Merged
203 changes: 203 additions & 0 deletions frontend/components/admin-user-management/admin-edit-user-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"use client";

import React, { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "../ui/dialog";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
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 { User } from "@/lib/schemas/user-schema";

interface AdminEditUserModalProps extends React.HTMLProps<HTMLDivElement> {
showModal?: boolean;
setShowModal: (show: boolean) => void;
user: User | undefined;
onUserUpdate: () => void;
}

const AdminEditUserModal: React.FC<AdminEditUserModalProps> = ({
...props
}) => {
const auth = useAuth();
const { toast } = useToast();
const [editingUser, setEditingUser] = useState<
| {
id?: string;
username?: string;
email?: string;
}
| undefined
>();

useEffect(() => {
setEditingUser(props.user);
}, [props.user]);

const closeModal = () => {
setEditingUser(props.user);
props.setShowModal(false);
};

const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();

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;
}

const response = await updateUser(
auth.token,
editingUser.id,
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;
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;
case 401:
toast({
title: "Access denied",
description: "Invalid session",
});
return;
case 403:
toast({
title: "Access denied",
description: "Only admins can update other user",
});
return;
case 404:
toast({
title: "User not found",
description: "User with specified ID not found",
});
return;
case 409:
toast({
title: "Duplicated Username or Email",
description: "The username or email you entered is already in use",
});
return;
case 500:
toast({
title: "Server Error",
description: "The server encountered an error",
});
return;
default:
toast({
title: "Unknown Error",
description: "An unexpected error has occured",
});
return;
}

// Remove old states, update UI and close modal
props.onUserUpdate();
closeModal();
};

return (
<>
{props.showModal && (
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit user</DialogTitle>
</DialogHeader>
<form>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="username">Username</Label>
</div>
<Input
id="username"
placeholder={props.user?.username}
value={editingUser?.username}
onChange={(e) =>
setEditingUser({ ...editingUser, username: e.target.value })
}
required
/>
</div>
<div className="grid gap-2 mt-5">
<div className="flex items-center">
<Label htmlFor="email">Email</Label>
</div>
<Input
id="email"
type="email"
placeholder={props.user?.email}
value={editingUser?.email}
onChange={(e) =>
setEditingUser({ ...editingUser, email: e.target.value })
}
required
/>
</div>
</form>
<DialogFooter>
<Button
onClick={handleSubmit}
disabled={
!auth?.token ||
!editingUser?.id ||
(!editingUser?.email && !editingUser?.username)
}
>
Save changes
</Button>
<Button variant="destructive" onClick={closeModal}>
Exit
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</>
);
};

export default AdminEditUserModal;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
} from "@/components/ui/table";
import UnauthorisedAccess from "@/components/common/unauthorised-access";
import LoadingScreen from "@/components/common/loading-screen";
import AdminEditUserModal from "@/components/admin-user-management/admin-edit-user-modal";
import { PencilIcon, Trash2Icon } from "lucide-react";
import { User, UserArraySchema } from "@/lib/schemas/user-schema";

const fetcher = async (url: string): Promise<User[]> => {
Expand All @@ -40,14 +42,16 @@ const fetcher = async (url: string): Promise<User[]> => {

export default function AdminUserManagement() {
const auth = useAuth();
const { data, error, isLoading } = useSWR(
const { data, error, isLoading, mutate } = useSWR(
"http://localhost:3001/users",
fetcher
);

const [users, setUsers] = useState<User[]>([]);
const [unauthorised, setUnauthorised] = useState<boolean>(false);
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(true);
const [showModal, setShowModal] = useState<boolean>(false);
const [selectedUser, setSelectedUser] = useState<User>();

useEffect(() => {
if (data) {
Expand Down Expand Up @@ -93,9 +97,20 @@ export default function AdminUserManagement() {
setUsers(users.filter((user) => user.id !== userId));
};

const onUserUpdate = () => {
mutate();
setSelectedUser(undefined);
};

return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">User Management</h1>
<AdminEditUserModal
showModal={showModal}
setShowModal={setShowModal}
user={selectedUser}
onUserUpdate={onUserUpdate}
/>
<Table>
<TableHeader>
<TableRow>
Expand All @@ -114,14 +129,21 @@ export default function AdminUserManagement() {
<TableCell>{user.isAdmin ? "Admin" : "User"}</TableCell>
<TableCell>{user.skillLevel}</TableCell>
<TableCell>
<Button variant="outline" className="mr-2" onClick={() => {}}>
Edit
<Button
variant="outline"
className="mr-2"
onClick={() => {
setSelectedUser(user);
setShowModal(true);
}}
>
<PencilIcon />
</Button>
<Button
variant="destructive"
onClick={() => handleDelete(user.id)}
>
Delete
<Trash2Icon />
</Button>
</TableCell>
</TableRow>
Expand Down
24 changes: 24 additions & 0 deletions frontend/lib/update-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const updateUser = async (
jwtToken: string,
id: string,
username?: string,
email?: string,
password?: string,
skillLevel?: string
) => {
if (!username && !email && !password && !skillLevel) {
throw new Error("Require at least one field");
}

const body = { username, email, password, skillLevel };

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

- Body

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

```json
{
"username": "SampleUserName",
"email": "[email protected]",
"password": "SecurePassword"
"password": "SecurePassword",
"skillLevel": "SkillLevel",
}
```

Expand Down