feat(lst): added update settings into the entire app
This commit is contained in:
121
frontend/src/components/admin/settings/SettingForm.tsx
Normal file
121
frontend/src/components/admin/settings/SettingForm.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Label} from "@/components/ui/label";
|
||||||
|
import {Settings} from "./SettingsPage";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {useForm} from "react-hook-form";
|
||||||
|
import {z} from "zod";
|
||||||
|
import {zodResolver} from "@hookform/resolvers/zod";
|
||||||
|
import {useQuery} from "@tanstack/react-query";
|
||||||
|
import {getSettings} from "@/utils/querys/settings";
|
||||||
|
import {useSessionStore} from "@/lib/store/sessionStore";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
value: z.string().min(1, "You must enter a value greater than 0"),
|
||||||
|
});
|
||||||
|
export function ChangeSetting({setting}: {setting: Settings}) {
|
||||||
|
const {token} = useSessionStore();
|
||||||
|
const {refetch} = useQuery(getSettings(token ?? ""));
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: {errors},
|
||||||
|
} = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
value: setting.value || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
|
||||||
|
setSaving(!saving);
|
||||||
|
const update = {...data, name: setting.name};
|
||||||
|
// console.log(update);
|
||||||
|
try {
|
||||||
|
const result = await axios.patch("/api/server/settings", update, {
|
||||||
|
headers: {Authorization: `Bearer ${token}`},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.data.success) {
|
||||||
|
setOpen(!open);
|
||||||
|
setSaving(false);
|
||||||
|
refetch();
|
||||||
|
toast.success(result.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!open) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
setOpen(isOpen);
|
||||||
|
// toast.message("Model was something", {
|
||||||
|
// description: isOpen ? "Modal is open" : "Modal is closed",
|
||||||
|
// });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Edit</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{setting.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update the setting and press save to complete the changes.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<Label htmlFor={"value"} className="m-1">
|
||||||
|
Value
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
{...register("value")}
|
||||||
|
className={errors.value ? "border-red-500" : ""}
|
||||||
|
aria-invalid={!!errors.value}
|
||||||
|
/>
|
||||||
|
{errors.value && <p className="text-red-500 text-sm mt-1">{errors.value.message}</p>}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<Button type="submit" disabled={saving}>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<span>Saving....</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Save setting</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,21 +1,24 @@
|
|||||||
import {LstCard} from "@/components/extendedUI/LstCard";
|
import {LstCard} from "@/components/extendedUI/LstCard";
|
||||||
|
import {Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
|
||||||
import {useSessionStore} from "@/lib/store/sessionStore";
|
import {useSessionStore} from "@/lib/store/sessionStore";
|
||||||
import {useModuleStore} from "@/lib/store/useModuleStore";
|
import {useModuleStore} from "@/lib/store/useModuleStore";
|
||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {useRouter} from "@tanstack/react-router";
|
import {useRouter} from "@tanstack/react-router";
|
||||||
import axios from "axios";
|
import {ChangeSetting} from "./SettingForm";
|
||||||
|
import {getSettings} from "@/utils/querys/settings";
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
settings_id?: string;
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const token = localStorage.getItem("auth_token");
|
const {user, token} = useSessionStore();
|
||||||
const {user} = useSessionStore();
|
|
||||||
const {modules} = useModuleStore();
|
const {modules} = useModuleStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const fetchSettings = async () => {
|
|
||||||
const {data} = await axios.get("/api/server/settings", {headers: {Authorization: `Bearer ${token}`}});
|
|
||||||
return data.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const adminModule = modules.filter((n) => n.name === "admin");
|
const adminModule = modules.filter((n) => n.name === "admin");
|
||||||
const userLevel = user?.roles.filter((r) => r.module_id === adminModule[0].module_id) || [];
|
const userLevel = user?.roles.filter((r) => r.module_id === adminModule[0].module_id) || [];
|
||||||
|
|
||||||
@@ -23,12 +26,7 @@ export default function SettingsPage() {
|
|||||||
router.navigate({to: "/"});
|
router.navigate({to: "/"});
|
||||||
}
|
}
|
||||||
|
|
||||||
const {data, isError, error, isLoading} = useQuery({
|
const {data, isError, error, isLoading} = useQuery(getSettings(token ?? ""));
|
||||||
queryKey: ["settings"],
|
|
||||||
queryFn: fetchSettings,
|
|
||||||
enabled: !!token,
|
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div>Loading.....</div>;
|
return <div>Loading.....</div>;
|
||||||
@@ -38,10 +36,29 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<LstCard className="m-2 flex place-content-center w-dvh">
|
||||||
{data.map((i: any) => {
|
<Table>
|
||||||
return <LstCard key={i.settings_id}>{i.name}</LstCard>;
|
<TableCaption>All Settings</TableCaption>
|
||||||
})}
|
<TableHeader>
|
||||||
</div>
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Value</TableHead>
|
||||||
|
<TableHead>Change</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((setting: Settings) => (
|
||||||
|
<TableRow key={setting.settings_id}>
|
||||||
|
<TableCell className="font-medium">{setting.name}</TableCell>
|
||||||
|
<TableCell className="font-medium">{setting.value}</TableCell>
|
||||||
|
<TableCell className="font-medium">{setting.description}</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<ChangeSetting setting={setting} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</LstCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
133
frontend/src/components/ui/dialog.tsx
Normal file
133
frontend/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
58
frontend/src/components/ui/spinner.tsx
Normal file
58
frontend/src/components/ui/spinner.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {Slot} from "@radix-ui/react-slot";
|
||||||
|
import {cva, type VariantProps} from "class-variance-authority";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
|
const spinnerVariants = cva("relative block opacity-[0.65]", {
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
sm: "w-4 h-4",
|
||||||
|
md: "w-6 h-6",
|
||||||
|
lg: "w-8 h-8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "sm",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface SpinnerProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof spinnerVariants> {
|
||||||
|
loading?: boolean;
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(
|
||||||
|
({className, size, loading = true, asChild = false, ...props}, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "span";
|
||||||
|
|
||||||
|
const [bgColorClass, filteredClassName] = React.useMemo(() => {
|
||||||
|
const bgClass = className?.match(/(?:dark:bg-|bg-)[a-zA-Z0-9-]+/g) || [];
|
||||||
|
const filteredClasses = className?.replace(/(?:dark:bg-|bg-)[a-zA-Z0-9-]+/g, "").trim();
|
||||||
|
|
||||||
|
return [bgClass, filteredClasses];
|
||||||
|
}, [className]);
|
||||||
|
|
||||||
|
if (!loading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp className={cn(spinnerVariants({size, className: filteredClassName}))} ref={ref} {...props}>
|
||||||
|
{Array.from({length: 8}).map((_, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="absolute top-0 left-1/2 w-[12.5%] h-full animate-spinner-leaf-fade"
|
||||||
|
style={{
|
||||||
|
transform: `rotate(${i * 45}deg)`,
|
||||||
|
animationDelay: `${-(7 - i) * 100}ms`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={cn("block w-full h-[30%] rounded-full", bgColorClass)}></span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Spinner.displayName = "Spinner";
|
||||||
|
|
||||||
|
export {Spinner};
|
||||||
114
frontend/src/components/ui/table.tsx
Normal file
114
frontend/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
import {SessionProvider} from "../components/providers/Providers";
|
import {SessionProvider} from "../components/providers/Providers";
|
||||||
import {Toaster} from "sonner";
|
import {Toaster} from "sonner";
|
||||||
import {Button} from "../components/ui/button";
|
import {Button} from "../components/ui/button";
|
||||||
import {useLogout} from "../lib/hooks/useLogout";
|
|
||||||
import {useSession} from "../lib/hooks/useSession";
|
|
||||||
import {useSessionStore} from "../lib/store/sessionStore";
|
import {useSessionStore} from "../lib/store/sessionStore";
|
||||||
|
import {useSession} from "@/hooks/useSession";
|
||||||
|
import {useLogout} from "@/hooks/useLogout";
|
||||||
|
|
||||||
// same as the layout
|
// same as the layout
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
|
|||||||
3
frontend/src/utils/delay.ts
Normal file
3
frontend/src/utils/delay.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function delay(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
17
frontend/src/utils/querys/settings.tsx
Normal file
17
frontend/src/utils/querys/settings.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {queryOptions} from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export function getSettings(token: string) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["settings"],
|
||||||
|
queryFn: () => fetchSettings(token),
|
||||||
|
enabled: !!token,
|
||||||
|
staleTime: 1000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSettings = async (token: string) => {
|
||||||
|
const {data} = await axios.get("/api/server/settings", {headers: {Authorization: `Bearer ${token}`}});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
37
server/services/server/controller/settings/updateSetting.ts
Normal file
37
server/services/server/controller/settings/updateSetting.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {and, eq, sql} from "drizzle-orm";
|
||||||
|
import {db} from "../../../../../database/dbclient.js";
|
||||||
|
import {settings} from "../../../../../database/schema/settings.js";
|
||||||
|
import {log} from "../../../logger/logger.js";
|
||||||
|
import {userRoles} from "../../../../../database/schema/userRoles.js";
|
||||||
|
import {users} from "../../../../../database/schema/users.js";
|
||||||
|
|
||||||
|
export const updateSetting = async (data: any, user_id: string) => {
|
||||||
|
log.info(user_id, "Adding a new setting");
|
||||||
|
|
||||||
|
// make sure the user is a system admin before moving forward
|
||||||
|
const sysAdmin = await db
|
||||||
|
.select()
|
||||||
|
.from(userRoles)
|
||||||
|
.where(and(eq(userRoles.user_id, user_id), eq(userRoles.role, "systemAdmin")));
|
||||||
|
|
||||||
|
if (sysAdmin) {
|
||||||
|
log.info(`Setting ${data.name} is being updated`);
|
||||||
|
|
||||||
|
//get the username so we can update the correct field
|
||||||
|
const user = await db.select().from(users).where(eq(users.user_id, user_id));
|
||||||
|
try {
|
||||||
|
//const settingID = await db.select().from(settings).where(eq(settings.name, data.module));
|
||||||
|
|
||||||
|
const updateSetting = await db
|
||||||
|
.update(settings)
|
||||||
|
.set({value: data.value, upd_user: user[0].username, upd_date: sql`NOW()`})
|
||||||
|
.where(eq(settings.name, data.name));
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error, "Error updating setting");
|
||||||
|
throw new Error("Error updating Setting");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("This user cannot add new roles");
|
||||||
|
throw new Error("The user trying to add a setting dose not have the correct permissions");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,7 +17,7 @@ const AddSetting = z.object({
|
|||||||
|
|
||||||
app.openapi(
|
app.openapi(
|
||||||
createRoute({
|
createRoute({
|
||||||
tags: ["server"],
|
tags: ["server:settings"],
|
||||||
summary: "Add Setting",
|
summary: "Add Setting",
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const app = new OpenAPIHono();
|
|||||||
|
|
||||||
app.openapi(
|
app.openapi(
|
||||||
createRoute({
|
createRoute({
|
||||||
tags: ["server"],
|
tags: ["server:settings"],
|
||||||
summary: "Returns all modules in the server",
|
summary: "Returns all modules in the server",
|
||||||
method: "delete",
|
method: "delete",
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const app = new OpenAPIHono();
|
|||||||
|
|
||||||
app.openapi(
|
app.openapi(
|
||||||
createRoute({
|
createRoute({
|
||||||
tags: ["server"],
|
tags: ["server:settings"],
|
||||||
summary: "Returns all settings based on your permissions",
|
summary: "Returns all settings based on your permissions",
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import {createRoute, OpenAPIHono, z} from "@hono/zod-openapi";
|
import {createRoute, OpenAPIHono, z} from "@hono/zod-openapi";
|
||||||
|
import type {User} from "../../../../types/users.js";
|
||||||
|
import {log} from "../../../logger/logger.js";
|
||||||
|
import {verify} from "hono/jwt";
|
||||||
|
import {updateSetting} from "../../controller/settings/updateSetting.js";
|
||||||
|
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
|
const UpdateSetting = z.object({
|
||||||
|
name: z.string().openapi({example: "server"}),
|
||||||
|
value: z.string().openapi({example: "localhost"}),
|
||||||
|
});
|
||||||
|
|
||||||
app.openapi(
|
app.openapi(
|
||||||
createRoute({
|
createRoute({
|
||||||
tags: ["server"],
|
tags: ["server:settings"],
|
||||||
summary: "Returns all modules in the server",
|
summary: "Updates A setting",
|
||||||
method: "post",
|
method: "patch",
|
||||||
path: "/",
|
path: "/settings",
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {schema: UpdateSetting},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
@@ -20,10 +36,63 @@ app.openapi(
|
|||||||
},
|
},
|
||||||
description: "Response message",
|
description: "Response message",
|
||||||
},
|
},
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: z.object({message: z.string().optional().openapi({example: "Internal Server error"})}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Internal Server Error",
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: z.object({message: z.string().optional().openapi({example: "Unauthenticated"})}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Unauthorized",
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: z.object({message: z.string().optional().openapi({example: "Internal Server error"})}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Internal Server Error",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json({success: true, message: "Example"}, 200);
|
// make sure we have a vaid user being accessed thats really logged in
|
||||||
|
const authHeader = c.req.header("Authorization");
|
||||||
|
|
||||||
|
if (authHeader?.includes("Basic")) {
|
||||||
|
return c.json({message: "You are a Basic user! Please login to get a token"}, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authHeader) {
|
||||||
|
return c.json({message: "Unauthorized"}, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader?.split("Bearer ")[1] || "";
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await verify(token, process.env.JWT_SECRET!);
|
||||||
|
user = payload.user as User;
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error, "Failed session check, user must be logged out");
|
||||||
|
return c.json({message: "Unauthorized"}, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now pass all the data over to update the user info
|
||||||
|
try {
|
||||||
|
const data = await c?.req.json();
|
||||||
|
await updateSetting(data, user.user_id ?? "");
|
||||||
|
return c.json({success: true, message: "The Setting was just updated", data}, 200);
|
||||||
|
} catch (error) {
|
||||||
|
return c.json({message: "There was an error updating the settings.", error}, 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import updateModule from "./route/modules/updateModules.js";
|
|||||||
import addModule from "./route/modules/addModule.js";
|
import addModule from "./route/modules/addModule.js";
|
||||||
import addSetting from "./route/settings/addSetting.js";
|
import addSetting from "./route/settings/addSetting.js";
|
||||||
import getSettings from "./route/settings/getSettings.js";
|
import getSettings from "./route/settings/getSettings.js";
|
||||||
|
import updateSetting from "./route/settings/updateSetting.js";
|
||||||
|
|
||||||
areModulesIn();
|
areModulesIn();
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
@@ -18,6 +19,7 @@ const routes = [
|
|||||||
// settings
|
// settings
|
||||||
addSetting,
|
addSetting,
|
||||||
getSettings,
|
getSettings,
|
||||||
|
updateSetting,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// app.route("/server", modules);
|
// app.route("/server", modules);
|
||||||
|
|||||||
Reference in New Issue
Block a user