6 Commits

Author SHA1 Message Date
36ac1dccb4 chore(release): 0.1.0-alpha.1
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m39s
Release and Build Image / release (push) Successful in 15s
2026-05-18 21:39:59 -05:00
514a44b6de refactor(servers): changed activeity around and trying to make use of it
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-18 21:38:08 -05:00
a7bb364a2f fix(settings): failed build due it dormant import
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m42s
2026-05-18 21:23:34 -05:00
047cc7cdf0 refactor(users): lots of auth stuff added to make it more easy to manage users
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 2m9s
2026-05-18 21:19:20 -05:00
8dc4d70e28 ci(app): added in chokidar to monitor folders 2026-05-18 21:18:42 -05:00
c8931c7249 fix(notifications): reprinting
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m20s
correction to external labeling

ref #20
2026-05-14 14:18:40 -05:00
21 changed files with 603 additions and 113 deletions

View File

@@ -1,5 +1,24 @@
# All Changes to LST can be found below. # All Changes to LST can be found below.
## [0.1.0-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.0...v0.1.0-alpha.1) (2026-05-19)
### 🐛 Bug fixes
* **notifications:** reprinting ([c8931c7](https://git.tuffraid.net/cowch/lst_v3/commits/c8931c7249b8f532b5dd37df3271da98f14ee710)), closes [#20](https://git.tuffraid.net/cowch/lst_v3/issues/20)
* **settings:** failed build due it dormant import ([a7bb364](https://git.tuffraid.net/cowch/lst_v3/commits/a7bb364a2fd49d96b6195aca0cd58ba57c58f3a6))
### 🛠️ Code Refactor
* **servers:** changed activeity around and trying to make use of it ([514a44b](https://git.tuffraid.net/cowch/lst_v3/commits/514a44b6de3efe8dd8b308d98bdbc82e31ed8427))
* **users:** lots of auth stuff added to make it more easy to manage users ([047cc7c](https://git.tuffraid.net/cowch/lst_v3/commits/047cc7cdf036c39a89a0b87ab59dda8328efe0c0))
### 📈 Project changes
* **app:** added in chokidar to monitor folders ([8dc4d70](https://git.tuffraid.net/cowch/lst_v3/commits/8dc4d70e2827f0a40d2f54886fd757c8a2dc5ac4))
## [0.1.0-alpha.0](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.10...v0.1.0-alpha.0) (2026-05-14) ## [0.1.0-alpha.0](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.10...v0.1.0-alpha.0) (2026-05-14)

View File

@@ -1,16 +1,17 @@
use [test1_AlplaPROD2.0_Read] use [test1_AlplaPROD2.0_Read]
SELECT SELECT
--JSON_VALUE(content, '$.EntityId') as labelId JSON_VALUE(content, '$.EntityId') as labelId,
a.id a.id
,ActorName ,ActorName
,FORMAT(PrintDate, 'yyyy-MM-dd HH:mm') as printDate --,FORMAT(l.PrintDate, 'yyyy-MM-dd HH:mm') as printDate
,Format(COALESCE(l.PrintDate, e.ProductionDate), 'yyyy-MM-dd HH:mm') as printDate
,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime ,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime
,l.ArticleHumanReadableId as av ,COALESCE(l.ArticleHumanReadableId,e.ArticleHumanReadableId) as av
,l.ArticleDescription as alias ,COALESCE(l.ArticleDescription, av.Name) as alias
,PrintedCopies ,COALESCE(l.PrintedCopies, 0) as PrintedCopies
,p.name as printerName ,COALESCE(p.name,'External Label not tracked') as printerName
,RunningNumber ,COALESCE(l.RunningNumber, e.RunningNumber) as runningNumber
--,* --,*
FROM [support].[AuditLog] (nolock) as a FROM [support].[AuditLog] (nolock) as a
@@ -18,10 +19,20 @@ left join
[labelling].[InternalLabel] (nolock) as l on [labelling].[InternalLabel] (nolock) as l on
l.id = JSON_VALUE(content, '$.EntityId') l.id = JSON_VALUE(content, '$.EntityId')
OUTER APPLY (
SELECT TOP 1 *
FROM labelling.ExternalLabel e
WHERE e.id = JSON_VALUE(a.content, '$.EntityId')
ORDER BY e.Id DESC
) e
left join left join
[masterData].[printer] (nolock) as p on [masterData].[printer] (nolock) as p on
p.id = l.PrinterId p.id = l.PrinterId
left join
[masterData].[article] (nolock) as av on
av.HumanReadableId = e.ArticleHumanReadableId
where message like '%reprint%' where message like '%reprint%'
and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET()) and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET())
and a.id > [ignoreList] and a.id > [ignoreList]

View File

@@ -1,10 +1,12 @@
import { createAccessControl } from "better-auth/plugins/access"; import { createAccessControl } from "better-auth/plugins/access";
import { adminAc } from "better-auth/plugins/admin/access"; import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
export const statement = { export const statement = {
...defaultStatements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"], notifications: ["read", "create", "share", "update", "delete", "readAll"],
} as const; } as const;
@@ -15,14 +17,22 @@ export const user = ac.newRole({
notifications: ["read", "create"], notifications: ["read", "create"],
}); });
export const manager = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
});
export const admin = ac.newRole({ export const admin = ac.newRole({
app: ["read", "create", "update"], app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
user: ["create", "update"],
}); });
export const systemAdmin = ac.newRole({ export const systemAdmin = ac.newRole({
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
...adminAc.statements, ...adminAc.statements,
app: ["read", "create", "share", "update", "delete", "readAll"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
}); });

View File

@@ -13,7 +13,7 @@ import {
//import { eq } from "drizzle-orm"; //import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import * as rawSchema from "../db/schema/auth.schema.js"; import * as rawSchema from "../db/schema/auth.schema.js";
import { ac, admin, systemAdmin, user } from "./auth.permissions.js"; import { ac, admin, manager, systemAdmin, user } from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js"; import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js"; import { sendEmail } from "./sendEmail.utils.js";
@@ -163,6 +163,7 @@ export const auth = betterAuth({
roles: { roles: {
admin, admin,
user, user,
manager,
systemAdmin, systemAdmin,
}, },
}), }),

View File

@@ -1,5 +1,4 @@
import { Tooltip as TooltipPrimitive } from "radix-ui"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -40,7 +39,7 @@ function TooltipContent({
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className, className,
)} )}
{...props} {...props}
@@ -52,4 +51,4 @@ function TooltipContent({
); );
} }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };

