From 5fcadb9fc837c1c773167f21d2ffd5a01e258ecf Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Wed, 5 Mar 2025 12:09:51 -0600 Subject: [PATCH] feat(lst): added update settings into the entire app --- .../components/admin/settings/SettingForm.tsx | 121 ++++++++++++++++ .../admin/settings/SettingsPage.tsx | 55 +++++--- frontend/src/components/ui/dialog.tsx | 133 ++++++++++++++++++ frontend/src/components/ui/spinner.tsx | 58 ++++++++ frontend/src/components/ui/table.tsx | 114 +++++++++++++++ frontend/src/routes/__root.tsx | 5 +- frontend/src/utils/delay.ts | 3 + frontend/src/utils/querys/settings.tsx | 17 +++ .../controller/settings/updateSetting.ts | 37 +++++ .../server/route/settings/addSetting.ts | 2 +- .../server/route/settings/deleteSetting.ts | 2 +- .../server/route/settings/getSettings.ts | 2 +- .../server/route/settings/updateSetting.ts | 79 ++++++++++- server/services/server/systemServer.ts | 2 + 14 files changed, 601 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/admin/settings/SettingForm.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/spinner.tsx create mode 100644 frontend/src/components/ui/table.tsx create mode 100644 frontend/src/utils/delay.ts create mode 100644 frontend/src/utils/querys/settings.tsx create mode 100644 server/services/server/controller/settings/updateSetting.ts diff --git a/frontend/src/components/admin/settings/SettingForm.tsx b/frontend/src/components/admin/settings/SettingForm.tsx new file mode 100644 index 0000000..c34ee0c --- /dev/null +++ b/frontend/src/components/admin/settings/SettingForm.tsx @@ -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>({ + resolver: zodResolver(FormSchema), + defaultValues: { + value: setting.value || "", + }, + }); + + const onSubmit = async (data: z.infer) => { + 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 ( + <> + { + if (!open) { + reset(); + } + setOpen(isOpen); + // toast.message("Model was something", { + // description: isOpen ? "Modal is open" : "Modal is closed", + // }); + }} + > + + + + + + {setting.name} + + Update the setting and press save to complete the changes. + + +
+
+ <> + + + {errors.value &&

{errors.value.message}

} + +
+ + +
+ +
+
+
+
+
+ + ); +} diff --git a/frontend/src/components/admin/settings/SettingsPage.tsx b/frontend/src/components/admin/settings/SettingsPage.tsx index 7e9a96f..1f6a2f4 100644 --- a/frontend/src/components/admin/settings/SettingsPage.tsx +++ b/frontend/src/components/admin/settings/SettingsPage.tsx @@ -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
Loading.....
; @@ -38,10 +36,29 @@ export default function SettingsPage() { } return ( -
- {data.map((i: any) => { - return {i.name}; - })} -
+ + + All Settings + + + Name + Value + Change + + + + {data?.map((setting: Settings) => ( + + {setting.name} + {setting.value} + {setting.description} + + + + + ))} + +
+
); } diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..1b608b2 --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -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) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/frontend/src/components/ui/spinner.tsx b/frontend/src/components/ui/spinner.tsx new file mode 100644 index 0000000..8b4186d --- /dev/null +++ b/frontend/src/components/ui/spinner.tsx @@ -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, VariantProps { + loading?: boolean; + asChild?: boolean; +} + +const Spinner = React.forwardRef( + ({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 ( + + {Array.from({length: 8}).map((_, i) => ( + + + + ))} + + ); + } +); + +Spinner.displayName = "Spinner"; + +export {Spinner}; diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..7b81be9 --- /dev/null +++ b/frontend/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 1500181..94c45cc 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -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({ diff --git a/frontend/src/utils/delay.ts b/frontend/src/utils/delay.ts new file mode 100644 index 0000000..13cb89b --- /dev/null +++ b/frontend/src/utils/delay.ts @@ -0,0 +1,3 @@ +export function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/frontend/src/utils/querys/settings.tsx b/frontend/src/utils/querys/settings.tsx new file mode 100644 index 0000000..29c53b1 --- /dev/null +++ b/frontend/src/utils/querys/settings.tsx @@ -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; +}; diff --git a/server/services/server/controller/settings/updateSetting.ts b/server/services/server/controller/settings/updateSetting.ts new file mode 100644 index 0000000..a9af329 --- /dev/null +++ b/server/services/server/controller/settings/updateSetting.ts @@ -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"); + } +}; diff --git a/server/services/server/route/settings/addSetting.ts b/server/services/server/route/settings/addSetting.ts index 8801ee1..73df904 100644 --- a/server/services/server/route/settings/addSetting.ts +++ b/server/services/server/route/settings/addSetting.ts @@ -17,7 +17,7 @@ const AddSetting = z.object({ app.openapi( createRoute({ - tags: ["server"], + tags: ["server:settings"], summary: "Add Setting", method: "post", path: "/settings", diff --git a/server/services/server/route/settings/deleteSetting.ts b/server/services/server/route/settings/deleteSetting.ts index 7c449c3..1f40a3a 100644 --- a/server/services/server/route/settings/deleteSetting.ts +++ b/server/services/server/route/settings/deleteSetting.ts @@ -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", diff --git a/server/services/server/route/settings/getSettings.ts b/server/services/server/route/settings/getSettings.ts index 05b458a..596530d 100644 --- a/server/services/server/route/settings/getSettings.ts +++ b/server/services/server/route/settings/getSettings.ts @@ -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", diff --git a/server/services/server/route/settings/updateSetting.ts b/server/services/server/route/settings/updateSetting.ts index 890705a..c3bed6c 100644 --- a/server/services/server/route/settings/updateSetting.ts +++ b/server/services/server/route/settings/updateSetting.ts @@ -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; diff --git a/server/services/server/systemServer.ts b/server/services/server/systemServer.ts index b49a73e..8b4b62e 100644 --- a/server/services/server/systemServer.ts +++ b/server/services/server/systemServer.ts @@ -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);