Compare commits

..

109 Commits

Author SHA1 Message Date
Daniel Hougaard
6e1dc7375c Update csharp.mdx 2024-07-30 22:24:43 +02:00
Daniel Hougaard
164627139e TLS docs 2024-07-30 22:24:23 +02:00
BlackMagiq
021dbf3558 Merge pull request #2200 from Infisical/secret-sharing-fix
Minor UI Improvements
2024-07-30 11:17:53 -07:00
Tuan Dang
7e9389cb26 Made with love 2024-07-30 10:32:58 -07:00
Tuan Dang
eda57881ec Minor UI adjustments 2024-07-30 10:31:30 -07:00
Maidul Islam
553d51e5b3 Merge pull request #2198 from Infisical/maidul-dwdqwdfwef
Lint fixes to unblock prod pipeline
2024-07-30 11:06:01 -04:00
Maidul Islam
16e0a441ae unblock prod pipeline 2024-07-30 11:00:27 -04:00
BlackMagiq
d6c0941fa9 Merge pull request #2190 from Infisical/secret-sharing-update
Secret Sharing Update
2024-07-30 07:27:56 -07:00
Tuan Dang
7cbd254f06 Add back hashed hex for secret sharing 2024-07-30 07:16:03 -07:00
Maidul Islam
4b83b92725 Merge pull request #2196 from Infisical/handbook-update
add envkey migration page
2024-07-30 08:54:40 -04:00
Maidul Islam
fe72f034c1 Update migrating-from-envkey.mdx 2024-07-30 08:54:22 -04:00
Vladyslav Matsiiako
6803553b21 add envkey migration page 2024-07-29 23:23:05 -07:00
Maidul Islam
1c8299054a Merge pull request #2192 from GLEF1X/perf/optimize-group-delete
perf(group-fns): optimize sequential delete to be concurrent
2024-07-29 22:13:00 -04:00
GLEF1X
98b6373d6a perf(group-fns): optimize sequential delete to be concurrent 2024-07-29 21:40:48 -04:00
Maidul Islam
1d97921c7c Merge pull request #2182 from LemmyMwaura/delete-secret-modal
feat: add confirm step (modal) before deleting a secret
2024-07-29 19:52:51 -04:00
Tuan Dang
0d4164ea81 Merge remote-tracking branch 'origin' into secret-sharing-update 2024-07-29 15:22:13 -07:00
Tuan Dang
79bd8613d3 Fix padding 2024-07-29 15:16:11 -07:00
Tuan Dang
8deea21a83 Bring back logo, promo text in secret sharing 2024-07-29 15:05:38 -07:00
Maidul Islam
3b3c2be933 Merge pull request #2186 from LemmyMwaura/persist-tab-state
feat: persist tab state on route change.
2024-07-29 17:35:07 -04:00
Tuan Dang
c041e44399 Continue secret sharing 2024-07-29 14:32:11 -07:00
Maidul Islam
c1aeb04174 Merge pull request #2188 from Infisical/vmatsiiako-changelog-patch-1
Update changelog
2024-07-29 17:26:28 -04:00
lemmyMwaura
3f3c0aab0f refactor: revert the org level enum to only types that existed before 2024-07-29 20:04:58 +03:00
lemmyMwaura
b740e8c900 Rename types to Types with correct case 2024-07-29 20:02:42 +03:00
lemmyMwaura
4416b11094 refactor: change folder name to uppercase for consistency 2024-07-29 19:48:49 +03:00
lemmyMwaura
d8169a866d refactor: update types import path 2024-07-29 19:41:02 +03:00
lemmyMwaura
7239158e7f refactor: localize tabs at both the org and project level 2024-07-29 19:37:19 +03:00
Vlad Matsiiako
fefe2d1de1 Update changelog 2024-07-28 10:53:44 -07:00
lemmyMwaura
3f3e41282d fix: remove unnecessary selectedTab div 2024-07-28 20:33:17 +03:00
Vlad Matsiiako
c14f94177a Merge pull request #2187 from Infisical/vmatsiiako-changelog-update-july2024
Update changelog
2024-07-28 10:14:59 -07:00
Vlad Matsiiako
ceb741955d Update changelog 2024-07-28 10:08:58 -07:00
lemmyMwaura
f5bc4e1b5f refactor: return value as Tabsection from isTabSection fn (avoids assertion at setState level) 2024-07-28 07:50:27 +03:00
lemmyMwaura
06900b9c99 refactor: create helper fn to check if string is in TabSections 2024-07-28 07:14:57 +03:00
lemmyMwaura
d71cb96adf fix(lint): resolve type error 2024-07-27 23:33:09 +03:00
lemmyMwaura
61ebec25b3 refactor: update envs to environments 2024-07-27 23:24:10 +03:00
lemmyMwaura
57320c51fb fix: add selectedtab when moving back from roles page 2024-07-27 23:10:12 +03:00
lemmyMwaura
4aa9cd0f72 feat: also persist the state on delete 2024-07-27 22:58:36 +03:00
lemmyMwaura
ea39ef9269 feat: persist state at the org level when tab switching 2024-07-27 22:45:53 +03:00
lemmyMwaura
15749a1f52 feat: update url onvalue change 2024-07-27 22:18:56 +03:00
lemmyMwaura
9e9aff129e feat: use shared enum for consistent values 2024-07-27 22:12:19 +03:00
lemmyMwaura
4ac487c974 feat: selectTab state from url 2024-07-27 22:04:43 +03:00
lemmyMwaura
2e50072caa feat: move shared enum to separate file 2024-07-27 22:04:11 +03:00
lemmyMwaura
2bd170df7d feat: add queryparam when switching tabs 2024-07-27 22:03:44 +03:00
BlackMagiq
938a7b7e72 Merge pull request #2185 from Infisical/secret-sharing
Secret Sharing UI/UX Adjustment
2024-07-27 10:09:03 -07:00
Tuan Dang
af864b456b Adjust secret sharing screen form padding 2024-07-27 07:32:56 -07:00
Tuan Dang
a30e3874cd Adjustments to secret sharing styling 2024-07-27 07:31:30 -07:00
lemmyMwaura
de886f8dd0 feat: make title dynamic when deleting folders and secrets 2024-07-27 12:27:06 +03:00
lemmyMwaura
b3db29ac37 refactor: update modal message to match other delete modals in the dashboard 2024-07-27 11:42:30 +03:00
lemmyMwaura
ce1db38afd refactor: re-use existing modal for deletion 2024-07-26 22:05:44 +03:00
BlackMagiq
0fa6b7a08a Merge pull request #2183 from Infisical/project-role-concept
Project Role Page
2024-07-26 11:27:25 -07:00
Tuan Dang
29c5bf5491 Remove top margin from RolePermissionSecretsRow 2024-07-26 11:22:15 -07:00
Tuan Dang
4d711ae149 Finish project role page 2024-07-26 11:00:47 -07:00
lemmyMwaura
9dd675ff98 refactor: move delete statement into body tag 2024-07-26 19:56:31 +03:00
lemmyMwaura
8fd3e50d04 feat: implement delete secret via modal logic 2024-07-26 19:48:30 +03:00
lemmyMwaura
391ed0723e feat: add delete secret modal 2024-07-26 19:47:35 +03:00
Tuan Dang
84af8e708e Merge remote-tracking branch 'origin' into project-role-concept 2024-07-26 07:28:17 -07:00
BlackMagiq
b39b5bd1a1 Merge pull request #2181 from Infisical/patch-org-role-update
Fix updating org role details should not send empty array of permissions
2024-07-26 07:27:51 -07:00
Tuan Dang
b3d9d91b52 Fix updating org role details should not send empty array of permissions 2024-07-26 06:52:21 -07:00
Tuan Dang
5ad4061881 Continue project role page 2024-07-26 06:43:09 -07:00
Maidul Islam
f29862eaf2 Merge pull request #2180 from Infisical/list-ca-endpoint-descriptions
Add descriptions for parameters for LIST (GET) CAs / certificates endpoints
2024-07-25 17:59:57 -04:00
Tuan Dang
7cb174b644 Add descriptions for list cas/certs endpoints 2024-07-25 14:53:41 -07:00
Tuan Dang
bf00d16c80 Continue progress on project role page 2024-07-25 14:45:02 -07:00
BlackMagiq
e30a0fe8be Merge pull request #2178 from Infisical/cert-search-filtering
Add List CAs / Certificates to Documentation + Filter Options
2024-07-25 09:40:44 -07:00
Tuan Dang
6e6f0252ae Adjust default offsets for cas/certs query 2024-07-25 08:09:21 -07:00
Tuan Dang
2348df7a4d Add list cert, ca + logical filters to docs 2024-07-25 08:06:18 -07:00
Maidul Islam
962cf67dfb Merge pull request #2173 from felixtrav/patch-1
Update envars.mdx - Added PORT
2024-07-25 10:21:06 -04:00
BlackMagiq
32627c20c4 Merge pull request #2176 from Infisical/org-role-cleanup
Cleanup frontend unused org role logic (moved)
2024-07-25 07:17:56 -07:00
Maidul Islam
c50f8fd78c Merge pull request #2175 from akhilmhdh/feat/cli-login-fallback-missing
Missing paste token option in CLI brower login flow
2024-07-25 10:08:57 -04:00
Tuan Dang
1cb4dc9e84 Start project role concept 2024-07-25 06:47:18 -07:00
Tuan Dang
977ce09245 Cleanup frontend unused org role logic (moved) 2024-07-25 05:43:57 -07:00
=
08d7dead8c fix(cli): resolved not printing the url on api override 2024-07-25 15:28:54 +05:30
=
a30e06e392 feat: added back missing token paste option in cli login from browser 2024-07-25 15:28:29 +05:30
Maidul Islam
23f3f09cb6 temporarily remove linux deployment 2024-07-24 23:42:36 -04:00
Aeto
5cd0f665fa Update envars.mdx - Added PORT
Added the PORT configuration option to the documentation which controls the port the application listens on.
2024-07-24 19:17:33 -04:00
Daniel Hougaard
443e76c1df Merge pull request #2171 from Infisical/daniel/aarch64-binary-fix
fix(binary): aarch64 binary native bindings fix
2024-07-24 16:33:15 +02:00
Daniel Hougaard
4ea22b6761 Updated ubuntu version 2024-07-24 14:17:19 +00:00
Maidul Islam
ae7e0d0963 Merge pull request #2168 from Infisical/misc/added-email-self-host-conditionals
misc: added checks for formatting email templates for self-hosted or cloud
2024-07-24 09:22:49 -04:00
Daniel Hougaard
ed6c6d54c0 Update build-binaries.yml 2024-07-24 11:16:58 +02:00
Daniel Hougaard
428ff5186f Removed compression for testing 2024-07-24 10:47:20 +02:00
Daniel Hougaard
d07b0d20d6 Update build-binaries.yml 2024-07-24 10:46:55 +02:00
Sheen Capadngan
8e373fe9bf misc: added email formatting for remaining templates 2024-07-24 16:33:41 +08:00
Sheen Capadngan
28087cdcc4 misc: added email self-host conditionals 2024-07-24 00:55:02 +08:00
Vlad Matsiiako
dcef49950d Merge pull request #2167 from Infisical/daniel/ruby-docs
feat(docs): Ruby sdk
2024-07-23 08:36:32 -07:00
Daniel Hougaard
1e5d567ef7 Update ruby.mdx 2024-07-23 15:30:13 +02:00
Daniel Hougaard
d09c320150 fix: bad documentation link 2024-07-23 15:27:23 +02:00
Daniel Hougaard
229599b8de docs: ruby sdk documentation 2024-07-23 15:27:11 +02:00
Sheen Capadngan
02eea4d886 Merge pull request #2166 from Infisical/misc/updated-cf-worker-integration-doc
misc: updated cf worker integration doc
2024-07-23 21:16:56 +08:00
Sheen Capadngan
d12144a7e7 misc: added highligting 2024-07-23 21:03:46 +08:00
Sheen Capadngan
5fa69235d1 misc: updated cf worker integration doc 2024-07-23 20:40:07 +08:00
Daniel Hougaard
7dd9337b1c Merge pull request #2165 from Infisical/daniel/deployment-doc-fix
chore(docs): typo in url
2024-07-23 10:19:59 +02:00
Daniel Hougaard
f9eaee4dbc Merge pull request #2164 from Infisical/daniel/deployment-doc-fix
chore(docs): remove redundant doc
2024-07-23 09:55:10 +02:00
Daniel Hougaard
cd3a64f3e7 Update standalone-binary.mdx 2024-07-23 09:52:42 +02:00
Daniel Hougaard
121254f98d Update standalone-binary.mdx 2024-07-23 09:52:14 +02:00
Sheen Capadngan
1591c1dbac Merge pull request #2163 from Infisical/misc/add-endpoint-for-terraform-environment
misc: add endpoint for environment terraform resource
2024-07-23 15:39:20 +08:00
BlackMagiq
da43f405c4 Merge pull request #2162 from Infisical/role-concept
Organization Role Page
2024-07-23 12:26:43 +07:00
Tuan Dang
d5c0abbc3b Opt for bulk save role permissions instead of save on each form change 2024-07-23 11:58:55 +07:00
Daniel Hougaard
7a642e7634 Merge pull request #2161 from Infisical/daniel/cli-security-warning
fix(cli): dependency security warning
2024-07-22 21:36:48 +02:00
Tuan Dang
b359f4278e Fix type issues 2024-07-22 19:15:18 +07:00
Tuan Dang
29d76c1deb Adjust OrgRoleTable 2024-07-22 19:02:03 +07:00
Tuan Dang
6ba1012f5b Add default role support for RolePage 2024-07-22 18:55:34 +07:00
Daniel Hougaard
4abb3ef348 Fix 2024-07-22 13:42:59 +02:00
Daniel Hougaard
73e764474d Update go.sum 2024-07-22 13:42:27 +02:00
Daniel Hougaard
7eb5689b4c Update go.mod 2024-07-22 13:42:24 +02:00
Tuan Dang
b64d4e57c4 Clean org roles concept refactor 2024-07-22 16:56:27 +07:00
Tuan Dang
bd860e6c5a Continue progress on role ui update 2024-07-22 12:41:26 +07:00
Tuan Dang
3731459e99 Make progress on org role detail modal 2024-07-19 16:53:04 +07:00
Tuan Dang
dc055c11ab Merge remote-tracking branch 'origin' into role-concept 2024-07-19 15:27:25 +07:00
Tuan Dang
22878a035b Continue RolePermissionsTable 2024-07-19 15:24:21 +07:00
Tuan Dang
7127f6d1e1 Begin role page 2024-07-18 20:12:52 +07:00
Tuan Dang
ce26a06129 role table restyle 2024-07-18 17:12:02 +07:00
127 changed files with 4050 additions and 2757 deletions

