diff --git a/backend/utils/auth.permissions.ts b/backend/utils/auth.permissions.ts index acaf699..34a8775 100644 --- a/backend/utils/auth.permissions.ts +++ b/backend/utils/auth.permissions.ts @@ -1,10 +1,12 @@ 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 = { + ...defaultStatements, app: ["read", "create", "share", "update", "delete", "readAll"], - //user: ["ban"], 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"], } as const; @@ -15,14 +17,22 @@ export const user = ac.newRole({ notifications: ["read", "create"], }); +export const manager = ac.newRole({ + app: ["read", "create", "update"], + mobile: ["read", "create", "update"], +}); + export const admin = ac.newRole({ app: ["read", "create", "update"], + mobile: ["read", "create", "update"], + user: ["create", "update"], }); 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, + 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"], }); diff --git a/backend/utils/auth.utils.ts b/backend/utils/auth.utils.ts index f9e7278..64cf515 100644 --- a/backend/utils/auth.utils.ts +++ b/backend/utils/auth.utils.ts @@ -13,7 +13,7 @@ import { //import { eq } from "drizzle-orm"; import { db } from "../db/db.controller.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 { sendEmail } from "./sendEmail.utils.js"; @@ -163,6 +163,7 @@ export const auth = betterAuth({ roles: { admin, user, + manager, systemAdmin, }, }), diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx index 300d32b..7413f6e 100644 --- a/frontend/src/components/ui/tooltip.tsx +++ b/frontend/src/components/ui/tooltip.tsx @@ -1,55 +1,55 @@ -import { Tooltip as TooltipPrimitive } from "radix-ui"; -import type * as React from "react"; +import * as React from "react" +import { Tooltip as TooltipPrimitive } from "radix-ui" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" function TooltipProvider({ - delayDuration = 0, - ...props + delayDuration = 0, + ...props }: React.ComponentProps) { - return ( - - ); + return ( + + ) } function Tooltip({ - ...props + ...props }: React.ComponentProps) { - return ; + return } function TooltipTrigger({ - ...props + ...props }: React.ComponentProps) { - return ; + return } function TooltipContent({ - className, - sideOffset = 0, - children, - ...props + className, + sideOffset = 0, + children, + ...props }: React.ComponentProps) { - return ( - - - {children} - - - - ); + return ( + + + {children} + + + + ) } -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } diff --git a/frontend/src/lib/apiHelper.ts b/frontend/src/lib/apiHelper.ts index d2d028e..4cafee3 100644 --- a/frontend/src/lib/apiHelper.ts +++ b/frontend/src/lib/apiHelper.ts @@ -31,6 +31,13 @@ api.interceptors.response.use( 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) { appRouter?.navigate({ to: "/app-down", replace: true }); } diff --git a/frontend/src/lib/auth-client.ts b/frontend/src/lib/auth-client.ts index 788df37..74ff5a5 100644 --- a/frontend/src/lib/auth-client.ts +++ b/frontend/src/lib/auth-client.ts @@ -1,5 +1,10 @@ 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 { ac, admin, manager, systemAdmin, user } from "./auth-permissions"; @@ -16,6 +21,7 @@ export const authClient = createAuthClient({ }, }), genericOAuthClient(), + usernameClient(), ], fetchOptions: { onError() { diff --git a/frontend/src/lib/auth-permissions.ts b/frontend/src/lib/auth-permissions.ts index e02f9c0..2f21db8 100644 --- a/frontend/src/lib/auth-permissions.ts +++ b/frontend/src/lib/auth-permissions.ts @@ -1,9 +1,25 @@ 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 = { + ...defaultStatements, app: ["read", "create", "share", "update", "delete", "readAll"], - //user: ["ban"], quality: ["read", "create", "share", "update", "delete", "readAll"], logistics: ["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({ app: ["read", "create", "update"], + mobile: ["read", "create", "update"], }); export const admin = ac.newRole({ app: ["read", "create", "update"], + mobile: ["read", "create", "update"], + user: ["create", "update"], }); export const systemAdmin = ac.newRole({ + ...adminAc.statements, app: ["read", "create", "share", "update", "delete", "readAll"], - //user: ["ban"], 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"], - ...adminAc.statements, }); /* example usage diff --git a/frontend/src/lib/queries/permsCheck.ts b/frontend/src/lib/queries/permsCheck.ts new file mode 100644 index 0000000..505814d --- /dev/null +++ b/frontend/src/lib/queries/permsCheck.ts @@ -0,0 +1,16 @@ +import { queryOptions } from "@tanstack/react-query"; +import { authClient } from "@/lib/auth-client"; + +export function permissionQuery(permissions: Record) { + return queryOptions({ + queryKey: ["permission", permissions], + queryFn: async () => { + const result = await authClient.admin.hasPermission({ + permissions, + }); + + return result.data?.success ?? false; + }, + staleTime: 30_000, + }); +} diff --git a/frontend/src/routes/(auth)/-components/LoginForm.tsx b/frontend/src/routes/(auth)/-components/LoginForm.tsx index 80bd9a4..b57bad2 100644 --- a/frontend/src/routes/(auth)/-components/LoginForm.tsx +++ b/frontend/src/routes/(auth)/-components/LoginForm.tsx @@ -29,30 +29,43 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) { const form = useAppForm({ defaultValues: { - email: loginEmail, + login: loginEmail, password: "", rememberMe: rememberMe, }, onSubmit: async ({ value }) => { // set remember me incase we want it later + const loginValue = value.login.trim(); + const isEmailLogin = loginValue.includes("@"); + if (value.rememberMe) { localStorage.setItem("rememberMe", value.rememberMe.toString()); - localStorage.setItem("loginEmail", value.email.toLocaleLowerCase()); + localStorage.setItem("loginEmail", loginValue.toLocaleLowerCase()); } else { localStorage.removeItem("rememberMe"); localStorage.removeItem("loginEmail"); } try { - const login = await authClient.signIn.email({ - email: value.email, - password: value.password, - fetchOptions: { - onSuccess: () => { - navigate({ to: redirectPath ?? "/" }); - }, - }, - }); + const login = isEmailLogin + ? await authClient.signIn.email({ + email: loginValue.toLowerCase(), + password: value.password, + fetchOptions: { + onSuccess: () => { + navigate({ to: redirectPath ?? "/" }); + }, + }, + }) + : await authClient.signIn.username({ + username: loginValue, + password: value.password, + fetchOptions: { + onSuccess: () => { + navigate({ to: redirectPath ?? "/" }); + }, + }, + }); if (login.error) { toast.error(`${login.error?.message}`); @@ -95,11 +108,11 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) { form.handleSubmit(); }} > - + {(field) => ( )} diff --git a/frontend/src/routes/(auth)/user.signup.tsx b/frontend/src/routes/(auth)/user.signup.tsx index fdb60fa..2295f3f 100644 --- a/frontend/src/routes/(auth)/user.signup.tsx +++ b/frontend/src/routes/(auth)/user.signup.tsx @@ -9,6 +9,7 @@ import { } from "@/components/ui/card"; import { authClient } from "@/lib/auth-client"; import { useAppForm } from "@/lib/formSutff"; +import { Separator } from "../../components/ui/separator"; export const Route = createFileRoute("/(auth)/user/signup")({ component: RouteComponent, @@ -22,6 +23,7 @@ function RouteComponent() { email: "", password: "", confirmPassword: "", + username: "", }, onSubmit: async ({ value }) => { if (value.password !== value.confirmPassword) { @@ -33,6 +35,7 @@ function RouteComponent() { name: value.name, email: value.email, password: value.password, + username: value.username ?? value.name, callbackURL: `${window.location.origin}/lst/app`, }); @@ -71,6 +74,15 @@ function RouteComponent() { /> )} +
+

Username is option if left blank it will be your name

+
+ + + {(field) => ( + + )} + {/* Email */} diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index db04b8e..03a956a 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -5,6 +5,7 @@ import Header from "@/components/Header"; import { AppSidebar } from "@/components/Sidebar/sidebar"; import { SidebarProvider } from "@/components/ui/sidebar"; import { ThemeProvider } from "@/lib/theme-provider"; +import { TooltipProvider } from "../components/ui/tooltip"; import { useSession } from "../lib/auth-client"; const RootLayout = () => { @@ -14,16 +15,17 @@ const RootLayout = () => {
+ +
+ -
- - -
-
- -
-
-
+
+
+ +
+
+
+
diff --git a/frontend/src/routes/admin/-components/NewScanUser.tsx b/frontend/src/routes/admin/-components/NewScanUser.tsx index ff05f94..7549aed 100644 --- a/frontend/src/routes/admin/-components/NewScanUser.tsx +++ b/frontend/src/routes/admin/-components/NewScanUser.tsx @@ -10,6 +10,7 @@ import { DialogHeader, DialogTitle, } from "../../../components/ui/dialog"; +import { api } from "../../../lib/apiHelper"; import { useAppForm } from "../../../lib/formSutff"; import { getScannerIds } from "../../../lib/queries/getScannerIds"; @@ -31,7 +32,7 @@ export default function NewScanUser({ refetch }: { refetch: any }) { } try { - const { data } = await axios.post( + const { data } = await api.post( "/lst/api/mobile/auth/user", { name: value.name, diff --git a/frontend/src/routes/admin/-components/Newuser.tsx b/frontend/src/routes/admin/-components/Newuser.tsx new file mode 100644 index 0000000..f5ed6f1 --- /dev/null +++ b/frontend/src/routes/admin/-components/Newuser.tsx @@ -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 ( + closeModel(e)} open={open}> + + + + + Create New Scan user. + + +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > +
+ + {(field) => ( + + )} + +
+
+

+ Username can be your windows or anything, if you do not fill this + out your name is used as your username +

+
+
+ + {(field) => ( + + )} + +
+
+ + {(field) => ( + + )} + +
+
+ + {(field) => ( + + )} + +
+ +
+ + {(field) => ( + + )} + +
+ +
+ + Submit + +
+
+
+
+ ); +} diff --git a/frontend/src/routes/admin/notifications.tsx b/frontend/src/routes/admin/notifications.tsx index 6d4ebaf..cb82572 100644 --- a/frontend/src/routes/admin/notifications.tsx +++ b/frontend/src/routes/admin/notifications.tsx @@ -21,6 +21,7 @@ import { TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip"; +import { api } from "../../lib/apiHelper"; import { authClient } from "../../lib/auth-client"; import { notificationSubs } from "../../lib/queries/notificationSubs"; import { notifications } from "../../lib/queries/notifications"; @@ -36,7 +37,7 @@ const updateNotifications = async ( //console.log(id, data); try { const res = await axios.patch( - `/lst/api/notification/${id}`, + `/notification/${id}`, { interval: data.interval }, { withCredentials: true, @@ -110,7 +111,7 @@ const NotificationTable = () => { const removeNotification = async (ns: any) => { try { - const res = await axios.delete(`/lst/api/notification/sub`, { + const res = await api.delete(`/notification/sub`, { withCredentials: true, data: { userId: ns.userId, @@ -168,7 +169,7 @@ const NotificationTable = () => { setActiveToggle(e); try { - const res = await axios.patch( + const res = await api.patch( `/lst/api/notification/${i.row.original.id}`, { active: !activeToggle, diff --git a/frontend/src/routes/admin/scanUsers.tsx b/frontend/src/routes/admin/scanUsers.tsx index a77af31..e188242 100644 --- a/frontend/src/routes/admin/scanUsers.tsx +++ b/frontend/src/routes/admin/scanUsers.tsx @@ -8,6 +8,7 @@ import { Suspense, useState } from "react"; import { toast } from "sonner"; import { Button } from "../../components/ui/button"; import { Spinner } from "../../components/ui/spinner"; +import { api } from "../../lib/apiHelper"; import { authClient } from "../../lib/auth-client"; import { getScanUsers } from "../../lib/queries/getScanUsers"; import EditableCellInput from "../../lib/tableStuff/EditableCellInput"; @@ -19,7 +20,13 @@ import NewScanUser from "./-components/NewScanUser"; export const Route = createFileRoute("/admin/scanUsers")({ beforeLoad: async ({ location }) => { 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) { 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({ to: "/", }); @@ -47,7 +56,7 @@ const updateSettings = async ( ) => { //console.log(id, data); 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, timeout: 15000, validateStatus: () => true, @@ -123,7 +132,7 @@ const ScanUserTable = () => { + + +

+ Update Password, fill out and press enter or update here +

+
+ + + ); + }, + }), + ); + } + + if (canImpersonate) { columns.push( columnHelper.accessor("banned", { header: ({ column }) => ( @@ -126,7 +286,28 @@ const UserTable = () => { ); } - return ; + columns.push( + columnHelper.accessor("updatedAt", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"), + }), + ); + + return ( +
+
+ +
+ +
+ ); }; function RouteComponent() {