show credential pw

This commit is contained in:
ff
2026-04-07 23:52:05 -04:00
parent cb97e249d0
commit b9edd6a5e6
16 changed files with 1846 additions and 318 deletions

View File

@@ -1,4 +1,4 @@
{
"is_task_list_visible": true,
"active_task": "backend#dev"
"active_task": "frontend#dev"
}

View File

@@ -2,8 +2,8 @@ NODE_ENV="development"
HOST=0.0.0.0
PORT=5000
# FRONTEND_URLS=http://localhost:3000,http://192.168.1.8:3000
# FRONTEND_URLS=http://localhost:3000
FRONTEND_URLS=http://192.168.1.37:3000
FRONTEND_URLS=http://localhost:3000
# FRONTEND_URLS=http://192.168.1.37:3000
SELENIUM_AGENT_BASE_URL=http://localhost:5002
JWT_SECRET = 'dentalsecret'
DB_HOST=localhost

View File

@@ -34,6 +34,21 @@ router.get("/", async (req: Request, res: Response): Promise<any> => {
}
});
// GET: List all users (admin only)
router.get("/list", async (req: Request, res: Response): Promise<any> => {
try {
if (req.user?.username !== "admin") {
return res.status(403).json({ error: "Forbidden" });
}
const users = await storage.getUsers(1000, 0);
const safeUsers = users.map(({ password, ...u }) => u);
res.json(safeUsers);
} 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<any> => {
try {
@@ -55,10 +70,18 @@ router.get("/:id", async (req: Request, res: Response): Promise<any> => {
});
// POST: Create new user
router.post("/", async (req: Request, res: Response) => {
router.post("/", async (req: Request, res: Response): Promise<any> => {
try {
if (req.user?.username !== "admin") {
return res.status(403).json({ error: "Forbidden" });
}
const input = userCreateSchema.parse(req.body);
const newUser = await storage.createUser(input);
const existing = await storage.getUserByUsername(input.username as string);
if (existing) {
return res.status(400).json({ error: "Username already exists" });
}
const hashed = await hashPassword(input.password as string);
const newUser = await storage.createUser({ ...input, password: hashed });
const { password, ...safeUser } = newUser;
res.status(201).json(safeUser);
} catch (err) {

View File

@@ -1,5 +1,5 @@
NODE_ENV=development
HOST=0.0.0.0
PORT=3000
VITE_API_BASE_URL_BACKEND=
# VITE_API_BASE_URL_BACKEND=http://localhost:5000
# VITE_API_BASE_URL_BACKEND=http://192.168.1.37:5000
VITE_API_BASE_URL_BACKEND=http://localhost:5000

View File

@@ -41,7 +41,7 @@ function Router() {
component={() => <AppointmentsPage />}
/>
<ProtectedRoute path="/patients" component={() => <PatientsPage />} />
<ProtectedRoute path="/settings" component={() => <SettingsPage />} />
<ProtectedRoute path="/settings" component={() => <SettingsPage />} adminOnly />
<ProtectedRoute path="/claims" component={() => <ClaimsPage />} />
<ProtectedRoute
path="/insurance-status"

View File

@@ -16,10 +16,13 @@ import {
import { cn } from "@/lib/utils";
import { useMemo } from "react";
import { useSidebar } from "@/components/ui/sidebar";
import { useAuth } from "@/hooks/use-auth";
export function Sidebar() {
const [location] = useLocation();
const { state, openMobile, setOpenMobile } = useSidebar(); // "expanded" | "collapsed"
const { user } = useAuth();
const isAdmin = user?.username === "admin";
const navItems = useMemo(
() => [
@@ -82,6 +85,7 @@ export function Sidebar() {
name: "Settings",
path: "/settings",
icon: <Settings className="h-5 w-5" />,
adminOnly: true,
},
],
[]
@@ -107,7 +111,7 @@ export function Sidebar() {
>
<div className="p-2">
<nav role="navigation" aria-label="Main">
{navItems.map((item) => (
{navItems.filter((item) => !item.adminOnly || isAdmin).map((item) => (
<div key={item.path}>
<Link to={item.path} onClick={() => setOpenMobile(false)}>
<div

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { toast } from "@/hooks/use-toast";
import { Eye, EyeOff } from "lucide-react";
type CredentialFormProps = {
onClose: () => void;
@@ -18,6 +19,7 @@ export function CredentialForm({ onClose, userId, defaultValues }: CredentialFor
const [siteKey, setSiteKey] = useState(defaultValues?.siteKey || "");
const [username, setUsername] = useState(defaultValues?.username || "");
const [password, setPassword] = useState(defaultValues?.password || "");
const [showPassword, setShowPassword] = useState(false);
const queryClient = useQueryClient();
@@ -111,12 +113,22 @@ export function CredentialForm({ onClose, userId, defaultValues }: CredentialFor
</div>
<div>
<label className="block text-sm font-medium">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 p-2 border rounded w-full"
/>
<div className="relative mt-1">
<input
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="p-2 border rounded w-full pr-10"
/>
<button
type="button"
onClick={() => setShowPassword((prev) => !prev)}
className="absolute inset-y-0 right-2 flex items-center text-gray-500 hover:text-gray-700"
tabIndex={-1}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<div className="flex justify-end gap-2">
<button

View File

@@ -9,23 +9,25 @@ type ComponentLike = React.ComponentType; // works for both lazy() and regular c
export function ProtectedRoute({
path,
component: Component,
adminOnly = false,
}: {
path: string;
component: ComponentLike;
adminOnly?: boolean;
}) {
const { user, isLoading } = useAuth();
return (
<Route path={path}>
{/* While auth is resolving: keep layout visible and show a small spinner in the content area */}
{isLoading ? (
<AppLayout>
<LoadingScreen />
</AppLayout>
) : !user ? (
<Redirect to="/auth" />
) : adminOnly && user.username !== "admin" ? (
<Redirect to="/insurance-status" />
) : (
// Authenticated: render page inside layout. Lazy pages load with an in-layout spinner.
<AppLayout>
<Suspense fallback={<LoadingScreen />}>
<Component />

View File

@@ -1,6 +1,6 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import {
@@ -12,23 +12,16 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Checkbox } from "@/components/ui/checkbox";
import { Card } from "@/components/ui/card";
import { CheckCircle, Torus } from "lucide-react";
import { CheckedState } from "@radix-ui/react-checkbox";
import LoadingScreen from "@/components/ui/LoadingScreen";
import { useLocation } from "wouter";
import {
LoginFormValues,
loginSchema,
RegisterFormValues,
registerSchema,
} from "@repo/db/types";
import { LoginFormValues, loginSchema } from "@repo/db/types";
export default function AuthPage() {
const [activeTab, setActiveTab] = useState<string>("login");
const { isLoading, user, loginMutation, registerMutation } = useAuth();
const { isLoading, user, loginMutation } = useAuth();
const [, navigate] = useLocation();
const loginForm = useForm<LoginFormValues>({
@@ -40,27 +33,10 @@ export default function AuthPage() {
},
});
const registerForm = useForm<RegisterFormValues>({
resolver: zodResolver(registerSchema),
defaultValues: {
username: "",
password: "",
confirmPassword: "",
agreeTerms: false,
},
});
const onLoginSubmit = (data: LoginFormValues) => {
loginMutation.mutate({ username: data.username, password: data.password });
};
const onRegisterSubmit = (data: RegisterFormValues) => {
registerMutation.mutate({
username: data.username,
password: data.password,
});
};
if (isLoading) {
return <LoadingScreen />;
}
@@ -74,7 +50,7 @@ export default function AuthPage() {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="w-full max-w-4xl grid grid-cols-1 md:grid-cols-2 shadow-lg rounded-lg overflow-hidden">
{/* Auth Forms */}
{/* Auth Form */}
<Card className="p-6 bg-white">
<div className="mb-10 text-center">
<h1 className="text-3xl font-medium text-primary mb-2">
@@ -86,193 +62,74 @@ export default function AuthPage() {
</p>
</div>
<Tabs
defaultValue="login"
value={activeTab}
onValueChange={setActiveTab}
>
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
</TabsList>
<Form {...loginForm}>
<form
onSubmit={loginForm.handleSubmit(onLoginSubmit)}
className="space-y-4"
>
<FormField
control={loginForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter your username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TabsContent value="login">
<Form {...loginForm}>
<form
onSubmit={loginForm.handleSubmit(onLoginSubmit)}
className="space-y-4"
>
<FormField
control={loginForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter your username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="••••••••"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="••••••••"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center justify-between">
<FormField
control={loginForm.control}
name="rememberMe"
render={({ field }) => (
<div className="flex items-center space-x-2">
<Checkbox
id="remember-me"
checked={field.value as CheckedState}
onCheckedChange={field.onChange}
/>
<label
htmlFor="remember-me"
className="text-sm font-medium text-gray-700"
>
Remember me
</label>
</div>
)}
/>
</div>
<div className="flex items-center justify-between">
<FormField
control={loginForm.control}
name="rememberMe"
render={({ field }) => (
<div className="flex items-center space-x-2">
<Checkbox
id="remember-me"
checked={field.value as CheckedState}
onCheckedChange={field.onChange}
/>
<label
htmlFor="remember-me"
className="text-sm font-medium text-gray-700"
>
Remember me
</label>
</div>
)}
/>
<button
type="button"
onClick={() => {
// do something if needed
}}
className="text-sm font-medium text-primary hover:text-primary/80"
>
Forgot password?
</button>
</div>
<Button
type="submit"
className="w-full"
disabled={loginMutation.isPending}
>
{loginMutation.isPending ? "Signing in..." : "Sign in"}
</Button>
</form>
</Form>
</TabsContent>
<TabsContent value="register">
<Form {...registerForm}>
<form
onSubmit={registerForm.handleSubmit(onRegisterSubmit)}
className="space-y-4"
>
<FormField
control={registerForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Choose a username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={registerForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="••••••••"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={registerForm.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
placeholder="••••••••"
type="password"
{...field}
value={
typeof field.value === "string" ? field.value : ""
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={registerForm.control}
name="agreeTerms"
render={({ field }) => (
<FormItem className="flex space-x-2 items-center">
<FormControl>
<Checkbox
checked={field.value as CheckedState}
onCheckedChange={field.onChange}
className="mt-2.5"
/>
</FormControl>
<div className="">
<FormLabel className="text-sm font-bold leading-tight">
I agree to the{" "}
<a href="#" className="text-primary underline">
Terms and Conditions
</a>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
disabled={registerMutation.isPending}
>
{registerMutation.isPending
? "Creating Account..."
: "Create Account"}
</Button>
</form>
</Form>
</TabsContent>
</Tabs>
<Button
type="submit"
className="w-full"
disabled={loginMutation.isPending}
>
{loginMutation.isPending ? "Signing in..." : "Sign in"}
</Button>
</form>
</Form>
</Card>
{/* Hero Section */}

View File

@@ -215,24 +215,80 @@ export default function SettingsPage() {
`Viewing staff member:\n${staff.name} (${staff.email || "No email"})`,
);
// MANAGE USER
// MANAGE USERS (admin only)
const [newUsername, setNewUsername] = useState("");
const [newPassword, setNewPassword] = useState("");
const {
data: allUsers = [],
refetch: refetchUsers,
} = useQuery<{ id: number; username: string }[]>({
queryKey: ["/api/users/list"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/users/list");
if (!res.ok) return [];
return res.json();
},
enabled: false, // loaded lazily below
});
const { user: currentUser } = useAuth();
const isAdmin = currentUser?.username === "admin";
useEffect(() => {
if (isAdmin) refetchUsers();
}, [isAdmin]);
const addUserMutation = useMutation({
mutationFn: async (data: { username: string; password: string }) => {
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 create user");
}
return res.json();
},
onSuccess: () => {
setNewUsername("");
setNewPassword("");
refetchUsers();
toast({ title: "User Created", description: "New user added successfully." });
},
onError: (err: any) => {
toast({ title: "Error", description: err?.message || "Failed to create user", variant: "destructive" });
},
});
const deleteUserMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("DELETE", `/api/users/${id}`);
if (!res.ok) throw new Error("Failed to delete user");
},
onSuccess: () => {
refetchUsers();
toast({ title: "User Deleted", description: "User removed successfully." });
},
onError: (err: any) => {
toast({ title: "Error", description: err?.message || "Failed to delete user", variant: "destructive" });
},
});
// MANAGE USER (own account)
const [usernameUser, setUsernameUser] = useState("");
//fetch user
const { user } = useAuth();
useEffect(() => {
if (user?.username) {
setUsernameUser(user.username);
if (currentUser?.username) {
setUsernameUser(currentUser.username);
}
}, [user]);
}, [currentUser]);
//update user mutation
const updateUserMutate = useMutation({
mutationFn: async (
updates: Partial<{ username: string; password: string }>,
) => {
if (!user?.id) throw new Error("User not loaded");
const res = await apiRequest("PUT", `/api/users/${user.id}`, updates);
if (!currentUser?.id) throw new Error("User not loaded");
const res = await apiRequest("PUT", `/api/users/${currentUser.id}`, updates);
if (!res.ok) {
const errorData = await res.json().catch(() => null);
throw new Error(errorData?.error || "Failed to update user");
@@ -303,6 +359,77 @@ export default function SettingsPage() {
</div>
)}
{/* Manage Users section (admin only) */}
{isAdmin && (
<Card className="mt-6">
<CardContent className="space-y-4 py-6">
<h3 className="text-lg font-semibold">Manage Users</h3>
{/* Existing users list */}
<div className="border rounded divide-y">
{allUsers.length === 0 && (
<p className="text-sm text-gray-500 p-3">No users found.</p>
)}
{allUsers.map((u) => (
<div key={u.id} className="flex items-center justify-between px-3 py-2">
<span className="text-sm font-medium">{u.username}</span>
{u.username !== "admin" && (
<button
className="text-sm text-red-600 hover:underline"
onClick={() => deleteUserMutation.mutate(u.id)}
disabled={deleteUserMutation.isPending}
>
Delete
</button>
)}
</div>
))}
</div>
{/* Add new user form */}
<form
className="space-y-3 pt-2"
onSubmit={(e) => {
e.preventDefault();
if (!newUsername.trim() || !newPassword.trim()) return;
addUserMutation.mutate({ username: newUsername.trim(), password: newPassword.trim() });
}}
>
<h4 className="text-sm font-semibold text-gray-700">Add New User</h4>
<div>
<label className="block text-sm font-medium">Username</label>
<input
type="text"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
className="mt-1 p-2 border rounded w-full"
placeholder="Enter username"
required
/>
</div>
<div>
<label className="block text-sm font-medium">Password</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="mt-1 p-2 border rounded w-full"
placeholder="••••••••"
required
/>
</div>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
disabled={addUserMutation.isPending}
>
{addUserMutation.isPending ? "Adding..." : "Add User"}
</button>
</form>
</CardContent>
</Card>
)}
{/* User Setting section */}
<Card className="mt-6">
<CardContent className="space-y-4 py-6">

View File

@@ -9,90 +9,16 @@ import bcrypt from "bcrypt";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter } as any);
function formatTime(date: Date): string {
return date.toTimeString().slice(0, 5); // "HH:MM"
}
async function main() {
const hashedPassword = await bcrypt.hash("123456", 10);
// Create multiple users
await prisma.user.createMany({
data: [
{ username: "admin2", password: hashedPassword },
{ username: "bob", password: hashedPassword },
],
await prisma.user.upsert({
where: { username: "admin" },
update: {},
create: { username: "admin", password: hashedPassword },
});
const createdUsers = await prisma.user.findMany();
// Create staff (userId links staff to a user account)
await prisma.staff.createMany({
data: [
{ name: "Dr. Kai Gao", role: "Doctor", userId: createdUsers[0].id },
{ name: "Dr. Jane Smith", role: "Doctor", userId: createdUsers[1].id },
],
});
const staffMembers = await prisma.staff.findMany();
// Create multiple patients
await prisma.patient.createMany({
data: [
{
firstName: "Emily",
lastName: "Clark",
dateOfBirth: new Date("1985-06-15"),
gender: "female",
phone: "555-0001",
email: "emily@example.com",
address: "101 Apple Rd",
city: "Newtown",
zipCode: "10001",
userId: createdUsers[0].id,
},
{
firstName: "Michael",
lastName: "Brown",
dateOfBirth: new Date("1979-09-10"),
gender: "male",
phone: "555-0002",
email: "michael@example.com",
address: "202 Banana Ave",
city: "Oldtown",
zipCode: "10002",
userId: createdUsers[1].id,
},
],
});
const createdPatients = await prisma.patient.findMany();
// Create multiple appointments
await prisma.appointment.createMany({
data: [
{
patientId: createdPatients[0].id,
userId: createdUsers[0].id,
staffId: staffMembers[0].id,
title: "Initial Consultation",
date: new Date("2025-06-01"),
startTime: formatTime(new Date("2025-06-01T10:00:00")),
endTime: formatTime(new Date("2025-06-01T10:30:00")),
type: "consultation",
},
{
patientId: createdPatients[1].id,
userId: createdUsers[1].id,
staffId: staffMembers[1].id,
title: "Follow-up",
date: new Date("2025-06-02"),
startTime: formatTime(new Date("2025-06-01T10:00:00")),
endTime: formatTime(new Date("2025-06-01T10:30:00")),
type: "checkup",
},
],
});
console.log("Seed complete: admin user created (username: admin, password: 123456)");
}
main()

View File

@@ -1,8 +1,8 @@
{
"version": "1.0",
"generatorVersion": "1.0.0",
"generatedAt": "2026-04-03T21:49:35.498Z",
"outputPath": "/home/gg/Desktop/DentalManagement/packages/db/shared",
"generatedAt": "2026-04-05T02:53:07.525Z",
"outputPath": "/home/ff/Desktop/DentalManagementMHAprilgg/packages/db/shared",
"files": [
"schemas/enums/TransactionIsolationLevel.schema.ts",
"schemas/enums/UserScalarFieldEnum.schema.ts",

File diff suppressed because it is too large Load Diff