From 6e316814384069ba6d6c327d9094564bb9148024 Mon Sep 17 00:00:00 2001 From: Gitead Date: Tue, 24 Feb 2026 00:24:15 -0500 Subject: [PATCH] Update auth, users, settings page, and prisma schema --- apps/Backend/src/routes/auth.ts | 11 +- apps/Backend/src/routes/users.ts | 63 +++-- apps/Frontend/src/App.tsx | 2 +- .../src/components/layout/sidebar.tsx | 23 +- .../src/components/layout/top-app-bar.tsx | 9 +- apps/Frontend/src/lib/protected-route.tsx | 10 +- apps/Frontend/src/pages/settings-page.tsx | 237 +++++++++++++++++- packages/db/prisma/schema.prisma | 6 + packages/db/prisma/seed.ts | 6 +- 9 files changed, 324 insertions(+), 43 deletions(-) diff --git a/apps/Backend/src/routes/auth.ts b/apps/Backend/src/routes/auth.ts index 8be85b0..24bb1fa 100644 --- a/apps/Backend/src/routes/auth.ts +++ b/apps/Backend/src/routes/auth.ts @@ -44,14 +44,16 @@ router.post( const hashedPassword = await hashPassword(req.body.password); const user = await storage.createUser({ - ...req.body, + username: req.body.username, password: hashedPassword, + role: "USER", }); // Generate a JWT token for the user after successful registration const token = generateToken(user); - const { password, ...safeUser } = user; + const { password, ...rest } = user; + const safeUser = { ...rest, role: rest.role ?? "USER" }; return res.status(201).json({ user: safeUser, token }); } catch (error) { console.error("Registration error:", error); @@ -77,12 +79,13 @@ router.post( ); if (!isPasswordMatch) { - return res.status(401).json({ error: "Invalid password or password" }); + return res.status(401).json({ error: "Invalid username or password" }); } // Generate a JWT token for the user after successful login const token = generateToken(user); - const { password, ...safeUser } = user; + const { password, ...rest } = user; + const safeUser = { ...rest, role: rest.role ?? "USER" }; return res.status(200).json({ user: safeUser, token }); } catch (error) { return res.status(500).json({ error: "Internal server error" }); diff --git a/apps/Backend/src/routes/users.ts b/apps/Backend/src/routes/users.ts index baaa7de..24b0694 100644 --- a/apps/Backend/src/routes/users.ts +++ b/apps/Backend/src/routes/users.ts @@ -3,8 +3,7 @@ import type { Request, Response } from "express"; import { storage } from "../storage"; import { z } from "zod"; import { UserUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; -import jwt from 'jsonwebtoken'; -import bcrypt from 'bcrypt'; +import bcrypt from "bcrypt"; const router = Router(); @@ -25,15 +24,33 @@ router.get("/", async (req: Request, res: Response): Promise => { const user = await storage.getUser(userId); if (!user) return res.status(404).send("User not found"); - - const { password, ...safeUser } = user; - res.json(safeUser); + const { password, ...rest } = user; + res.json({ ...rest, role: rest.role ?? "USER" }); } catch (error) { console.error(error); res.status(500).send("Failed to fetch user"); } }); +// GET: List all users (for admin/settings; no passwords) +router.get("/list", async (req: Request, res: Response): Promise => { + try { + if (!req.user?.id) return res.status(401).send("Unauthorized"); + + const limit = Math.min(Number(req.query.limit) || 100, 500); + const offset = Number(req.query.offset) || 0; + const users = await storage.getUsers(limit, offset); + const safe = users.map((u) => { + const { password: _p, ...rest } = u; + return { ...rest, role: rest.role ?? "USER" }; + }); + res.json(safe); + } catch (error) { + console.error(error); + res.status(500).send("Failed to fetch users"); + } +}); + // GET: User by ID router.get("/:id", async (req: Request, res: Response): Promise => { try { @@ -46,32 +63,36 @@ router.get("/:id", async (req: Request, res: Response): Promise => { const user = await storage.getUser(id); if (!user) return res.status(404).send("User not found"); - const { password, ...safeUser } = user; - res.json(safeUser); + const { password, ...rest } = user; + res.json({ ...rest, role: rest.role ?? "USER" }); } catch (error) { console.error(error); res.status(500).send("Failed to fetch user"); } }); -// POST: Create new user +// POST: Create new user (password is hashed) router.post("/", async (req: Request, res: Response) => { try { const input = userCreateSchema.parse(req.body); - const newUser = await storage.createUser(input); - const { password, ...safeUser } = newUser; - res.status(201).json(safeUser); + const existing = await storage.getUserByUsername(input.username); + if (existing) { + return res.status(400).json({ error: "Username already exists" }); + } + const hashed = await hashPassword(input.password); + const newUser = await storage.createUser({ ...input, password: hashed }); + const { password: _p, ...rest } = newUser; + res.status(201).json({ ...rest, role: rest.role ?? "USER" }); } catch (err) { console.error(err); res.status(400).json({ error: "Invalid user data", details: err }); } }); -// Function to hash password using bcrypt +// Hash password using bcrypt (used for create and update) async function hashPassword(password: string) { - const saltRounds = 10; // Salt rounds for bcrypt - const hashedPassword = await bcrypt.hash(password, saltRounds); - return hashedPassword; + const saltRounds = 10; + return bcrypt.hash(password, saltRounds); } // PUT: Update user @@ -97,16 +118,16 @@ router.put("/:id", async (req: Request, res: Response):Promise => { const updatedUser = await storage.updateUser(id, updates); if (!updatedUser) return res.status(404).send("User not found"); - const { password, ...safeUser } = updatedUser; - res.json(safeUser); + const { password, ...rest } = updatedUser; + res.json({ ...rest, role: rest.role ?? "USER" }); } catch (err) { console.error(err); res.status(400).json({ error: "Invalid update data", details: err }); } }); -// DELETE: Delete user -router.delete("/:id", async (req: Request, res: Response):Promise => { +// DELETE: Delete user (cannot delete current user) +router.delete("/:id", async (req: Request, res: Response): Promise => { try { const idParam = req.params.id; if (!idParam) return res.status(400).send("User ID is required"); @@ -114,6 +135,10 @@ router.delete("/:id", async (req: Request, res: Response):Promise => { const id = parseInt(idParam); if (isNaN(id)) return res.status(400).send("Invalid user ID"); + if (req.user?.id === id) { + return res.status(403).json({ error: "Cannot delete your own account" }); + } + const success = await storage.deleteUser(id); if (!success) return res.status(404).send("User not found"); diff --git a/apps/Frontend/src/App.tsx b/apps/Frontend/src/App.tsx index bea2914..465a3f1 100644 --- a/apps/Frontend/src/App.tsx +++ b/apps/Frontend/src/App.tsx @@ -41,7 +41,7 @@ function Router() { component={() => } /> } /> - } /> + } adminOnly /> } /> [ @@ -78,13 +83,17 @@ export function Sidebar() { path: "/database-management", icon: , }, - { - name: "Settings", - path: "/settings", - icon: , - }, + ...(isAdmin + ? [ + { + name: "Settings", + path: "/settings", + icon: , + }, + ] + : []), ], - [] + [isAdmin] ); return ( diff --git a/apps/Frontend/src/components/layout/top-app-bar.tsx b/apps/Frontend/src/components/layout/top-app-bar.tsx index b5e9754..20a6c1f 100644 --- a/apps/Frontend/src/components/layout/top-app-bar.tsx +++ b/apps/Frontend/src/components/layout/top-app-bar.tsx @@ -70,9 +70,12 @@ export function TopAppBar() { {user?.username} My Profile - setLocation("/settings")}> - Account Settings - + {(user?.role?.toUpperCase() === "ADMIN" || + user?.username?.toLowerCase() === "admin") && ( + setLocation("/settings")}> + Account Settings + + )} Log out diff --git a/apps/Frontend/src/lib/protected-route.tsx b/apps/Frontend/src/lib/protected-route.tsx index c9369b1..2ec8db0 100644 --- a/apps/Frontend/src/lib/protected-route.tsx +++ b/apps/Frontend/src/lib/protected-route.tsx @@ -4,28 +4,32 @@ import { useAuth } from "@/hooks/use-auth"; import { Suspense } from "react"; import { Redirect, Route } from "wouter"; -type ComponentLike = React.ComponentType; // works for both lazy() and regular components +type ComponentLike = React.ComponentType; export function ProtectedRoute({ path, component: Component, + adminOnly, }: { path: string; component: ComponentLike; + adminOnly?: boolean; }) { const { user, isLoading } = useAuth(); return ( - {/* While auth is resolving: keep layout visible and show a small spinner in the content area */} {isLoading ? ( ) : !user ? ( + ) : adminOnly && + user.role?.toUpperCase() !== "ADMIN" && + user.username?.toLowerCase() !== "admin" ? ( + ) : ( - // Authenticated: render page inside layout. Lazy pages load with an in-layout spinner. }> diff --git a/apps/Frontend/src/pages/settings-page.tsx b/apps/Frontend/src/pages/settings-page.tsx index 066fac7..9df4c0d 100644 --- a/apps/Frontend/src/pages/settings-page.tsx +++ b/apps/Frontend/src/pages/settings-page.tsx @@ -10,6 +10,7 @@ import { CredentialTable } from "@/components/settings/insuranceCredTable"; import { useAuth } from "@/hooks/use-auth"; import { Staff } from "@repo/db/types"; +type SafeUser = { id: number; username: string; role: "ADMIN" | "USER" }; export default function SettingsPage() { const { toast } = useToast(); @@ -215,7 +216,84 @@ export default function SettingsPage() { `Viewing staff member:\n${staff.name} (${staff.email || "No email"})` ); - // MANAGE USER + // --- Users control (list, add, edit password, delete) --- + const { + data: usersList = [], + isLoading: usersLoading, + isError: usersError, + error: usersErrorObj, + } = useQuery({ + queryKey: ["/api/users/list"], + queryFn: async () => { + const res = await apiRequest("GET", "/api/users/list"); + if (!res.ok) throw new Error("Failed to fetch users"); + return res.json(); + }, + staleTime: 1000 * 60 * 2, + }); + + const addUserMutate = useMutation({ + mutationFn: async (data) => { + const res = await apiRequest("POST", "/api/users/", data); + if (!res.ok) { + const err = await res.json().catch(() => null); + throw new Error(err?.error || "Failed to add user"); + } + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/users/list"] }); + setAddUserModalOpen(false); + toast({ title: "User Added", description: "User created successfully.", variant: "default" }); + }, + onError: (e: any) => { + toast({ title: "Error", description: e?.message || "Failed to add user", variant: "destructive" }); + }, + }); + + const updateUserPasswordMutate = useMutation({ + mutationFn: async ({ id, password }) => { + const res = await apiRequest("PUT", `/api/users/${id}`, { password }); + if (!res.ok) { + const err = await res.json().catch(() => null); + throw new Error(err?.error || "Failed to update password"); + } + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/users/list"] }); + setEditPasswordUser(null); + toast({ title: "Password Updated", description: "Password changed successfully.", variant: "default" }); + }, + onError: (e: any) => { + toast({ title: "Error", description: e?.message || "Failed to update password", variant: "destructive" }); + }, + }); + + const deleteUserMutate = useMutation({ + mutationFn: async (id) => { + const res = await apiRequest("DELETE", `/api/users/${id}`); + if (!res.ok) { + const err = await res.json().catch(() => null); + throw new Error(err?.error || "Failed to delete user"); + } + return id; + }, + onSuccess: () => { + setUserToDelete(null); + queryClient.invalidateQueries({ queryKey: ["/api/users/list"] }); + toast({ title: "User Removed", description: "User deleted.", variant: "default" }); + }, + onError: (e: any) => { + toast({ title: "Error", description: e?.message || "Failed to delete user", variant: "destructive" }); + }, + }); + + const [addUserModalOpen, setAddUserModalOpen] = useState(false); + const [editPasswordUser, setEditPasswordUser] = useState(null); + const [userToDelete, setUserToDelete] = useState(null); + + // MANAGE USER (current user profile) const [usernameUser, setUsernameUser] = useState(""); //fetch user @@ -303,10 +381,73 @@ export default function SettingsPage() { )} - {/* User Setting section */} + {/* Users control section */} + + +
+

User Accounts

+ +
+
+ + + + + + + + + + {usersLoading && ( + + )} + {usersError && ( + + )} + {!usersLoading && !usersError && usersList.filter((u) => u.id !== user?.id).length === 0 && ( + + )} + {!usersLoading && usersList.filter((u) => u.id !== user?.id).map((u) => ( + + + + + + ))} + +
UsernameRoleActions
Loading users...
{(usersErrorObj as Error)?.message}
No other users.
+ {u.username} + {u.role} + + +
+
+
+
+ + {/* User Setting section (current user profile) */} -

User Settings

+

Admin Setting

{ @@ -358,6 +499,96 @@ export default function SettingsPage() { + {/* Add User modal */} + {addUserModalOpen && ( +
+
+

Add User

+ { + e.preventDefault(); + const form = e.currentTarget; + const username = (form.querySelector('[name="new-username"]') as HTMLInputElement)?.value?.trim(); + const password = (form.querySelector('[name="new-password"]') as HTMLInputElement)?.value; + const role = (form.querySelector('[name="new-role"]') as HTMLSelectElement)?.value as "ADMIN" | "USER"; + if (!username || !password) { + toast({ title: "Error", description: "Username and password are required.", variant: "destructive" }); + return; + } + addUserMutate.mutate({ username, password, role: role || "USER" }); + }} + className="space-y-4" + > +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ )} + + {/* Edit password modal */} + {editPasswordUser && ( +
+
+

Change password for {editPasswordUser.username}

+
{ + e.preventDefault(); + const form = e.currentTarget; + const password = (form.querySelector('[name="edit-password"]') as HTMLInputElement)?.value; + if (!password?.trim()) { + toast({ title: "Error", description: "Password is required.", variant: "destructive" }); + return; + } + updateUserPasswordMutate.mutate({ id: editPasswordUser.id, password }); + }} + className="space-y-4" + > +
+ + +
+
+ + +
+
+
+
+ )} + + userToDelete && deleteUserMutate.mutate(userToDelete.id)} + onCancel={() => setUserToDelete(null)} + entityName={userToDelete?.username} + /> + {/* Credential Section */}
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index c32303a..f25741f 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -18,10 +18,16 @@ datasource db { provider = "postgresql" } +enum UserRole { + ADMIN + USER +} + model User { id Int @id @default(autoincrement()) username String @unique password String + role UserRole @default(USER) patients Patient[] appointments Appointment[] staff Staff[] diff --git a/packages/db/prisma/seed.ts b/packages/db/prisma/seed.ts index ac5a22f..701cfe4 100644 --- a/packages/db/prisma/seed.ts +++ b/packages/db/prisma/seed.ts @@ -6,11 +6,11 @@ function formatTime(date: Date): string { } async function main() { - // Create multiple users + // Create multiple users (role: ADMIN | USER) const users = await prisma.user.createMany({ data: [ - { username: "admin2", password: "123456" }, - { username: "bob", password: "123456" }, + { username: "admin2", password: "123456", role: "ADMIN" }, + { username: "bob", password: "123456", role: "USER" }, ], });