Skip to content

Commit

Permalink
Merge branch 'feature/user-service/admin-edit-user' into feature/user…
Browse files Browse the repository at this point in the history
…-service/admin-update-privilege
  • Loading branch information
jq1836 committed Sep 26, 2024
2 parents e9b4ecc + 4b9679f commit ae73b82
Show file tree
Hide file tree
Showing 15 changed files with 609 additions and 309 deletions.
5 changes: 5 additions & 0 deletions frontend/app/app/questions/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import QuestionCreate from "@/components/questions/question-create";

export default function QuestionCreatePage() {
return <QuestionCreate />;
}
117 changes: 52 additions & 65 deletions frontend/components/admin-user-management/admin-user-management.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
TableHeader,
TableRow,
} 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 AuthPageWrapper from "@/components/auth/auth-page-wrapper";
import { User, UserArraySchema } from "@/lib/schemas/user-schema";

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

export default function AdminUserManagement() {
const auth = useAuth();
const { data, error, isLoading, mutate } = useSWR(

const { data, 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>();

Expand All @@ -59,24 +58,10 @@ export default function AdminUserManagement() {
}
}, [data]);

useEffect(() => {
if (error && error.message === "No authentication token found") {
setUnauthorised(true);
setIsLoggedIn(false);
}
if (error && error.message === "401") {
setUnauthorised(true);
}
}, [error]);

if (isLoading) {
return <LoadingScreen />;
}

if (unauthorised) {
return <UnauthorisedAccess isLoggedIn={isLoggedIn} />;
}

const handleDelete = async (userId: string) => {
const token = auth?.token;
if (!token) {
Expand All @@ -103,53 +88,55 @@ export default function AdminUserManagement() {
};

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>
<TableHead>Username</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Skill Level</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.isAdmin ? "Admin" : "User"}</TableCell>
<TableCell>{user.skillLevel}</TableCell>
<TableCell>
<Button
variant="outline"
className="mr-2"
onClick={() => {
setSelectedUser(user);
setShowModal(true);
}}
>
<PencilIcon />
</Button>
<Button
variant="destructive"
onClick={() => handleDelete(user.id)}
>
<Trash2Icon />
</Button>
</TableCell>
<AuthPageWrapper requireAdmin>
<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>
<TableHead>Username</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Skill Level</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.isAdmin ? "Admin" : "User"}</TableCell>
<TableCell>{user.skillLevel}</TableCell>
<TableCell>
<Button
variant="outline"
className="mr-2"
onClick={() => {
setSelectedUser(user);
setShowModal(true);
}}
>
<PencilIcon />
</Button>
<Button
variant="destructive"
onClick={() => handleDelete(user.id)}
>
<Trash2Icon />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</AuthPageWrapper>
);
}
68 changes: 68 additions & 0 deletions frontend/components/auth/auth-page-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import React from "react";
import { ReactNode } from "react";
import { useAuth } from "@/app/auth/auth-context";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";

type AuthCheck = (user: { isAdmin: boolean } | undefined | null) => boolean;

interface AuthPageWrapperProps extends React.HTMLProps<HTMLDivElement> {
children: ReactNode;

// User access rules
authCheck?: AuthCheck; // Custom predicate which is true when user is to be granted access
requireAdmin?: boolean;
requireLoggedIn?: boolean;
}

const AuthPageWrapper: React.FC<AuthPageWrapperProps> = ({
children,
...props
}) => {
const auth = useAuth();
const router = useRouter();

const authCheck = (
user: { isAdmin: boolean } | undefined | null
): boolean => {
if (props?.requireLoggedIn && !user) {
return false;
}
if (props?.requireAdmin && !user?.isAdmin) {
return false;
}
if (props?.authCheck) {
return props.authCheck(user);
}
// Allow access if no user access rule is defined
return true;
};

return (
<div>
{authCheck(auth?.user) ? (
children
) : (
<div className="flex items-start justify-center h-2/6">
<div className="text-center mt-[20vh]">
<h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl mb-6">
Uh Oh! You&apos;re not supposed to be here!
</h1>
<Button
size="lg"
onClick={() => {
auth?.user ? router.push("/") : router.push("/auth/login");
}}
>
Return Home
</Button>
</div>
</div>
)}
</div>
);
};

export default AuthPageWrapper;
12 changes: 10 additions & 2 deletions frontend/components/auth/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,18 @@ export function LoginForm() {
} else if (user) {
router.push("/app/questions");
} else {
toast({ title: "Error", variant: "destructive", description: "Login Failed." });
toast({
title: "Error",
variant: "destructive",
description: "Login Failed.",
});
}
} catch (err) {
toast({ title: "Error", variant: "destructive", description: "Login Failed." });
toast({
title: "Error",
variant: "destructive",
description: "Login Failed.",
});
}
};

Expand Down
29 changes: 0 additions & 29 deletions frontend/components/common/unauthorised-access.tsx

This file was deleted.

64 changes: 64 additions & 0 deletions frontend/components/questions/question-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import { Question } from "@/lib/schemas/question-schema";
import QuestionForm from "@/components/questions/question-form";
import { useAuth } from "@/app/auth/auth-context";
import { useRouter } from "next/navigation";
import { useToast } from "@/components/hooks/use-toast";

export default function QuestionCreate() {
const auth = useAuth();
const router = useRouter();
const { toast } = useToast();

const handleCreate = async (newQuestion: Question) => {
try {
const response = await fetch("http://localhost:8000/questions", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: newQuestion.title,
description: newQuestion.description,
category: newQuestion.category,
complexity: newQuestion.complexity,
}),
});

if (!response.ok) {
if (response.status == 409) {
throw new Error("A question with this title already exists.");
}
}

toast({
title: "Success",
description: "Question created successfully!",
variant: "success",
duration: 3000,
});

router.push(`/app/questions/`);
} catch (err) {
toast({
title: "An error occured!",
description:
err instanceof Error ? err.message : "An unknown error occurred",
variant: "destructive",
duration: 5000,
});
}
};

return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Create a New Question</h1>
<QuestionForm
isAdmin={auth?.user?.isAdmin}
handleSubmit={handleCreate}
submitButtonText="Create Question"
/>
</div>
);
}
Loading

0 comments on commit ae73b82

Please sign in to comment.