refactor(users): some user refactoring and configuring
This commit is contained in:
46
backend/admin/admin.users.ts
Normal file
46
backend/admin/admin.users.ts
Normal file
@@ -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;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createAccessControl } from "better-auth/plugins/access";
|
import { createAccessControl } from "better-auth/plugins/access";
|
||||||
|
import { adminAc } from "better-auth/plugins/admin/access";
|
||||||
|
|
||||||
export const statement = {
|
export const statement = {
|
||||||
app: ["read", "create", "share", "update", "delete", "readAll"],
|
app: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
user: ["ban"],
|
//user: ["ban"],
|
||||||
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -20,7 +21,8 @@ export const admin = ac.newRole({
|
|||||||
|
|
||||||
export const systemAdmin = ac.newRole({
|
export const systemAdmin = ac.newRole({
|
||||||
app: ["read", "create", "share", "update", "delete", "readAll"],
|
app: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
user: ["ban"],
|
//user: ["ban"],
|
||||||
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
|
...adminAc.statements,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
|
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
|
||||||
import { createAuthClient } from "better-auth/react";
|
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({
|
export const authClient = createAuthClient({
|
||||||
baseURL: `${window.location.origin}/lst/api/auth`,
|
baseURL: `${window.location.origin}/lst/api/auth`,
|
||||||
@@ -10,6 +10,7 @@ export const authClient = createAuthClient({
|
|||||||
roles: {
|
roles: {
|
||||||
admin,
|
admin,
|
||||||
user,
|
user,
|
||||||
|
manager,
|
||||||
systemAdmin,
|
systemAdmin,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,21 +1,53 @@
|
|||||||
import { createAccessControl } from "better-auth/plugins/access";
|
import { createAccessControl } from "better-auth/plugins/access";
|
||||||
|
import { adminAc } from "better-auth/plugins/admin/access";
|
||||||
|
|
||||||
export const statement = {
|
export const statement = {
|
||||||
project: ["create", "share", "update", "delete"],
|
app: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
user: ["ban"],
|
//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;
|
} as const;
|
||||||
|
|
||||||
export const ac = createAccessControl(statement);
|
export const ac = createAccessControl(statement);
|
||||||
|
|
||||||
export const user = ac.newRole({
|
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({
|
export const admin = ac.newRole({
|
||||||
project: ["create", "update"],
|
app: ["read", "create", "update"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const systemAdmin = ac.newRole({
|
export const systemAdmin = ac.newRole({
|
||||||
project: ["create", "update", "delete"],
|
app: ["read", "create", "share", "update", "delete", "readAll"],
|
||||||
user: ["ban"],
|
//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"]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
40
frontend/src/lib/queries/getUsers.ts
Normal file
40
frontend/src/lib/queries/getUsers.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { authClient, useSession } from "@/lib/auth-client";
|
import { authClient, useSession } from "@/lib/auth-client";
|
||||||
import { useAppForm } from "@/lib/formSutff";
|
import { useAppForm } from "@/lib/formSutff";
|
||||||
|
|
||||||
import { Spinner } from "../../components/ui/spinner";
|
import { Spinner } from "../../components/ui/spinner";
|
||||||
import ChangePassword from "./-components/ChangePassword";
|
import ChangePassword from "./-components/ChangePassword";
|
||||||
import NotificationsSubCard from "./-components/NotificationsSubCard";
|
import NotificationsSubCard from "./-components/NotificationsSubCard";
|
||||||
@@ -37,6 +38,7 @@ export const Route = createFileRoute("/(auth)/user/profile")({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: session?.user.name,
|
name: session?.user.name,
|
||||||
|
|||||||
156
frontend/src/routes/admin/users.tsx
Normal file
156
frontend/src/routes/admin/users.tsx
Normal file
@@ -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<any>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Name" searchable={true} />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => i.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Email" searchable={true} />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => i.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("role", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Role" searchable={false} />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => i.getValue(),
|
||||||
|
}),
|
||||||
|
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"),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (session && session.user.role === "systemAdmin") {
|
||||||
|
columns.push(
|
||||||
|
columnHelper.accessor("banned", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Banned" searchable={false} />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => <span>{i.getValue() ? "True" : "False"}</span>,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("impersonate", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader
|
||||||
|
column={column}
|
||||||
|
title="Impersonate User"
|
||||||
|
searchable={false}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
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 <Button onClick={beSomeone}>Become user</Button>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LstTable data={data} columns={columns} pageSize={50} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Suspense fallback={<SkellyTable />}>
|
||||||
|
<UserTable />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user