🚧 add device approval in the roles page

This commit is contained in:
Fred KISSIE
2026-01-06 01:51:33 +01:00
parent 1f80845a7a
commit cb21cab117
5 changed files with 88 additions and 23 deletions

View File

@@ -1548,6 +1548,8 @@
"IntervalSeconds": "Healthy Interval",
"timeoutSeconds": "Timeout (sec)",
"timeIsInSeconds": "Time is in seconds",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need their devices approved by an admin before they can access resources",
"retryAttempts": "Retry Attempts",
"expectedResponseCodes": "Expected Response Codes",
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",

View File

@@ -255,7 +255,9 @@ export const siteResources = sqliteTable("siteResources", {
aliasAddress: text("aliasAddress"),
tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"),
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
disableIcmp: integer("disableIcmp", { mode: "boolean" }).notNull().default(false)
disableIcmp: integer("disableIcmp", { mode: "boolean" })
.notNull()
.default(false)
});
export const clientSiteResources = sqliteTable("clientSiteResources", {
@@ -796,7 +798,10 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", {
identifierPath: text("identifierPath").notNull(),
emailPath: text("emailPath"),
namePath: text("namePath"),
scopes: text("scopes").notNull()
scopes: text("scopes").notNull(),
approvalState: text("approvalState")
.$type<"pending" | "approved" | "denied">()
.default("approved")
});
export const licenseKey = sqliteTable("licenseKey", {

View File

@@ -36,7 +36,8 @@ async function queryRoles(orgId: string, limit: number, offset: number) {
isAdmin: roles.isAdmin,
name: roles.name,
description: roles.description,
orgName: orgs.name
orgName: orgs.name,
requireDeviceApproval: roles.requireDeviceApproval
})
.from(roles)
.leftJoin(orgs, eq(roles.orgId, orgs.orgId))

View File

@@ -4,6 +4,7 @@ import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -27,11 +28,13 @@ import {
CredenzaTitle
} from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import { CheckboxWithLabel } from "./ui/checkbox";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
type CreateRoleFormProps = {
open: boolean;
@@ -46,10 +49,12 @@ export default function CreateRoleForm({
}: CreateRoleFormProps) {
const { org } = useOrgContext();
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const formSchema = z.object({
name: z.string({ message: t("nameRequired") }).max(32),
description: z.string().max(255).optional()
description: z.string().max(255).optional(),
requireDeviceApproval: z.boolean().optional()
});
const [loading, setLoading] = useState(false);
@@ -60,7 +65,8 @@ export default function CreateRoleForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
description: ""
description: "",
requireDeviceApproval: false
}
});
@@ -159,6 +165,36 @@ export default function CreateRoleForm({
</FormItem>
)}
/>
{isPaidUser && (
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="pt-3">
<FormControl>
<CheckboxWithLabel
{...field}
value={undefined}
defaultChecked={
field.value
}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
</CredenzaBody>

View File

@@ -1,27 +1,21 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button";
import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
import { useState } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast";
import { RolesDataTable } from "@app/components/RolesDataTable";
import { Role } from "@server/db";
import CreateRoleForm from "@app/components/CreateRoleForm";
import DeleteRoleForm from "@app/components/DeleteRoleForm";
import { createApiClient } from "@app/lib/api";
import { RolesDataTable } from "@app/components/RolesDataTable";
import { Button } from "@app/components/ui/button";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { Role } from "@server/db";
import { ArrowUpDown } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Switch } from "./ui/switch";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
export type RoleRow = Role;
@@ -41,6 +35,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
const api = createApiClient(useEnvContext());
const { org } = useOrgContext();
const { isPaidUser } = usePaidStatus();
const t = useTranslations();
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -86,6 +81,32 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
friendlyName: t("description"),
header: () => <span className="p-3">{t("description")}</span>
},
...(isPaidUser
? ([
{
accessorKey: "requireDeviceApproval",
friendlyName: t("requireDeviceApproval"),
header: () => (
<span className="p-3">
{t("requireDeviceApproval")}
</span>
),
cell: ({ row }) => (
<Switch
defaultChecked={
!!row.original.requireDeviceApproval
}
disabled={!!row.original.isAdmin}
onCheckedChange={(val) => {
// ...
}}
/>
)
}
] as ExtendedColumnDef<RoleRow>[])
: []),
{
id: "actions",
enableHiding: false,