Compare commits

...

4 Commits

21 changed files with 694 additions and 46 deletions

View File

@@ -10,7 +10,7 @@
{"type": "test", "section": "📝 Testing Code"},
{"type": "ci", "section": "📈 Project changes"}
],
"commitUrlFormat": "https://git.tuffraid.net/cowch/lstV2/commits{{hash}}",
"commitUrlFormat": "https://git.tuffraid.net/cowch/lstV2/commits/{{hash}}",
"compareUrlFormat": "https://git.tuffraid.net/cowch/lstV2/compare/{{previousTag}}...{{currentTag}}",
"header": "# All CHanges to LST can be found below.\n"
}

View File

@@ -381,6 +381,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -397,6 +398,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -413,6 +415,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -429,6 +432,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -445,6 +449,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -461,6 +466,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -477,6 +483,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -493,6 +500,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -509,6 +517,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -525,6 +534,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -541,6 +551,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -557,6 +568,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -573,6 +585,7 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -589,6 +602,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -605,6 +619,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -621,6 +636,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -637,6 +653,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -653,6 +670,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -669,6 +687,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -685,6 +704,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -701,6 +721,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -717,6 +738,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -733,6 +755,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -749,6 +772,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -765,6 +789,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1883,6 +1908,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1896,6 +1922,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1909,6 +1936,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1922,6 +1950,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1935,6 +1964,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1948,6 +1978,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1961,6 +1992,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1974,6 +2006,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1987,6 +2020,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2000,6 +2034,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2013,6 +2048,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2026,6 +2062,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2039,6 +2076,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2052,6 +2090,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2065,6 +2104,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2078,6 +2118,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2091,6 +2132,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2104,6 +2146,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2117,6 +2160,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2918,6 +2962,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -2931,7 +2976,7 @@
"version": "19.0.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -2941,7 +2986,7 @@
"version": "19.0.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz",
"integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
@@ -3531,7 +3576,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -3632,6 +3677,7 @@
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -4003,6 +4049,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -4036,7 +4083,7 @@
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz",
"integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@@ -4724,6 +4771,7 @@
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
"type": "github",
@@ -4868,6 +4916,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -4887,6 +4936,7 @@
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -5178,7 +5228,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
@@ -5199,6 +5249,7 @@
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
"integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
@@ -5332,6 +5383,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -5445,7 +5497,7 @@
"version": "4.19.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz",
"integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
@@ -5622,6 +5674,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",

View 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>
</>
);
}

View File