View File

@@ -14,7 +14,6 @@ defaults:
jobs:
build-and-deploy:
runs-on: ubuntu-20.04
strategy:
matrix:
arch: [x64, arm64]
@@ -24,6 +23,7 @@ jobs:
target: node20-linux
- os: win
target: node20-win
runs-on: ${{ (matrix.arch == 'arm64' && matrix.os == 'linux') && 'ubuntu24-arm64' || 'ubuntu-latest' }}
steps:
- name: Checkout code
@@ -49,9 +49,9 @@ jobs:
- name: Package into node binary
run: |
if [ "${{ matrix.os }}" != "linux" ]; then
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
else
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
fi
# Set up .deb package structure (Debian/Ubuntu only)
@@ -84,7 +84,12 @@ jobs:
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
- uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli
with:
python-version: "3.x" # Specify the Python version you need
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade cloudsmith-cli
# Publish .deb file to Cloudsmith (Debian/Ubuntu only)
- name: Publish to Cloudsmith (Debian/Ubuntu)

View File

@@ -0,0 +1,39 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name");
if (!doesNameExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.string("name").nullable();
});
}
const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt");
if (!doesLastViewedAtExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.timestamp("lastViewedAt").nullable();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name");
if (doesNameExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.dropColumn("name");
});
}
const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt");
if (doesLastViewedAtExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.dropColumn("lastViewedAt");
});
}
}
}

View File

