diff --git a/messages/en-US.json b/messages/en-US.json index 075bc307..a805c2f0 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -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.", diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index cb524480..ef6fff6d 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -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", { diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts index cf6b90df..ec7f3b4b 100644 --- a/server/routers/role/listRoles.ts +++ b/server/routers/role/listRoles.ts @@ -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)) diff --git a/src/components/CreateRoleForm.tsx b/src/components/CreateRoleForm.tsx index b8df1f78..30c37693 100644 --- a/src/components/CreateRoleForm.tsx +++ b/src/components/CreateRoleForm.tsx @@ -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({ )} /> + {isPaidUser && ( + ( + + + + + + + {t( + "requireDeviceApprovalDescription" + )} + + + + + )} + /> + )} diff --git a/src/components/RolesTable.tsx b/src/components/RolesTable.tsx index 6ecfbbac..2fd3353d 100644 --- a/src/components/RolesTable.tsx +++ b/src/components/RolesTable.tsx @@ -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: () => {t("description")} }, + + ...(isPaidUser + ? ([ + { + accessorKey: "requireDeviceApproval", + friendlyName: t("requireDeviceApproval"), + header: () => ( + + {t("requireDeviceApproval")} + + ), + cell: ({ row }) => ( + { + // ... + }} + /> + ) + } + ] as ExtendedColumnDef[]) + : []), + { id: "actions", enableHiding: false,