filter by auth state

This commit is contained in:
Fred KISSIE
2026-02-04 03:42:05 +01:00
parent bb1a375484
commit 1fc40b3017
2 changed files with 107 additions and 72 deletions

View File

@@ -17,7 +17,18 @@ import {
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { sql, eq, or, inArray, and, count, ilike } from "drizzle-orm";
import {
sql,
eq,
or,
inArray,
and,
count,
ilike,
asc,
not,
isNull
} from "drizzle-orm";
import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
@@ -48,7 +59,7 @@ const listResourcesSchema = z.object({
.optional()
.catch(undefined),
authState: z
.enum(["protected", "not_protected"])
.enum(["protected", "not_protected", "none"])
.optional()
.catch(undefined)
});
@@ -277,9 +288,63 @@ export async function listResources(
conditions = and(conditions, eq(resources.enabled, enabled));
}
if (typeof authState !== "undefined") {
switch (authState) {
case "none":
conditions = and(conditions, eq(resources.http, false));
break;
case "protected":
conditions = and(
conditions,
or(
eq(resources.sso, true),
eq(resources.emailWhitelistEnabled, true),
not(isNull(resourceHeaderAuth.headerAuthId)),
not(isNull(resourcePincode.pincodeId)),
not(isNull(resourcePassword.passwordId))
)
);
break;
case "not_protected":
conditions = and(
conditions,
not(eq(resources.sso, true)),
not(eq(resources.emailWhitelistEnabled, true)),
isNull(resourceHeaderAuth.headerAuthId),
isNull(resourcePincode.pincodeId),
isNull(resourcePassword.passwordId)
);
break;
}
}
const countQuery: any = db
.select({ count: count() })
.from(resources)
.leftJoin(
resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId)
)
.leftJoin(
resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId)
)
.leftJoin(
resourceHeaderAuth,
eq(resourceHeaderAuth.resourceId, resources.resourceId)
)
.leftJoin(
resourceHeaderAuthExtendedCompatibility,
eq(
resourceHeaderAuthExtendedCompatibility.resourceId,
resources.resourceId
)
)
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
.leftJoin(
targetHealthCheck,
eq(targetHealthCheck.targetId, targets.targetId)
)
.where(conditions);
const baseQuery = queryResourcesBase();
@@ -287,7 +352,8 @@ export async function listResources(
const rows: JoinedRow[] = await baseQuery
.where(conditions)
.limit(pageSize)
.offset(pageSize * (page - 1));
.offset(pageSize * (page - 1))
.orderBy(asc(resources.resourceId));
// avoids TS issues with reduce/never[]
const map = new Map<number, ResourceWithTargets>();

View File

@@ -185,23 +185,24 @@ export default function ProxyResourcesTable({
};
async function toggleResourceEnabled(val: boolean, resourceId: number) {
await api
.post<AxiosResponse<UpdateResourceResponse>>(
try {
await api.post<AxiosResponse<UpdateResourceResponse>>(
`resource/${resourceId}`,
{
enabled: val
}
)
.catch((e) => {
toast({
variant: "destructive",
title: t("resourcesErrorUpdate"),
description: formatAxiosError(
e,
t("resourcesErrorUpdateDescription")
)
});
);
router.refresh();
} catch (e) {
toast({
variant: "destructive",
title: t("resourcesErrorUpdate"),
description: formatAxiosError(
e,
t("resourcesErrorUpdateDescription")
)
});
}
}
function TargetStatusCell({ targets }: { targets?: TargetHealth[] }) {
@@ -313,38 +314,14 @@ export default function ProxyResourcesTable({
accessorKey: "name",
enableHiding: false,
friendlyName: t("name"),
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
header: () => <span className="p-3">{t("name")}</span>
},
{
id: "niceId",
accessorKey: "nice",
friendlyName: t("identifier"),
enableHiding: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("identifier")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
header: () => <span className="p-3">{t("identifier")}</span>,
cell: ({ row }) => {
return <span>{row.original.nice || "-"}</span>;
}
@@ -370,19 +347,7 @@ export default function ProxyResourcesTable({
id: "status",
accessorKey: "status",
friendlyName: t("status"),
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("status")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
header: () => <span className="p-3">{t("status")}</span>,
cell: ({ row }) => {
const resourceRow = row.original;
return <TargetStatusCell targets={resourceRow.targets} />;
@@ -430,19 +395,23 @@ export default function ProxyResourcesTable({
{
accessorKey: "authState",
friendlyName: t("authentication"),
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("authentication")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
header: () => (
<ColumnFilterButton
options={[
{ value: "protected", label: t("protected") },
{ value: "not_protected", label: t("notProtected") },
{ value: "none", label: t("none") }
]}
selectedValue={searchParams.get("authState") ?? undefined}
onValueChange={(value) =>
handleFilterChange("authState", value)
}
searchPlaceholder={t("searchPlaceholder")}
emptyMessage={t("emptySearchOptions")}
label={t("authentication")}
className="p-3"
/>
),
cell: ({ row }) => {
const resourceRow = row.original;
return (
@@ -487,16 +456,16 @@ export default function ProxyResourcesTable({
),
cell: ({ row }) => (
<Switch
defaultChecked={
checked={
row.original.http
? !!row.original.domainId && row.original.enabled
: row.original.enabled
}
disabled={
row.original.http ? !row.original.domainId : false
}
disabled={row.original.http && !row.original.domainId}
onCheckedChange={(val) =>
toggleResourceEnabled(val, row.original.id)
startTransition(() =>
toggleResourceEnabled(val, row.original.id)
)
}
/>
)