diff --git a/backend/admin/admin.users.ts b/backend/admin/admin.users.ts new file mode 100644 index 0000000..2ec7de1 --- /dev/null +++ b/backend/admin/admin.users.ts @@ -0,0 +1,46 @@ +/** + * To be able to run this we need to set our dev pc in the .env. + * if its empty just ignore it. this will just be the double catch + */ + +import { fromNodeHeaders } from "better-auth/node"; +import { Router } from "express"; +import { auth } from "../utils/auth.utils.js"; +import { apiReturn } from "../utils/returnHelper.utils.js"; + +const r = Router(); + +r.get("/", async (req, res) => { + const { users } = await auth.api.listUsers({ + query: { + limit: 50, + }, + headers: fromNodeHeaders(req.headers), + }); + + // console.log(error); + + // if (error) { + // return apiReturn(res, { + // success: false, + // level: "info", + // module: "admin", + // subModule: "user", + // message: `There was an error getting the users.`, + // data: users, + // status: 400, + // }); + // } + + return apiReturn(res, { + success: true, + level: "info", + module: "admin", + subModule: "users", + message: `Current active users.`, + data: users, + status: 200, + }); +}); + +export default r; diff --git a/backend/utils/auth.permissions.ts b/backend/utils/auth.permissions.ts index 54b6874..acaf699 100644 --- a/backend/utils/auth.permissions.ts +++ b/backend/utils/auth.permissions.ts @@ -1,8 +1,9 @@ import { createAccessControl } from "better-auth/plugins/access"; +import { adminAc } from "better-auth/plugins/admin/access"; export const statement = { app: ["read", "create", "share", "update", "delete", "readAll"], - user: ["ban"], + //user: ["ban"], quality: ["read", "create", "share", "update", "delete", "readAll"], notifications: ["read", "create", "share", "update", "delete", "readAll"], } as const; @@ -20,7 +21,8 @@ export const admin = ac.newRole({ export const systemAdmin = ac.newRole({ app: ["read", "create", "share", "update", "delete", "readAll"], - user: ["ban"], + //user: ["ban"], quality: ["read", "create", "share", "update", "delete", "readAll"], notifications: ["read", "create", "share", "update", "delete", "readAll"], + ...adminAc.statements, }); diff --git a/frontend/src/lib/auth-client.ts b/frontend/src/lib/auth-client.ts index 60ae7a5..b2f7847 100644 --- a/frontend/src/lib/auth-client.ts +++ b/frontend/src/lib/auth-client.ts @@ -1,6 +1,6 @@ import { adminClient, genericOAuthClient } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; -import { ac, admin, systemAdmin, user } from "./auth-permissions"; +import { ac, admin, manager, systemAdmin, user } from "./auth-permissions"; export const authClient = createAuthClient({ baseURL: `${window.location.origin}/lst/api/auth`, @@ -10,6 +10,7 @@ export const authClient = createAuthClient({ roles: { admin, user, + manager, systemAdmin, }, }), diff --git a/frontend/src/lib/auth-permissions.ts b/frontend/src/lib/auth-permissions.ts index 00d392e..e02f9c0 100644 --- a/frontend/src/lib/auth-permissions.ts +++ b/frontend/src/lib/auth-permissions.ts @@ -1,21 +1,53 @@ import { createAccessControl } from "better-auth/plugins/access"; +import { adminAc } from "better-auth/plugins/admin/access"; export const statement = { - project: ["create", "share", "update", "delete"], - user: ["ban"], + 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; export const ac = createAccessControl(statement); export const user = ac.newRole({ - project: ["create"], + app: ["read", "create"], + notifications: ["read", "create"], +}); + +export const manager = ac.newRole({ + app: ["read", "create", "update"], }); export const admin = ac.newRole({ - project: ["create", "update"], + app: ["read", "create", "update"], }); export const systemAdmin = ac.newRole({ - project: ["create", "update", "delete"], - user: ["ban"], + 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 +const canCreateProject = await authClient.admin.hasPermission({ + permissions: { + project: ["create"], + }, +}); +// You can also check multiple resource permissions at the same time +const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({ + permissions: { + project: ["create"], + sale: ["create"] + }, +}); + + +*/ diff --git a/frontend/src/lib/queries/getUsers.ts b/frontend/src/lib/queries/getUsers.ts new file mode 100644 index 0000000..9eb85f2 --- /dev/null +++ b/frontend/src/lib/queries/getUsers.ts @@ -0,0 +1,40 @@ +import { keepPreviousData, queryOptions } from "@tanstack/react-query"; + +import { authClient } from "../auth-client"; + +export function getUsers() { + return queryOptions({ + queryKey: ["getUsers"], + queryFn: () => fetch(), + staleTime: 5000, + refetchOnWindowFocus: true, + placeholderData: keepPreviousData, + }); +} + +const fetch = async () => { + if (window.location.hostname === "localhost") { + await new Promise((res) => setTimeout(res, 1500)); + } + + const { data, error } = await authClient.admin.listUsers({ + query: { + // searchValue: "some name", + // searchField: "name", + // searchOperator: "contains", + limit: 100, + offset: 0, + sortBy: "name", + // sortDirection: "desc", + // filterField: "email", + // filterValue: "hello@example.com", + // filterOperator: "eq", + }, + }); + + if (error) { + return error; + } + + return data.users; +}; diff --git a/frontend/src/routes/(auth)/user.profile.tsx b/frontend/src/routes/(auth)/user.profile.tsx index 19ecf9b..7348b9c 100644 --- a/frontend/src/routes/(auth)/user.profile.tsx +++ b/frontend/src/routes/(auth)/user.profile.tsx @@ -10,6 +10,7 @@ import { } from "@/components/ui/card"; import { authClient, useSession } from "@/lib/auth-client"; import { useAppForm } from "@/lib/formSutff"; + import { Spinner } from "../../components/ui/spinner"; import ChangePassword from "./-components/ChangePassword"; import NotificationsSubCard from "./-components/NotificationsSubCard"; @@ -37,6 +38,7 @@ export const Route = createFileRoute("/(auth)/user/profile")({ function RouteComponent() { const { data: session } = useSession(); + const form = useAppForm({ defaultValues: { name: session?.user.name, diff --git a/frontend/src/routes/admin/users.tsx b/frontend/src/routes/admin/users.tsx new file mode 100644 index 0000000..70cfa88 --- /dev/null +++ b/frontend/src/routes/admin/users.tsx @@ -0,0 +1,156 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { createColumnHelper } from "@tanstack/react-table"; +import { format } from "date-fns-tz"; +import { Suspense } from "react"; +import { Button } from "../../components/ui/button"; +import { authClient, useSession } from "../../lib/auth-client"; +import { getUsers } from "../../lib/queries/getUsers"; +import LstTable from "../../lib/tableStuff/LstTable"; +import SearchableHeader from "../../lib/tableStuff/SearchableHeader"; +import SkellyTable from "../../lib/tableStuff/SkellyTable"; +import { trackLstEvent } from "../../lib/umami.utils"; + +export const Route = createFileRoute("/admin/users")({ + beforeLoad: async ({ location }) => { + const { data: session } = await authClient.getSession(); + const allowedRole = ["systemAdmin", "admin"]; + + if (!session?.user) { + throw redirect({ + to: "/", + search: { + redirect: location.href, + }, + }); + } + + if (!allowedRole.includes(session.user.role as string)) { + throw redirect({ + to: "/", + }); + } + + return { user: session.user }; + }, + component: RouteComponent, +}); + +const UserTable = () => { + const { data } = useSuspenseQuery(getUsers()); + const { data: session } = useSession(); + + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor("name", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => i.getValue(), + }), + columnHelper.accessor("email", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => i.getValue(), + }), + columnHelper.accessor("role", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => i.getValue(), + }), + columnHelper.accessor("updatedAt", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"), + }), + ]; + + if (session && session.user.role === "systemAdmin") { + columns.push( + columnHelper.accessor("banned", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => {i.getValue() ? "True" : "False"}, + }), + columnHelper.accessor("impersonate", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => { + const beSomeone = async () => { + trackLstEvent("impersonateUser_click", { + module: "users", + action: "click", + label: "impersonating user", + page: window.location.pathname, + }); + + const { data, error } = await authClient.admin.impersonateUser({ + userId: i.row.original.id, // required + }); + + if (data) { + await authClient.getSession(); + window.location.replace("/lst/app"); + } + + if (error) { + console.log(error); + } + }; + + const cantImpersonate = ["admin", "systemAdmin"]; + if (cantImpersonate.includes(i.row.original.role)) return; + return ; + }, + }), + ); + } + + return ; +}; + +function RouteComponent() { + // const createUser = async () => { + // const { data: newUser, error } = await authClient.admin.createUser({ + // email: "cowch@gmail.com", // required + // password: "crazypassword", // required + // name: "James Smith", // required + // role: "manager", + // }); + // }; + + // const besomeone = async () => { + // const { data, error } = await authClient.admin.impersonateUser({ + // userId: "iswCNVzQ9cWulbmsaMbeX6e7fV6Eme6t", // required + // }); + + // await authClient.getSession(); + // window.location.replace("/lst/app"); + // }; + + return ( + }> + + + ); +}