View File

@@ -31,6 +31,13 @@ api.interceptors.response.use(
appRouter?.navigate({ to: "/forbidden", replace: true }); appRouter?.navigate({ to: "/forbidden", replace: true });
} }
if (error.response?.status === 401) {
// redirect, toast, or show forbidden page
toast.error("Unauthorized to be here");
appRouter?.navigate({ to: "/login", replace: true });
}
if (isNetworkError) { if (isNetworkError) {
appRouter?.navigate({ to: "/app-down", replace: true }); appRouter?.navigate({ to: "/app-down", replace: true });
} }

View File

@@ -1,5 +1,10 @@
import { redirect } from "@tanstack/react-router"; import { redirect } from "@tanstack/react-router";
import { adminClient, genericOAuthClient } from "better-auth/client/plugins"; import {
adminClient,
genericOAuthClient,
usernameClient,
} from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { ac, admin, manager, systemAdmin, user } from "./auth-permissions"; import { ac, admin, manager, systemAdmin, user } from "./auth-permissions";
@@ -16,6 +21,7 @@ export const authClient = createAuthClient({
}, },
}), }),
genericOAuthClient(), genericOAuthClient(),
usernameClient(),
], ],
fetchOptions: { fetchOptions: {
onError() { onError() {

View File

@@ -1,9 +1,25 @@
import { createAccessControl } from "better-auth/plugins/access"; import { createAccessControl } from "better-auth/plugins/access";
import { adminAc } from "better-auth/plugins/admin/access"; import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
/*
When new perms are added based on there criteria make sure they are added here as well
*/
type SelectableRole = {
label: string;
value: string;
};
export const selectableRoles: SelectableRole[] = [
{ label: "User", value: "user" },
{ label: "Manager", value: "manager" },
{ label: "Admin", value: "admin" },
{ label: "System Admin", value: "systemAdmin" },
];
export const statement = { export const statement = {
...defaultStatements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"], logistics: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"], mobile: ["read", "create", "share", "update", "delete", "readAll"],
@@ -19,20 +35,22 @@ export const user = ac.newRole({
export const manager = ac.newRole({ export const manager = ac.newRole({
app: ["read", "create", "update"], app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
}); });
export const admin = ac.newRole({ export const admin = ac.newRole({
app: ["read", "create", "update"], app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
user: ["create", "update"],
}); });
export const systemAdmin = ac.newRole({ export const systemAdmin = ac.newRole({
...adminAc.statements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"], mobile: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"], logistics: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"], notifications: ["read", "create", "share", "update", "delete", "readAll"],
...adminAc.statements,
}); });
/* example usage /* example usage

View File

@@ -0,0 +1,16 @@
import { queryOptions } from "@tanstack/react-query";
import { authClient } from "@/lib/auth-client";
export function permissionQuery(permissions: Record<string, string[]>) {
return queryOptions({
queryKey: ["permission", permissions],
queryFn: async () => {
const result = await authClient.admin.hasPermission({
permissions,
});
return result.data?.success ?? false;
},
staleTime: 30_000,
});
}

View File

@@ -29,30 +29,43 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: {
email: loginEmail, login: loginEmail,
password: "", password: "",
rememberMe: rememberMe, rememberMe: rememberMe,
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
// set remember me incase we want it later // set remember me incase we want it later
const loginValue = value.login.trim();
const isEmailLogin = loginValue.includes("@");
if (value.rememberMe) { if (value.rememberMe) {
localStorage.setItem("rememberMe", value.rememberMe.toString()); localStorage.setItem("rememberMe", value.rememberMe.toString());
localStorage.setItem("loginEmail", value.email.toLocaleLowerCase()); localStorage.setItem("loginEmail", loginValue.toLocaleLowerCase());
} else { } else {
localStorage.removeItem("rememberMe"); localStorage.removeItem("rememberMe");
localStorage.removeItem("loginEmail"); localStorage.removeItem("loginEmail");
} }
try { try {
const login = await authClient.signIn.email({ const login = isEmailLogin
email: value.email, ? await authClient.signIn.email({
password: value.password, email: loginValue.toLowerCase(),
fetchOptions: { password: value.password,
onSuccess: () => { fetchOptions: {
navigate({ to: redirectPath ?? "/" }); onSuccess: () => {
}, navigate({ to: redirectPath ?? "/" });
}, },
}); },
})
: await authClient.signIn.username({
username: loginValue,
password: value.password,
fetchOptions: {
onSuccess: () => {
navigate({ to: redirectPath ?? "/" });
},
},
});
if (login.error) { if (login.error) {
toast.error(`${login.error?.message}`); toast.error(`${login.error?.message}`);
@@ -95,11 +108,11 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
form.handleSubmit(); form.handleSubmit();
}} }}
> >
<form.AppField name="email"> <form.AppField name="login">
{(field) => ( {(field) => (
<field.InputField <field.InputField
label="Email" label="Username or Email Address"
inputType="email" inputType="text"
required={rememberMe} required={rememberMe}
/> />
)} )}

View File

@@ -9,6 +9,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff"; import { useAppForm } from "@/lib/formSutff";
import { Separator } from "../../components/ui/separator";
export const Route = createFileRoute("/(auth)/user/signup")({ export const Route = createFileRoute("/(auth)/user/signup")({
component: RouteComponent, component: RouteComponent,
@@ -22,6 +23,7 @@ function RouteComponent() {
email: "", email: "",
password: "", password: "",
confirmPassword: "", confirmPassword: "",
username: "",
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
if (value.password !== value.confirmPassword) { if (value.password !== value.confirmPassword) {
@@ -33,6 +35,7 @@ function RouteComponent() {
name: value.name, name: value.name,
email: value.email, email: value.email,
password: value.password, password: value.password,
username: value.username ?? value.name,
callbackURL: `${window.location.origin}/lst/app`, callbackURL: `${window.location.origin}/lst/app`,
}); });
@@ -71,6 +74,15 @@ function RouteComponent() {
/> />
)} )}
</form.AppField> </form.AppField>
<div className="m-2">
<p>Username is option if left blank it will be your name</p>
</div>
<Separator />
<form.AppField name="username">
{(field) => (
<field.InputField label="Username" inputType="text" />
)}
</form.AppField>
{/* Email */} {/* Email */}
<form.AppField name="email"> <form.AppField name="email">

View File

@@ -5,6 +5,7 @@ import Header from "@/components/Header";
import { AppSidebar } from "@/components/Sidebar/sidebar"; import { AppSidebar } from "@/components/Sidebar/sidebar";
import { SidebarProvider } from "@/components/ui/sidebar"; import { SidebarProvider } from "@/components/ui/sidebar";
import { ThemeProvider } from "@/lib/theme-provider"; import { ThemeProvider } from "@/lib/theme-provider";
import { TooltipProvider } from "../components/ui/tooltip";
import { useSession } from "../lib/auth-client"; import { useSession } from "../lib/auth-client";
const RootLayout = () => { const RootLayout = () => {
@@ -14,16 +15,17 @@ const RootLayout = () => {
<ThemeProvider> <ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}> <SidebarProvider className="flex flex-col" defaultOpen={false}>
<Header /> <Header />
<TooltipProvider>
<div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar />
<div className="relative min-h-[calc(100svh-var(--header-height))]"> <main className="w-full p-4">
<AppSidebar /> <div className="mx-auto w-full max-w-7xl">
<Outlet />
<main className="w-full p-4"> </div>
<div className="mx-auto w-full max-w-7xl"> </main>
<Outlet /> </div>
</div> </TooltipProvider>
</main>
</div>
<Toaster expand richColors closeButton /> <Toaster expand richColors closeButton />
</SidebarProvider> </SidebarProvider>

View File

@@ -10,6 +10,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "../../../components/ui/dialog"; } from "../../../components/ui/dialog";
import { api } from "../../../lib/apiHelper";
import { useAppForm } from "../../../lib/formSutff"; import { useAppForm } from "../../../lib/formSutff";
import { getScannerIds } from "../../../lib/queries/getScannerIds"; import { getScannerIds } from "../../../lib/queries/getScannerIds";
@@ -31,7 +32,7 @@ export default function NewScanUser({ refetch }: { refetch: any }) {
} }
try { try {
const { data } = await axios.post( const { data } = await api.post(
"/lst/api/mobile/auth/user", "/lst/api/mobile/auth/user",
{ {
name: value.name, name: value.name,

View File

@@ -0,0 +1,153 @@
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../../../components/ui/dialog";
import { authClient } from "../../../lib/auth-client";
import { selectableRoles } from "../../../lib/auth-permissions";
import { useAppForm } from "../../../lib/formSutff";
export default function NewUser({ refetch }: { refetch: any }) {
const [open, setOpen] = useState(false);
const form = useAppForm({
defaultValues: {
name: "",
email: "",
password: "",
role: "",
username: "",
},
onSubmit: async ({ value }) => {
if (value.name === "" || value.email === "" || value.password === "") {
toast.error("Missing Mandatory data please try again ");
return;
}
try {
const { data, error } = await authClient.admin.createUser({
email: value.email, // required
password: value.password, // required
name: value.name, // required
role: (value.role ?? "user") as any,
data: { username: value.username },
});
if (data?.user) {
toast.success(`${value.name}, was just created `);
form.reset();
setOpen(false);
refetch();
}
if (error) {
toast.error(error.message);
return;
}
} catch (error) {
console.error(error);
}
},
});
const closeModel = (e: boolean) => {
setOpen(e);
if (!e) {
form.reset();
}
};
const openForm = () => {
setOpen(true);
};
return (
<Dialog onOpenChange={(e) => closeModel(e)} open={open}>
<Button onClick={openForm}>Create new user</Button>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Create New Scan user.</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div className="mb-2">
<form.AppField name="name">
{(field) => (
<field.InputField
label="Name"
inputType="text"
required={true}
/>
)}
</form.AppField>
</div>
<div>
<p>
Username can be your windows or anything, if you do not fill this
out your name is used as your username
</p>
</div>
<div className="mb-2">
<form.AppField name="username">
{(field) => (
<field.InputField label="Username" inputType="text" />
)}
</form.AppField>
</div>
<div className="mb-2">
<form.AppField name="email">
{(field) => (
<field.InputField
label="Email"
inputType="email"
required={true}
/>
)}
</form.AppField>
</div>
<div className="mb-2">
<form.AppField name="password">
{(field) => (
<field.InputField
label="Password"
inputType="text"
required={true}
/>
)}
</form.AppField>
</div>
<div className="w-32">
<form.AppField name="role">
{(field) => (
<field.SelectField
label="Roles"
placeholder="Select role"
options={selectableRoles}
/>
)}
</form.AppField>
</div>
<div className="flex justify-end mt-2 ">
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -21,6 +21,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "../../components/ui/tooltip"; } from "../../components/ui/tooltip";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client"; import { authClient } from "../../lib/auth-client";
import { notificationSubs } from "../../lib/queries/notificationSubs"; import { notificationSubs } from "../../lib/queries/notificationSubs";
import { notifications } from "../../lib/queries/notifications"; import { notifications } from "../../lib/queries/notifications";
@@ -36,7 +37,7 @@ const updateNotifications = async (
//console.log(id, data); //console.log(id, data);
try { try {
const res = await axios.patch( const res = await axios.patch(
`/lst/api/notification/${id}`, `/notification/${id}`,
{ interval: data.interval }, { interval: data.interval },
{ {
withCredentials: true, withCredentials: true,
@@ -110,7 +111,7 @@ const NotificationTable = () => {
const removeNotification = async (ns: any) => { const removeNotification = async (ns: any) => {
try { try {
const res = await axios.delete(`/lst/api/notification/sub`, { const res = await api.delete(`/notification/sub`, {
withCredentials: true, withCredentials: true,
data: { data: {
userId: ns.userId, userId: ns.userId,
@@ -168,7 +169,7 @@ const NotificationTable = () => {
setActiveToggle(e); setActiveToggle(e);
try { try {
const res = await axios.patch( const res = await api.patch(
`/lst/api/notification/${i.row.original.id}`, `/lst/api/notification/${i.row.original.id}`,
{ {
active: !activeToggle, active: !activeToggle,

View File

@@ -8,6 +8,7 @@ import { Suspense, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Spinner } from "../../components/ui/spinner"; import { Spinner } from "../../components/ui/spinner";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client"; import { authClient } from "../../lib/auth-client";
import { getScanUsers } from "../../lib/queries/getScanUsers"; import { getScanUsers } from "../../lib/queries/getScanUsers";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput"; import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
@@ -19,7 +20,13 @@ import NewScanUser from "./-components/NewScanUser";
export const Route = createFileRoute("/admin/scanUsers")({ export const Route = createFileRoute("/admin/scanUsers")({
beforeLoad: async ({ location }) => { beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession(); const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin", "manager"]; //const allowedRole = ["systemAdmin", "admin", "manager"];
const canAccess = await authClient.admin.hasPermission({
permissions: {
mobile: ["create"],
},
});
if (!session?.user) { if (!session?.user) {
throw redirect({ throw redirect({
@@ -30,7 +37,9 @@ export const Route = createFileRoute("/admin/scanUsers")({
}); });
} }
if (!allowedRole.includes(session.user.role as string)) { //if (!allowedRole.includes(session.user.role as string)) {
if (!canAccess) {
throw redirect({ throw redirect({
to: "/", to: "/",
}); });
@@ -47,7 +56,7 @@ const updateSettings = async (
) => { ) => {
//console.log(id, data); //console.log(id, data);
try { try {
const res = await axios.patch(`/lst/api/mobile/auth/user/${id}`, data, { const res = await axios.patch(`/mobile/auth/user/${id}`, data, {
withCredentials: true, withCredentials: true,
timeout: 15000, timeout: 15000,
validateStatus: () => true, validateStatus: () => true,
@@ -123,7 +132,7 @@ const ScanUserTable = () => {
<Button <Button
type="button" type="button"
onClick={async () => { onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new"); const { data } = await api.get("/mobile/pin/new");
updateSetting.mutate({ updateSetting.mutate({
id: row.original.id, id: row.original.id,
field: "pinNumber", field: "pinNumber",
@@ -171,7 +180,7 @@ const ScanUserTable = () => {
setActiveToggle(true); setActiveToggle(true);
try { try {
const res = await axios.delete( const res = await api.delete(
`/lst/api/mobile/auth/user/${i.row.original.id}`, `/lst/api/mobile/auth/user/${i.row.original.id}`,
{ {

View File

@@ -1,7 +1,7 @@
import { useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table"; import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import { CircleFadingArrowUp, Trash } from "lucide-react"; import { CircleFadingArrowUp, Trash } from "lucide-react";
import { Suspense, useState } from "react"; import { Suspense, useState } from "react";
@@ -14,6 +14,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "../../components/ui/tooltip"; } from "../../components/ui/tooltip";
import { useSocketRoom } from "../../hooks/socket.io.hook"; import { useSocketRoom } from "../../hooks/socket.io.hook";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client"; import { authClient } from "../../lib/auth-client";
import { servers } from "../../lib/queries/servers"; import { servers } from "../../lib/queries/servers";
import LstTable from "../../lib/tableStuff/LstTable"; import LstTable from "../../lib/tableStuff/LstTable";
@@ -111,19 +112,20 @@ const ServerTable = () => {
const [activeToggle, setActiveToggle] = useState(false); const [activeToggle, setActiveToggle] = useState(false);
const onToggle = async () => { const onToggle = async () => {
setActiveToggle(true);
toast.success( toast.success(
`${i.row.original.name} just started the upgrade monitor logs for errors.`, `${i.row.original.name} just started the upgrade monitor logs for errors.`,
); );
setActiveToggle(activeToggle);
try { try {
const res = await axios.post( const res = await api.post(
`/lst/api/admin/build/updateServer`, `/admin/build/updateServer`,
{ {
server: i.row.original.server, server: i.row.original.server,
destination: i.row.original.serverLoc, destination: i.row.original.serverLoc,
token: i.row.original.plantToken, token: i.row.original.plantToken,
}, },
{ withCredentials: true }, { withCredentials: true, timeout: 5 * 60 * 1000 },
); );
if (res.data.success) { if (res.data.success) {
@@ -218,8 +220,8 @@ function RouteComponent() {
]; ];
const triggerBuild = async () => { const triggerBuild = async () => {
try { try {
const res = await axios.post( const res = await api.post(
`/lst/api/admin/build/release`, `/admin/build/release`,
{ {
withCredentials: true, withCredentials: true,

View File

@@ -1,8 +1,6 @@
import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table"; import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { Suspense, useMemo } from "react"; import { Suspense, useMemo } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -24,6 +22,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "../../components/ui/tooltip"; } from "../../components/ui/tooltip";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client"; import { authClient } from "../../lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings"; import { getSettings } from "../../lib/queries/getSettings";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput"; import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
@@ -48,7 +47,7 @@ const updateSettings = async (
) => { ) => {
//console.log(id, data); //console.log(id, data);
try { try {
const res = await axios.patch(`/lst/api/settings/${id}`, data, { const res = await api.patch(`/settings/${id}`, data, {
withCredentials: true, withCredentials: true,
}); });
toast.success(`Setting just updated`); toast.success(`Setting just updated`);

View File

@@ -1,20 +1,43 @@
import { useSuspenseQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table"; import { createColumnHelper } from "@tanstack/react-table";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import { KeyRound } from "lucide-react";
import { Suspense } from "react"; import { Suspense } from "react";
import { toast } from "sonner";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { authClient, useSession } from "../../lib/auth-client"; import { Input } from "../../components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import { authClient } from "../../lib/auth-client";
import { selectableRoles } from "../../lib/auth-permissions";
import { getUsers } from "../../lib/queries/getUsers"; import { getUsers } from "../../lib/queries/getUsers";
import { permissionQuery } from "../../lib/queries/permsCheck";
import LstTable from "../../lib/tableStuff/LstTable"; import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader"; import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable"; import SkellyTable from "../../lib/tableStuff/SkellyTable";
import { trackLstEvent } from "../../lib/umami.utils"; import { trackLstEvent } from "../../lib/umami.utils";
import NewUser from "./-components/Newuser";
export const Route = createFileRoute("/admin/users")({ export const Route = createFileRoute("/admin/users")({
beforeLoad: async ({ location }) => { beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession(); const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin"]; // const allowedRole = ["systemAdmin", "admin"];
const canAccess = await authClient.admin.hasPermission({
permissions: {
user: ["create"],
},
});
if (!session?.user) { if (!session?.user) {
throw redirect({ throw redirect({
@@ -25,7 +48,8 @@ export const Route = createFileRoute("/admin/users")({
}); });
} }
if (!allowedRole.includes(session.user.role as string)) { //if (!allowedRole.includes(session.user.role as string)) {
if (!canAccess) {
throw redirect({ throw redirect({
to: "/", to: "/",
}); });
@@ -37,8 +61,51 @@ export const Route = createFileRoute("/admin/users")({
}); });
const UserTable = () => { const UserTable = () => {
const { data } = useSuspenseQuery(getUsers()); const { data, refetch } = useSuspenseQuery(getUsers());
const { data: session } = useSession(); //const { data: session } = useSession();
const { data: canImpersonate = false } = useQuery(
permissionQuery({
user: ["impersonate"],
}),
);
const { data: canUpdate = false } = useQuery(
permissionQuery({
user: ["update"],
}),
);
const updatePassword = useMutation({
mutationFn: async ({ user, password }: { user: any; password: string }) => {
return authClient.admin.setUserPassword({
userId: user.id,
newPassword: password,
});
},
onSuccess: () => {
toast.success("Password updated");
},
onError: (error) => {
toast.error(error.message);
},
});
const handleRoleChange = async (row: any, newRole: string) => {
//console.log("update this user", row, newRole);
const { data, error } = await authClient.admin.updateUser({
userId: row.id,
data: { role: newRole },
});
if (error) {
console.error(error);
toast.error(error.message);
return;
}
toast.success(`${data.name}, role was just changed to: ${newRole}`);
refetch();
};
const columnHelper = createColumnHelper<any>(); const columnHelper = createColumnHelper<any>();
@@ -50,6 +117,13 @@ const UserTable = () => {
filterFn: "includesString", filterFn: "includesString",
cell: (i) => i.getValue(), cell: (i) => i.getValue(),
}), }),
columnHelper.accessor("username", {
header: ({ column }) => (
<SearchableHeader column={column} title="Username" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("email", { columnHelper.accessor("email", {
header: ({ column }) => ( header: ({ column }) => (
<SearchableHeader column={column} title="Email" searchable={true} /> <SearchableHeader column={column} title="Email" searchable={true} />
@@ -57,27 +131,113 @@ const UserTable = () => {
filterFn: "includesString", filterFn: "includesString",
cell: (i) => i.getValue(), cell: (i) => i.getValue(),
}), }),
// columnHelper.accessor("role", {
// header: ({ column }) => (
// <SearchableHeader column={column} title="Role" searchable={false} />
// ),
// filterFn: "includesString",
// cell: (i) => i.getValue(),
// }),
columnHelper.accessor("role", { columnHelper.accessor("role", {
header: ({ column }) => ( header: ({ column }) => <SearchableHeader column={column} title="Role" />,
<SearchableHeader column={column} title="Role" searchable={false} />
),
filterFn: "includesString", filterFn: "includesString",
cell: (i) => i.getValue(), cell: ({ row, getValue }) => {
}), const currentRole = getValue();
columnHelper.accessor("updatedAt", {
header: ({ column }) => ( return (
<SearchableHeader <Select
column={column} value={currentRole}
title="Updated at" onValueChange={(newRole) => {
searchable={false} handleRoleChange(row.original, newRole);
/> }}
), >
filterFn: "includesString", <SelectTrigger className="w-[180px]">
cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"), <SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
{selectableRoles.map((role) => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
},
}), }),
]; ];
if (session && session.user.role === "systemAdmin") { if (canUpdate) {
columns.push(
columnHelper.accessor("changePassword", {
header: ({ column }) => (
<SearchableHeader column={column} title="Change Password" />
),
filterFn: "includesString",
cell: ({ row }) => {
return (
<div className="flex flex-row items-center gap-2">
<Input
type="password"
placeholder="New password"
className="w-[200px]"
onKeyDown={(e) => {
if (e.key !== "Enter") return;
const password = e.currentTarget.value.trim();
if (!password) return;
updatePassword.mutate({
user: row.original,
password,
});
e.currentTarget.value = "";
}}
/>
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
variant="outline"
onClick={(e) => {
const input =
e.currentTarget.parentElement?.querySelector("input");
const password = input?.value.trim();
if (!password) return;
updatePassword.mutate({
user: row.original,
password,
});
if (input) {
input.value = "";
}
}}
>
<KeyRound className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
Update Password, fill out and press enter or update here
</p>
</TooltipContent>
</Tooltip>
</div>
);
},
}),
);
}
if (canImpersonate) {
columns.push( columns.push(
columnHelper.accessor("banned", { columnHelper.accessor("banned", {
header: ({ column }) => ( header: ({ column }) => (
@@ -126,7 +286,28 @@ const UserTable = () => {
); );
} }
return <LstTable data={data} columns={columns} pageSize={50} />; columns.push(
columnHelper.accessor("updatedAt", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Updated at"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"),
}),
);
return (
<div>
<div className="flex justify-end m-2">
<NewUser refetch={refetch} />
</div>
<LstTable data={data} columns={columns} pageSize={50} />
</div>
);
}; };
function RouteComponent() { function RouteComponent() {

79
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "lst_v3", "name": "lst_v3",
"version": "0.1.0-alpha.0", "version": "0.1.0-alpha.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lst_v3", "name": "lst_v3",
"version": "0.1.0-alpha.0", "version": "0.1.0-alpha.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@dotenvx/dotenvx": "^1.57.0", "@dotenvx/dotenvx": "^1.57.0",
@@ -16,6 +16,7 @@
"axios": "^1.13.6", "axios": "^1.13.6",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"better-auth": "^1.5.5", "better-auth": "^1.5.5",
"chokidar": "^5.0.0",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"cors": "^2.8.6", "cors": "^2.8.6",
"croner": "^10.0.1", "croner": "^10.0.1",
@@ -3801,28 +3802,18 @@
} }
}, },
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.6.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"anymatch": "~3.1.2", "readdirp": "^5.0.0"
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}, },
"engines": { "engines": {
"node": ">= 8.10.0" "node": ">= 20.19.0"
}, },
"funding": { "funding": {
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
} }
}, },
"node_modules/cli-cursor": { "node_modules/cli-cursor": {
@@ -10734,16 +10725,16 @@
} }
}, },
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": { "engines": {
"node": ">=8.10.0" "node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/real-require": { "node_modules/real-require": {
@@ -11978,6 +11969,44 @@
} }
} }
}, },
"node_modules/ts-node-dev/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/ts-node-dev/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/tsconfig": { "node_modules/tsconfig": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "lst_v3", "name": "lst_v3",
"version": "0.1.0-alpha.0", "version": "0.1.0-alpha.1",
"description": "The tool that supports us in our everyday alplaprod", "description": "The tool that supports us in our everyday alplaprod",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -71,6 +71,7 @@
"axios": "^1.13.6", "axios": "^1.13.6",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"better-auth": "^1.5.5", "better-auth": "^1.5.5",
"chokidar": "^5.0.0",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"cors": "^2.8.6", "cors": "^2.8.6",
"croner": "^10.0.1", "croner": "^10.0.1",