show credential pw
This commit is contained in:
0
.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-05
Normal file
0
.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-05
Normal file
0
.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-08
Normal file
0
.turbo/daemon/53b0054db79f7114-turbo.log.2026-04-08
Normal file
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"is_task_list_visible": true,
|
"is_task_list_visible": true,
|
||||||
"active_task": "backend#dev"
|
"active_task": "frontend#dev"
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@ NODE_ENV="development"
|
|||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=5000
|
PORT=5000
|
||||||
# FRONTEND_URLS=http://localhost:3000,http://192.168.1.8:3000
|
# FRONTEND_URLS=http://localhost:3000,http://192.168.1.8:3000
|
||||||
# FRONTEND_URLS=http://localhost:3000
|
FRONTEND_URLS=http://localhost:3000
|
||||||
FRONTEND_URLS=http://192.168.1.37:3000
|
# FRONTEND_URLS=http://192.168.1.37:3000
|
||||||
SELENIUM_AGENT_BASE_URL=http://localhost:5002
|
SELENIUM_AGENT_BASE_URL=http://localhost:5002
|
||||||
JWT_SECRET = 'dentalsecret'
|
JWT_SECRET = 'dentalsecret'
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
|
|||||||
@@ -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
|
// GET: User by ID
|
||||||
router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
@@ -55,10 +70,18 @@ router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// POST: Create new user
|
// POST: Create new user
|
||||||
router.post("/", async (req: Request, res: Response) => {
|
router.post("/", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
|
if (req.user?.username !== "admin") {
|
||||||
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
}
|
||||||
const input = userCreateSchema.parse(req.body);
|
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;
|
const { password, ...safeUser } = newUser;
|
||||||
res.status(201).json(safeUser);
|
res.status(201).json(safeUser);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=3000
|
PORT=3000
|
||||||
VITE_API_BASE_URL_BACKEND=
|
# VITE_API_BASE_URL_BACKEND=http://192.168.1.37:5000
|
||||||
# VITE_API_BASE_URL_BACKEND=http://localhost:5000
|
VITE_API_BASE_URL_BACKEND=http://localhost:5000
|
||||||
@@ -41,7 +41,7 @@ function Router() {
|
|||||||
component={() => <AppointmentsPage />}
|
component={() => <AppointmentsPage />}
|
||||||
/>
|
/>
|
||||||
<ProtectedRoute path="/patients" component={() => <PatientsPage />} />
|
<ProtectedRoute path="/patients" component={() => <PatientsPage />} />
|
||||||
<ProtectedRoute path="/settings" component={() => <SettingsPage />} />
|
<ProtectedRoute path="/settings" component={() => <SettingsPage />} adminOnly />
|
||||||
<ProtectedRoute path="/claims" component={() => <ClaimsPage />} />
|
<ProtectedRoute path="/claims" component={() => <ClaimsPage />} />
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
path="/insurance-status"
|
path="/insurance-status"
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useSidebar } from "@/components/ui/sidebar";
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const [location] = useLocation();
|
const [location] = useLocation();
|
||||||
const { state, openMobile, setOpenMobile } = useSidebar(); // "expanded" | "collapsed"
|
const { state, openMobile, setOpenMobile } = useSidebar(); // "expanded" | "collapsed"
|
||||||
|
const { user } = useAuth();
|
||||||
|
const isAdmin = user?.username === "admin";
|
||||||
|
|
||||||
const navItems = useMemo(
|
const navItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -82,6 +85,7 @@ export function Sidebar() {
|
|||||||
name: "Settings",
|
name: "Settings",
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
icon: <Settings className="h-5 w-5" />,
|
icon: <Settings className="h-5 w-5" />,
|
||||||
|
adminOnly: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
@@ -107,7 +111,7 @@ export function Sidebar() {
|
|||||||
>
|
>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<nav role="navigation" aria-label="Main">
|
<nav role="navigation" aria-label="Main">
|
||||||
{navItems.map((item) => (
|
{navItems.filter((item) => !item.adminOnly || isAdmin).map((item) => (
|
||||||
<div key={item.path}>
|
<div key={item.path}>
|
||||||
<Link to={item.path} onClick={() => setOpenMobile(false)}>
|
<Link to={item.path} onClick={() => setOpenMobile(false)}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
|
||||||
type CredentialFormProps = {
|
type CredentialFormProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -18,6 +19,7 @@ export function CredentialForm({ onClose, userId, defaultValues }: CredentialFor
|
|||||||
const [siteKey, setSiteKey] = useState(defaultValues?.siteKey || "");
|
const [siteKey, setSiteKey] = useState(defaultValues?.siteKey || "");
|
||||||
const [username, setUsername] = useState(defaultValues?.username || "");
|
const [username, setUsername] = useState(defaultValues?.username || "");
|
||||||
const [password, setPassword] = useState(defaultValues?.password || "");
|
const [password, setPassword] = useState(defaultValues?.password || "");
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -111,12 +113,22 @@ export function CredentialForm({ onClose, userId, defaultValues }: CredentialFor
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium">Password</label>
|
<label className="block text-sm font-medium">Password</label>
|
||||||
<input
|
<div className="relative mt-1">
|
||||||
type="password"
|
<input
|
||||||
value={password}
|
type={showPassword ? "text" : "password"}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
value={password}
|
||||||
className="mt-1 p-2 border rounded w-full"
|
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>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -9,23 +9,25 @@ type ComponentLike = React.ComponentType; // works for both lazy() and regular c
|
|||||||
export function ProtectedRoute({
|
export function ProtectedRoute({
|
||||||
path,
|
path,
|
||||||
component: Component,
|
component: Component,
|
||||||
|
adminOnly = false,
|
||||||
}: {
|
}: {
|
||||||
path: string;
|
path: string;
|
||||||
component: ComponentLike;
|
component: ComponentLike;
|
||||||
|
adminOnly?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { user, isLoading } = useAuth();
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route path={path}>
|
<Route path={path}>
|
||||||
{/* While auth is resolving: keep layout visible and show a small spinner in the content area */}
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<LoadingScreen />
|
<LoadingScreen />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
) : !user ? (
|
) : !user ? (
|
||||||
<Redirect to="/auth" />
|
<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>
|
<AppLayout>
|
||||||
<Suspense fallback={<LoadingScreen />}>
|
<Suspense fallback={<LoadingScreen />}>
|
||||||
<Component />
|
<Component />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -12,23 +12,16 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { CheckCircle, Torus } from "lucide-react";
|
import { CheckCircle, Torus } from "lucide-react";
|
||||||
import { CheckedState } from "@radix-ui/react-checkbox";
|
import { CheckedState } from "@radix-ui/react-checkbox";
|
||||||
import LoadingScreen from "@/components/ui/LoadingScreen";
|
import LoadingScreen from "@/components/ui/LoadingScreen";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import {
|
import { LoginFormValues, loginSchema } from "@repo/db/types";
|
||||||
LoginFormValues,
|
|
||||||
loginSchema,
|
|
||||||
RegisterFormValues,
|
|
||||||
registerSchema,
|
|
||||||
} from "@repo/db/types";
|
|
||||||
|
|
||||||
export default function AuthPage() {
|
export default function AuthPage() {
|
||||||
const [activeTab, setActiveTab] = useState<string>("login");
|
const { isLoading, user, loginMutation } = useAuth();
|
||||||
const { isLoading, user, loginMutation, registerMutation } = useAuth();
|
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
|
|
||||||
const loginForm = useForm<LoginFormValues>({
|
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) => {
|
const onLoginSubmit = (data: LoginFormValues) => {
|
||||||
loginMutation.mutate({ username: data.username, password: data.password });
|
loginMutation.mutate({ username: data.username, password: data.password });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRegisterSubmit = (data: RegisterFormValues) => {
|
|
||||||
registerMutation.mutate({
|
|
||||||
username: data.username,
|
|
||||||
password: data.password,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingScreen />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
@@ -74,7 +50,7 @@ export default function AuthPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
<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">
|
<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">
|
<Card className="p-6 bg-white">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-10 text-center">
|
||||||
<h1 className="text-3xl font-medium text-primary mb-2">
|
<h1 className="text-3xl font-medium text-primary mb-2">
|
||||||
@@ -86,193 +62,74 @@ export default function AuthPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Form {...loginForm}>
|
||||||
defaultValue="login"
|
<form
|
||||||
value={activeTab}
|
onSubmit={loginForm.handleSubmit(onLoginSubmit)}
|
||||||
onValueChange={setActiveTab}
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
<FormField
|
||||||
<TabsTrigger value="login">Login</TabsTrigger>
|
control={loginForm.control}
|
||||||
<TabsTrigger value="register">Register</TabsTrigger>
|
name="username"
|
||||||
</TabsList>
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Enter your username" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<TabsContent value="login">
|
<FormField
|
||||||
<Form {...loginForm}>
|
control={loginForm.control}
|
||||||
<form
|
name="password"
|
||||||
onSubmit={loginForm.handleSubmit(onLoginSubmit)}
|
render={({ field }) => (
|
||||||
className="space-y-4"
|
<FormItem>
|
||||||
>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={loginForm.control}
|
<Input
|
||||||
name="username"
|
placeholder="••••••••"
|
||||||
render={({ field }) => (
|
type="password"
|
||||||
<FormItem>
|
{...field}
|
||||||
<FormLabel>Username</FormLabel>
|
/>
|
||||||
<FormControl>
|
</FormControl>
|
||||||
<Input placeholder="Enter your username" {...field} />
|
<FormMessage />
|
||||||
</FormControl>
|
</FormItem>
|
||||||
<FormMessage />
|
)}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<div className="flex items-center justify-between">
|
||||||
control={loginForm.control}
|
<FormField
|
||||||
name="password"
|
control={loginForm.control}
|
||||||
render={({ field }) => (
|
name="rememberMe"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Password</FormLabel>
|
<div className="flex items-center space-x-2">
|
||||||
<FormControl>
|
<Checkbox
|
||||||
<Input
|
id="remember-me"
|
||||||
placeholder="••••••••"
|
checked={field.value as CheckedState}
|
||||||
type="password"
|
onCheckedChange={field.onChange}
|
||||||
{...field}
|
/>
|
||||||
/>
|
<label
|
||||||
</FormControl>
|
htmlFor="remember-me"
|
||||||
<FormMessage />
|
className="text-sm font-medium text-gray-700"
|
||||||
</FormItem>
|
>
|
||||||
)}
|
Remember me
|
||||||
/>
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<Button
|
||||||
<FormField
|
type="submit"
|
||||||
control={loginForm.control}
|
className="w-full"
|
||||||
name="rememberMe"
|
disabled={loginMutation.isPending}
|
||||||
render={({ field }) => (
|
>
|
||||||
<div className="flex items-center space-x-2">
|
{loginMutation.isPending ? "Signing in..." : "Sign in"}
|
||||||
<Checkbox
|
</Button>
|
||||||
id="remember-me"
|
</form>
|
||||||
checked={field.value as CheckedState}
|
</Form>
|
||||||
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>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
|
|||||||
@@ -215,24 +215,80 @@ export default function SettingsPage() {
|
|||||||
`Viewing staff member:\n${staff.name} (${staff.email || "No email"})`,
|
`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("");
|
const [usernameUser, setUsernameUser] = useState("");
|
||||||
|
|
||||||
//fetch user
|
|
||||||
const { user } = useAuth();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.username) {
|
if (currentUser?.username) {
|
||||||
setUsernameUser(user.username);
|
setUsernameUser(currentUser.username);
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [currentUser]);
|
||||||
|
|
||||||
//update user mutation
|
//update user mutation
|
||||||
const updateUserMutate = useMutation({
|
const updateUserMutate = useMutation({
|
||||||
mutationFn: async (
|
mutationFn: async (
|
||||||
updates: Partial<{ username: string; password: string }>,
|
updates: Partial<{ username: string; password: string }>,
|
||||||
) => {
|
) => {
|
||||||
if (!user?.id) throw new Error("User not loaded");
|
if (!currentUser?.id) throw new Error("User not loaded");
|
||||||
const res = await apiRequest("PUT", `/api/users/${user.id}`, updates);
|
const res = await apiRequest("PUT", `/api/users/${currentUser.id}`, updates);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json().catch(() => null);
|
const errorData = await res.json().catch(() => null);
|
||||||
throw new Error(errorData?.error || "Failed to update user");
|
throw new Error(errorData?.error || "Failed to update user");
|
||||||
@@ -303,6 +359,77 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</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 */}
|
{/* User Setting section */}
|
||||||
<Card className="mt-6">
|
<Card className="mt-6">
|
||||||
<CardContent className="space-y-4 py-6">
|
<CardContent className="space-y-4 py-6">
|
||||||
|
|||||||
Binary file not shown.
@@ -9,90 +9,16 @@ import bcrypt from "bcrypt";
|
|||||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
const prisma = new PrismaClient({ adapter } as any);
|
const prisma = new PrismaClient({ adapter } as any);
|
||||||
|
|
||||||
function formatTime(date: Date): string {
|
|
||||||
return date.toTimeString().slice(0, 5); // "HH:MM"
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const hashedPassword = await bcrypt.hash("123456", 10);
|
const hashedPassword = await bcrypt.hash("123456", 10);
|
||||||
|
|
||||||
// Create multiple users
|
await prisma.user.upsert({
|
||||||
await prisma.user.createMany({
|
where: { username: "admin" },
|
||||||
data: [
|
update: {},
|
||||||
{ username: "admin2", password: hashedPassword },
|
create: { username: "admin", password: hashedPassword },
|
||||||
{ username: "bob", password: hashedPassword },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdUsers = await prisma.user.findMany();
|
console.log("Seed complete: admin user created (username: admin, password: 123456)");
|
||||||
|
|
||||||
// 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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"generatorVersion": "1.0.0",
|
"generatorVersion": "1.0.0",
|
||||||
"generatedAt": "2026-04-03T21:49:35.498Z",
|
"generatedAt": "2026-04-05T02:53:07.525Z",
|
||||||
"outputPath": "/home/gg/Desktop/DentalManagement/packages/db/shared",
|
"outputPath": "/home/ff/Desktop/DentalManagementMHAprilgg/packages/db/shared",
|
||||||
"files": [
|
"files": [
|
||||||
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
||||||
"schemas/enums/UserScalarFieldEnum.schema.ts",
|
"schemas/enums/UserScalarFieldEnum.schema.ts",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user