mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
✨ create & update role with device approval
This commit is contained in:
@@ -734,6 +734,11 @@
|
|||||||
"accessRoleCreatedDescription": "The role has been successfully created.",
|
"accessRoleCreatedDescription": "The role has been successfully created.",
|
||||||
"accessRoleErrorCreate": "Failed to create role",
|
"accessRoleErrorCreate": "Failed to create role",
|
||||||
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
|
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
|
||||||
|
"accessRoleUpdateSubmit": "Update Role",
|
||||||
|
"accessRoleUpdated": "Role updated",
|
||||||
|
"accessRoleUpdatedDescription": "The role has been successfully updated.",
|
||||||
|
"accessRoleErrorUpdate": "Failed to update role",
|
||||||
|
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
|
||||||
"accessRoleErrorNewRequired": "New role is required",
|
"accessRoleErrorNewRequired": "New role is required",
|
||||||
"accessRoleErrorRemove": "Failed to remove role",
|
"accessRoleErrorRemove": "Failed to remove role",
|
||||||
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
||||||
|
|||||||
@@ -554,6 +554,14 @@ authenticated.get(
|
|||||||
verifyUserHasAction(ActionsEnum.listRoles),
|
verifyUserHasAction(ActionsEnum.listRoles),
|
||||||
role.listRoles
|
role.listRoles
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/role/:roleId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateRole),
|
||||||
|
logActionAudit(ActionsEnum.updateRole),
|
||||||
|
role.updateRole
|
||||||
|
);
|
||||||
// authenticated.get(
|
// authenticated.get(
|
||||||
// "/role/:roleId",
|
// "/role/:roleId",
|
||||||
// verifyRoleAccess,
|
// verifyRoleAccess,
|
||||||
|
|||||||
@@ -467,6 +467,14 @@ authenticated.put(
|
|||||||
role.createRole
|
role.createRole
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/role/:roleId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateRole),
|
||||||
|
logActionAudit(ActionsEnum.updateRole),
|
||||||
|
role.updateRole
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/roles",
|
"/org/:orgId/roles",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||||
|
|
||||||
const createRoleParamsSchema = z.strictObject({
|
const createRoleParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -17,7 +19,8 @@ const createRoleParamsSchema = z.strictObject({
|
|||||||
|
|
||||||
const createRoleSchema = z.strictObject({
|
const createRoleSchema = z.strictObject({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
description: z.string().optional()
|
description: z.string().optional(),
|
||||||
|
requireDeviceApproval: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const defaultRoleAllowedActions: ActionsEnum[] = [
|
export const defaultRoleAllowedActions: ActionsEnum[] = [
|
||||||
@@ -97,6 +100,11 @@ export async function createRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
|
if (build === "oss" || !isLicensed) {
|
||||||
|
roleData.requireDeviceApproval = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const newRole = await trx
|
const newRole = await trx
|
||||||
.insert(roles)
|
.insert(roles)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, orgs, type Role } from "@server/db";
|
||||||
import { roles } from "@server/db";
|
import { roles } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -8,20 +8,28 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||||
|
|
||||||
const updateRoleParamsSchema = z.strictObject({
|
const updateRoleParamsSchema = z.strictObject({
|
||||||
|
orgId: z.string(),
|
||||||
roleId: z.string().transform(Number).pipe(z.int().positive())
|
roleId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateRoleBodySchema = z
|
const updateRoleBodySchema = z
|
||||||
.strictObject({
|
.strictObject({
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
description: z.string().optional()
|
description: z.string().optional(),
|
||||||
|
requireDeviceApproval: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type UpdateRoleBody = z.infer<typeof updateRoleBodySchema>;
|
||||||
|
|
||||||
|
export type UpdateRoleResponse = Role;
|
||||||
|
|
||||||
export async function updateRole(
|
export async function updateRole(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -48,13 +56,14 @@ export async function updateRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { roleId } = parsedParams.data;
|
const { roleId, orgId } = parsedParams.data;
|
||||||
const updateData = parsedBody.data;
|
const updateData = parsedBody.data;
|
||||||
|
|
||||||
const role = await db
|
const role = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.where(eq(roles.roleId, roleId))
|
.where(eq(roles.roleId, roleId))
|
||||||
|
.innerJoin(orgs, eq(roles.orgId, orgs.orgId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (role.length === 0) {
|
if (role.length === 0) {
|
||||||
@@ -66,7 +75,7 @@ export async function updateRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role[0].isAdmin) {
|
if (role[0].roles.isAdmin) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
@@ -75,6 +84,11 @@ export async function updateRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
|
if (build === "oss" || !isLicensed) {
|
||||||
|
updateData.requireDeviceApproval = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedRole = await db
|
const updatedRole = await db
|
||||||
.update(roles)
|
.update(roles)
|
||||||
.set(updateData)
|
.set(updateData)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GetOrgResponse } from "@server/routers/org";
|
|||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import { ListRolesResponse } from "@server/routers/role";
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
import RolesTable, { RoleRow } from "../../../../../components/RolesTable";
|
import RolesTable, { type RoleRow } from "@app/components/RolesTable";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaClose,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaFooter,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle
|
||||||
|
} from "@app/components/Credenza";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -11,37 +21,26 @@ import {
|
|||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useState } from "react";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useTransition } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
|
||||||
Credenza,
|
|
||||||
CredenzaBody,
|
|
||||||
CredenzaClose,
|
|
||||||
CredenzaContent,
|
|
||||||
CredenzaDescription,
|
|
||||||
CredenzaFooter,
|
|
||||||
CredenzaHeader,
|
|
||||||
CredenzaTitle
|
|
||||||
} from "@app/components/Credenza";
|
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
|
||||||
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";
|
|
||||||
import { build } from "@server/build";
|
|
||||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||||
|
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||||
|
|
||||||
type CreateRoleFormProps = {
|
type CreateRoleFormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
afterCreate?: (res: CreateRoleResponse) => Promise<void>;
|
afterCreate?: (res: CreateRoleResponse) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CreateRoleForm({
|
export default function CreateRoleForm({
|
||||||
@@ -54,13 +53,14 @@ export default function CreateRoleForm({
|
|||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string({ message: t("nameRequired") }).max(32),
|
name: z
|
||||||
|
.string({ message: t("nameRequired") })
|
||||||
|
.min(1)
|
||||||
|
.max(32),
|
||||||
description: z.string().max(255).optional(),
|
description: z.string().max(255).optional(),
|
||||||
requireDeviceApproval: z.boolean().optional()
|
requireDeviceApproval: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
@@ -72,17 +72,13 @@ export default function CreateRoleForm({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
const [loading, startTransition] = useTransition();
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
const res = await api
|
const res = await api
|
||||||
.put<AxiosResponse<CreateRoleResponse>>(
|
.put<
|
||||||
`/org/${org?.org.orgId}/role`,
|
AxiosResponse<CreateRoleResponse>
|
||||||
{
|
>(`/org/${org?.org.orgId}/role`, values satisfies CreateRoleBody)
|
||||||
name: values.name,
|
|
||||||
description: values.description
|
|
||||||
} as CreateRoleBody
|
|
||||||
)
|
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
@@ -105,12 +101,8 @@ export default function CreateRoleForm({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (afterCreate) {
|
afterCreate?.(res.data.data);
|
||||||
afterCreate(res.data.data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -119,7 +111,6 @@ export default function CreateRoleForm({
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(val) => {
|
onOpenChange={(val) => {
|
||||||
setOpen(val);
|
setOpen(val);
|
||||||
setLoading(false);
|
|
||||||
form.reset();
|
form.reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -133,7 +124,9 @@ export default function CreateRoleForm({
|
|||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit((values) =>
|
||||||
|
startTransition(() => onSubmit(values))
|
||||||
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-role-form"
|
id="create-role-form"
|
||||||
>
|
>
|
||||||
@@ -168,23 +161,37 @@ export default function CreateRoleForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{build !== "oss" && (
|
{build !== "oss" && (
|
||||||
<>
|
<div className="pt-3">
|
||||||
<PaidFeaturesAlert />
|
<PaidFeaturesAlert />
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="requireDeviceApproval"
|
name="requireDeviceApproval"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="pt-3">
|
<FormItem className="my-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CheckboxWithLabel
|
<CheckboxWithLabel
|
||||||
{...field}
|
{...field}
|
||||||
disabled={
|
disabled={
|
||||||
!isPaidUser
|
!isPaidUser
|
||||||
}
|
}
|
||||||
value={undefined}
|
value="on"
|
||||||
defaultChecked={
|
checked={form.watch(
|
||||||
field.value
|
"requireDeviceApproval"
|
||||||
}
|
)}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
checked !==
|
||||||
|
"indeterminate"
|
||||||
|
) {
|
||||||
|
form.setValue(
|
||||||
|
"requireDeviceApproval",
|
||||||
|
checked
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
label={t(
|
label={t(
|
||||||
"requireDeviceApproval"
|
"requireDeviceApproval"
|
||||||
)}
|
)}
|
||||||
@@ -201,7 +208,7 @@ export default function CreateRoleForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -78,10 +78,7 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
|||||||
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaClose
|
<CredenzaClose className={cn("", className)} {...props}>
|
||||||
className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
);
|
);
|
||||||
@@ -172,14 +169,13 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
|||||||
|
|
||||||
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
// const isDesktop = true;
|
|
||||||
|
|
||||||
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaFooter
|
<CredenzaFooter
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-8 md:mt-0 -mx-6 px-6 pt-4 border-t border-border",
|
"mt-8 md:mt-0 -mx-6 px-6 py-4 border-t border-border",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
241
src/components/EditRoleForm.tsx
Normal file
241
src/components/EditRoleForm.tsx
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaClose,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaFooter,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle
|
||||||
|
} from "@app/components/Credenza";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from "@app/components/ui/form";
|
||||||
|
import { Input } from "@app/components/ui/input";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import type { Role } from "@server/db";
|
||||||
|
import type {
|
||||||
|
CreateRoleBody,
|
||||||
|
CreateRoleResponse,
|
||||||
|
UpdateRoleBody,
|
||||||
|
UpdateRoleResponse
|
||||||
|
} from "@server/routers/role";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useTransition } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||||
|
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||||
|
|
||||||
|
type CreateRoleFormProps = {
|
||||||
|
role: Role;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
onSuccess?: (res: CreateRoleResponse) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EditRoleForm({
|
||||||
|
open,
|
||||||
|
role,
|
||||||
|
setOpen,
|
||||||
|
onSuccess
|
||||||
|
}: CreateRoleFormProps) {
|
||||||
|
const { org } = useOrgContext();
|
||||||
|
const t = useTranslations();
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string({ message: t("nameRequired") })
|
||||||
|
.min(1)
|
||||||
|
.max(32),
|
||||||
|
description: z.string().max(255).optional(),
|
||||||
|
requireDeviceApproval: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: role.name,
|
||||||
|
description: role.description ?? "",
|
||||||
|
requireDeviceApproval: role.requireDeviceApproval ?? false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, startTransition] = useTransition();
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
const res = await api
|
||||||
|
.post<
|
||||||
|
AxiosResponse<UpdateRoleResponse>
|
||||||
|
>(`/org/${org?.org.orgId}/role/${role.roleId}`, values satisfies UpdateRoleBody)
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("accessRoleErrorUpdate"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("accessRoleErrorUpdateDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
toast({
|
||||||
|
variant: "default",
|
||||||
|
title: t("accessRoleUpdated"),
|
||||||
|
description: t("accessRoleUpdatedDescription")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess?.(res.data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Credenza
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(val) => {
|
||||||
|
setOpen(val);
|
||||||
|
form.reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredenzaContent>
|
||||||
|
<CredenzaHeader>
|
||||||
|
<CredenzaTitle>{t("accessRoleCreate")}</CredenzaTitle>
|
||||||
|
<CredenzaDescription>
|
||||||
|
{t("accessRoleCreateDescription")}
|
||||||
|
</CredenzaDescription>
|
||||||
|
</CredenzaHeader>
|
||||||
|
<CredenzaBody>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit((values) =>
|
||||||
|
startTransition(() => onSubmit(values))
|
||||||
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
id="create-role-form"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("accessRoleName")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("description")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{build !== "oss" && (
|
||||||
|
<div className="pt-3">
|
||||||
|
<PaidFeaturesAlert />
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="requireDeviceApproval"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="my-2">
|
||||||
|
<FormControl>
|
||||||
|
<CheckboxWithLabel
|
||||||
|
{...field}
|
||||||
|
disabled={
|
||||||
|
!isPaidUser
|
||||||
|
}
|
||||||
|
value="on"
|
||||||
|
checked={form.watch(
|
||||||
|
"requireDeviceApproval"
|
||||||
|
)}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
checked !==
|
||||||
|
"indeterminate"
|
||||||
|
) {
|
||||||
|
form.setValue(
|
||||||
|
"requireDeviceApproval",
|
||||||
|
checked
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label={t(
|
||||||
|
"requireDeviceApproval"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"requireDeviceApprovalDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CredenzaBody>
|
||||||
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">{t("close")}</Button>
|
||||||
|
</CredenzaClose>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="create-role-form"
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t("accessRoleUpdateSubmit")}
|
||||||
|
</Button>
|
||||||
|
</CredenzaFooter>
|
||||||
|
</CredenzaContent>
|
||||||
|
</Credenza>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { DataTable } from "@app/components/ui/data-table";
|
|
||||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -13,18 +12,12 @@ import {
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import {
|
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
ArrowRight,
|
|
||||||
ArrowUpDown,
|
|
||||||
ArrowUpRight,
|
|
||||||
MoreHorizontal
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useMemo, useState, useTransition } from "react";
|
import { useMemo, useState, useTransition } from "react";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { InfoPopup } from "./ui/info-popup";
|
|
||||||
|
|
||||||
export type ClientRow = {
|
export type ClientRow = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { Role } from "@server/db";
|
import { Role } from "@server/db";
|
||||||
import { ArrowUpDown } from "lucide-react";
|
import { ArrowRight, ArrowUpDown, Link, MoreHorizontal } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { Switch } from "./ui/switch";
|
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem
|
||||||
|
} from "./ui/dropdown-menu";
|
||||||
|
import EditRoleForm from "./EditRoleForm";
|
||||||
|
|
||||||
export type RoleRow = Role;
|
export type RoleRow = Role;
|
||||||
|
|
||||||
@@ -23,13 +29,13 @@ type RolesTableProps = {
|
|||||||
roles: RoleRow[];
|
roles: RoleRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UsersTable({ roles: r }: RolesTableProps) {
|
export default function UsersTable({ roles }: RolesTableProps) {
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [editingRole, setEditingRole] = useState<RoleRow | null>(null);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [roles, setRoles] = useState<RoleRow[]>(r);
|
|
||||||
|
|
||||||
const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null);
|
const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null);
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
@@ -38,13 +44,11 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
|
|
||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
console.log("Data refreshed");
|
console.log("Data refreshed");
|
||||||
setIsRefreshing(true);
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
@@ -52,8 +56,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
description: t("refreshError"),
|
description: t("refreshError"),
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
setIsRefreshing(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,52 +83,76 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
friendlyName: t("description"),
|
friendlyName: t("description"),
|
||||||
header: () => <span className="p-3">{t("description")}</span>
|
header: () => <span className="p-3">{t("description")}</span>
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// id: "actions",
|
||||||
|
// enableHiding: false,
|
||||||
|
// header: () => <span className="p-3"></span>,
|
||||||
|
// cell: ({ row }) => {
|
||||||
|
// const roleRow = row.original;
|
||||||
|
|
||||||
...(isPaidUser
|
// return (
|
||||||
? ([
|
// <div className="flex items-center gap-2 justify-end">
|
||||||
{
|
// <Button
|
||||||
accessorKey: "requireDeviceApproval",
|
// variant={"outline"}
|
||||||
friendlyName: t("requireDeviceApproval"),
|
// disabled={roleRow.isAdmin || false}
|
||||||
header: () => (
|
// onClick={() => {
|
||||||
<span className="p-3">
|
// setIsDeleteModalOpen(true);
|
||||||
{t("requireDeviceApproval")}
|
// setUserToRemove(roleRow);
|
||||||
</span>
|
// }}
|
||||||
),
|
// >
|
||||||
cell: ({ row }) => (
|
// {t("accessRoleDelete")}
|
||||||
<Switch
|
// </Button>
|
||||||
defaultChecked={
|
// </div>
|
||||||
!!row.original.requireDeviceApproval
|
// );
|
||||||
}
|
// }
|
||||||
disabled={!!row.original.isAdmin}
|
// },
|
||||||
onCheckedChange={(val) => {
|
|
||||||
// ...
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
] as ExtendedColumnDef<RoleRow>[])
|
|
||||||
: []),
|
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
header: () => <span className="p-3"></span>,
|
header: () => <span className="p-3"></span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const roleRow = row.original;
|
const roleRow = row.original;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 justify-end">
|
!roleRow.isAdmin && (
|
||||||
<Button
|
<div className="flex items-center gap-2 justify-end">
|
||||||
variant={"outline"}
|
<DropdownMenu>
|
||||||
disabled={roleRow.isAdmin || false}
|
<DropdownMenuTrigger asChild>
|
||||||
onClick={() => {
|
<Button
|
||||||
setIsDeleteModalOpen(true);
|
variant="ghost"
|
||||||
setUserToRemove(roleRow);
|
className="h-8 w-8 p-0"
|
||||||
}}
|
>
|
||||||
>
|
<span className="sr-only">
|
||||||
{t("accessRoleDelete")}
|
{t("openMenu")}
|
||||||
</Button>
|
</span>
|
||||||
</div>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
// setSelectedInternalResource(
|
||||||
|
// resourceRow
|
||||||
|
// );
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-red-500">
|
||||||
|
{t("delete")}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingRole(roleRow);
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("edit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,11 +160,26 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{editingRole && (
|
||||||
|
<EditRoleForm
|
||||||
|
role={editingRole}
|
||||||
|
open={isEditDialogOpen}
|
||||||
|
key={editingRole.roleId}
|
||||||
|
setOpen={setIsEditDialogOpen}
|
||||||
|
onSuccess={() => {
|
||||||
|
// Delay refresh to allow modal to close smoothly
|
||||||
|
setTimeout(() => {
|
||||||
|
router.refresh();
|
||||||
|
setEditingRole(null);
|
||||||
|
}, 150);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<CreateRoleForm
|
<CreateRoleForm
|
||||||
open={isCreateModalOpen}
|
open={isCreateModalOpen}
|
||||||
setOpen={setIsCreateModalOpen}
|
setOpen={setIsCreateModalOpen}
|
||||||
afterCreate={async (role) => {
|
afterCreate={() => {
|
||||||
setRoles((prev) => [...prev, role]);
|
startTransition(refreshData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -148,9 +189,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
setOpen={setIsDeleteModalOpen}
|
setOpen={setIsDeleteModalOpen}
|
||||||
roleToDelete={roleToRemove}
|
roleToDelete={roleToRemove}
|
||||||
afterDelete={() => {
|
afterDelete={() => {
|
||||||
setRoles((prev) =>
|
startTransition(refreshData);
|
||||||
prev.filter((r) => r.roleId !== roleToRemove.roleId)
|
|
||||||
);
|
|
||||||
setUserToRemove(null);
|
setUserToRemove(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -162,7 +201,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
createRole={() => {
|
createRole={() => {
|
||||||
setIsCreateModalOpen(true);
|
setIsCreateModalOpen(true);
|
||||||
}}
|
}}
|
||||||
onRefresh={refreshData}
|
onRefresh={() => startTransition(refreshData)}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ const checkboxVariants = cva(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface CheckboxProps
|
interface CheckboxProps
|
||||||
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
extends
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
||||||
VariantProps<typeof checkboxVariants> {}
|
VariantProps<typeof checkboxVariants> {}
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
@@ -49,17 +50,18 @@ const Checkbox = React.forwardRef<
|
|||||||
));
|
));
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
interface CheckboxWithLabelProps
|
interface CheckboxWithLabelProps extends React.ComponentPropsWithoutRef<
|
||||||
extends React.ComponentPropsWithoutRef<typeof Checkbox> {
|
typeof Checkbox
|
||||||
|
> {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckboxWithLabel = React.forwardRef<
|
const CheckboxWithLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof Checkbox>,
|
React.ComponentRef<typeof Checkbox>,
|
||||||
CheckboxWithLabelProps
|
CheckboxWithLabelProps
|
||||||
>(({ className, label, id, ...props }, ref) => {
|
>(({ className, label, id, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex items-center space-x-2", className)}>
|
<div className={cn("flex items-center gap-x-2", className)}>
|
||||||
<Checkbox id={id} ref={ref} {...props} />
|
<Checkbox id={id} ref={ref} {...props} />
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
|
|||||||
Reference in New Issue
Block a user