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

This commit is contained in:
2026-05-18 21:19:20 -05:00
parent 8dc4d70e28
commit 047cc7cdf0
17 changed files with 542 additions and 110 deletions

View File

@@ -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,

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,
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,

View File

@@ -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 = () => {
<Button
type="button"
onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new");
const { data } = await api.get("/mobile/pin/new");
updateSetting.mutate({
id: row.original.id,
field: "pinNumber",
@@ -171,7 +180,7 @@ const ScanUserTable = () => {
setActiveToggle(true);
try {
const res = await axios.delete(
const res = await api.delete(
`/lst/api/mobile/auth/user/${i.row.original.id}`,
{

View File

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

View File

@@ -24,6 +24,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
@@ -48,7 +49,7 @@ const updateSettings = async (
) => {
//console.log(id, data);
try {
const res = await axios.patch(`/lst/api/settings/${id}`, data, {
const res = await api.patch(`/settings/${id}`, data, {
withCredentials: true,
});
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 { 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 { 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 { 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 allowedRole = ["systemAdmin", "admin"];
const canAccess = await authClient.admin.hasPermission({
permissions: {
user: ["create"],
},
});
if (!session?.user) {
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({
to: "/",
});
@@ -37,8 +61,51 @@ export const Route = createFileRoute("/admin/users")({
});
const UserTable = () => {
const { data } = useSuspenseQuery(getUsers());
const { data: session } = useSession();
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>();
@@ -50,6 +117,13 @@ const UserTable = () => {
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} />
@@ -57,27 +131,113 @@ const UserTable = () => {
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" searchable={false} />
),
header: ({ column }) => <SearchableHeader column={column} title="Role" />,
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"),
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 (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(
columnHelper.accessor("banned", {
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() {