@@ -5,8 +5,6 @@
import { z } from "zod";
import { EnforcementLevel } from "@app/lib/types";
import { TImmutableDBKeys } from "./models";
export const AccessApprovalPoliciesSchema = z.object({
@@ -17,7 +15,7 @@ export const AccessApprovalPoliciesSchema = z.object({
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
enforcementLevel: z.string().default("hard")
});
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;

View File

@@ -13,9 +13,9 @@ export const KmsKeysSchema = z.object({
isDisabled: z.boolean().default(false).nullable().optional(),
isReserved: z.boolean().default(true).nullable().optional(),
orgId: z.string().uuid(),
slug: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
slug: z.string()
updatedAt: z.date()
});
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;

View File

@@ -18,7 +18,7 @@ export const OrgMembershipsSchema = z.object({
orgId: z.string().uuid(),
roleId: z.string().uuid().nullable().optional(),
projectFavorites: z.string().array().nullable().optional(),
isActive: z.boolean()
isActive: z.boolean().default(true)
});
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;

View File

@@ -15,12 +15,12 @@ export const SecretApprovalRequestsSchema = z.object({
conflicts: z.unknown().nullable().optional(),
slug: z.string(),
folderId: z.string().uuid(),
bypassReason: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
isReplicated: z.boolean().nullable().optional(),
committerUserId: z.string().uuid(),
statusChangedByUserId: z.string().uuid().nullable().optional()
statusChangedByUserId: z.string().uuid().nullable().optional(),
bypassReason: z.string().nullable().optional()
});
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;

View File

@@ -5,8 +5,6 @@
import { z } from "zod";
import { SecretSharingAccessType } from "@app/lib/types";
import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
@@ -18,10 +16,12 @@ export const SecretSharingSchema = z.object({
expiresAt: z.date(),
userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid().nullable().optional(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization),
createdAt: z.date(),
updatedAt: z.date(),
expiresAfterViews: z.number().nullable().optional()
expiresAfterViews: z.number().nullable().optional(),
accessType: z.string().default("anyone"),
name: z.string().nullable().optional(),
lastViewedAt: z.date().nullable().optional()
});
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;

View File

@@ -52,6 +52,36 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/:organizationId/roles/:roleId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
organizationId: z.string().trim(),
roleId: z.string().trim()
}),
response: {
200: z.object({
role: OrgRolesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const role = await server.services.orgRole.getRole(
req.permission.id,
req.params.organizationId,
req.params.roleId,
req.permission.authMethod,
req.permission.orgId
);
return { role };
}
});
server.route({
method: "PATCH",
url: "/:organizationId/roles/:roleId",
@@ -69,7 +99,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
.trim()
.optional()
.refine(
(val) => typeof val === "undefined" || Object.keys(OrgMembershipRole).includes(val),
(val) => typeof val !== "undefined" && !Object.keys(OrgMembershipRole).includes(val),
"Please choose a different slug, the slug you have entered is reserved."
)
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
@@ -77,7 +107,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
permissions: z.any().array()
permissions: z.any().array().optional()
}),
response: {
200: z.object({

View File

@@ -101,7 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid"
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions)
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}),
response: {
200: z.object({
@@ -120,7 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: req.params.roleId,
data: {
...req.body,
permissions: JSON.stringify(packRules(req.body.permissions))
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
}
});
return { role };

View File

@@ -336,31 +336,36 @@ export const removeUsersFromGroupByUserIds = async ({
)
);
// TODO: this part can be optimized
for await (const userId of userIds) {
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
const promises: Array<Promise<void>> = [];
for (const userId of userIds) {
promises.push(
(async () => {
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
if (projectsToDeleteKeyFor.length) {
await projectKeyDAL.delete(
{
receiverId: userId,
$in: {
projectId: projectsToDeleteKeyFor
}
},
tx
);
}
if (projectsToDeleteKeyFor.length) {
await projectKeyDAL.delete(
{
receiverId: userId,
$in: {
projectId: projectsToDeleteKeyFor
}
},
tx
);
}
await userGroupMembershipDAL.delete(
{
groupId: group.id,
userId
},
tx
await userGroupMembershipDAL.delete(
{
groupId: group.id,
userId
},
tx
);
})()
);
}
await Promise.all(promises);
}
if (membersToRemoveFromGroupPending.length) {

View File

@@ -425,6 +425,21 @@ export const PROJECTS = {
},
LIST_INTEGRATION_AUTHORIZATION: {
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;

View File

@@ -19,21 +19,31 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
rateLimit: readLimit
},
schema: {
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0),
limit: z.coerce.number().min(1).max(100).default(25)
}),
response: {
200: z.array(SecretSharingSchema)
200: z.object({
secrets: z.array(SecretSharingSchema),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const sharedSecrets = await req.server.services.secretSharing.getSharedSecrets({
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
actorOrgId: req.permission.orgId,
...req.query
});
return sharedSecrets;
return {
secrets,
totalCount
};
}
});
@@ -48,7 +58,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
id: z.string().uuid()
}),
querystring: z.object({
hashedHex: z.string()
hashedHex: z.string().min(1)
}),
response: {
200: SecretSharingSchema.pick({
@@ -64,11 +74,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}
},
handler: async (req) => {
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
req.params.id,
req.query.hashedHex,
req.permission?.orgId
);
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
sharedSecretId: req.params.id,
hashedHex: req.query.hashedHex,
orgId: req.permission?.orgId
});
if (!sharedSecret) return undefined;
return {
encryptedValue: sharedSecret.encryptedValue,
@@ -91,11 +101,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: {
body: z.object({
encryptedValue: z.string(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.string(),
expiresAfterViews: z.number()
expiresAfterViews: z.number().min(1).optional()
}),
response: {
200: z.object({
@@ -104,14 +114,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}
},
handler: async (req) => {
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
encryptedValue,
iv,
tag,
hashedHex,
expiresAt: new Date(expiresAt),
expiresAfterViews,
...req.body,
accessType: SecretSharingAccessType.Anyone
});
return { id: sharedSecret.id };
@@ -126,12 +130,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
},
schema: {
body: z.object({
name: z.string().max(50).optional(),
encryptedValue: z.string(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.string(),
expiresAfterViews: z.number(),
expiresAfterViews: z.number().min(1).optional(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
}),
response: {
@@ -142,20 +147,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
encryptedValue,
iv,
tag,
hashedHex,
expiresAt: new Date(expiresAt),
expiresAfterViews,
accessType: req.body.accessType
...req.body
});
return { id: sharedSecret.id };
}

View File

@@ -317,10 +317,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema.describe("The slug of the project to list CAs.")
slug: slugSchema.describe(PROJECTS.LIST_CAS.slug)
}),
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: {
200: z.object({
@@ -336,11 +340,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId,
type: ProjectFilterType.SLUG
},
status: req.query.status,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
actor: req.permission.type,
...req.query
});
return { cas };
}
@@ -354,11 +358,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema.describe("The slug of the project to list certificates.")
slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug)
}),
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0),
limit: z.coerce.number().min(1).max(100).default(25)
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
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: {
200: z.object({

View File

@@ -8,19 +8,35 @@ export type TCertificateDALFactory = ReturnType<typeof certificateDALFactory>;
export const certificateDALFactory = (db: TDbClient) => {
const certificateOrm = ormify(db, TableName.Certificate);
const countCertificatesInProject = async (projectId: string) => {
const countCertificatesInProject = async ({
projectId,
friendlyName,
commonName
}: {
projectId: string;
friendlyName?: string;
commonName?: string;
}) => {
try {
interface CountResult {
count: string;
}
const count = await db
let query = db
.replicaNode()(TableName.Certificate)
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.id`, projectId)
.count("*")
.first();
.where(`${TableName.Project}.id`, projectId);
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);
} catch (error) {

View File

@@ -42,6 +42,61 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
return role;
};
const getRole = async (
userId: string,
orgId: string,
roleId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
switch (roleId) {
case "b11b49a9-09a9-4443-916a-4246f9ff2c69": {
return {
id: roleId,
orgId,
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
};
}
case "b11b49a9-09a9-4443-916a-4246f9ff2c70": {
return {
id: roleId,
orgId,
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
};
}
case "b10d49a9-09a9-4443-916a-4246f9ff2c72": {
return {
id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
orgId,
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: packRules(orgNoAccessPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
};
}
default: {
const role = await orgRoleDAL.findOne({ id: roleId, orgId });
if (!role) throw new BadRequestError({ message: "Role not found", name: "Get role" });
return role;
}
}
};
const updateRole = async (
userId: string,
orgId: string,
@@ -144,5 +199,5 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
return { permissions: packRules(permission.rules), membership };
};
return { createRole, updateRole, deleteRole, listRoles, getUserPermission };
return { createRole, getRole, updateRole, deleteRole, listRoles, getUserPermission };
};

View File

@@ -162,12 +162,19 @@ export const projectRoleServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
if (data?.slug) {
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId });
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
}
const [updatedRole] = await projectRoleDAL.update({ id: roleId, projectId }, data);
const [updatedRole] = await projectRoleDAL.update(
{ id: roleId, projectId },
{
...data,
permissions: data.permissions ? data.permissions : undefined
}
);
if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) };
};

View File

@@ -575,6 +575,10 @@ export const projectServiceFactory = ({
*/
const listProjectCas = async ({
status,
friendlyName,
commonName,
limit = 25,
offset = 0,
actorId,
actorOrgId,
actorAuthMethod,
@@ -596,10 +600,15 @@ export const projectServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
const cas = await certificateAuthorityDAL.find({
projectId: project.id,
...(status && { status })
});
const cas = await certificateAuthorityDAL.find(
{
projectId: project.id,
...(status && { status }),
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
return cas;
};
@@ -608,8 +617,10 @@ export const projectServiceFactory = ({
* Return list of certificates for project
*/
const listProjectCertificates = async ({
offset,
limit,
limit = 25,
offset = 0,
friendlyName,
commonName,
actorId,
actorOrgId,
actorAuthMethod,
@@ -634,12 +645,18 @@ export const projectServiceFactory = ({
{
$in: {
caId: cas.map((ca) => ca.id)
}
},
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
const count = await certificateDAL.countCertificatesInProject(project.id);
const count = await certificateDAL.countCertificatesInProject({
projectId: project.id,
friendlyName,
commonName
});
return {
certificates,

View File

@@ -89,6 +89,10 @@ export type AddUserToWsDTO = {
export type TListProjectCasDTO = {
status?: CaStatus;
friendlyName?: string;
offset?: number;
limit?: number;
commonName?: string;
filter: Filter;
} & Omit<TProjectPermission, "projectId">;
@@ -96,4 +100,6 @@ export type TListProjectCertsDTO = {
filter: Filter;
offset: number;
limit: number;
friendlyName?: string;
commonName?: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -10,6 +10,25 @@ export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory
export const secretSharingDALFactory = (db: TDbClient) => {
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => {
try {
interface CountResult {
count: string;
}
const count = await db
.replicaNode()(TableName.SecretSharing)
.where(`${TableName.SecretSharing}.orgId`, orgId)
.where(`${TableName.SecretSharing}.userId`, userId)
.count("*")
.first();
return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count all user-org shared secrets" });
}
};
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
try {
const today = new Date();
@@ -19,8 +38,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
.update({
encryptedValue: "",
tag: "",
iv: "",
hashedHex: ""
iv: ""
});
return docs;
} catch (error) {
@@ -50,8 +68,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
await sharedSecretOrm.updateById(id, {
encryptedValue: "",
iv: "",
tag: "",
hashedHex: ""
tag: ""
});
} catch (error) {
throw new DatabaseError({
@@ -63,6 +80,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
return {
...sharedSecretOrm,
countAllUserOrgSharedSecrets,
pruneExpiredSharedSecrets,
softDeleteById,
findActiveSharedSecrets

View File

@@ -1,5 +1,5 @@
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
import { TOrgDALFactory } from "../org/org-dal";
@@ -8,7 +8,8 @@ import {
TCreatePublicSharedSecretDTO,
TCreateSharedSecretDTO,
TDeleteSharedSecretDTO,
TSharedSecretPermission
TGetActiveSharedSecretByIdDTO,
TGetSharedSecretsDTO
} from "./secret-sharing-types";
type TSecretSharingServiceFactoryDep = {
@@ -24,21 +25,21 @@ export const secretSharingServiceFactory = ({
secretSharingDAL,
orgDAL
}: TSecretSharingServiceFactoryDep) => {
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
const {
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId,
encryptedValue,
iv,
tag,
accessType,
hashedHex,
expiresAt,
expiresAfterViews
} = createSharedSecretInput;
const createSharedSecret = async ({
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId,
encryptedValue,
hashedHex,
iv,
tag,
name,
accessType,
expiresAt,
expiresAfterViews
}: TCreateSharedSecretDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
@@ -60,21 +61,30 @@ export const secretSharingServiceFactory = ({
}
const newSharedSecret = await secretSharingDAL.create({
name,
encryptedValue,
hashedHex,
iv,
tag,
hashedHex,
expiresAt,
expiresAt: new Date(expiresAt),
expiresAfterViews,
userId: actorId,
orgId,
accessType
});
return { id: newSharedSecret.id };
};
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput;
const createPublicSharedSecret = async ({
encryptedValue,
hashedHex,
iv,
tag,
expiresAt,
expiresAfterViews,
accessType
}: TCreatePublicSharedSecretDTO) => {
if (new Date(expiresAt) < new Date()) {
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
}
@@ -94,53 +104,103 @@ export const secretSharingServiceFactory = ({
const newSharedSecret = await secretSharingDAL.create({
encryptedValue,
hashedHex,
iv,
tag,
hashedHex,
expiresAt,
expiresAt: new Date(expiresAt),
expiresAfterViews,
accessType
});
return { id: newSharedSecret.id };
};
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const getSharedSecrets = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
offset,
limit
}: TGetSharedSecretsDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
const userSharedSecrets = await secretSharingDAL.findActiveSharedSecrets({ userId: actorId, orgId });
return userSharedSecrets;
const secrets = await secretSharingDAL.find(
{
userId: actorId,
orgId: actorOrgId
},
{ offset, limit, sort: [["createdAt", "desc"]] }
);
const count = await secretSharingDAL.countAllUserOrgSharedSecrets({
orgId: actorOrgId,
userId: actorId
});
return {
secrets,
totalCount: count
};
};
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string, orgId?: string) => {
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
if (!sharedSecret) return;
const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
const sharedSecret = await secretSharingDAL.findOne({
id: sharedSecretId,
hashedHex
});
if (!sharedSecret)
throw new NotFoundError({
message: "Shared secret not found"
});
const { accessType, expiresAt, expiresAfterViews } = sharedSecret;
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
// Support organization level access for secret sharing
if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) {
return {
...sharedSecret,
encryptedValue: "",
iv: "",
tag: "",
orgName
};
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
throw new UnauthorizedError();
if (expiresAt !== null && expiresAt < new Date()) {
// check lifetime expiry
await secretSharingDAL.softDeleteById(sharedSecretId);
throw new ForbiddenRequestError({
message: "Access denied: Secret has expired by lifetime"
});
}
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
return;
if (expiresAfterViews !== null && expiresAfterViews === 0) {
// check view count expiry
await secretSharingDAL.softDeleteById(sharedSecretId);
throw new ForbiddenRequestError({
message: "Access denied: Secret has expired by view count"
});
}
if (sharedSecret.expiresAfterViews != null && sharedSecret.expiresAfterViews >= 0) {
if (sharedSecret.expiresAfterViews === 0) {
await secretSharingDAL.softDeleteById(sharedSecretId);
return;
}
if (expiresAfterViews) {
// decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
}
if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId) {
return { ...sharedSecret, orgName };
}
return { ...sharedSecret, orgName: undefined };
await secretSharingDAL.updateById(sharedSecretId, {
lastViewedAt: new Date()
});
return {
...sharedSecret,
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
: undefined
};
};
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {
@@ -156,6 +216,6 @@ export const secretSharingServiceFactory = ({
createPublicSharedSecret,
getSharedSecrets,
deleteSharedSecretById,
getActiveSharedSecretByIdAndHashedHex
getActiveSharedSecretById
};
};

View File

@@ -1,7 +1,12 @@
import { SecretSharingAccessType } from "@app/lib/types";
import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export type TGetSharedSecretsDTO = {
offset: number;
limit: number;
} & TGenericPermission;
export type TSharedSecretPermission = {
actor: ActorType;
actorId: string;
@@ -9,18 +14,25 @@ export type TSharedSecretPermission = {
actorOrgId: string;
orgId: string;
accessType?: SecretSharingAccessType;
name?: string;
};
export type TCreatePublicSharedSecretDTO = {
encryptedValue: string;
hashedHex: string;
iv: string;
tag: string;
hashedHex: string;
expiresAt: Date;
expiresAfterViews: number;
expiresAt: string;
expiresAfterViews?: number;
accessType: SecretSharingAccessType;
};
export type TGetActiveSharedSecretByIdDTO = {
sharedSecretId: string;
hashedHex: string;
orgId?: string;
};
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
export type TDeleteSharedSecretDTO = {

View File

@@ -5,6 +5,7 @@ import handlebars from "handlebars";
import { createTransport } from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
export type TSmtpConfig = SMTPTransport.Options;
@@ -12,7 +13,7 @@ export type TSmtpSendMail = {
template: SmtpTemplates;
subjectLine: string;
recipients: string[];
substitutions: unknown;
substitutions: object;
};
export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
@@ -47,9 +48,11 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
const isSmtpOn = Boolean(cfg.host);
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
const appCfg = getConfig();
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions);
const htmlToSend = temp({ isCloud: appCfg.isCloud, siteUrl: appCfg.SITE_URL, ...substitutions });
if (isSmtpOn) {
await smtp.sendMail({
from: cfg.from,

View File

@@ -1,19 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>MFA Code</title>
</head>
</head>
<body>
<body>
<h2>Infisical</h2>
<h2>Sign in attempt requires further verification</h2>
<p>Your MFA code is below — enter it where you started signing in to Infisical.</p>
<h2>{{code}}</h2>
<p>The MFA code will be valid for 2 minutes.</p>
<p>Not you? Contact Infisical or your administrator immediately.</p>
</body>
<p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
</body>
</html>

View File

@@ -9,12 +9,12 @@
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo</h3>
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your <a
href="https://app.infisical.com/">Infisical
href="{{siteUrl}}">Infisical
dashboard</a>.</p>
</body>

View File

@@ -1,19 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Successful login for {{email}} from new device</title>
</head>
</head>
<body>
<body>
<h2>Infisical</h2>
<p>We're verifying a recent login for {{email}}:</p>
<p><strong>Timestamp</strong>: {{timestamp}}</p>
<p><strong>IP address</strong>: {{ip}}</p>
<p><strong>User agent</strong>: {{userAgent}}</p>
<p>If you believe that this login is suspicious, please contact Infisical or reset your password immediately.</p>
</body>
<p>If you believe that this login is suspicious, please contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} or reset your password immediately.</p>
</body>
</html>

View File

@@ -9,6 +9,6 @@
<h2>Reset your password</h2>
<p>Someone requested a password reset.</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
<p>If you didn't initiate this request, please contact us immediately at team@infisical.com</p>
<p>If you didn't initiate this request, please contact {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
</body>
</html>

View File

@@ -9,7 +9,7 @@
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
by {{pusher_name}} ({{pusher_email}}). If
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
@@ -18,7 +18,7 @@
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your <a
href="https://app.infisical.com/">Infisical
href="{{siteUrl}}">Infisical
dashboard</a>.</p>
</body>

View File

@@ -11,7 +11,7 @@
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
<p>Questions about setting up Infisical? {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.</p>
</body>
</html>

View File

@@ -11,7 +11,7 @@ require (
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.3.0
github.com/infisical/go-sdk v0.3.3
github.com/mattn/go-isatty v0.0.18
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
github.com/muesli/mango-cobra v1.2.0
@@ -24,16 +24,16 @@ require (
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.23.0
golang.org/x/term v0.20.0
golang.org/x/crypto v0.25.0
golang.org/x/term v0.22.0
gopkg.in/yaml.v2 v2.4.0
)
require (
cloud.google.com/go/auth v0.5.1 // indirect
cloud.google.com/go/auth v0.7.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.3.0 // indirect
cloud.google.com/go/iam v1.1.8 // indirect
cloud.google.com/go/compute/metadata v0.4.0 // indirect
cloud.google.com/go/iam v1.1.11 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/aws/aws-sdk-go-v2 v1.27.2 // indirect
@@ -64,7 +64,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
@@ -91,17 +91,17 @@ require (
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.183.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
google.golang.org/api v0.188.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
google.golang.org/grpc v1.64.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -18,8 +18,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw=
cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts=
cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
@@ -28,13 +28,13 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c=
cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw=
cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -233,8 +233,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@@ -265,8 +265,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/infisical/go-sdk v0.3.0 h1:Ls71t227F4CWVQWdStcwv8WDyfHe8eRlyAuMRNHsmlQ=
github.com/infisical/go-sdk v0.3.0/go.mod h1:vHTDVw3k+wfStXab513TGk1n53kaKF2xgLqpw/xvtl4=
github.com/infisical/go-sdk v0.3.3 h1:TE2WNMmiDej+TCkPKgHk3h8zVlEQUtM5rz8ouVnXTcU=
github.com/infisical/go-sdk v0.3.3/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -453,8 +453,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -534,8 +535,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -627,8 +629,9 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -641,8 +644,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -728,8 +732,8 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE=
google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ=
google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -778,10 +782,10 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0=
google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -802,8 +806,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -816,8 +820,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -550,7 +550,7 @@ func (tm *AgentManager) FetchAzureAuthAccessToken() (credential infisicalSdk.Mac
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err)
}
return tm.infisicalClient.Auth().AzureAuthLogin(identityId)
return tm.infisicalClient.Auth().AzureAuthLogin(identityId, "")
}

View File

@@ -24,7 +24,6 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/srp"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/chzyer/readline"
"github.com/fatih/color"
"github.com/go-resty/resty/v2"
"github.com/manifoldco/promptui"
@@ -84,7 +83,7 @@ func handleAzureAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.Infis
return infisicalSdk.MachineIdentityCredential{}, err
}
return infisicalClient.Auth().AzureAuthLogin(identityId)
return infisicalClient.Auth().AzureAuthLogin(identityId, "")
}
func handleGcpIdTokenAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
@@ -205,6 +204,7 @@ var loginCmd = &cobra.Command{
if !overrideDomain {
domainQuery = false
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
}
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)
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.")
@@ -807,26 +807,22 @@ func browserCliLogin() (models.UserCredentials, error) {
log.Debug().Msgf("Callback server listening on port %d", callbackPort)
stdin := readline.NewCancelableStdin(os.Stdin)
go http.Serve(listener, corsHandler)
go askToPasteJwtToken(stdin, success, failure)
go askToPasteJwtToken(success, failure)
for {
select {
case loginResponse := <-success:
_ = closeListener(&listener)
_ = stdin.Close()
fmt.Println("Browser login successful")
return loginResponse, nil
case err := <-failure:
serverErr := closeListener(&listener)
stdErr := stdin.Close()
return models.UserCredentials{}, errors.Join(err, serverErr, stdErr)
return models.UserCredentials{}, errors.Join(err, serverErr)
case <-timeout:
_ = closeListener(&listener)
_ = stdin.Close()
return models.UserCredentials{}, errors.New("server timeout")
}
}

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/workspace/{slug}/cas"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/workspace/{slug}/certificates"
---

View File

@@ -4,6 +4,30 @@ title: "Changelog"
The changelog below reflects new product developments and updates on a monthly basis.
## July 2024
- Released the official [Ruby SDK](https://infisical.com/docs/sdks/languages/ruby).
- Increased the speed and efficiency of secret operations.
- Released AWS KMS wrapping (bring your own key).
- Users can now log in to CLI via SSO in non-browser environments.
- Released [Slack Webhooks](https://infisical.com/docs/documentation/platform/webhooks).
- Added [Dynamic Secrets with MS SQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mssql).
- Redesigned and simplified the Machine Identities page.
- Added the ability to move secrets/folders to another location.
- Added [OIDC](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) support to CLI, Go SDK, and more.
- Released [Linux installer for Infisical](https://infisical.com/docs/self-hosting/deployment-options/native/standalone-binary).
## June 2024
- Released [Infisical PKI](https://infisical.com/docs/documentation/platform/pki/overview).
- Released the official [Go SDK](https://infisical.com/docs/sdks/languages/go).
- Released [OIDC Authentication method](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general).
- Allowed users to configure log retention periods on self-hosted instances.
- Added [tags](https://registry.terraform.io/providers/Infisical/infisical/latest/docs/resources/secret_tag) to terraform provider.
- Released [public secret sharing](https://share.infisical.com).
- Built a [native integration with Rundeck](https://infisical.com/docs/integrations/cicd/rundeck).
- Added list view for projects in the dashboard.
- Fixed offline coding mode in CLI.
- Users are now able to leave a particular project themselves.
## May 2024
- Released [AWS](https://infisical.com/docs/documentation/platform/identities/aws-auth), [GCP](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure](https://infisical.com/docs/documentation/platform/identities/azure-auth), and [Kubernetes](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth) Native Auth Methods.
- Added [Secret Sharing](https://infisical.com/docs/documentation/platform/secret-sharing) functionality for sharing sensitive data through encrypted links within and outside of an organization.

View File

@@ -0,0 +1,23 @@
---
title: "Migrating from EnvKey to Infisical"
sidebarTitle: "Migration"
description: "Learn how to migrate from EnvKey to Infisical in the easiest way possible."
---
## What is Infisical?
[Infisical](https://infisical.com) is an open-source all-in-one secret management platform that helps developers manage secrets (e.g., API-keys, DB access tokens, [certificates](https://infisical.com/docs/documentation/platform/pki/overview)) across their infrastructure. In addition, Infisical provides [secret sharing](https://infisical.com/docs/documentation/platform/secret-sharing) functionality, ability to [prevent secret leaks](https://infisical.com/docs/cli/scanning-overview), and more.
Infisical is used by 10,000+ organizations across all indsutries including First American Financial Corporation, Deivery Hero, and [Hugging Face](https://infisical.com/customers/hugging-face).
## Migrating from EnvKey
To facilitate customer transition from EnvKey to Infisical, we have been working closely with the EnvKey team to provide a simple migration path for all EnvKey customers.
## Automated migration
Our team is currently working on creating an automated migration process that would include secrets, policies, and other important resources. If you are interested in that, please [reach out to our team](mailto:support@infisical.com) with any questions.
## Talk to our team
To make the migration process even more seamless, you can [schedule a meeting with our team](https://infisical.cal.com/vlad/migration-from-envkey-to-infisical) to learn more about how Infisical compares to EnvKey and discuss unique needs of your organization. You are also welcome to email us at [support@infisical.com](mailto:support@infisical.com) to ask any questions or get any technical help.

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

View File

@@ -15,7 +15,7 @@ Prerequisites:
![integrations cloudflare credentials 1](../../images/integrations/cloudflare/integrations-cloudflare-credentials-1.png)
![integrations cloudflare credentials 2](../../images/integrations/cloudflare/integrations-cloudflare-credentials-2.png)
![integrations cloudflare credentials 3](../../images/integrations/cloudflare/integrations-cloudflare-credentials-3.png)
![integrations cloudflare credentials 3](../../images/integrations/cloudflare/integrations-cloudflare-workers-permission.png)
Copy your [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/) from Account > Workers & Pages > Overview
@@ -35,10 +35,12 @@ Prerequisites:
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
</Step>
<Step title="Start integration">
Select which Infisical environment secrets you want to sync to Cloudflare Workers and press create integration to start syncing secrets.
![integrations cloudflare](../../images/integrations/cloudflare/integration-cloudflare-workers-create.png)
</Step>
</Steps>
</Steps>

View File

@@ -216,13 +216,6 @@
"group": "Self-host Infisical",
"pages": [
"self-hosting/overview",
{
"group": "Native installation methods",
"pages": [
"self-hosting/deployment-options/native/standalone-binary",
"self-hosting/deployment-options/native/high-availability"
]
},
{
"group": "Containerized installation methods",
"pages": [
@@ -413,6 +406,7 @@
"sdks/languages/node",
"sdks/languages/python",
"sdks/languages/go",
"sdks/languages/ruby",
"sdks/languages/java",
"sdks/languages/csharp"
]
@@ -660,6 +654,7 @@
{
"group": "Certificate Authorities",
"pages": [
"api-reference/endpoints/certificate-authorities/list",
"api-reference/endpoints/certificate-authorities/create",
"api-reference/endpoints/certificate-authorities/read",
"api-reference/endpoints/certificate-authorities/update",
@@ -675,6 +670,7 @@
{
"group": "Certificates",
"pages": [
"api-reference/endpoints/certificates/list",
"api-reference/endpoints/certificates/read",
"api-reference/endpoints/certificates/revoke",
"api-reference/endpoints/certificates/delete",

View File

@@ -118,6 +118,10 @@ namespace Example
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
</ParamField>
<ParamField query="SslCertificatePath" optional>
Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate.
</ParamField>
<ParamField query="Auth" type="AuthenticationOptions">
The authentication object to use for the client. This is required unless you're using environment variables.
</ParamField>

View File

@@ -122,6 +122,10 @@ public class App {
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
</ParamField>
<ParamField query="setSSLCertificatePath()">
Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate.
</ParamField>
<ParamField query="setAuth()" type="AuthenticationOptions">
The authentication object to use for the client. This is required unless you're using environment variables.
</ParamField>

View File

@@ -137,6 +137,10 @@ Import the SDK and create a client instance with your [Machine Identity](/docume
The level of logs you wish to log The logs are derived from Rust, as we have written our base SDK in Rust.
</ParamField>
<ParamField query="sslCertificatePath" optional>
Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate.
</ParamField>
<ParamField query="auth" type="AuthenticationOptions">
The authentication object to use for the client. This is required unless you're using environment variables.
</ParamField>

View File

@@ -97,16 +97,14 @@ client = InfisicalClient(ClientSettings(
If manually set to 0, caching will be disabled, this is not recommended.
</ParamField>
<ParamField
query="site_url"
type="string"
default="https://app.infisical.com"
optional
>
<ParamField query="site_url" type="string" default="https://app.infisical.com" optional>
Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`)
</ParamField>
<ParamField query="ssl_certificate_path" optional>
Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate.
</ParamField>
<ParamField query="auth" type="AuthenticationOptions">
The authentication object to use for the client. This is required unless you're using environment variables.
</ParamField>

View File

@@ -0,0 +1,436 @@
---
title: "Infisical Ruby SDK"
sidebarTitle: "Ruby"
icon: "diamond"
---
If you're working with Ruby , the official [Infisical Ruby SDK](https://github.com/infisical/sdk) package is the easiest way to fetch and work with secrets for your application.
- [Ruby Package](https://rubygems.org/gems/infisical-sdk)
- [Github Repository](https://github.com/infisical/sdk)
## Basic Usage
```ruby
require 'infisical-sdk'
# 1. Create the Infisical client
infisical = InfisicalSDK::InfisicalClient.new('https://app.infisical.com')
infisical.auth.universal_auth(client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_CLIENT_SECRET')
test_secret = infisical.secrets.get(
secret_name: 'API_KEY',
project_id: 'project-id',
environment: 'dev'
)
puts "Secret: #{single_test_secret}"
```
This example demonstrates how to use the Infisical Ruby SDK in a simple Ruby application. The application retrieves a secret named `API_KEY` from the `dev` environment of the `YOUR_PROJECT_ID` project.
<Warning>
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
</Warning>
# Installation
```console
$ gem install infisical-sdk
```
# Configuration
Import the SDK and create a client instance.
```ruby
infisical = InfisicalSDK::InfisicalClient.new('https://app.infisical.com') # Optional parameter, default is https://api.infisical.com
```
### Client parameters
<ParamField query="options" type="object">
<Expandable title="properties">
<ParamField query="Site URL" type="string" optional>
The URL of the Infisical API. Default is `https://api.infisical.com`.
</ParamField>
<ParamField query="Cache TTL" type="string" required>
How long the client should cache secrets for. Default is 5 minutes. Disable by setting to 0.
</ParamField>
</Expandable>
</ParamField>
### Authentication
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
#### Universal Auth
**Using environment variables**
Call `auth.universal_auth()` with empty arguments to use the following environment variables:
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
**Using the SDK directly**
```ruby
infisical.auth.universal_auth(client_id: 'your-client-id', client_secret: 'your-client-secret')
```
#### GCP ID Token Auth
<Info>
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
</Info>
**Using environment variables**
Call `.auth.gcp_id_token_auth()` with empty arguments to use the following environment variables:
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```ruby
infisical.auth.gcp_id_token_auth(identity_id: 'MACHINE_IDENTITY_ID')
```
#### GCP IAM Auth
**Using environment variables**
Call `.auth.gcp_iam_auth()` with empty arguments to use the following environment variables:
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
**Using the SDK directly**
```ruby
infisical.auth.gcp_iam_auth(identity_id: 'MACHINE_IDENTITY_ID', service_account_key_file_path: 'SERVICE_ACCOUNT_KEY_FILE_PATH')
```
#### AWS IAM Auth
<Info>
Please note that this authentication method will only work if you're running your application on AWS.
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
</Info>
**Using environment variables**
Call `.auth.aws_iam_auth()` with empty arguments to use the following environment variables:
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```ruby
infisical.auth.aws_iam_auth(identity_id: 'MACHINE_IDENTITY_ID')
```
#### Azure Auth
<Info>
Please note that this authentication method will only work if you're running your application on Azure.
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
</Info>
**Using environment variables**
Call `.auth.azure_auth()` with empty arguments to use the following environment variables:
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```ruby
infisical.auth.azure_auth(identity_id: 'MACHINE_IDENTITY_ID')
```
#### Kubernetes Auth
<Info>
Please note that this authentication method will only work if you're running your application on Kubernetes.
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
</Info>
**Using environment variables**
Call `.auth.kubernetes_auth()` with empty arguments to use the following environment variables:
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
**Using the SDK directly**
```ruby
# Service account token path will default to /var/run/secrets/kubernetes.io/serviceaccount/token if empty value is passed
infisical.auth.kubernetes_auth(identity_id: 'MACHINE_IDENTITY_ID', service_account_token_path: nil)
```
## Working with Secrets
### client.secrets.list(options)
```ruby
secrets = infisical.secrets.list(
project_id: 'PROJECT_ID',
environment: 'dev',
path: '/foo/bar',
)
```
Retrieve all secrets within the Infisical project and environment that client is connected to
#### Parameters
<ParamField query="Parameters" type="object">
<Expandable title="properties">
<ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="project_id" type="string">
The project ID where the secret lives in.
</ParamField>
<ParamField query="path" type="string" optional>
The path from where secrets should be fetched from.
</ParamField>
<ParamField query="attach_to_process_env" type="boolean" default="false" optional>
Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `System.getenv("SECRET_NAME")`.
</ParamField>
<ParamField query="include_imports" type="boolean" default="false" optional>
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
</ParamField>
<ParamField query="recursive" type="boolean" default="false" optional>
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
</ParamField>
<ParamField query="expand_secret_references" type="boolean" default="true" optional>
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
</ParamField>
</Expandable>
</ParamField>
### client.secrets.get(options)
```ruby
secret = infisical.secrets.get(
secret_name: 'API_KEY',
project_id: project_id,
environment: env_slug
)
```
Retrieve a secret from Infisical.
By default, `Secrets().Retrieve()` fetches and returns a shared secret.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="secret_name" type="string" required>
The key of the secret to retrieve.
</ParamField>
<ParamField query="project_id" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="path" type="string" optional>
The path from where secret should be fetched from.
</ParamField>
<ParamField query="type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField>
</Expandable>
</ParamField>
### client.secrets.create(options)
```ruby
new_secret = infisical.secrets.create(
secret_name: 'NEW_SECRET',
secret_value: 'SECRET_VALUE',
project_id: 'PROJECT_ID',
environment: 'dev',
)
```
Create a new secret in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="secret_name" type="string" required>
The key of the secret to create.
</ParamField>
<ParamField query="secret_value" type="string" required>
The value of the secret.
</ParamField>
<ParamField query="secret_comment" type="string" optional>
A comment for the secret.
</ParamField>
<ParamField query="project_id" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="path" type="string" optional>
The path from where secret should be created.
</ParamField>
<ParamField query="type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField>
</Expandable>
</ParamField>
### client.secrets.update(options)
```ruby
updated_secret = infisical.secrets.update(
secret_name: 'SECRET_KEY_TO_UPDATE',
secret_value: 'NEW_SECRET_VALUE',
project_id: 'PROJECT_ID',
environment: 'dev',
)
```
Update an existing secret in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="secret_name" type="string" required>
The key of the secret to update.
</ParamField>
<ParamField query="secret_value" type="string" required>
The new value of the secret.
</ParamField>
<ParamField query="skip_multiline_encoding" type="boolean" default="false" optional>
Whether or not to skip multiline encoding for the new secret value.
</ParamField>
<ParamField query="project_id" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="path" type="string" optional>
The path from where secret should be updated.
</ParamField>
<ParamField query="type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField>
</Expandable>
</ParamField>
### client.secrets.delete(options)
```ruby
deleted_secret = infisical.secrets.delete(
secret_name: 'SECRET_TO_DELETE',
project_id: 'PROJECT_ID',
environment: 'dev',
)
```
Delete a secret in Infisical.
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="secret_name" type="string">
The key of the secret to update.
</ParamField>
<ParamField query="project_id" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="path" type="string" optional>
The path from where secret should be deleted.
</ParamField>
<ParamField query="type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField>
</Expandable>
</ParamField>
## Cryptography
### Create a symmetric key
Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption.
```ruby
key = infisical.cryptography.create_symmetric_key
```
#### Returns (string)
`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes.
### Encrypt symmetric
```ruby
encrypted_data = infisical.cryptography.encrypt_symmetric(data: "Hello World!", key: key)
```
#### Parameters
<ParamField query="Parameters" type="object" required>
<Expandable title="properties">
<ParamField query="data" type="string">
The plaintext you want to encrypt.
</ParamField>
<ParamField query="key" type="string" required>
The symmetric key to use for encryption.
</ParamField>
</Expandable>
</ParamField>
#### Returns (object)
`tag` (string): A base64-encoded, 128-bit authentication tag.
`iv` (string): A base64-encoded, 96-bit initialization vector.
`ciphertext` (string): A base64-encoded, encrypted ciphertext.
### Decrypt symmetric
```ruby
decrypted_data = infisical.cryptography.decrypt_symmetric(
ciphertext: encrypted_data['ciphertext'],
iv: encrypted_data['iv'],
tag: encrypted_data['tag'],
key: key
)
```
#### Parameters
<ParamField query="Parameters" type="object" required>
<Expandable title="properties">
<ParamField query="ciphertext" type="string">
The ciphertext you want to decrypt.
</ParamField>
<ParamField query="key" type="string" required>
The symmetric key to use for encryption.
</ParamField>
<ParamField query="iv" type="string" required>
The initialization vector to use for decryption.
</ParamField>
<ParamField query="tag" type="string" required>
The authentication tag to use for decryption.
</ParamField>
</Expandable>
</ParamField>
#### Returns (string)
`Plaintext` (string): The decrypted plaintext.

View File

@@ -25,6 +25,10 @@ From local development to production, Infisical SDKs provide the easiest way for
<Card href="/sdks/languages/csharp" title="C#" icon="bars" color="#368833">
Manage secrets for your C#/.NET application on demand
</Card>
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
Manage secrets for your Ruby application on demand
</Card>
</CardGroup>
## FAQ

View File

@@ -25,6 +25,10 @@ Used to configure platform-specific security and operational settings
https://app.infisical.com).
</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>
Telemetry helps us improve Infisical but if you want to dsiable it you may set this to `false`.
</ParamField>

View File

@@ -10,9 +10,8 @@ This is one of the easiest ways to deploy Infisical. It is a single executable,
The standalone deployment implements the "bring your own database" (BYOD) approach. This means that you will need to provide your own databases (specifically Postgres and Redis) for the Infisical services to use. The standalone deployment does not include any databases.
If you wish to streamline the deployment process, we recommend using the Ansible role for Infisical. The Ansible role automates the deployment process and includes the databases:
- [Automated Deployment](https://google.com)
- [Automated Deployment with high availability (HA)](https://google.com)
If you wish to streamline the deployment process, we recommend using the Ansible role for Infisical. The Ansible role automates the end to end deployment process, and will take care of everything like databases, redis deployment, web serving, and availability.
- [Automated Deployment with high availability (HA)](/self-hosting/deployment-options/native/high-availability)
## Prerequisites

View File

@@ -33,7 +33,7 @@ Choose from a number of deployment options listed below to get started.
Use our Helm chart to Install Infisical on your Kubernetes cluster.
</Card>
</CardGroup>
<CardGroup cols={2}>
{/* <CardGroup cols={2}>
<Card
title="Native Deployment"
color="#000000"
@@ -50,4 +50,4 @@ Choose from a number of deployment options listed below to get started.
>
Install Infisical on your Debian-based instances without containers using our standalone binary with high availability out of the box.
</Card>
</CardGroup>
</CardGroup> */}

View File

@@ -29,6 +29,7 @@ export * from "./secretFolders";
export * from "./secretImports";
export * from "./secretRotation";
export * from "./secrets";
export * from "./secretSharing";
export * from "./secretSnapshots";
export * from "./serverDetails";
export * from "./serviceTokens";

View File

@@ -7,6 +7,7 @@ export {
useUpdateProjectRole
} from "./mutation";
export {
useGetOrgRole,
useGetOrgRoles,
useGetProjectRoleBySlug,
useGetProjectRoles,

View File

@@ -9,6 +9,8 @@ import {
TCreateProjectRoleDTO,
TDeleteOrgRoleDTO,
TDeleteProjectRoleDTO,
TOrgRole,
TProjectRole,
TUpdateOrgRoleDTO,
TUpdateProjectRoleDTO
} from "./types";
@@ -16,9 +18,13 @@ import {
export const useCreateProjectRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) =>
apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto),
return useMutation<TProjectRole, {}, TCreateProjectRoleDTO>({
mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto);
return role;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}
@@ -28,9 +34,13 @@ export const useCreateProjectRole = () => {
export const useUpdateProjectRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) =>
apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto),
return useMutation<TProjectRole, {}, TUpdateProjectRoleDTO>({
mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto);
return role;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}
@@ -39,10 +49,13 @@ export const useUpdateProjectRole = () => {
export const useDeleteProjectRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) =>
apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`),
return useMutation<TProjectRole, {}, TDeleteProjectRoleDTO>({
mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`);
return role;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}
@@ -52,12 +65,17 @@ export const useDeleteProjectRole = () => {
export const useCreateOrgRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ orgId, permissions, ...dto }: TCreateOrgRoleDTO) =>
apiRequest.post(`/api/v1/organization/${orgId}/roles`, {
return useMutation<TOrgRole, {}, TCreateOrgRoleDTO>({
mutationFn: async ({ orgId, permissions, ...dto }: TCreateOrgRoleDTO) => {
const {
data: { role }
} = await apiRequest.post(`/api/v1/organization/${orgId}/roles`, {
...dto,
permissions: permissions.length ? packRules(permissions) : []
}),
});
return role;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(roleQueryKeys.getOrgRoles(orgId));
}
@@ -67,14 +85,20 @@ export const useCreateOrgRole = () => {
export const useUpdateOrgRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, orgId, permissions, ...dto }: TUpdateOrgRoleDTO) =>
apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
return useMutation<TOrgRole, {}, TUpdateOrgRoleDTO>({
mutationFn: async ({ id, orgId, permissions, ...dto }: TUpdateOrgRoleDTO) => {
const {
data: { role }
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
...dto,
permissions: permissions?.length ? packRules(permissions) : []
}),
onSuccess: (_, { orgId }) => {
permissions: permissions?.length ? packRules(permissions) : undefined
});
return role;
},
onSuccess: (_, { id, orgId }) => {
queryClient.invalidateQueries(roleQueryKeys.getOrgRoles(orgId));
queryClient.invalidateQueries(roleQueryKeys.getOrgRole(orgId, id));
}
});
};
@@ -82,13 +106,19 @@ export const useUpdateOrgRole = () => {
export const useDeleteOrgRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ orgId, id }: TDeleteOrgRoleDTO) =>
apiRequest.delete(`/api/v1/organization/${orgId}/roles/${id}`, {
return useMutation<TOrgRole, {}, TDeleteOrgRoleDTO>({
mutationFn: async ({ orgId, id }: TDeleteOrgRoleDTO) => {
const {
data: { role }
} = await apiRequest.delete(`/api/v1/organization/${orgId}/roles/${id}`, {
data: { orgId }
}),
onSuccess: (_, { orgId }) => {
});
return role;
},
onSuccess: (_, { id, orgId }) => {
queryClient.invalidateQueries(roleQueryKeys.getOrgRoles(orgId));
queryClient.invalidateQueries(roleQueryKeys.getOrgRole(orgId, id));
}
});
};

View File

@@ -40,6 +40,7 @@ export const roleQueryKeys = {
getProjectRoleBySlug: (projectSlug: string, roleSlug: string) =>
["roles", { projectSlug, roleSlug }] as const,
getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const,
getOrgRole: (orgId: string, roleId: string) => [{ orgId, roleId }, "org-role"] as const,
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
["user-permissions", { orgId }] as const,
getUserProjectPermissions: ({ workspaceId }: TGetUserProjectPermissionDTO) =>
@@ -89,6 +90,21 @@ export const useGetOrgRoles = (orgId: string, enable = true) =>
enabled: Boolean(orgId) && enable
});
export const useGetOrgRole = (orgId: string, roleId: string) =>
useQuery({
queryKey: roleQueryKeys.getOrgRole(orgId, roleId),
queryFn: async () => {
const { data } = await apiRequest.get<{
role: Omit<TOrgRole, "permissions"> & { permissions: unknown };
}>(`/api/v1/organization/${orgId}/roles/${roleId}`);
return {
...data.role,
permissions: unpackRules(data.role.permissions as PackRule<TPermission>[])
};
},
enabled: Boolean(orgId && roleId)
});
const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
if (orgId === "") return { permissions: [], membership: null };

View File

@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { secretSharingKeys } from "./queries";
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types";
export const useCreateSharedSecret = () => {
@@ -11,7 +12,7 @@ export const useCreateSharedSecret = () => {
const { data } = await apiRequest.post<TSharedSecret>("/api/v1/secret-sharing", inputData);
return data;
},
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
});
};
@@ -25,7 +26,7 @@ export const useCreatePublicSharedSecret = () => {
);
return data;
},
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
});
};
@@ -38,8 +39,6 @@ export const useDeleteSharedSecret = () => {
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(["sharedSecrets"]);
}
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
});
};

View File

@@ -2,24 +2,59 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { SecretSharingAccessType, TSharedSecret, TViewSharedSecretResponse } from "./types";
import { TSharedSecret, TViewSharedSecretResponse } from "./types";
export const useGetSharedSecrets = () => {
export const secretSharingKeys = {
allSharedSecrets: () => ["sharedSecrets"] as const,
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const
};
export const useGetSharedSecrets = ({
offset = 0,
limit = 25
}: {
offset: number;
limit: number;
}) => {
return useQuery({
queryKey: ["sharedSecrets"],
queryKey: secretSharingKeys.specificSharedSecrets({ offset, limit }),
queryFn: async () => {
const { data } = await apiRequest.get<TSharedSecret[]>("/api/v1/secret-sharing/");
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit)
});
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
"/api/v1/secret-sharing/",
{
params
}
);
return data;
}
});
};
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
export const useGetActiveSharedSecretById = ({
sharedSecretId,
hashedHex
}: {
sharedSecretId: string;
hashedHex: string;
}) => {
return useQuery<TViewSharedSecretResponse, [string]>({
enabled: Boolean(sharedSecretId) && Boolean(hashedHex),
queryFn: async () => {
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "", accessType: SecretSharingAccessType.Organization, orgName: "" });
const params = new URLSearchParams({
hashedHex
});
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
`/api/v1/secret-sharing/public/${sharedSecretId}`,
{
params
}
);
return {
encryptedValue: data.encryptedValue,

View File

@@ -4,16 +4,24 @@ export type TSharedSecret = {
orgId: string;
createdAt: Date;
updatedAt: Date;
} & TCreateSharedSecretRequest;
export type TCreateSharedSecretRequest = {
name: string | null;
lastViewedAt?: Date;
expiresAt: Date;
expiresAfterViews: number | null;
encryptedValue: string;
iv: string;
tag: string;
};
export type TCreateSharedSecretRequest = {
name?: string;
encryptedValue: string;
hashedHex: string;
iv: string;
tag: string;
expiresAt: Date;
expiresAfterViews: number;
accessType: SecretSharingAccessType;
expiresAfterViews?: number;
accessType?: SecretSharingAccessType;
};
export type TViewSharedSecretResponse = {
@@ -31,4 +39,4 @@ export type TDeleteSharedSecretRequest = {
export enum SecretSharingAccessType {
Anyone = "anyone",
Organization = "organization"
}
}

View File

@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { RolePage } from "@app/views/Org/RolePage";
export default function Role() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<RolePage />
</>
);
}
Role.requireAuth = true;

View File

@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { RolePage } from "@app/views/Project/RolePage";
export default function Role() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: "Project Settings" })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<RolePage />
</>
);
}
Role.requireAuth = true;

View File

@@ -13,7 +13,7 @@ const ShareNewPublicSecretPage = () => {
<meta name="og:description" content="" />
</Head>
<div className="dark h-full">
<ShareSecretPublicPage isNewSession />
<ShareSecretPublicPage />
</div>
</>
);

View File

@@ -1,6 +1,6 @@
import Head from "next/head";
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
import { ViewSecretPublicPage } from "@app/views/ViewSecretPublicPage";
const SecretSharedPublicPage = () => {
return (
@@ -12,9 +12,7 @@ const SecretSharedPublicPage = () => {
<meta property="og:title" content="" />
<meta name="og:description" content="" />
</Head>
<div className="dark h-full">
<ShareSecretPublicPage isNewSession={false} />
</div>
<ViewSecretPublicPage />
</>
);
};

View File

@@ -168,16 +168,27 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
const { token: newJwtToken } = await selectOrganization({ organizationId });
const instance = axios.create();
await instance.post(cliUrl, {
const payload = {
...isCliLoginSuccessful.loginResponse,
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))
})
);
});
await navigateUserToOrg(router, organizationId);
router.push("/cli-redirect");
return;
}
// 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
else {
const userOrgs = await fetchOrganizations();
// 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 {
await navigateUserToOrg(router);
}
}
}
} else {
const isLoginSuccessful = await attemptLoginMfa({

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import axios from "axios";
import { addSeconds, formatISO } from "date-fns";
import jwt_decode from "jwt-decode";
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 SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
@@ -79,11 +81,24 @@ export const PasswordStep = ({
if (callbackPort) {
console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken);
const instance = axios.create();
await instance.post(cliUrl, {
const payload = {
privateKey,
email,
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);
@@ -165,26 +180,35 @@ export const PasswordStep = ({
);
const instance = axios.create();
await instance.post(cliUrl, {
const payload = {
...isCliLoginSuccessful.loginResponse,
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))
})
);
});
await navigateUserToOrg(router, organizationId);
router.push("/cli-redirect");
return;
}
// 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
else {
const userOrgs = await fetchOrganizations();
const userOrgs = await fetchOrganizations();
// case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) {
navigateToSelectOrganization(callbackPort);
}
// case: no orgs found, so we navigate the user to create an org
else {
await navigateUserToOrg(router);
}
// case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) {
navigateToSelectOrganization(callbackPort);
}
// case: no orgs found, so we navigate the user to create an org
else {
await navigateUserToOrg(router);
}
}
} else {

View File

@@ -24,6 +24,7 @@ import {
useRevokeIdentityTokenAuthToken,
useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from"@app/views/Org/Types";
import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
@@ -75,7 +76,7 @@ export const IdentityPage = withPermission(
});
handlePopUpClose("deleteIdentity");
router.push(`/org/${orgId}/members`);
router.push(`/org/${orgId}/members?selectedTab=${TabSections.Identities}`);
} catch (err) {
console.error(err);
const error = err as any;
@@ -154,7 +155,7 @@ export const IdentityPage = withPermission(
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/org/${orgId}/members`);
router.push(`/org/${orgId}/members?selectedTab=${TabSections.Identities}`);
}}
className="mb-4"
>

View File

@@ -25,7 +25,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Details</h3>
<h3 className="text-lg font-semibold text-mineshaft-100">Identity Details</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (

View File

@@ -10,6 +10,7 @@ import { useWorkspace } from "@app/context";
import { IdentityMembership } from "@app/hooks/api/identities/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { TabSections } from "@app/views/Org/Types";
type Props = {
membership: IdentityMembership;
@@ -47,11 +48,11 @@ export const IdentityProjectRow = ({
return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`identity-project-membership-${id}`}
onClick={() => {
if (isAccessible) {
router.push(`/project/${project.id}/members`);
router.push(`/project/${project.id}/members?selectedTab=${TabSections.Identities}`);
return;
}

View File

@@ -1,23 +1,39 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { isTabSection, TabSections } from "@app/views/Org/Types";
import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components";
enum TabSections {
Member = "members",
Roles = "roles",
Identities = "identities"
}
export const MembersPage = withPermission(
() => {
const router = useRouter();
const { query } = router;
const selectedTab = query.selectedTab as string;
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Member);
useEffect(() => {
if (selectedTab && isTabSection(selectedTab)) {
setActiveTab(selectedTab);
}
}, [isTabSection, selectedTab]);
const updateSelectedTab = (tab: string) => {
router.push({
pathname: router.pathname,
query: { ...router.query, selectedTab: tab },
});
}
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Organization Access Control</p>
<Tabs defaultValue={TabSections.Member}>
<Tabs value={activeTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={TabSections.Member}>Users</Tab>
<Tab value={TabSections.Identities}>
@@ -25,7 +41,7 @@ export const MembersPage = withPermission(
<p>Machine Identities</p>
</div>
</Tab>
<Tab value={TabSections.Roles}>Organization Roles</Tab>
<Tab value={TabSections.Roles}>Organization Roles</Tab>
</TabList>
<TabPanel value={TabSections.Member}>
<OrgMembersTab />

View File

@@ -68,7 +68,7 @@ export const IdentitySection = withPermission(
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<div className="flex w-full justify-end pr-4">

View File

@@ -86,7 +86,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
data?.map(({ identity: { id, name }, role, customRole }) => {
return (
<Tr
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`identity-${id}`}
onClick={() => router.push(`/org/${orgId}/identities/${id}`)}
>

View File

@@ -184,7 +184,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
return (
<Tr
key={`org-membership-${orgMembershipId}`}
className="h-10 w-full cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => router.push(`/org/${orgId}/memberships/${orgMembershipId}`)}
>
<Td className={isActive ? "" : "text-mineshaft-400"}>{name}</Td>

View File

@@ -1,251 +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>
);
};

View File

@@ -8,7 +8,7 @@ import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;

View File

@@ -1 +0,0 @@
export { OrgRoleModifySection } from "./OrgRoleModifySection";

View File

@@ -1,27 +1,9 @@
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";
export const OrgRoleTabSection = () => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
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>
) : (
return (
<motion.div
key="role-list"
transition={{ duration: 0.1 }}
@@ -29,7 +11,7 @@ export const OrgRoleTabSection = () => {
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<OrgRoleTable onSelectRole={(role) => handlePopUpOpen("editRole", role)} />
<OrgRoleTable />
</motion.div>
);
};

View File

@@ -1,14 +1,17 @@
import { useState } from "react";
import { faEdit, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { useRouter } from "next/router";
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
IconButton,
Input,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Table,
TableContainer,
TableSkeleton,
@@ -22,17 +25,17 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@a
import { usePopUp } from "@app/hooks";
import { useDeleteOrgRole, useGetOrgRoles } from "@app/hooks/api";
import { TOrgRole } from "@app/hooks/api/roles/types";
import { RoleModal } from "@app/views/Org/RolePage/components";
type Props = {
onSelectRole: (role?: TOrgRole) => void;
};
export const OrgRoleTable = ({ onSelectRole }: Props) => {
const [searchRoles, setSearchRoles] = useState("");
export const OrgRoleTable = () => {
const router = useRouter();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"role",
"deleteRole"
] as const);
const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId);
@@ -54,100 +57,113 @@ export const OrgRoleTable = ({ onSelectRole }: Props) => {
};
return (
<div className="w-full">
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchRoles}
onChange={(e) => setSearchRoles(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search roles..."
/>
</div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Organization Roles</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => onSelectRole()}
onClick={() => {
handlePopUpOpen("role");
}}
isDisabled={!isAllowed}
>
Add Role
</Button>
)}
</OrgPermissionCan>
</div>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{roles?.map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
return (
<Tr key={`role-list-${id}`}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex justify-end">
<div className="flex space-x-2">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th aria-label="actions" className="w-5" />
</Tr>
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={3} innerKey="org-roles" />}
{roles?.map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
return (
<Tr
key={`role-list-${id}`}
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => router.push(`/org/${orgId}/roles/${id}`)}
>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Role}
renderTooltip
allowedLabel="Edit"
>
{(isAllowed) => (
<IconButton
isDisabled={!isAllowed}
ariaLabel="edit"
onClick={() => onSelectRole(role)}
variant="plain"
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
router.push(`/org/${orgId}/roles/${id}`);
}}
disabled={!isAllowed}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
{`${isNonMutatable ? "View" : "Edit"} Role`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Role}
renderTooltip
allowedLabel={
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
}
>
{(isAllowed) => (
<IconButton
ariaLabel="delete"
onClick={() => handlePopUpOpen("deleteRole", role)}
variant="plain"
isDisabled={isNonMutatable || !isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
</TableContainer>
</div>
{!isNonMutatable && (
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
}}
disabled={!isAllowed}
>
Delete Role
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
</TableContainer>
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteRole?.data as TOrgRole)?.name || " "
} role?`}
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
deleteKey={(popUp?.deleteRole?.data as TOrgRole)?.slug || ""}
onClose={() => handlePopUpClose("deleteRole")}
onDeleteApproved={handleRoleDelete}

View File

@@ -0,0 +1,158 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useDeleteOrgRole, useGetOrgRole } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from "@app/views/Org/Types";
import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components";
export const RolePage = withPermission(
() => {
const router = useRouter();
const roleId = router.query.roleId as string;
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetOrgRole(orgId, roleId);
const { mutateAsync: deleteOrgRole } = useDeleteOrgRole();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"role",
"deleteOrgRole"
] as const);
const onDeleteOrgRoleSubmit = async () => {
try {
if (!orgId || !roleId) return;
await deleteOrgRole({
orgId,
id: roleId
});
createNotification({
text: "Successfully deleted organization role",
type: "success"
});
handlePopUpClose("deleteOrgRole");
router.push(`/org/${orgId}/members?selectedTab=${TabSections.Roles}`);
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete organization role";
createNotification({
text,
type: "error"
});
}
};
const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? "");
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/org/${orgId}/members?selectedTab=${TabSections.Roles}`);
}}
className="mb-4"
>
Roles
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
{isCustomRole && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("role", {
roleId
});
}}
disabled={!isAllowed}
>
Edit Role
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("deleteOrgRole");
}}
disabled={!isAllowed}
>
Delete Role
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<div className="flex">
<div className="mr-4 w-96">
<RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} />
</div>
<RolePermissionsSection roleId={roleId} />
</div>
</div>
)}
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteOrgRole.isOpen}
title={`Are you sure want to delete the organization role ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrgRole", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => onDeleteOrgRoleSubmit()}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Role }
);

View File

@@ -0,0 +1,95 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetOrgRole } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
roleId: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: {}) => void;
};
export const RoleDetailsSection = ({ roleId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetOrgRole(orgId, roleId);
const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? "");
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Org Role Details</h3>
{isCustomRole && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
{(isAllowed) => {
return (
<Tooltip content="Edit Role">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() =>
handlePopUpOpen("role", {
roleId
})
}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
)}
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Role ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{roleId}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(roleId);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
<p className="text-sm text-mineshaft-300">{data.slug}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
<p className="text-sm text-mineshaft-300">
{data.description?.length ? data.description : "-"}
</p>
</div>
</div>
</div>
) : (
<div />
);
};

View File

@@ -0,0 +1,200 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useCreateOrgRole, useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
name: z.string(),
description: z.string(),
slug: z.string()
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["role"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["role"]>, state?: boolean) => void;
};
export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
const router = useRouter();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const popupData = popUp?.role?.data as {
roleId: string;
};
const { data: role } = useGetOrgRole(orgId, popupData?.roleId ?? "");
const { mutateAsync: createOrgRole } = useCreateOrgRole();
const { mutateAsync: updateOrgRole } = useUpdateOrgRole();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: ""
}
});
useEffect(() => {
if (role) {
reset({
name: role.name,
description: role.description,
slug: role.slug
});
} else {
reset({
name: "",
description: "",
slug: ""
});
}
}, [role]);
const onFormSubmit = async ({ name, description, slug }: FormData) => {
try {
console.log("onFormSubmit args: ", {
name,
description,
slug
});
if (!orgId) return;
if (role) {
// update
await updateOrgRole({
orgId,
id: role.id,
name,
description,
slug
});
handlePopUpToggle("role", false);
} else {
// create
const newRole = await createOrgRole({
orgId,
name,
description,
slug,
permissions: []
});
handlePopUpToggle("role", false);
router.push(`/org/${orgId}/roles/${newRole.id}`);
}
createNotification({
text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ??
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.role?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("role", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.role?.data ? "Update" : "Create"} Role`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Billing Team" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Slug"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="billing" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="To manage billing" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.role?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("role", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

View File

@@ -1,31 +1,12 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
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 { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } 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"
}
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
const PERMISSIONS = [
{ action: "read", label: "View" },
@@ -62,7 +43,7 @@ const BILLING_PERMISSIONS = [
{ action: "delete", label: "Remove payments" }
] as const;
const getPermissionList = (option: Props["formName"]) => {
const getPermissionList = (option: string) => {
switch (option) {
case "secret-scanning":
return SECRET_SCANNING_PERMISSIONS;
@@ -77,20 +58,29 @@ const getPermissionList = (option: Props["formName"]) => {
}
};
export const SimpleLevelPermissionOption = ({
isNonEditable,
setValue,
control,
formName,
subtitle,
title,
icon
}: Props) => {
type Props = {
isEditable: boolean;
title: string;
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
export const RolePermissionRow = ({ isEditable, title, formName, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: `permissions.${formName}`
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
@@ -110,9 +100,20 @@ export const SimpleLevelPermissionOption = ({
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.NoAccess:
@@ -147,58 +148,68 @@ export const SimpleLevelPermissionOption = ({
};
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>
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>{title}</Td>
<Td>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<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>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{getPermissionList(formName).map(({ action, label }) => {
return (
<Controller
name={`permissions.${formName}.${action}`}
key={`permissions.${formName}.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.${formName}.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};

View File

@@ -0,0 +1,162 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import {
formRolePermission2API,
formSchema,
rolePermission2Form,
TFormSchema
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
import { RolePermissionRow } from "./RolePermissionRow";
const SIMPLE_PERMISSION_OPTIONS = [
{
title: "User Management",
formName: "member"
},
{
title: "Group Management",
formName: "groups"
},
{
title: "Machine Identity Management",
formName: "identity"
},
{
title: "Usage & Billing",
formName: "billing"
},
{
title: "Role Management",
formName: "role"
},
{
title: "Incident Contacts",
formName: "incident-contact"
},
{
title: "Organization Profile",
formName: "settings"
},
{
title: "Secret Scanning",
formName: "secret-scanning"
},
{
title: "SSO",
formName: "sso"
},
{
title: "LDAP",
formName: "ldap"
},
{
title: "SCIM",
formName: "scim"
}
] as const;
type Props = {
roleId: string;
};
export const RolePermissionsSection = ({ roleId }: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: role } = useGetOrgRole(orgId, roleId);
const {
setValue,
control,
handleSubmit,
formState: { isDirty, isSubmitting },
reset
} = useForm<TFormSchema>({
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
resolver: zodResolver(formSchema)
});
const { mutateAsync: updateRole } = useUpdateOrgRole();
const onSubmit = async (el: TFormSchema) => {
try {
await updateRole({
orgId,
id: roleId,
...el,
permissions: formRolePermission2API(el.permissions)
});
createNotification({ type: "success", text: "Successfully updated role" });
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update role" });
}
};
const isCustomRole = !["admin", "member", "no-access"].includes(role?.slug ?? "");
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Permissions</h3>
{isCustomRole && (
<div className="flex items-center">
<Button
colorSchema="primary"
type="submit"
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
>
Save
</Button>
<Button
className="ml-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
onClick={() => reset()}
>
Cancel
</Button>
</div>
)}
</div>
<div className="py-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-5" />
<Th>Resource</Th>
<Th>Permission</Th>
</Tr>
</THead>
<TBody>
{SIMPLE_PERMISSION_OPTIONS.map((permission) => {
return (
<RolePermissionRow
title={permission.title}
formName={permission.formName}
control={control}
setValue={setValue}
key={`org-role-${roleId}-permission-${permission.formName}`}
isEditable={isCustomRole}
/>
);
})}
</TBody>
</Table>
</TableContainer>
</div>
</form>
);
};

View File

@@ -0,0 +1 @@
export { RolePermissionsSection } from "./RolePermissionsSection";

View File

@@ -0,0 +1,3 @@
export { RoleDetailsSection } from "./RoleDetailsSection";
export { RoleModal } from "./RoleModal";
export { RolePermissionsSection } from "./RolePermissionsSection";

View File

@@ -0,0 +1 @@
export { RolePage } from "./RolePage";

View File

@@ -0,0 +1,9 @@
export enum TabSections {
Member = "members",
Roles = "roles",
Identities = "identities"
}
export const isTabSection = (value: string): value is TabSections => {
return (Object.values(TabSections) as string[]).includes(value);
}

View File

@@ -0,0 +1,3 @@
import { TabSections, isTabSection } from "./TabSections";
export { TabSections, isTabSection };

View File

@@ -29,6 +29,7 @@ import {
useUpdateOrgMembership
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from "@app/views/Org/Types";
import { UserDetailsSection, UserOrgMembershipModal, UserProjectsSection } from "./components";
@@ -90,7 +91,7 @@ export const UserPage = withPermission(
});
handlePopUpClose("removeMember");
router.push(`/org/${orgId}/members`);
router.push(`/org/${orgId}/members?selectedTab=${TabSections.Member}`);
} catch (err) {
console.error(err);
createNotification({
@@ -111,7 +112,7 @@ export const UserPage = withPermission(
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/org/${orgId}/members`);
router.push(`/org/${orgId}/members?selectedTab=${TabSections.Member}`);
}}
className="mb-4"
>

View File

@@ -3,7 +3,8 @@ import {
faCheckCircle,
faCircleXmark,
faCopy,
faPencil} from "@fortawesome/free-solid-svg-icons";
faPencil
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
@@ -82,7 +83,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
return membership ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Details</h3>
<h3 className="text-lg font-semibold text-mineshaft-100">User Details</h3>
{userId !== membership.user.id && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {

View File

@@ -9,6 +9,7 @@ import { useWorkspace } from "@app/context";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { TWorkspaceUser } from "@app/hooks/api/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { TabSections } from "@app/views/Org/Types";
type Props = {
membership: TWorkspaceUser;
@@ -43,11 +44,11 @@ export const UserProjectRow = ({
return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`user-project-membership-${id}`}
onClick={() => {
if (isAccessible) {
router.push(`/project/${project.id}/members`);
router.push(`/project/${project.id}/members?selectedTab=${TabSections.Member}`);
return;
}

View File

@@ -1,25 +1,40 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { isTabSection,TabSections } from "../Types";
import { IdentityTab, MembersTab,ProjectRoleListTab, ServiceTokenTab } from "./components";
enum TabSections {
Member = "members",
Roles = "roles",
Groups = "groups",
Identities = "identities",
ServiceTokens = "service-tokens"
}
export const MembersPage = withProjectPermission(
() => {
const router = useRouter();
const { query } = router;
const selectedTab = query.selectedTab as string;
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Member);
useEffect(() => {
if (selectedTab && isTabSection(selectedTab)) {
setActiveTab(selectedTab);
}
}, [isTabSection, selectedTab]);
const updateSelectedTab = (tab: string) => {
router.push({
pathname: router.pathname,
query: { ...router.query, selectedTab: tab },
});
}
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Project Access Control</p>
<Tabs defaultValue={TabSections.Member}>
<Tabs value={activeTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={TabSections.Member}>Users</Tab>
<Tab value={TabSections.Identities}>

View File

@@ -2,29 +2,12 @@ import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { ProjectRoleList } from "./components/ProjectRoleList";
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
export const ProjectRoleListTab = withProjectPermission(
() => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
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 }}
>
<ProjectRoleModifySection
roleSlug={popUp.editRole.data as string}
onGoBack={() => handlePopUpClose("editRole")}
/>
</motion.div>
) : (
return (
<motion.div
key="role-list"
transition={{ duration: 0.1 }}
@@ -32,7 +15,7 @@ export const ProjectRoleListTab = withProjectPermission(
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<ProjectRoleList onSelectRole={(slug) => handlePopUpOpen("editRole", slug)} />
<ProjectRoleList />
</motion.div>
);
},

View File

@@ -1,14 +1,17 @@
import { useState } from "react";
import { faEdit, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { useRouter } from "next/router";
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
IconButton,
Input,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Table,
TableContainer,
TableSkeleton,
@@ -22,17 +25,17 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a
import { usePopUp } from "@app/hooks";
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { RoleModal } from "@app/views/Project/RolePage/components";
type Props = {
onSelectRole: (slug?: string) => void;
};
export const ProjectRoleList = ({ onSelectRole }: Props) => {
const [searchRoles, setSearchRoles] = useState("");
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
export const ProjectRoleList = () => {
const router = useRouter();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"role",
"deleteRole"
] as const);
const { currentWorkspace } = useWorkspace();
const projectSlug = currentWorkspace?.slug || "";
const projectId = currentWorkspace?.id || "";
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
@@ -54,21 +57,16 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
};
return (
<div className="w-full">
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchRoles}
onChange={(e) => setSearchRoles(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search roles..."
/>
</div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Project Roles</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => onSelectRole()}
onClick={() => handlePopUpOpen("role")}
isDisabled={!isAllowed}
>
Add Role
@@ -76,77 +74,94 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
)}
</ProjectPermissionCan>
</div>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{roles?.map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th aria-label="actions" className="w-5" />
</Tr>
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{roles?.map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
return (
<Tr key={`role-list-${id}`}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<div className="flex justify-end space-x-2">
return (
<Tr
key={`role-list-${id}`}
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => router.push(`/project/${projectId}/roles/${slug}`)}
>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role}
renderTooltip
allowedLabel="Edit"
>
{(isAllowed) => (
<IconButton
isDisabled={!isAllowed}
ariaLabel="edit"
onClick={() => onSelectRole(role.slug)}
variant="plain"
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
router.push(`/project/${projectId}/roles/${slug}`);
}}
disabled={!isAllowed}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
{`${isNonMutatable ? "View" : "Edit"} Role`}
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Role}
renderTooltip
allowedLabel={
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
}
>
{(isAllowed) => (
<IconButton
ariaLabel="delete"
onClick={() => handlePopUpOpen("deleteRole", role)}
variant="plain"
isDisabled={isNonMutatable || !isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
</TableContainer>
</div>
{!isNonMutatable && (
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
}}
disabled={!isAllowed}
>
Delete Role
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
</TableContainer>
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete ${(popUp?.deleteRole?.data as TProjectRole)?.name || " "
} role?`}
title={`Are you sure want to delete ${
(popUp?.deleteRole?.data as TProjectRole)?.name || " "
} role?`}
deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""}
onClose={() => handlePopUpClose("deleteRole")}
onDeleteApproved={handleRoleDelete}

View File

@@ -1,258 +0,0 @@
import { useMemo } from "react";
import { Control, Controller, UseFormGetValues, 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 GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
import {
Checkbox,
FormControl,
Input,
Select,
SelectItem,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
formName: "secrets";
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
getValue: UseFormGetValues<TFormSchema>;
control: Control<TFormSchema>;
title: string;
subtitle: string;
icon: IconProp;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
export const MultiEnvProjectPermission = ({
isNonEditable,
setValue,
getValue,
control,
formName,
title,
subtitle,
icon
}: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
const customRule = useWatch({
control,
name: `permissions.${formName}.custom`
});
const isCustom = Boolean(customRule);
const allRule = useWatch({ control, name: `permissions.${formName}.all` });
const selectedPermissionCategory = useMemo(() => {
const { read, delete: del, edit, create } = allRule || {};
if (read && del && edit && create) return Permission.FullAccess;
if (read) return Permission.ReadOnly;
return Permission.NoAccess;
}, [allRule]);
const handlePermissionChange = (val: Permission) => {
if(!val) return
switch (val) {
case Permission.NoAccess: {
const permissions = getValue("permissions");
if (permissions) delete permissions[formName];
setValue("permissions", permissions, { shouldDirty: true });
break;
}
case Permission.FullAccess:
setValue(
`permissions.${formName}`,
{ all: { read: true, edit: true, create: true, delete: true } },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
`permissions.${formName}`,
{ all: { read: true, edit: false, create: false, delete: false } },
{ shouldDirty: true }
);
break;
default:
setValue(
`permissions.${formName}`,
{ custom: { 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 || isCustom) &&
"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={isCustom ? Permission.Custom : 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 ? "auto" : 0 }}
className="overflow-hidden"
>
<TableContainer className="mt-6 border-mineshaft-500">
<Table>
<THead>
<Tr>
<Th />
<Th className="min-w-[8rem]">
<div className="flex items-center gap-2">
Secret Path
<span className="text-xs normal-case">
<GlobPatternExamples />
</span>
</div>
</Th>
<Th className="text-center">View</Th>
<Th className="text-center">Create</Th>
<Th className="text-center">Modify</Th>
<Th className="text-center">Delete</Th>
</Tr>
</THead>
<TBody>
{isCustom &&
environments.map(({ name, slug }) => (
<Tr key={`custom-role-project-secret-${slug}`}>
<Td>{name}</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.secretPath`}
control={control}
render={({ field }) => (
/* eslint-disable-next-line no-template-curly-in-string */
<FormControl helperText="Supports glob path pattern string">
<Input
{...field}
className="w-full overflow-ellipsis"
placeholder="Glob patterns are supported"
/>
</FormControl>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.read`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.${formName}.${slug}.read`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.create`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
onBlur={field.onBlur}
id={`permissions.${formName}.${slug}.modify`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.edit`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
onBlur={field.onBlur}
id={`permissions.${formName}.${slug}.modify`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
defaultValue={false}
name={`permissions.${formName}.${slug}.delete`}
control={control}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.${formName}.${slug}.delete`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</motion.div>
</div>
);
};

View File

@@ -1,318 +0,0 @@
import { useForm } from "react-hook-form";
import { faElementor } from "@fortawesome/free-brands-svg-icons";
import {
faAnchorLock,
faArrowLeft,
faBook,
faCertificate,
faCog,
faKey,
faLock,
faNetworkWired,
faPuzzlePiece,
faServer,
faShield,
faTags,
faUser,
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, Spinner } from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import {
useCreateProjectRole,
useGetProjectRoleBySlug,
useUpdateProjectRole
} from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
import {
formRolePermission2API,
formSchema,
rolePermission2Form,
TFormSchema
} from "./ProjectRoleModifySection.utils";
import { SecretRollbackPermission } from "./SecretRollbackPermission";
import { SingleProjectPermission } from "./SingleProjectPermission";
import { WsProjectPermission } from "./WsProjectPermission";
const SINGLE_PERMISSION_LIST = [
{
title: "Integrations",
subtitle: "Integration management control",
icon: faPuzzlePiece,
formName: "integrations"
},
{
title: "Secret Protect policy",
subtitle: "Manage policies for secret protection for unauthorized secret changes",
icon: faShield,
formName: ProjectPermissionSub.SecretApproval
},
{
title: "Roles",
subtitle: "Role management control",
icon: faUsers,
formName: "role"
},
{
title: "User management",
subtitle: "Add, view and remove users from the project",
icon: faUser,
formName: "member"
},
{
title: "Group management",
subtitle: "Add, view and remove user groups from the project",
icon: faUsers,
formName: "groups"
},
{
title: "Machine identity management",
subtitle: "Add, view, update and remove (machine) identities from the project",
icon: faServer,
formName: "identity"
},
{
title: "Webhooks",
subtitle: "Webhook management control",
icon: faAnchorLock,
formName: "webhooks"
},
{
title: "Service Tokens",
subtitle: "Token management control",
icon: faKey,
formName: "service-tokens"
},
{
title: "Settings",
subtitle: "Settings control",
icon: faCog,
formName: "settings"
},
{
title: "Environments",
subtitle: "Environment management control",
icon: faElementor,
formName: "environments"
},
{
title: "Tags",
subtitle: "Tag management control",
icon: faTags,
formName: "tags"
},
{
title: "Audit Logs",
subtitle: "Audit log management control",
icon: faBook,
formName: "audit-logs"
},
{
title: "IP Allowlist",
subtitle: "IP allowlist management control",
icon: faNetworkWired,
formName: "ip-allowlist"
},
{
title: "Certificate Authorities",
subtitle: "CA management control",
icon: faCertificate,
formName: "certificate-authorities"
},
{
title: "Certificates",
subtitle: "Certificate management control",
icon: faCertificate,
formName: "certificates"
}
] as const;
type Props = {
roleSlug?: string;
onGoBack: VoidFunction;
};
export const ProjectRoleModifySection = ({ roleSlug, onGoBack }: Props) => {
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(roleSlug || "");
const isNewRole = !roleSlug;
const { currentWorkspace } = useWorkspace();
const projectSlug = currentWorkspace?.slug || "";
const { data: roleDetails, isLoading: isRoleDetailsLoading } = useGetProjectRoleBySlug(
currentWorkspace?.slug || "",
roleSlug as string
);
const {
handleSubmit,
register,
formState: { isSubmitting, isDirty, errors },
setValue,
getValues,
control
} = useForm<TFormSchema>({
values: roleDetails
? { ...roleDetails, permissions: rolePermission2Form(roleDetails.permissions) }
: ({} as TProjectRole),
resolver: zodResolver(formSchema)
});
const { mutateAsync: createRole } = useCreateProjectRole();
const { mutateAsync: updateRole } = useUpdateProjectRole();
const handleRoleUpdate = async (el: TFormSchema) => {
if (!roleDetails?.id) return;
try {
await updateRole({
id: roleDetails?.id as string,
projectSlug,
...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({
projectSlug,
...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" });
}
};
if (!isNewRole && isRoleDetailsLoading) {
return (
<div className="flex w-full items-center justify-center p-8">
<Spinner />
</div>
);
}
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">
Project-level roles allow you to define permissions for resources within projects at a
granular level
</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>
<MultiEnvProjectPermission
getValue={getValues}
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
icon={faLock}
title="Secrets"
subtitle="Create, modify and remove secrets, folders and secret imports"
formName="secrets"
/>
</div>
<div key="permission-ws">
<WsProjectPermission
control={control}
setValue={setValue}
isNonEditable={isNonEditable}
/>
</div>
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
<div key={`permission-${title}`}>
<SingleProjectPermission
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
icon={icon}
title={title}
subtitle={subtitle}
formName={formName}
/>
</div>
))}
<div key="permission-secret-rollback">
<SecretRollbackPermission
control={control}
setValue={setValue}
isNonEditable={isNonEditable}
/>
</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>
);
};

View File

@@ -1,147 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
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 "./ProjectRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "create", label: "Perform Rollback" },
{ action: "read", label: "View" }
] as const;
export const SecretRollbackPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.secret-rollback"
});
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;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if(!val) return;
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.secret-rollback",
{ read: false, create: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.secret-rollback",
{ read: true, create: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.secret-rollback",
{ read: true, create: false },
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.secret-rollback",
{ read: false, create: 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={faPuzzlePiece} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">Secret Rollback</div>
<div className="text-xs font-light">Secret rollback control actions</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 &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.secret-rollback.${action}`}
key={`permissions.secret-rollback.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.secret-rollback.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};

View File

@@ -1,194 +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 { ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
formName:
| "role"
| "member"
| "groups"
| "integrations"
| "webhooks"
| "service-tokens"
| "settings"
| "environments"
| "tags"
| "audit-logs"
| "ip-allowlist"
| "identity"
| "certificate-authorities"
| "certificates"
| ProjectPermissionSub.SecretApproval;
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 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 getPermissionList = (option: Props["formName"]) => {
switch (option) {
case "member":
return MEMBERS_PERMISSIONS;
default:
return PERMISSIONS;
}
};
export const SingleProjectPermission = ({
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) return;
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>
);
};

View File

@@ -1,126 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
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 "./ProjectRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "edit", label: "Update project details" },
{ action: "delete", label: "Delete projects" }
] as const;
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.workspace"
});
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;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if(!val) return;
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
break;
case Permission.FullAccess:
setValue("permissions.workspace", { edit: true, delete: true }, { shouldDirty: true });
break;
default:
setValue("permissions.workspace", { edit: 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={faPuzzlePiece} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">Project</div>
<div className="text-xs font-light">Project control actions</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</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 &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.workspace.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};

View File

@@ -1 +0,0 @@
export { ProjectRoleModifySection } from "./ProjectRoleModifySection";

View File

@@ -0,0 +1,158 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDeleteProjectRole,useGetProjectRoleBySlug } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from "../Types";
import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components";
export const RolePage = withProjectPermission(
() => {
const router = useRouter();
const roleSlug = router.query.roleSlug as string;
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string);
const { mutateAsync: deleteProjectRole } = useDeleteProjectRole();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"role",
"deleteRole"
] as const);
const onDeleteRoleSubmit = async () => {
try {
if (!currentWorkspace?.slug || !data?.id) return;
await deleteProjectRole({
projectSlug: currentWorkspace.slug,
id: data.id
});
createNotification({
text: "Successfully deleted project role",
type: "success"
});
handlePopUpClose("deleteRole");
router.push(`/project/${projectId}/members?selectedTab=${TabSections.Roles}`);
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete project role";
createNotification({
text,
type: "error"
});
}
};
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? "");
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => router.push(`/project/${projectId}/members?selectedTab=${TabSections.Roles}`)}
className="mb-4"
>
Roles
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.name}</p>
{isCustomRole && (
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() =>
handlePopUpOpen("role", {
roleSlug
})
}
disabled={!isAllowed}
>
Edit Role
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => handlePopUpOpen("deleteRole")}
disabled={!isAllowed}
>
Delete Role
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<div className="flex">
<div className="mr-4 w-96">
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />
</div>
<RolePermissionsSection roleSlug={roleSlug} />
</div>
</div>
)}
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete the project role ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => onDeleteRoleSubmit()}
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Role }
);

View File

@@ -0,0 +1,95 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetProjectRoleBySlug } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
roleSlug: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: {}) => void;
};
export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { currentWorkspace } = useWorkspace();
const { data } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string);
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? "");
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Project Role Details</h3>
{isCustomRole && (
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Role}>
{(isAllowed) => {
return (
<Tooltip content="Edit Role">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("role", {
roleSlug
});
}}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</ProjectPermissionCan>
)}
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Role ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{data.id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(data.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
<p className="text-sm text-mineshaft-300">{data.slug}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
<p className="text-sm text-mineshaft-300">
{data.description?.length ? data.description : "-"}
</p>
</div>
</div>
</div>
) : (
<div />
);
};

View File

@@ -0,0 +1,196 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useCreateProjectRole,
useGetProjectRoleBySlug,
useUpdateProjectRole} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
name: z.string(),
description: z.string(),
slug: z.string()
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["role"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["role"]>, state?: boolean) => void;
};
export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
const router = useRouter();
const popupData = popUp?.role?.data as {
roleSlug: string;
};
const { currentWorkspace } = useWorkspace();
const projectSlug = currentWorkspace?.slug || "";
const { data: role } = useGetProjectRoleBySlug(projectSlug, popupData?.roleSlug ?? "");
const { mutateAsync: createProjectRole } = useCreateProjectRole();
const { mutateAsync: updateProjectRole } = useUpdateProjectRole();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: ""
}
});
useEffect(() => {
if (role) {
reset({
name: role.name,
description: role.description,
slug: role.slug
});
} else {
reset({
name: "",
description: "",
slug: ""
});
}
}, [role]);
const onFormSubmit = async ({ name, description, slug }: FormData) => {
try {
if (!projectSlug) return;
if (role) {
// update
await updateProjectRole({
id: role.id,
projectSlug,
name,
description,
slug
});
handlePopUpToggle("role", false);
} else {
// create
const newRole = await createProjectRole({
projectSlug,
name,
description,
slug,
permissions: []
});
router.push(`/project/${currentWorkspace?.id}/roles/${newRole.slug}`);
handlePopUpToggle("role", false);
}
createNotification({
text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ??
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.role?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("role", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.role?.data ? "Update" : "Create"} Role`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Billing Team" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Slug"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="billing" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="To manage billing" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.role?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("role", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

Some files were not shown because too many files have changed in this diff Show More