Merge remote-tracking branch 'origin' into project-role-concept

This commit is contained in:
Tuan Dang
2024-07-26 07:28:17 -07:00
22 changed files with 159 additions and 526 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}), }),
name: z.string().trim().optional(), name: z.string().trim().optional(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
permissions: z.any().array() permissions: z.any().array().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
+15
View File
@@ -425,6 +425,21 @@ export const PROJECTS = {
}, },
LIST_INTEGRATION_AUTHORIZATION: { LIST_INTEGRATION_AUTHORIZATION: {
workspaceId: "The ID of the project to list integration auths for." workspaceId: "The ID of the project to list integration auths for."
},
LIST_CAS: {
slug: "The slug of the project to list CAs for.",
status: "The status of the CA to filter by.",
friendlyName: "The friendly name of the CA to filter by.",
commonName: "The common name of the CA to filter by.",
offset: "The offset to start from. If you enter 10, it will start from the 10th CA.",
limit: "The number of CAs to return."
},
LIST_CERTIFICATES: {
slug: "The slug of the project to list certificates for.",
friendlyName: "The friendly name of the certificate to filter by.",
commonName: "The common name of the certificate to filter by.",
offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.",
limit: "The number of certificates to return."
} }
} as const; } as const;
+13 -7
View File
@@ -317,10 +317,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema.describe("The slug of the project to list CAs.") slug: slugSchema.describe(PROJECTS.LIST_CAS.slug)
}), }),
querystring: z.object({ querystring: z.object({
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional() status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
friendlyName: z.string().optional().describe(PROJECTS.LIST_CAS.friendlyName),
commonName: z.string().optional().describe(PROJECTS.LIST_CAS.commonName),
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CAS.offset),
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CAS.limit)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -336,11 +340,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId, orgId: req.permission.orgId,
type: ProjectFilterType.SLUG type: ProjectFilterType.SLUG
}, },
status: req.query.status,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type actor: req.permission.type,
...req.query
}); });
return { cas }; return { cas };
} }
@@ -354,11 +358,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema.describe("The slug of the project to list certificates.") slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug)
}), }),
querystring: z.object({ querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0), friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
limit: z.coerce.number().min(1).max(100).default(25) commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -8,19 +8,35 @@ export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
export const certificateDALFactory = (db: TDbClient) => { export const certificateDALFactory = (db: TDbClient) => {
const certificateOrm = ormify(db, TableName.Certificate); const certificateOrm = ormify(db, TableName.Certificate);
const countCertificatesInProject = async (projectId: string) => { const countCertificatesInProject = async ({
projectId,
friendlyName,
commonName
}: {
projectId: string;
friendlyName?: string;
commonName?: string;
}) => {
try { try {
interface CountResult { interface CountResult {
count: string; count: string;
} }
const count = await db let query = db
.replicaNode()(TableName.Certificate) .replicaNode()(TableName.Certificate)
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`) .join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`) .join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.id`, projectId) .where(`${TableName.Project}.id`, projectId);
.count("*")
.first(); if (friendlyName) {
query = query.andWhere(`${TableName.Certificate}.friendlyName`, friendlyName);
}
if (commonName) {
query = query.andWhere(`${TableName.Certificate}.commonName`, commonName);
}
const count = await query.count("*").first();
return parseInt((count as unknown as CountResult).count || "0", 10); return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) { } catch (error) {
@@ -575,6 +575,10 @@ export const projectServiceFactory = ({
*/ */
const listProjectCas = async ({ const listProjectCas = async ({
status, status,
friendlyName,
commonName,
limit = 25,
offset = 0,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
@@ -596,10 +600,15 @@ export const projectServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const cas = await certificateAuthorityDAL.find({ const cas = await certificateAuthorityDAL.find(
projectId: project.id, {
...(status && { status }) projectId: project.id,
}); ...(status && { status }),
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
return cas; return cas;
}; };
@@ -608,8 +617,10 @@ export const projectServiceFactory = ({
* Return list of certificates for project * Return list of certificates for project
*/ */
const listProjectCertificates = async ({ const listProjectCertificates = async ({
offset, limit = 25,
limit, offset = 0,
friendlyName,
commonName,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
@@ -634,12 +645,18 @@ export const projectServiceFactory = ({
{ {
$in: { $in: {
caId: cas.map((ca) => ca.id) caId: cas.map((ca) => ca.id)
} },
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
}, },
{ offset, limit, sort: [["updatedAt", "desc"]] } { offset, limit, sort: [["updatedAt", "desc"]] }
); );
const count = await certificateDAL.countCertificatesInProject(project.id); const count = await certificateDAL.countCertificatesInProject({
projectId: project.id,
friendlyName,
commonName
});
return { return {
certificates, certificates,
@@ -89,6 +89,10 @@ export type AddUserToWsDTO = {
export type TListProjectCasDTO = { export type TListProjectCasDTO = {
status?: CaStatus; status?: CaStatus;
friendlyName?: string;
offset?: number;
limit?: number;
commonName?: string;
filter: Filter; filter: Filter;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -96,4 +100,6 @@ export type TListProjectCertsDTO = {
filter: Filter; filter: Filter;
offset: number; offset: number;
limit: number; limit: number;
friendlyName?: string;
commonName?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
+4 -8
View File
@@ -24,7 +24,6 @@ import (
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/srp" "github.com/Infisical/infisical-merge/packages/srp"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/chzyer/readline"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
@@ -205,6 +204,7 @@ var loginCmd = &cobra.Command{
if !overrideDomain { if !overrideDomain {
domainQuery = false domainQuery = false
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE) config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api"))
} }
} }
@@ -713,7 +713,7 @@ func askForMFACode() string {
return mfaVerifyCode return mfaVerifyCode
} }
func askToPasteJwtToken(stdin *readline.CancelableStdin, success chan models.UserCredentials, failure chan error) { func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) {
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
fmt.Println("\n\nOnce login is completed via browser, the CLI should be authenticated automatically.") fmt.Println("\n\nOnce login is completed via browser, the CLI should be authenticated automatically.")
fmt.Println("However, if browser fails to communicate with the CLI, please paste the token from the browser below.") fmt.Println("However, if browser fails to communicate with the CLI, please paste the token from the browser below.")
@@ -807,26 +807,22 @@ func browserCliLogin() (models.UserCredentials, error) {
log.Debug().Msgf("Callback server listening on port %d", callbackPort) log.Debug().Msgf("Callback server listening on port %d", callbackPort)
stdin := readline.NewCancelableStdin(os.Stdin)
go http.Serve(listener, corsHandler) go http.Serve(listener, corsHandler)
go askToPasteJwtToken(stdin, success, failure) go askToPasteJwtToken(success, failure)
for { for {
select { select {
case loginResponse := <-success: case loginResponse := <-success:
_ = closeListener(&listener) _ = closeListener(&listener)
_ = stdin.Close()
fmt.Println("Browser login successful") fmt.Println("Browser login successful")
return loginResponse, nil return loginResponse, nil
case err := <-failure: case err := <-failure:
serverErr := closeListener(&listener) serverErr := closeListener(&listener)
stdErr := stdin.Close() return models.UserCredentials{}, errors.Join(err, serverErr)
return models.UserCredentials{}, errors.Join(err, serverErr, stdErr)
case <-timeout: case <-timeout:
_ = closeListener(&listener) _ = closeListener(&listener)
_ = stdin.Close()
return models.UserCredentials{}, errors.New("server timeout") return models.UserCredentials{}, errors.New("server timeout")
} }
} }
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/workspace/{slug}/cas"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/workspace/{slug}/certificates"
---
+2
View File
@@ -654,6 +654,7 @@
{ {
"group": "Certificate Authorities", "group": "Certificate Authorities",
"pages": [ "pages": [
"api-reference/endpoints/certificate-authorities/list",
"api-reference/endpoints/certificate-authorities/create", "api-reference/endpoints/certificate-authorities/create",
"api-reference/endpoints/certificate-authorities/read", "api-reference/endpoints/certificate-authorities/read",
"api-reference/endpoints/certificate-authorities/update", "api-reference/endpoints/certificate-authorities/update",
@@ -669,6 +670,7 @@
{ {
"group": "Certificates", "group": "Certificates",
"pages": [ "pages": [
"api-reference/endpoints/certificates/list",
"api-reference/endpoints/certificates/read", "api-reference/endpoints/certificates/read",
"api-reference/endpoints/certificates/revoke", "api-reference/endpoints/certificates/revoke",
"api-reference/endpoints/certificates/delete", "api-reference/endpoints/certificates/delete",
@@ -25,6 +25,10 @@ Used to configure platform-specific security and operational settings
https://app.infisical.com). https://app.infisical.com).
</ParamField> </ParamField>
<ParamField query="PORT" type="int" default="8080" optional>
Specifies the internal port on which the application listens.
</ParamField>
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional> <ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional>
Telemetry helps us improve Infisical but if you want to dsiable it you may set this to `false`. Telemetry helps us improve Infisical but if you want to dsiable it you may set this to `false`.
</ParamField> </ParamField>
+1 -1
View File
@@ -79,7 +79,7 @@ export const useUpdateOrgRole = () => {
data: { role } data: { role }
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, { } = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
...dto, ...dto,
permissions: permissions?.length ? packRules(permissions) : [] permissions: permissions?.length ? packRules(permissions) : undefined
}); });
return role; return role;
@@ -168,16 +168,27 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
const { token: newJwtToken } = await selectOrganization({ organizationId }); const { token: newJwtToken } = await selectOrganization({ organizationId });
const instance = axios.create(); const instance = axios.create();
await instance.post(cliUrl, { const payload = {
...isCliLoginSuccessful.loginResponse, ...isCliLoginSuccessful.loginResponse,
JTWToken: newJwtToken JTWToken: newJwtToken
};
await instance.post(cliUrl, payload).catch(() => {
// if error happens to communicate we set the token with an expiry in sessino storage
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
sessionStorage.setItem(
SessionStorageKeys.CLI_TERMINAL_TOKEN,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 30)),
data: window.btoa(JSON.stringify(payload))
})
);
}); });
router.push("/cli-redirect");
await navigateUserToOrg(router, organizationId); return;
} }
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs // case: no organization ID is present -- navigate to the select org page IF the user has any orgs
// if the user has no orgs, navigate to the create org page // if the user has no orgs, navigate to the create org page
else {
const userOrgs = await fetchOrganizations(); const userOrgs = await fetchOrganizations();
// case: user has orgs, so we navigate the user to select an org // case: user has orgs, so we navigate the user to select an org
@@ -189,7 +200,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
else { else {
await navigateUserToOrg(router); await navigateUserToOrg(router);
} }
}
} }
} else { } else {
const isLoginSuccessful = await attemptLoginMfa({ const isLoginSuccessful = await attemptLoginMfa({
@@ -4,6 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import HCaptcha from "@hcaptcha/react-hcaptcha"; import HCaptcha from "@hcaptcha/react-hcaptcha";
import axios from "axios"; import axios from "axios";
import { addSeconds, formatISO } from "date-fns";
import jwt_decode from "jwt-decode"; import jwt_decode from "jwt-decode";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -12,6 +13,7 @@ import attemptLogin from "@app/components/utilities/attemptLogin";
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config"; import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input, Spinner } from "@app/components/v2"; import { Button, Input, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
@@ -79,11 +81,24 @@ export const PasswordStep = ({
if (callbackPort) { if (callbackPort) {
console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken); console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken);
const instance = axios.create(); const instance = axios.create();
await instance.post(cliUrl, { const payload = {
privateKey, privateKey,
email, email,
JTWToken: newJwtToken JTWToken: newJwtToken
};
await instance.post(cliUrl, payload).catch(() => {
// if error happens to communicate we set the token with an expiry in sessino storage
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
sessionStorage.setItem(
SessionStorageKeys.CLI_TERMINAL_TOKEN,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 30)),
data: window.btoa(JSON.stringify(payload))
})
);
}); });
router.push("/cli-redirect");
return;
} }
await navigateUserToOrg(router, organizationId); await navigateUserToOrg(router, organizationId);
@@ -165,26 +180,35 @@ export const PasswordStep = ({
); );
const instance = axios.create(); const instance = axios.create();
await instance.post(cliUrl, { const payload = {
...isCliLoginSuccessful.loginResponse, ...isCliLoginSuccessful.loginResponse,
JTWToken: newJwtToken JTWToken: newJwtToken
};
await instance.post(cliUrl, payload).catch(() => {
// if error happens to communicate we set the token with an expiry in sessino storage
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
sessionStorage.setItem(
SessionStorageKeys.CLI_TERMINAL_TOKEN,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 30)),
data: window.btoa(JSON.stringify(payload))
})
);
}); });
router.push("/cli-redirect");
await navigateUserToOrg(router, organizationId); return;
} }
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs // case: no organization ID is present -- navigate to the select org page IF the user has any orgs
// if the user has no orgs, navigate to the create org page // if the user has no orgs, navigate to the create org page
else { const userOrgs = await fetchOrganizations();
const userOrgs = await fetchOrganizations();
// case: user has orgs, so we navigate the user to select an org // case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) { if (userOrgs.length > 0) {
navigateToSelectOrganization(callbackPort); navigateToSelectOrganization(callbackPort);
} }
// case: no orgs found, so we navigate the user to create an org // case: no orgs found, so we navigate the user to create an org
else { else {
await navigateUserToOrg(router); await navigateUserToOrg(router);
}
} }
} }
} else { } else {
@@ -1,249 +0,0 @@
import { useForm } from "react-hook-form";
import {
faArrowLeft,
faCog,
faContactCard,
faMagnifyingGlass,
faMoneyBill,
faServer,
faSignIn,
faUser,
faUserCog,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useCreateOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import { TOrgRole } from "@app/hooks/api/roles/types";
import {
formRolePermission2API,
formSchema,
rolePermission2Form,
TFormSchema
} from "./OrgRoleModifySection.utils";
import { SimpleLevelPermissionOption } from "./SimpleLevelPermissionOptions";
import { WorkspacePermission } from "./WorkspacePermission";
type Props = {
role?: TOrgRole;
onGoBack: VoidFunction;
};
const SIMPLE_PERMISSION_OPTIONS = [
{
title: "User management",
subtitle: "Invite, view and remove users from the organization",
icon: faUser,
formName: "member"
},
{
title: "Group management",
subtitle: "Invite, view and remove user groups from the organization",
icon: faUsers,
formName: "groups"
},
{
title: "Machine identity management",
subtitle: "Create, view, update and remove (machine) identities from the organization",
icon: faServer,
formName: "identity"
},
{
title: "Billing & usage",
subtitle: "Modify organization subscription plan",
icon: faMoneyBill,
formName: "billing"
},
{
title: "Role management",
subtitle: "Create, modify and remove organization roles",
icon: faUserCog,
formName: "role"
},
{
title: "Incident Contacts",
subtitle: "Incident contacts management control",
icon: faContactCard,
formName: "incident-contact"
},
{
title: "Organization profile",
subtitle: "View & update organization metadata such as name",
icon: faCog,
formName: "settings"
},
{
title: "Secret Scanning",
subtitle: "Secret scanning management control",
icon: faMagnifyingGlass,
formName: "secret-scanning"
},
{
title: "SSO",
subtitle: "Define organization level SSO requirements",
icon: faSignIn,
formName: "sso"
},
{
title: "LDAP",
subtitle: "Define organization level LDAP requirements",
icon: faSignIn,
formName: "ldap"
},
{
title: "SCIM",
subtitle: "Define organization level SCIM requirements",
icon: faUsers,
formName: "scim"
}
] as const;
export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
const isNonEditable = ["owner", "admin", "member", "no-access"].includes(role?.slug || "");
const isNewRole = !role?.slug;
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const {
handleSubmit,
register,
formState: { isSubmitting, isDirty, errors },
setValue,
control
} = useForm<TFormSchema>({
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
resolver: zodResolver(formSchema)
});
const { mutateAsync: createRole } = useCreateOrgRole();
const { mutateAsync: updateRole } = useUpdateOrgRole();
const handleRoleUpdate = async (el: TFormSchema) => {
if (!role?.id) return;
try {
await updateRole({
orgId,
id: role?.id,
...el,
permissions: formRolePermission2API(el.permissions)
});
createNotification({ type: "success", text: "Successfully updated role" });
onGoBack();
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update role" });
}
};
const handleFormSubmit = async (el: TFormSchema) => {
if (!isNewRole) {
await handleRoleUpdate(el);
return;
}
try {
await createRole({
orgId,
...el,
permissions: formRolePermission2API(el.permissions)
});
createNotification({ type: "success", text: "Created new role" });
onGoBack();
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to create role" });
}
};
return (
<div>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="mb-2 flex items-center justify-between">
<h1 className="text-xl font-semibold text-mineshaft-100">
{isNewRole ? "New" : "Edit"} Role
</h1>
<Button
onClick={onGoBack}
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
>
Go back
</Button>
</div>
<p className="mb-8 text-gray-400">
Organization-level roles allow you to define permissions for resources at a high level
across the organization
</p>
<div className="flex flex-col space-y-6">
<FormControl
label="Name"
isRequired
className="mb-0"
isError={Boolean(errors?.name)}
errorText={errors?.name?.message}
>
<Input {...register("name")} placeholder="Billing Team" isReadOnly={isNonEditable} />
</FormControl>
<FormControl
label="Slug"
isRequired
isError={Boolean(errors?.slug)}
errorText={errors?.slug?.message}
>
<Input {...register("slug")} placeholder="biller" isReadOnly={isNonEditable} />
</FormControl>
<FormControl
label="Description"
helperText="A short description about this role"
isError={Boolean(errors?.description)}
errorText={errors?.description?.message}
>
<Input {...register("description")} isReadOnly={isNonEditable} />
</FormControl>
<div className="flex items-center justify-between border-t border-t-mineshaft-800 pt-6">
<div>
<h2 className="text-xl font-medium">Add Permission</h2>
</div>
</div>
<div className="">
<WorkspacePermission
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
/>
</div>
{SIMPLE_PERMISSION_OPTIONS.map(({ title, subtitle, icon, formName }) => (
<div key={`permission-${title}`}>
<SimpleLevelPermissionOption
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
icon={icon}
title={title}
subtitle={subtitle}
formName={formName}
/>
</div>
))}
</div>
<div className="mt-12 flex items-center space-x-4">
<Button
type="submit"
isDisabled={isSubmitting || isNonEditable || !isDirty}
isLoading={isSubmitting}
>
{isNewRole ? "Create Role" : "Save Role"}
</Button>
<Button onClick={onGoBack} variant="outline_bg">
Cancel
</Button>
</div>
</form>
</div>
);
};
@@ -1,204 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
title: string;
subtitle: string;
icon: IconProp;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "View" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Modify" },
{ action: "delete", label: "Remove" }
] as const;
const SECRET_SCANNING_PERMISSIONS = [
{ action: "read", label: "View risks" },
{ action: "create", label: "Add integrations" },
{ action: "edit", label: "Edit risk status" },
{ action: "delete", label: "Remove integrations" }
] as const;
const INCIDENT_CONTACTS_PERMISSIONS = [
{ action: "read", label: "View contacts" },
{ action: "create", label: "Add new contacts" },
{ action: "edit", label: "Edit contacts" },
{ action: "delete", label: "Remove contacts" }
] as const;
const MEMBERS_PERMISSIONS = [
{ action: "read", label: "View all members" },
{ action: "create", label: "Invite members" },
{ action: "edit", label: "Edit members" },
{ action: "delete", label: "Remove members" }
] as const;
const BILLING_PERMISSIONS = [
{ action: "read", label: "View bills" },
{ action: "create", label: "Add payment methods" },
{ action: "edit", label: "Edit payments" },
{ action: "delete", label: "Remove payments" }
] as const;
const getPermissionList = (option: Props["formName"]) => {
switch (option) {
case "secret-scanning":
return SECRET_SCANNING_PERMISSIONS;
case "billing":
return BILLING_PERMISSIONS;
case "incident-contact":
return INCIDENT_CONTACTS_PERMISSIONS;
case "member":
return MEMBERS_PERMISSIONS;
default:
return PERMISSIONS;
}
};
export const SimpleLevelPermissionOption = ({
isNonEditable,
setValue,
control,
formName,
subtitle,
title,
icon
}: Props) => {
const rule = useWatch({
control,
name: `permissions.${formName}`
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
`permissions.${formName}`,
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
`permissions.${formName}`,
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
`permissions.${formName}`,
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setValue(
`permissions.${formName}`,
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={icon} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">{title}</div>
<div className="text-xs font-light">{subtitle}</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
>
{isCustom &&
getPermissionList(formName).map(({ action, label }) => (
<Controller
name={`permissions.${formName}.${action}`}
key={`permissions.${formName}.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.${formName}.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -8,7 +8,7 @@ import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2"; import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils"; import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils";
type Props = { type Props = {
isNonEditable?: boolean; isNonEditable?: boolean;
@@ -1 +0,0 @@
export { OrgRoleModifySection } from "./OrgRoleModifySection";
@@ -1,27 +1,9 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { usePopUp } from "@app/hooks";
import { TOrgRole } from "@app/hooks/api/roles/types";
import { OrgRoleModifySection } from "./OrgRoleModifySection";
import { OrgRoleTable } from "./OrgRoleTable"; import { OrgRoleTable } from "./OrgRoleTable";
export const OrgRoleTabSection = () => { export const OrgRoleTabSection = () => {
const { popUp, handlePopUpClose } = usePopUp(["editRole"] as const); return (
return popUp.editRole.isOpen ? (
<motion.div
key="role-modify"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgRoleModifySection
role={popUp.editRole.data as TOrgRole}
onGoBack={() => handlePopUpClose("editRole")}
/>
</motion.div>
) : (
<motion.div <motion.div
key="role-list" key="role-list"
transition={{ duration: 0.1 }} transition={{ duration: 0.1 }}
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { TFormSchema } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils"; import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
const PERMISSIONS = [ const PERMISSIONS = [
{ action: "read", label: "View" }, { action: "read", label: "View" },
@@ -2,7 +2,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button , Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2"; import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api"; import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import { import {
@@ -10,7 +10,7 @@ import {
formSchema, formSchema,
rolePermission2Form, rolePermission2Form,
TFormSchema TFormSchema
} from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils"; } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
import { RolePermissionRow } from "./RolePermissionRow"; import { RolePermissionRow } from "./RolePermissionRow";