@@ -1,21 +1,24 @@
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 {useModuleStore} from "@/lib/store/useModuleStore";
import {useQuery} from "@tanstack/react-query";
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() {
const token = localStorage.getItem("auth_token");
const {user} = useSessionStore();
const {user, token} = useSessionStore();
const {modules} = useModuleStore();
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 userLevel = user?.roles.filter((r) => r.module_id === adminModule[0].module_id) || [];
@@ -23,12 +26,7 @@ export default function SettingsPage() {
router.navigate({to: "/"});
}
const {data, isError, error, isLoading} = useQuery({
queryKey: ["settings"],
queryFn: fetchSettings,
enabled: !!token,
refetchOnWindowFocus: true,
});
const {data, isError, error, isLoading} = useQuery(getSettings(token ?? ""));
if (isLoading) {
return <div>Loading.....</div>;
@@ -38,10 +36,29 @@ export default function SettingsPage() {
}
return (
<div>
{data.map((i: any) => {
return <LstCard key={i.settings_id}>{i.name}</LstCard>;
})}
</div>
<LstCard className="m-2 flex place-content-center w-dvh">
<Table>
<TableCaption>All Settings</TableCaption>
<TableHeader>
<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>
);
}

View File

@@ -81,7 +81,9 @@ const LoginForm = () => {
<hr className="rounded"></hr>
<form onSubmit={handleSubmit(onSubmitLogin)}>
<div>
<Label>Username</Label>
<Label htmlFor="username" className="m-1">
Username
</Label>
<Input
placeholder="smith001"
{...register("username")}
@@ -92,7 +94,9 @@ const LoginForm = () => {
</div>
<div>
<>
<Label htmlFor={"password"}>Password</Label>
<Label htmlFor={"password"} className="m-1">
Password
</Label>
<Input
type="password"
{...register("password")}
@@ -106,15 +110,24 @@ const LoginForm = () => {
<div className="flex">
<Controller
render={({field}) => (
<Checkbox id="remember" checked={field.value} onCheckedChange={field.onChange} />
<>
<Checkbox
id="remember"
checked={field.value}
onCheckedChange={field.onChange}
/>
<label
htmlFor="remember"
className="pl-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
remember me
</label>
</>
)}
control={control}
name="rememberMe"
defaultValue={rememeberMe}
/>
<Label htmlFor="remember" className="pl-2">
remember me
</Label>
</div>
<div className="flex justify-end">

View 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,
}

View 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};

View 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,
}

View File

@@ -1,5 +1,5 @@
import {useSessionStore} from "@/lib/store/sessionStore";
import {useRouter} from "@tanstack/react-router";
import {useSessionStore} from "../store/sessionStore";
export const useLogout = () => {
const {clearSession} = useSessionStore();

View File

@@ -1,5 +1,5 @@
import {useQuery} from "@tanstack/react-query";
import {useSessionStore} from "../store/sessionStore";
import {useSessionStore} from "../lib/store/sessionStore";
import {useEffect} from "react";
const fetchSession = async () => {
@@ -34,6 +34,7 @@ export const useSession = () => {
staleTime: 60 * 1000,
gcTime: 10 * 60 * 1000, // 10 mins
refetchOnWindowFocus: true,
//refetchInterval: 1000 * 60 * 2, // Auto-refetch every 2 minutes
});
useEffect(() => {
if (data) {

View File

@@ -17,9 +17,10 @@ import {
import {SessionProvider} from "../components/providers/Providers";
import {Toaster} from "sonner";
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 {useSession} from "@/hooks/useSession";
import {useLogout} from "@/hooks/useLogout";
// same as the layout
export const Route = createRootRoute({

View File

@@ -0,0 +1,3 @@
export function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View 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;
};

View File

@@ -9,7 +9,7 @@ import {log} from "../../logger/logger.js";
/**
* Authenticate a user and return a JWT.
*/
const {sign, verify} = jwt;
const {sign} = jwt;
export async function login(
username: string,
@@ -56,5 +56,6 @@ export async function login(
}
const token = sign({user: userData}, secret, {expiresIn: expiresIn * 60});
return {token, user: userData};
}

View File

@@ -2,9 +2,13 @@ import {z, createRoute, OpenAPIHono} from "@hono/zod-openapi";
import {verify} from "hono/jwt";
import {log} from "../../logger/logger.js";
import {authMiddleware} from "../middleware/authMiddleware.js";
import jwt from "jsonwebtoken";
const session = new OpenAPIHono();
const expiresIn = Number(process.env.JWT_EXPIRES!) || 60;
const secret: string = process.env.JWT_SECRET!;
const {sign} = jwt;
const UserSchema = z.object({
username: z
.string()
@@ -79,7 +83,11 @@ session.openapi(
try {
const payload = await verify(token, process.env.JWT_SECRET!);
return c.json({data: {token: token, user: payload.user}}, 200);
// If it's valid, return a new token
const newToken = sign({user: payload.user}, secret, {expiresIn: expiresIn * 60});
return c.json({data: {token: newToken, user: payload.user}}, 200);
} catch (error) {
log.error(error, "Failed session check, user must be logged out");
return c.json({message: "Unauthorized"}, 401);

View 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");
}
};

View File

@@ -17,7 +17,7 @@ const AddSetting = z.object({
app.openapi(
createRoute({
tags: ["server"],
tags: ["server:settings"],
summary: "Add Setting",
method: "post",
path: "/settings",

View File

@@ -4,7 +4,7 @@ const app = new OpenAPIHono();
app.openapi(
createRoute({
tags: ["server"],
tags: ["server:settings"],
summary: "Returns all modules in the server",
method: "delete",
path: "/settings",

View File

@@ -8,7 +8,7 @@ const app = new OpenAPIHono();
app.openapi(
createRoute({
tags: ["server"],
tags: ["server:settings"],
summary: "Returns all settings based on your permissions",
method: "get",
path: "/settings",

View File

@@ -1,13 +1,29 @@
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 UpdateSetting = z.object({
name: z.string().openapi({example: "server"}),
value: z.string().openapi({example: "localhost"}),
});
app.openapi(
createRoute({
tags: ["server"],
summary: "Returns all modules in the server",
method: "post",
path: "/",
tags: ["server:settings"],
summary: "Updates A setting",
method: "patch",
path: "/settings",
request: {
body: {
content: {
"application/json": {schema: UpdateSetting},
},
},
},
responses: {
200: {
content: {
@@ -20,10 +36,63 @@ app.openapi(
},
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) => {
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;

View File

@@ -7,6 +7,7 @@ import updateModule from "./route/modules/updateModules.js";
import addModule from "./route/modules/addModule.js";
import addSetting from "./route/settings/addSetting.js";
import getSettings from "./route/settings/getSettings.js";
import updateSetting from "./route/settings/updateSetting.js";
areModulesIn();
const app = new OpenAPIHono();
@@ -18,6 +19,7 @@ const routes = [
// settings
addSetting,
getSettings,
updateSetting,
] as const;
// app.route("/server", modules);