Files
lst_v3/frontend/src/routes/admin/users.tsx
Blake Matthes 047cc7cdf0
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 2m9s
refactor(users): lots of auth stuff added to make it more easy to manage users
2026-05-18 21:19:20 -05:00

338 lines
8.3 KiB
TypeScript

import { useMutation, useQuery, 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 { KeyRound } from "lucide-react";
import { Suspense } from "react";
import { toast } from "sonner";
import { Button } from "../../components/ui/button";
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 { permissionQuery } from "../../lib/queries/permsCheck";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable";
import { trackLstEvent } from "../../lib/umami.utils";
import NewUser from "./-components/Newuser";
export const Route = createFileRoute("/admin/users")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
// const allowedRole = ["systemAdmin", "admin"];
const canAccess = await authClient.admin.hasPermission({
permissions: {
user: ["create"],
},
});
if (!session?.user) {
throw redirect({
to: "/",
search: {
redirect: location.href,
},
});
}
//if (!allowedRole.includes(session.user.role as string)) {
if (!canAccess) {
throw redirect({
to: "/",
});
}
return { user: session.user };
},
component: RouteComponent,
});
const UserTable = () => {
const { data, refetch } = useSuspenseQuery(getUsers());
//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 columns = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
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", {
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("role", {
header: ({ column }) => <SearchableHeader column={column} title="Role" />,
filterFn: "includesString",
cell: ({ row, getValue }) => {
const currentRole = getValue();
return (
<Select
value={currentRole}
onValueChange={(newRole) => {
handleRoleChange(row.original, newRole);
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
{selectableRoles.map((role) => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
},
}),
];
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(
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>;
},
}),
);
}
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() {
// 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>
);
}