resolve merge conflict

This commit is contained in:
Scott Wilson
2024-09-12 08:03:10 -07:00
64 changed files with 1157 additions and 573 deletions
+5 -1
View File
@@ -122,6 +122,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}) })
.merge( .merge(
z.object({ z.object({
project: z.object({
name: z.string(),
slug: z.string()
}),
event: z.object({ event: z.object({
type: z.string(), type: z.string(),
metadata: z.any() metadata: z.any()
@@ -138,7 +142,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const auditLogs = await server.services.auditLog.listProjectAuditLogs({ const auditLogs = await server.services.auditLog.listAuditLogs({
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
+7
View File
@@ -103,6 +103,13 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
if (!email || !profile.firstName) { if (!email || !profile.firstName) {
logger.info(
{
err: new Error("Invalid saml request. Missing email or first name"),
profile
},
`email: ${email} firstName: ${profile.firstName as string}`
);
throw new BadRequestError({ message: "Invalid request. Missing email or first name" }); throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
} }
@@ -1,9 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { AuditLogsSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, stripUndefinedInWhere } from "@app/lib/knex"; import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -33,23 +33,44 @@ export const auditLogDALFactory = (db: TDbClient) => {
.where( .where(
stripUndefinedInWhere({ stripUndefinedInWhere({
projectId, projectId,
orgId, [`${TableName.AuditLog}.orgId`]: orgId,
eventType, eventType,
actor,
userAgentType userAgentType
}) })
) )
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.AuditLog))
.select(
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
)
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
.orderBy("createdAt", "desc"); .orderBy(`${TableName.AuditLog}.createdAt`, "desc");
if (actor) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]);
}
if (startDate) { if (startDate) {
void sqlQuery.where("createdAt", ">=", startDate); void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
} }
if (endDate) { if (endDate) {
void sqlQuery.where("createdAt", "<=", endDate); void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
} }
const docs = await sqlQuery; const docs = await sqlQuery;
return docs;
return docs.map((doc) => ({
...AuditLogsSchema.parse(doc),
project: {
name: doc.projectName,
slug: doc.projectSlug
}
}));
} catch (error) { } catch (error) {
throw new DatabaseError({ error }); throw new DatabaseError({ error });
} }
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TAuditLogDALFactory } from "./audit-log-dal"; import { TAuditLogDALFactory } from "./audit-log-dal";
@@ -11,7 +12,7 @@ import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-
type TAuditLogServiceFactoryDep = { type TAuditLogServiceFactoryDep = {
auditLogDAL: TAuditLogDALFactory; auditLogDAL: TAuditLogDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
auditLogQueue: TAuditLogQueueServiceFactory; auditLogQueue: TAuditLogQueueServiceFactory;
}; };
@@ -22,7 +23,7 @@ export const auditLogServiceFactory = ({
auditLogQueue, auditLogQueue,
permissionService permissionService
}: TAuditLogServiceFactoryDep) => { }: TAuditLogServiceFactoryDep) => {
const listProjectAuditLogs = async ({ const listAuditLogs = async ({
userAgentType, userAgentType,
eventType, eventType,
offset, offset,
@@ -36,14 +37,33 @@ export const auditLogServiceFactory = ({
projectId, projectId,
auditLogActor auditLogActor
}: TListProjectAuditLogDTO) => { }: TListProjectAuditLogDTO) => {
const { permission } = await permissionService.getProjectPermission( if (projectId) {
actor, const { permission } = await permissionService.getProjectPermission(
actorId, actor,
projectId, actorId,
actorAuthMethod, projectId,
actorOrgId actorAuthMethod,
); actorOrgId
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
} else {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
/**
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
* to the organization level
*/
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
}
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
const auditLogs = await auditLogDAL.find({ const auditLogs = await auditLogDAL.find({
startDate, startDate,
endDate, endDate,
@@ -52,8 +72,9 @@ export const auditLogServiceFactory = ({
eventType, eventType,
userAgentType, userAgentType,
actor: auditLogActor, actor: auditLogActor,
projectId ...(projectId ? { projectId } : { orgId: actorOrgId })
}); });
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({ return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
...el, ...el,
event: { type: logEventType, metadata: eventMetadata }, event: { type: logEventType, metadata: eventMetadata },
@@ -76,6 +97,6 @@ export const auditLogServiceFactory = ({
return { return {
createAuditLog, createAuditLog,
listProjectAuditLogs listAuditLogs
}; };
}; };
@@ -6,14 +6,14 @@ import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export type TListProjectAuditLogDTO = { export type TListProjectAuditLogDTO = {
auditLogActor?: string; auditLogActor?: string;
projectId: string; projectId?: string;
eventType?: string; eventType?: string;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
userAgentType?: string; userAgentType?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
} & TProjectPermission; } & Omit<TProjectPermission, "projectId">;
export type TCreateAuditLogDTO = { export type TCreateAuditLogDTO = {
event: Event; event: Event;
+3 -1
View File
@@ -452,7 +452,9 @@ export const PROJECT_USERS = {
INVITE_MEMBER: { INVITE_MEMBER: {
projectId: "The ID of the project to invite the member to.", projectId: "The ID of the project to invite the member to.",
emails: "A list of organization member emails to invite to the project.", emails: "A list of organization member emails to invite to the project.",
usernames: "A list of usernames to invite to the project." usernames: "A list of usernames to invite to the project.",
roleSlugs:
"A list of role slugs to assign to the newly created project membership. If nothing is provided, it will default to the Member role."
}, },
REMOVE_MEMBER: { REMOVE_MEMBER: {
projectId: "The ID of the project to remove the member from.", projectId: "The ID of the project to remove the member from.",
+4 -2
View File
@@ -473,6 +473,8 @@ export const registerRoutes = async (
userAliasDAL, userAliasDAL,
orgMembershipDAL, orgMembershipDAL,
tokenService, tokenService,
permissionService,
groupProjectDAL,
smtpService, smtpService,
projectMembershipDAL projectMembershipDAL
}); });
@@ -497,6 +499,7 @@ export const registerRoutes = async (
tokenService, tokenService,
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
projectRoleDAL,
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
orgMembershipDAL, orgMembershipDAL,
@@ -504,8 +507,7 @@ export const registerRoutes = async (
smtpService, smtpService,
userDAL, userDAL,
groupDAL, groupDAL,
orgBotDAL, orgBotDAL
projectRoleDAL
}); });
const signupService = authSignupServiceFactory({ const signupService = authSignupServiceFactory({
tokenService, tokenService,
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { import {
AuditLogsSchema,
GroupsSchema, GroupsSchema,
IncidentContactsSchema, IncidentContactsSchema,
OrganizationsSchema, OrganizationsSchema,
@@ -8,7 +9,9 @@ import {
OrgRolesSchema, OrgRolesSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -62,6 +65,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/audit-logs",
config: {
rateLimit: readLimit
},
schema: {
description: "Get all audit logs for an organization",
querystring: z.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit),
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
}),
response: {
200: z.object({
auditLogs: AuditLogsSchema.omit({
eventMetadata: true,
eventType: true,
actor: true,
actorMetadata: true
})
.merge(
z.object({
project: z.object({
name: z.string(),
slug: z.string()
}),
event: z.object({
type: z.string(),
metadata: z.any()
}),
actor: z.object({
type: z.string(),
metadata: z.any()
})
})
)
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const auditLogs = await server.services.auditLog.listAuditLogs({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.query,
endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActor: req.query.actor,
actor: req.permission.type
});
return { auditLogs };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:organizationId/users", url: "/:organizationId/users",
@@ -134,4 +134,39 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
); );
} }
}); });
server.route({
method: "GET",
url: "/me/:username/groups",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
username: z.string().trim()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string(),
slug: z.string(),
orgId: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const groupMemberships = await server.services.user.listUserGroups({
username: req.params.username,
actorOrgId: req.permission.orgId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type
});
return groupMemberships;
}
});
}; };
@@ -26,7 +26,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails), emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails),
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames) usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames),
roleSlugs: z.string().array().optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -95,6 +95,30 @@ export const groupProjectDALFactory = (db: TDbClient) => {
} }
}; };
const findByUserId = async (userId: string, orgId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.userId`, userId)
.join(TableName.Groups, function () {
this.on(`${TableName.UserGroupMembership}.groupId`, "=", `${TableName.Groups}.id`).andOn(
`${TableName.Groups}.orgId`,
"=",
db.raw("?", [orgId])
);
})
.select(
db.ref("id").withSchema(TableName.Groups),
db.ref("name").withSchema(TableName.Groups),
db.ref("slug").withSchema(TableName.Groups),
db.ref("orgId").withSchema(TableName.Groups)
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindByUserId" });
}
};
// The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId). // The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId).
// We need to join the GroupProjectMembership table with the Groups table to get the group name and slug. // We need to join the GroupProjectMembership table with the Groups table to get the group name and slug.
// We also need to join the GroupProjectMembershipRole table to get the role of the group in the project. // We also need to join the GroupProjectMembershipRole table to get the role of the group in the project.
@@ -197,5 +221,5 @@ export const groupProjectDALFactory = (db: TDbClient) => {
return members; return members;
}; };
return { ...groupProjectOrm, findByProjectId, findAllProjectGroupMembers }; return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers };
}; };
@@ -153,7 +153,6 @@ export const orgAdminServiceFactory = ({
members: [ members: [
{ {
orgMembershipId: membership.id, orgMembershipId: membership.id,
projectMembershipRole: ProjectMembershipRole.Admin,
userPublicKey: userEncryptionKey.publicKey userPublicKey: userEncryptionKey.publicKey
} }
] ]
+2 -2
View File
@@ -114,11 +114,11 @@ export const orgServiceFactory = ({
tokenService, tokenService,
orgBotDAL, orgBotDAL,
licenseService, licenseService,
projectRoleDAL,
samlConfigDAL, samlConfigDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
projectBotDAL, projectBotDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL
projectRoleDAL
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
/* /*
* Get organization details by the organization id * Get organization details by the organization id
@@ -1,190 +0,0 @@
import { Knex } from "knex";
import { ProjectMembershipRole, SecretKeyEncoding, TProjectMemberships } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
type TAddMembersToProjectArg = {
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectById" | "findProjectGhostUser">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "insertMany">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
smtpService: Pick<TSmtpService, "sendMail">;
};
type AddMembersToNonE2EEProjectDTO = {
emails: string[];
usernames: string[];
projectId: string;
projectMembershipRole: ProjectMembershipRole;
sendEmails?: boolean;
};
type AddMembersToNonE2EEProjectOptions = {
tx?: Knex;
throwOnProjectNotFound?: boolean;
};
export const addMembersToProject = ({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
projectBotDAL,
userGroupMembershipDAL,
projectUserMembershipRoleDAL,
smtpService
}: TAddMembersToProjectArg) => {
// Can create multiple memberships for a singular project, based on user email / username
const addMembersToNonE2EEProject = async (
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
) => {
const processTransaction = async (tx: Knex) => {
const usernamesAndEmails = [...emails, ...usernames];
const project = await projectDAL.findProjectById(projectId);
if (!project) {
if (options.throwOnProjectNotFound) {
throw new BadRequestError({ message: "Project not found when attempting to add user to project" });
}
return [];
}
const orgMembers = await orgDAL.findOrgMembersByUsername(
project.orgId,
[...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))],
tx
);
if (orgMembers.length !== usernamesAndEmails.length)
throw new BadRequestError({ message: "Some users are not part of org" });
if (!orgMembers.length) return [];
const existingMembers = await projectMembershipDAL.find({
projectId,
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const newWsMembers = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: orgMembers.map((membership) => ({
orgMembershipId: membership.id,
projectMembershipRole,
userPublicKey: membership.user.publicKey
}))
});
const members: TProjectMemberships[] = [];
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id
})),
tx
);
await projectUserMembershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
tx
);
members.push(...projectMemberships);
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
tx
);
if (sendEmails) {
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
const appCfg = getConfig();
if (recipients.length) {
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
}
return members;
};
if (options.tx) {
return processTransaction(options.tx);
}
return projectMembershipDAL.transaction(processTransaction);
};
return {
addMembersToNonE2EEProject
};
};
@@ -42,7 +42,7 @@ type TProjectMembershipServiceFactoryDep = {
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">; userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory; userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "findOne">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">; projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
@@ -53,4 +53,5 @@ export type TAddUsersToWorkspaceNonE2EEDTO = {
sendEmails?: boolean; sendEmails?: boolean;
emails: string[]; emails: string[];
usernames: string[]; usernames: string[];
roleSlugs?: string[];
} & TProjectPermission; } & TProjectPermission;
+1 -2
View File
@@ -16,7 +16,7 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK
privateKey: userPrivateKey privateKey: userPrivateKey
}); });
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => { const newWsMembers = members.map(({ orgMembershipId, userPublicKey }) => {
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric( const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
plaintextProjectKey, plaintextProjectKey,
userPublicKey, userPublicKey,
@@ -25,7 +25,6 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK
return { return {
orgMembershipId, orgMembershipId,
projectRole: projectMembershipRole,
workspaceEncryptedKey: inviteeCipherText, workspaceEncryptedKey: inviteeCipherText,
workspaceEncryptedNonce: inviteeNonce workspaceEncryptedNonce: inviteeNonce
}; };
@@ -300,8 +300,7 @@ export const projectQueueFactory = ({
members: [ members: [
{ {
userPublicKey: user.publicKey, userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id, orgMembershipId: orgMembership.id
projectMembershipRole: ProjectMembershipRole.Admin
} }
] ]
}); });
@@ -277,8 +277,7 @@ export const projectServiceFactory = ({
members: [ members: [
{ {
userPublicKey: user.publicKey, userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id, orgMembershipId: orgMembership.id
projectMembershipRole: ProjectMembershipRole.Admin
} }
] ]
}); });
@@ -292,7 +291,7 @@ export const projectServiceFactory = ({
tx tx
); );
await projectUserMembershipRoleDAL.create( await projectUserMembershipRoleDAL.create(
{ projectMembershipId: userProjectMembership.id, role: projectAdmin.projectRole }, { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
tx tx
); );
@@ -1,4 +1,4 @@
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas"; import { TProjectKeys } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
@@ -88,7 +88,6 @@ export type AddUserToWsDTO = {
userPrivateKey: string; userPrivateKey: string;
members: { members: {
orgMembershipId: string; orgMembershipId: string;
projectMembershipRole: ProjectMembershipRole;
userPublicKey: string; userPublicKey: string;
}[]; }[];
}; };
+33 -1
View File
@@ -1,4 +1,8 @@
import { ForbiddenError } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas"; import { SecretKeyEncoding } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
@@ -8,8 +12,10 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { AuthMethod } from "../auth/auth-type"; import { AuthMethod } from "../auth/auth-type";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TUserDALFactory } from "./user-dal"; import { TUserDALFactory } from "./user-dal";
import { TListUserGroupsDTO } from "./user-types";
type TUserServiceFactoryDep = { type TUserServiceFactoryDep = {
userDAL: Pick< userDAL: Pick<
@@ -27,10 +33,12 @@ type TUserServiceFactoryDep = {
| "delete" | "delete"
>; >;
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">; userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "findByUserId">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">; tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
permissionService: TPermissionServiceFactory;
}; };
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>; export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
@@ -40,8 +48,10 @@ export const userServiceFactory = ({
userAliasDAL, userAliasDAL,
orgMembershipDAL, orgMembershipDAL,
projectMembershipDAL, projectMembershipDAL,
groupProjectDAL,
tokenService, tokenService,
smtpService smtpService,
permissionService
}: TUserServiceFactoryDep) => { }: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (username: string) => { const sendEmailVerificationCode = async (username: string) => {
const user = await userDAL.findOne({ username }); const user = await userDAL.findOne({ username });
@@ -295,6 +305,27 @@ export const userServiceFactory = ({
return updatedOrgMembership.projectFavorites; return updatedOrgMembership.projectFavorites;
}; };
const listUserGroups = async ({ username, actorOrgId, actor, actorId, actorAuthMethod }: TListUserGroupsDTO) => {
const user = await userDAL.findOne({
username
});
// This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission.
if (user.id !== actorId) {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
}
const memberships = await groupProjectDAL.findByUserId(user.id, actorOrgId);
return memberships;
};
return { return {
sendEmailVerificationCode, sendEmailVerificationCode,
verifyEmailVerificationCode, verifyEmailVerificationCode,
@@ -304,6 +335,7 @@ export const userServiceFactory = ({
deleteUser, deleteUser,
getMe, getMe,
createUserAction, createUserAction,
listUserGroups,
getUserAction, getUserAction,
unlockUser, unlockUser,
getUserPrivateKey, getUserPrivateKey,
+6
View File
@@ -1,3 +1,9 @@
import { TOrgPermission } from "@app/lib/types";
export type TListUserGroupsDTO = {
username: string;
} & Omit<TOrgPermission, "orgId">;
export enum UserEncryption { export enum UserEncryption {
V1 = 1, V1 = 1,
V2 = 2 V2 = 2
@@ -6,6 +6,55 @@ description: "Learn how to setup Slack integration"
This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects. This guide will provide step by step instructions on how to configure Slack integration for your Infisical projects.
<Tabs> <Tabs>
<Tab title="Infisical Cloud">
## Create Slack workflow integration
<Steps>
<Step title="Navigate to the Workflow Integrations tab in your organization settings">
In order to use Slack integration in your projects, you will first have to
configure a Slack workflow integration in your organization.
![org-slack-overview](/images/platform/workflow-integrations/slack-integration/org-slack-integration-overview.png)
</Step>
<Step title="Install Slack app to workspace">
Press "Add" and select "Slack" as the platform.
![org-slack-initial-add](/images/platform/workflow-integrations/slack-integration/org-slack-integration-initial-add.png)
Give your Slack integration a descriptive alias. You will use this to select the Slack integration for your project.
![org-slack-add-form](/images/platform/workflow-integrations/slack-integration/org-slack-integration-add-form.png)
Press **Connect Slack**. This opens up the Slack app installation flow. Select the Slack workspace you want to install the custom Slack app to and press **Allow**.
![org-slack-authenticate](/images/platform/workflow-integrations/slack-integration/cloud-org-slack-integration-authenticate.png)
This completes the workflow integration creation flow. The projects in your organization can now use this Slack integration to send real-time updates to your Slack workspace.
![org-slack-workspace](/images/platform/workflow-integrations/slack-integration/cloud-org-slack-integration-workspace.png)
![org-slack-created](/images/platform/workflow-integrations/slack-integration/org-slack-integration-created.png)
</Step>
</Steps>
## Configure project to use Slack workflow integration
<Steps>
<Step title="Navigate to the Workflow Integrations tab in the project settings">
![project-slack-overview](/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png)
</Step>
<Step title="Select the Slack integration to use for the project">
Your project will send notifications to the connected Slack workspace of the
selected Slack integration when the configured events are triggered.
![project-slack-select](/images/platform/workflow-integrations/slack-integration/project-slack-integration-select.png)
</Step>
<Step title="Configure the Slack notification settings for the project and click Save.">
![project-slack-select](/images/platform/workflow-integrations/slack-integration/project-slack-integration-config.png)
<Info>
To enable notifications in private Slack channels, you need to invite the Infisical Slack bot to join those channels.
</Info>
You now have a working native integration with Slack!
</Step>
</Steps>
</Tab>
<Tab title="Self-hosted setup"> <Tab title="Self-hosted setup">
## Configure admin settings ## Configure admin settings
Note that this step only has to be done once for the entire instance. Note that this step only has to be done once for the entire instance.
@@ -41,7 +90,7 @@ This guide will provide step by step instructions on how to configure Slack inte
</Steps> </Steps>
## Create Slack workflow integration ## Create Slack workflow integration
<Steps> <Steps>
<Step title="Navigate to the Workflow Integrations tab in your organization settings"> <Step title="Navigate to the Workflow Integrations tab in your organization settings">
@@ -67,26 +116,27 @@ This guide will provide step by step instructions on how to configure Slack inte
</Steps> </Steps>
## Configure project to use Slack workflow integration ## Configure project to use Slack workflow integration
<Steps> <Steps>
<Step title="Navigate to the Workflow Integrations tab in the project settings"> <Step title="Navigate to the Workflow Integrations tab in the project settings">
![project-slack-overview](/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png) ![project-slack-overview](/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png)
</Step> </Step>
<Step title="Select the Slack integration to use for the project"> <Step title="Select the Slack integration to use for the project">
Your project will send notifications to the connected Slack workspace of the Your project will send notifications to the connected Slack workspace of the
selected Slack integration when the configured events are triggered. selected Slack integration when the configured events are triggered.
![project-slack-select](/images/platform/workflow-integrations/slack-integration/project-slack-integration-select.png) ![project-slack-select](/images/platform/workflow-integrations/slack-integration/project-slack-integration-select.png)
</Step> </Step>
<Step title="Configure the Slack notification settings for the project and click Save."> <Step title="Configure the Slack notification settings for the project and click Save.">
![project-slack-select](/images/platform/workflow-integrations/slack-integration/project-slack-integration-config.png) ![project-slack-select](/images/platform/workflow-integrations/slack-integration/project-slack-integration-config.png)
<Info> <Info>
To enable notifications in private Slack channels, you need to invite your Slack bot to join those channels. To enable notifications in private Slack channels, you need to invite your Slack bot to join those channels.
</Info> </Info>
You now have a working native integration with Slack! You now have a working native integration with Slack!
</Step> </Step>
</Steps>
</Steps>
</Tab> </Tab>
</Tabs> </Tabs>
Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

@@ -50,7 +50,8 @@ export default function EnterEmailStep({
// If everything is correct, go to the next step // If everything is correct, go to the next step
if (!emailCheckBool) { if (!emailCheckBool) {
try { try {
await mutateAsync({ email }); await mutateAsync({ email: email.toLowerCase() });
setEmail(email.toLowerCase())
incrementStep(); incrementStep();
} catch (e) { } catch (e) {
if (axios.isAxiosError(e)) { if (axios.isAxiosError(e)) {
@@ -60,7 +60,7 @@ export default function TeamInviteStep(): JSX.Element {
placeholder="[email protected], [email protected]..." placeholder="[email protected], [email protected]..."
/> />
</div> </div>
<div className="mx-auto mt-0 mt-2 flex w-full flex-row items-end justify-end text-sm md:mt-4 md:mb-2 md:min-w-[30rem] md:max-w-md"> <div className="mx-auto mt-2 flex w-full flex-row items-end justify-end text-sm md:mt-4 md:mb-2 md:min-w-[30rem] md:max-w-md">
<Button <Button
onClick={() => { onClick={() => {
if (serverDetails?.emailConfigured) { if (serverDetails?.emailConfigured) {
+1 -1
View File
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { userKeys } from "../users/queries"; import { userKeys } from "../users";
import { import {
APIKeyDataV2, APIKeyDataV2,
CreateAPIKeyDataV2DTO, CreateAPIKeyDataV2DTO,
+15 -15
View File
@@ -4,28 +4,28 @@ import { apiRequest } from "@app/config/request";
import { Actor, AuditLog, AuditLogFilters } from "./types"; import { Actor, AuditLog, AuditLogFilters } from "./types";
export const workspaceKeys = { export const auditLogKeys = {
getAuditLogs: (workspaceId: string, filters: AuditLogFilters) => getAuditLogs: (workspaceId: string | null, filters: AuditLogFilters) =>
[{ workspaceId, filters }, "audit-logs"] as const, [{ workspaceId, filters }, "audit-logs"] as const,
getAuditLogActorFilterOpts: (workspaceId: string) => getAuditLogActorFilterOpts: (workspaceId: string) =>
[{ workspaceId }, "audit-log-actor-filters"] as const [{ workspaceId }, "audit-log-actor-filters"] as const
}; };
export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => { export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string | null) => {
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: workspaceKeys.getAuditLogs(workspaceId, filters), queryKey: auditLogKeys.getAuditLogs(workspaceId, filters),
queryFn: async ({ pageParam }) => { queryFn: async ({ pageParam }) => {
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>( const auditLogEndpoint = workspaceId
`/api/v1/workspace/${workspaceId}/audit-logs`, ? `/api/v1/workspace/${workspaceId}/audit-logs`
{ : "/api/v1/organization/audit-logs";
params: { const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(auditLogEndpoint, {
...filters, params: {
offset: pageParam, ...filters,
startDate: filters?.startDate?.toISOString(), offset: pageParam,
endDate: filters?.endDate?.toISOString() startDate: filters?.startDate?.toISOString(),
} endDate: filters?.endDate?.toISOString()
} }
); });
return data.auditLogs; return data.auditLogs;
}, },
getNextPageParam: (lastPage, pages) => getNextPageParam: (lastPage, pages) =>
@@ -35,7 +35,7 @@ export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) =
export const useGetAuditLogActorFilterOpts = (workspaceId: string) => { export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
return useQuery({ return useQuery({
queryKey: workspaceKeys.getAuditLogActorFilterOpts(workspaceId), queryKey: auditLogKeys.getAuditLogActorFilterOpts(workspaceId),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ actors: Actor[] }>( const { data } = await apiRequest.get<{ actors: Actor[] }>(
`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors` `/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`
@@ -851,6 +851,10 @@ export type AuditLog = {
userAgentType: UserAgentType; userAgentType: UserAgentType;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
project: {
name: string;
slug: string;
};
}; };
export type AuditLogFilters = { export type AuditLogFilters = {
+1 -1
View File
@@ -5,7 +5,7 @@ import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery"; import { setAuthToken } from "@app/reactQuery";
import { organizationKeys } from "../organization/queries"; import { organizationKeys } from "../organization/queries";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { import {
ChangePasswordDTO, ChangePasswordDTO,
CompleteAccountDTO, CompleteAccountDTO,
+1 -1
View File
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { caKeys } from "./queries"; import { caKeys } from "./queries";
import { import {
TCertificateAuthority, TCertificateAuthority,
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { caKeys } from "../ca/queries"; import { caKeys } from "../ca/queries";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { certTemplateKeys } from "./queries"; import { certTemplateKeys } from "./queries";
import { import {
TCertificateTemplate, TCertificateTemplate,
@@ -54,7 +54,9 @@ export const useDeleteCertTemplate = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<TCertificateTemplate, {}, TDeleteCertificateTemplateDTO>({ return useMutation<TCertificateTemplate, {}, TDeleteCertificateTemplateDTO>({
mutationFn: async (data) => { mutationFn: async (data) => {
const { data: certificateTemplate } = await apiRequest.delete<TCertificateTemplate>(`/api/v1/pki/certificate-templates/${data.id}`); const { data: certificateTemplate } = await apiRequest.delete<TCertificateTemplate>(
`/api/v1/pki/certificate-templates/${data.id}`
);
return certificateTemplate; return certificateTemplate;
}, },
onSuccess: ({ caId }, { projectId, id }) => { onSuccess: ({ caId }, { projectId, id }) => {
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types"; import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
export const useDeleteCert = () => { export const useDeleteCert = () => {
+82 -106
View File
@@ -3,128 +3,104 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { organizationKeys } from "../organization/queries"; import { organizationKeys } from "../organization/queries";
import { userKeys } from "../users/query-keys";
import { groupKeys } from "./queries"; import { groupKeys } from "./queries";
import { TGroup } from "./types"; import { TGroup } from "./types";
export const useCreateGroup = () => { export const useCreateGroup = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
name, name,
slug, slug,
role role
}: { }: {
name: string; name: string;
slug: string; slug: string;
organizationId: string; organizationId: string;
role?: string; role?: string;
}) => { }) => {
const { const { data: group } = await apiRequest.post<TGroup>("/api/v1/groups", {
data: group name,
} = await apiRequest.post<TGroup>("/api/v1/groups", { slug,
name, role
slug, });
role
}); return group;
},
return group; onSuccess: (_, { organizationId }) => {
}, queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId));
onSuccess: (_, { organizationId }) => { }
queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId)); });
}
});
}; };
export const useUpdateGroup = () => { export const useUpdateGroup = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
currentSlug, currentSlug,
name, name,
slug, slug,
role role
}: { }: {
currentSlug: string; currentSlug: string;
name?: string; name?: string;
slug?: string; slug?: string;
role?: string; role?: string;
}) => { }) => {
const { const { data: group } = await apiRequest.patch<TGroup>(`/api/v1/groups/${currentSlug}`, {
data: group name,
} = await apiRequest.patch<TGroup>(`/api/v1/groups/${currentSlug}`, { slug,
name, role
slug, });
role
}); return group;
},
return group; onSuccess: ({ orgId }) => {
}, queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
onSuccess: ({ orgId }) => { }
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId)); });
}
});
}; };
export const useDeleteGroup = () => { export const useDeleteGroup = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({ slug }: { slug: string }) => {
slug const { data: group } = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}`);
}: {
slug: string;
}) => {
const {
data: group
} = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}`);
return group; return group;
}, },
onSuccess: ({ orgId }) => { onSuccess: ({ orgId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId)); queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
} }
}); });
}; };
export const useAddUserToGroup = () => { export const useAddUserToGroup = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({ slug, username }: { slug: string; username: string }) => {
slug, const { data } = await apiRequest.post<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
username
}: {
slug: string;
username: string;
}) => {
const {
data
} = await apiRequest.post<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
return data; return data;
}, },
onSuccess: (_, { slug }) => { onSuccess: (_, { slug }) => {
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug)); queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
} }
}); });
}; };
export const useRemoveUserFromGroup = () => { export const useRemoveUserFromGroup = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({ slug, username }: { slug: string; username: string }) => {
slug, const { data } = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
username
}: {
slug: string;
username: string;
}) => {
const {
data
} = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
return data; return data;
}, },
onSuccess: (_, { slug }) => { onSuccess: (_, { slug, username }) => {
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug)); queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
} queryClient.invalidateQueries(userKeys.listUserGroupMemberships(username));
}); }
}; });
};
+9 -2
View File
@@ -4,7 +4,7 @@ import { TOrgRole } from "../roles/types";
export type TGroupOrgMembership = TGroup & { export type TGroupOrgMembership = TGroup & {
customRole?: TOrgRole; customRole?: TOrgRole;
} };
export type TGroup = { export type TGroup = {
id: string; id: string;
@@ -33,4 +33,11 @@ export type TGroupMembership = {
}[]; }[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
export type TGroupWithProjectMemberships = {
id: string;
name: string;
slug: string;
orgId: string;
};
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { import {
App, App,
BitBucketWorkspace, BitBucketWorkspace,
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { TCloudIntegration } from "./types"; import { TCloudIntegration } from "./types";
export const integrationQueryKeys = { export const integrationQueryKeys = {
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { pkiAlertKeys } from "./queries"; import { pkiAlertKeys } from "./queries";
import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types"; import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types";
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { pkiCollectionKeys } from "./queries"; import { pkiCollectionKeys } from "./queries";
import { import {
TAddItemToPkiCollectionDTO, TAddItemToPkiCollectionDTO,
@@ -11,7 +11,8 @@ import {
TPkiCollection, TPkiCollection,
TPkiCollectionItem, TPkiCollectionItem,
TRemoveItemFromPkiCollectionDTO, TRemoveItemFromPkiCollectionDTO,
TUpdatePkiCollectionTO} from "./types"; TUpdatePkiCollectionTO
} from "./types";
export const useCreatePkiCollection = () => { export const useCreatePkiCollection = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
+2
View File
@@ -21,6 +21,7 @@ export {
useGetOrgUsers, useGetOrgUsers,
useGetUser, useGetUser,
useGetUserAction, useGetUserAction,
useListUserGroupMemberships,
useLogoutUser, useLogoutUser,
useRegisterUserAction, useRegisterUserAction,
useRevokeMySessions, useRevokeMySessions,
@@ -28,3 +29,4 @@ export {
useUpdateOrgMembership, useUpdateOrgMembership,
useUpdateUserAuthMethods useUpdateUserAuthMethods
} from "./queries"; } from "./queries";
export { userKeys } from "./query-keys";
+5 -4
View File
@@ -6,8 +6,8 @@ import {
} from "@app/components/utilities/cryptography/crypto"; } from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace";
import { userKeys } from "./queries"; import { userKeys } from "./query-keys";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types"; import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
export const useAddUserToWsE2EE = () => { export const useAddUserToWsE2EE = () => {
@@ -51,9 +51,10 @@ export const useAddUserToWsNonE2EE = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({ return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
mutationFn: async ({ projectId, usernames }) => { mutationFn: async ({ projectId, usernames, roleSlugs }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, { const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
usernames usernames,
roleSlugs
}); });
return data; return data;
}, },
+24 -23
View File
@@ -5,6 +5,9 @@ import { SessionStorageKeys } from "@app/const";
import { setAuthToken } from "@app/reactQuery"; import { setAuthToken } from "@app/reactQuery";
import { APIKeyDataV2 } from "../apiKeys/types"; import { APIKeyDataV2 } from "../apiKeys/types";
import { TGroupWithProjectMemberships } from "../groups/types";
import { workspaceKeys } from "../workspace";
import { userKeys } from "./query-keys";
import { import {
AddUserToOrgDTO, AddUserToOrgDTO,
APIKeyData, APIKeyData,
@@ -20,28 +23,6 @@ import {
UserEnc UserEnc
} from "./types"; } from "./types";
export const userKeys = {
getUser: ["user"] as const,
getPrivateKey: ["user"] as const,
userAction: ["user-action"] as const,
userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const,
getOrgMembership: (orgId: string, orgMembershipId: string) =>
[{ orgId, orgMembershipId }, "org-membership"] as const,
allOrgMembershipProjectMemberships: (orgId: string) => [orgId, "all-user-memberships"] as const,
forOrgMembershipProjectMemberships: (orgId: string, orgMembershipId: string) =>
[...userKeys.allOrgMembershipProjectMemberships(orgId), { orgMembershipId }] as const,
getOrgMembershipProjectMemberships: (orgId: string, username: string) =>
[{ orgId, username }, "org-membership-project-memberships"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const,
listUsers: ["user-list"] as const,
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
};
export const fetchUserDetails = async () => { export const fetchUserDetails = async () => {
const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user"); const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user");
@@ -173,8 +154,15 @@ export const useAddUsersToOrg = () => {
mutationFn: (dto) => { mutationFn: (dto) => {
return apiRequest.post("/api/v1/invite-org/signup", dto); return apiRequest.post("/api/v1/invite-org/signup", dto);
}, },
onSuccess: (_, { organizationId }) => { onSuccess: (_, { organizationId, projects }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
projects?.forEach((project) => {
if (project.slug) {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(project.slug));
}
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(project.id));
});
} }
}); });
}; };
@@ -444,3 +432,16 @@ export const fetchMyPrivateKey = async () => {
return privateKey; return privateKey;
}; };
export const useListUserGroupMemberships = (username: string) => {
return useQuery({
queryKey: userKeys.listUserGroupMemberships(username),
queryFn: async () => {
const { data } = await apiRequest.get<TGroupWithProjectMemberships[]>(
`/api/v1/user/me/${username}/groups`
);
return data;
}
});
};
@@ -0,0 +1,21 @@
export const userKeys = {
getUser: ["user"] as const,
getPrivateKey: ["user"] as const,
userAction: ["user-action"] as const,
userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const,
getOrgMembership: (orgId: string, orgMembershipId: string) =>
[{ orgId, orgMembershipId }, "org-membership"] as const,
allOrgMembershipProjectMemberships: (orgId: string) => [orgId, "all-user-memberships"] as const,
forOrgMembershipProjectMemberships: (orgId: string, orgMembershipId: string) =>
[...userKeys.allOrgMembershipProjectMemberships(orgId), { orgMembershipId }] as const,
getOrgMembershipProjectMemberships: (orgId: string, username: string) =>
[{ orgId, username }, "org-membership-project-memberships"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const,
listUsers: ["user-list"] as const,
listUserGroupMemberships: (username: string) => [{ username }, "user-group-memberships"] as const,
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
};
+4 -1
View File
@@ -133,6 +133,7 @@ export type AddUserToWsDTOE2EE = {
export type AddUserToWsDTONonE2EE = { export type AddUserToWsDTONonE2EE = {
projectId: string; projectId: string;
usernames: string[]; usernames: string[];
roleSlugs?: string[];
orgId: string; orgId: string;
}; };
@@ -150,9 +151,11 @@ export type DeletOrgMembershipDTO = {
export type AddUserToOrgDTO = { export type AddUserToOrgDTO = {
inviteeEmails: string[]; inviteeEmails: string[];
projects?: { id: string; projectRoleSlug: string[] }[];
organizationRoleSlug: string; organizationRoleSlug: string;
organizationId: string; organizationId: string;
// We need the slug in order to invalidate the groups query. `slug` is only used for invalidation purposes.
projects?: { id: string; slug?: string; projectRoleSlug: string[] }[];
}; };
export type CreateAPIKeyRes = { export type CreateAPIKeyRes = {
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace/query-keys";
import { workflowIntegrationKeys } from "./queries"; import { workflowIntegrationKeys } from "./queries";
import { import {
TDeleteSlackIntegrationDTO, TDeleteSlackIntegrationDTO,
@@ -38,3 +38,4 @@ export {
useUpdateWsEnvironment, useUpdateWsEnvironment,
useUpgradeProject useUpgradeProject
} from "./queries"; } from "./queries";
export { workspaceKeys } from "./query-keys";
+15 -3
View File
@@ -2,7 +2,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "./queries"; import { userKeys } from "../users/query-keys";
import { workspaceKeys } from "./query-keys";
import { TUpdateWorkspaceGroupRoleDTO } from "./types"; import { TUpdateWorkspaceGroupRoleDTO } from "./types";
export const useAddGroupToWorkspace = () => { export const useAddGroupToWorkspace = () => {
@@ -51,14 +52,25 @@ export const useUpdateGroupWorkspaceRole = () => {
export const useDeleteGroupFromWorkspace = () => { export const useDeleteGroupFromWorkspace = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ groupSlug, projectSlug }: { groupSlug: string; projectSlug: string }) => { mutationFn: async ({
groupSlug,
projectSlug
}: {
groupSlug: string;
projectSlug: string;
username?: string;
}) => {
const { const {
data: { groupMembership } data: { groupMembership }
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`); } = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`);
return groupMembership; return groupMembership;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: (_, { projectSlug, username }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
if (username) {
queryClient.invalidateQueries(userKeys.listUserGroupMemberships(username));
}
} }
}); });
}; };
+2 -50
View File
@@ -15,9 +15,10 @@ import { TIntegration } from "../integrations/types";
import { TPkiAlert } from "../pkiAlerts/types"; import { TPkiAlert } from "../pkiAlerts/types";
import { TPkiCollection } from "../pkiCollections/types"; import { TPkiCollection } from "../pkiCollections/types";
import { EncryptedSecret } from "../secrets/types"; import { EncryptedSecret } from "../secrets/types";
import { userKeys } from "../users/queries"; import { userKeys } from "../users/query-keys";
import { TWorkspaceUser } from "../users/types"; import { TWorkspaceUser } from "../users/types";
import { ProjectSlackConfig } from "../workflowIntegrations/types"; import { ProjectSlackConfig } from "../workflowIntegrations/types";
import { workspaceKeys } from "./query-keys";
import { import {
CreateEnvironmentDTO, CreateEnvironmentDTO,
CreateWorkspaceDTO, CreateWorkspaceDTO,
@@ -37,55 +38,6 @@ import {
Workspace Workspace
} from "./types"; } from "./types";
export const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
getWorkspaceIndexStatus: (workspaceId: string) =>
[{ workspaceId }, "workspace-index-status"] as const,
getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"],
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: ["workspaces"] as const,
getWorkspaceAuditLogs: (workspaceId: string) =>
[{ workspaceId }, "workspace-audit-logs"] as const,
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
getWorkspaceIdentityMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-identity-memberships"] as const,
// allows invalidation using above key without knowing params
getWorkspaceIdentityMembershipsWithParams: ({
workspaceId,
...params
}: TListProjectIdentitiesDTO) =>
[...workspaceKeys.getWorkspaceIdentityMemberships(workspaceId), params] as const,
getWorkspaceGroupMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-groups"] as const,
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
[{ projectSlug }, "workspace-cas"] as const,
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
[...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const,
allWorkspaceCertificates: () => ["workspace-certificates"] as const,
forWorkspaceCertificates: (slug: string) =>
[...workspaceKeys.allWorkspaceCertificates(), slug] as const,
specificWorkspaceCertificates: ({
slug,
offset,
limit
}: {
slug: string;
offset: number;
limit: number;
}) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const,
getWorkspacePkiAlerts: (workspaceId: string) =>
[{ workspaceId }, "workspace-pki-alerts"] as const,
getWorkspacePkiCollections: (workspaceId: string) =>
[{ workspaceId }, "workspace-pki-collections"] as const,
getWorkspaceCertificateTemplates: (workspaceId: string) =>
[{ workspaceId }, "workspace-certificate-templates"] as const,
getWorkspaceSlackConfig: (workspaceId: string) =>
[{ workspaceId }, "workspace-slack-config"] as const
};
const fetchWorkspaceById = async (workspaceId: string) => { const fetchWorkspaceById = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ workspace: Workspace }>( const { data } = await apiRequest.get<{ workspace: Workspace }>(
`/api/v1/workspace/${workspaceId}` `/api/v1/workspace/${workspaceId}`
@@ -0,0 +1,52 @@
import { TListProjectIdentitiesDTO } from "@app/hooks/api/workspace/types";
import type { CaStatus } from "../ca";
export const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
getWorkspaceIndexStatus: (workspaceId: string) =>
[{ workspaceId }, "workspace-index-status"] as const,
getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"],
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: ["workspaces"] as const,
getWorkspaceAuditLogs: (workspaceId: string) =>
[{ workspaceId }, "workspace-audit-logs"] as const,
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
getWorkspaceIdentityMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-identity-memberships"] as const,
// allows invalidation using above key without knowing params
getWorkspaceIdentityMembershipsWithParams: ({
workspaceId,
...params
}: TListProjectIdentitiesDTO) =>
[...workspaceKeys.getWorkspaceIdentityMemberships(workspaceId), params] as const,
getWorkspaceGroupMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-groups"] as const,
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
[{ projectSlug }, "workspace-cas"] as const,
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
[...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const,
allWorkspaceCertificates: () => ["workspace-certificates"] as const,
forWorkspaceCertificates: (slug: string) =>
[...workspaceKeys.allWorkspaceCertificates(), slug] as const,
specificWorkspaceCertificates: ({
slug,
offset,
limit
}: {
slug: string;
offset: number;
limit: number;
}) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const,
getWorkspacePkiAlerts: (workspaceId: string) =>
[{ workspaceId }, "workspace-pki-alerts"] as const,
getWorkspacePkiCollections: (workspaceId: string) =>
[{ workspaceId }, "workspace-pki-collections"] as const,
getWorkspaceCertificateTemplates: (workspaceId: string) =>
[{ workspaceId }, "workspace-certificate-templates"] as const,
getWorkspaceSlackConfig: (workspaceId: string) =>
[{ workspaceId }, "workspace-slack-config"] as const
};
+6 -1
View File
@@ -3,7 +3,7 @@ import { NextRouter, useRouter } from "next/router";
import { useServerConfig } from "@app/context"; import { useServerConfig } from "@app/context";
import { useSelectOrganization } from "@app/hooks/api"; import { useSelectOrganization } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { userKeys } from "@app/hooks/api/users/queries"; import { userKeys } from "@app/hooks/api/users";
import { queryClient } from "@app/reactQuery"; import { queryClient } from "@app/reactQuery";
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => { export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
@@ -43,6 +43,11 @@ export const useNavigateToSelectOrganization = () => {
await navigateUserToOrg(router, config.defaultAuthOrgId); await navigateUserToOrg(router, config.defaultAuthOrgId);
} }
const localOrgId = localStorage.getItem("orgData.id")
if(!cliCallbackPort && localOrgId != null){
await navigateUserToOrg(router, localOrgId);
}
queryClient.invalidateQueries(userKeys.getUser); queryClient.invalidateQueries(userKeys.getUser);
let redirectTo = "/login/select-organization"; let redirectTo = "/login/select-organization";
+11 -2
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { faChevronLeft, faEllipsis, faFolder, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -31,6 +31,8 @@ import {
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from "@app/views/Org/Types"; import { TabSections } from "@app/views/Org/Types";
import { UserAuditLogsSection } from "./components/UserProjectsSection/UserAuditLogsSection";
import { UserGroupsSection } from "./components/UserProjectsSection/UserGroupsSection";
import { UserDetailsSection, UserOrgMembershipModal, UserProjectsSection } from "./components"; import { UserDetailsSection, UserOrgMembershipModal, UserProjectsSection } from "./components";
export const UserPage = withPermission( export const UserPage = withPermission(
@@ -241,7 +243,13 @@ export const UserPage = withPermission(
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} /> <UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
</div> </div>
<UserProjectsSection membershipId={membershipId} /> <div className="w-full space-y-2">
<div className="w-full space-y-4">
<UserProjectsSection membershipId={membershipId} />
<UserGroupsSection orgMembership={membership} />
<UserAuditLogsSection orgMembership={membership} />
</div>
</div>
</div> </div>
</div> </div>
)} )}
@@ -272,6 +280,7 @@ export const UserPage = withPermission(
} }
buttonText="Deactivate" buttonText="Deactivate"
/> />
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
@@ -0,0 +1,84 @@
import { useState } from "react";
import Link from "next/link";
import { faFilter } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgUser } from "@app/hooks/api/types";
import { LogsSection } from "@app/views/Project/AuditLogsPage/components";
type Props = {
orgMembership: OrgUser;
};
export const UserAuditLogsSection = withPermission(
({ orgMembership }: Props) => {
const [showFilter, setShowFilter] = useState(false);
const { subscription, isLoading } = useSubscription();
// eslint-disable-next-line no-nested-ternary
return subscription?.auditLogs ? (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Audit Logs</p>
<Tooltip content="Show audit log filters">
<IconButton
colorSchema="primary"
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => setShowFilter(!showFilter)}
>
<div className="flex items-center space-x-2">
<p>Filter</p>
<FontAwesomeIcon icon={faFilter} />
</div>
</IconButton>
</Tooltip>
</div>
<LogsSection
showFilters={showFilter}
filterClassName="bg-mineshaft-900 static"
presetActor={orgMembership.user.id}
isOrgAuditLogs
/>
</div>
) : !isLoading ? (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Audit Logs</p>
</div>
<EmptyState
className="rounded-lg"
title={
<div>
<p>
Please{" "}
<Link
href={
subscription && subscription.slug !== null
? `/org/${orgMembership.organization}/billing`
: "https://infisical.com/scheduledemo"
}
passHref
>
<a
className="cursor-pointer font-medium text-primary-500 transition-all hover:text-primary-600"
target="_blank"
>
upgrade your subscription
</a>
</Link>{" "}
to view audit logs for this user
</p>
</div>
}
/>
</div>
) : null;
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Member }
);
@@ -0,0 +1,45 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
group: TGroupWithProjectMemberships;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
};
export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => {
return (
<>
<Tr
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`user-project-membership-${group.id}`}
>
<Td>{group.name}</Td>
<Td>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Unassign user from group">
<IconButton
colorSchema="danger"
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeUserFromGroup", {
groupSlug: group.slug
});
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
</Td>
</Tr>
</>
);
};
@@ -0,0 +1,68 @@
import { useCallback } from "react";
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { useRemoveUserFromGroup } from "@app/hooks/api";
import { OrgUser } from "@app/hooks/api/users/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { UserGroupsTable } from "./UserGroupsTable";
type Props = {
orgMembership: OrgUser;
};
export const UserGroupsSection = ({ orgMembership }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"removeUserFromGroup"
] as const);
const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup();
const handleRemoveUserFromGroup = useCallback(async (groupSlug: string) => {
try {
await removeUserFromGroup({
slug: groupSlug,
username: orgMembership.user.username
});
createNotification({
type: "success",
text: "User removed from group successfully"
});
handlePopUpClose("removeUserFromGroup");
} catch (error) {
createNotification({
type: "error",
text: "Failed to remove user from group"
});
}
}, []);
return (
<>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Groups</h3>
</div>
<UserGroupsTable orgMembership={orgMembership} handlePopUpOpen={handlePopUpOpen} />
</div>
<DeleteActionModal
isOpen={popUp.removeUserFromGroup.isOpen}
title="Are you sure want to unassign user from group?"
onChange={(isOpen) => handlePopUpToggle("removeUserFromGroup", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const popupData = popUp?.removeUserFromGroup?.data as {
groupSlug: string;
};
return handleRemoveUserFromGroup(popupData.groupSlug);
}}
/>
</>
);
};
@@ -0,0 +1,42 @@
import { faFolder } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
import { OrgUser } from "@app/hooks/api/types";
import { useListUserGroupMemberships } from "@app/hooks/api/users/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { UserGroupsRow } from "./UserGroupsRow";
type Props = {
orgMembership: OrgUser;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
};
export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
const { data: groups, isLoading } = useListUserGroupMemberships(orgMembership.user.username);
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{groups?.map((group) => (
<UserGroupsRow
key={`user-group-${group.id}`}
group={group}
handlePopUpOpen={handlePopUpOpen}
/>
))}
</TBody>
</Table>
{!isLoading && !groups?.length && (
<EmptyState title="This user has not been assigned to any groups" icon={faFolder} />
)}
</TableContainer>
);
};
@@ -12,7 +12,7 @@ export const AuditLogsPage = withProjectPermission(
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p> <p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
<div /> <div />
</div> </div>
<LogsSection /> <LogsSection showFilters />
</div> </div>
</div> </div>
); );
@@ -2,6 +2,7 @@ import { useState } from "react";
import { Control, Controller, UseFormReset } from "react-hook-form"; import { Control, Controller, UseFormReset } from "react-hook-form";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2"; import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
@@ -19,11 +20,13 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
})); }));
type Props = { type Props = {
presetActor?: string;
className?: string;
control: Control<AuditLogFilterFormData>; control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>; reset: UseFormReset<AuditLogFilterFormData>;
}; };
export const LogsFilter = ({ control, reset }: Props) => { export const LogsFilter = ({ presetActor, className, control, reset }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
@@ -69,8 +72,13 @@ export const LogsFilter = ({ control, reset }: Props) => {
}; };
return ( return (
<div className="sticky top-20 z-10 flex items-center justify-between bg-bunker-800"> <div
<div className="flex items-center"> className={twMerge(
"sticky top-20 z-10 flex items-center justify-between bg-bunker-800",
className
)}
>
<div className="flex items-center space-x-2">
<Controller <Controller
control={control} control={control}
name="eventType" name="eventType"
@@ -79,7 +87,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Event" label="Event"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="mr-4 w-40" className="w-40"
> >
<Select <Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })} {...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -96,7 +104,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
{!isLoading && data && data.length > 0 && ( {!isLoading && data && data.length > 0 && !presetActor && (
<Controller <Controller
control={control} control={control}
name="actor" name="actor"
@@ -105,7 +113,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Actor" label="Actor"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="mr-4 w-40" className="w-40"
> >
<Select <Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })} {...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -127,7 +135,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Source" label="Source"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="mr-4 w-40" className="w-40"
> >
<Select <Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })} {...(field.value ? { value: field.value } : { placeholder: "Select" })}
@@ -149,12 +157,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
control={control} control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => { render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return ( return (
<FormControl <FormControl label="Start date" errorText={error?.message} isError={Boolean(error)}>
label="Start date"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker <DatePicker
value={field.value || undefined} value={field.value || undefined}
onChange={(date) => { onChange={(date) => {
@@ -195,26 +198,25 @@ export const LogsFilter = ({ control, reset }: Props) => {
}} }}
/> />
</div> </div>
<div> <Button
<Button isLoading={false}
isLoading={false} colorSchema="primary"
colorSchema="primary" variant="outline_bg"
variant="outline_bg" className="mt-1.5"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
onClick={() => onClick={() =>
reset({ reset({
eventType: undefined, eventType: undefined,
actor: undefined, actor: presetActor,
userAgentType: undefined, userAgentType: undefined,
startDate: undefined, startDate: undefined,
endDate: undefined endDate: undefined
}) })
} }
> >
Clear filters Clear filters
</Button> </Button>
</div>
</div> </div>
); );
}; };
@@ -12,7 +12,19 @@ import { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable"; import { LogsTable } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types"; import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
export const LogsSection = () => { type Props = {
presetActor?: string;
showFilters?: boolean;
filterClassName?: string;
isOrgAuditLogs?: boolean;
};
export const LogsSection = ({
presetActor,
filterClassName,
isOrgAuditLogs,
showFilters
}: Props) => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const router = useRouter(); const router = useRouter();
@@ -21,6 +33,7 @@ export const LogsSection = () => {
const { control, reset, watch } = useForm<AuditLogFilterFormData>({ const { control, reset, watch } = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema), resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: { defaultValues: {
actor: presetActor,
page: 1, page: 1,
perPage: 10, perPage: 10,
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
@@ -43,10 +56,19 @@ export const LogsSection = () => {
return ( return (
<div> <div>
<LogsFilter control={control} reset={reset} /> {showFilters && (
<LogsFilter
className={filterClassName}
presetActor={presetActor}
control={control}
reset={reset}
/>
)}
<LogsTable <LogsTable
isOrgAuditLogs={isOrgAuditLogs}
eventType={eventType} eventType={eventType}
userAgentType={userAgentType} userAgentType={userAgentType}
showActorColumn={!presetActor}
actor={actor} actor={actor}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
@@ -25,15 +25,24 @@ type Props = {
actor?: string; actor?: string;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
isOrgAuditLogs?: boolean;
showActorColumn: boolean;
}; };
const AUDIT_LOG_LIMIT = 15; const AUDIT_LOG_LIMIT = 15;
export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate }: Props) => { export const LogsTable = ({
eventType,
userAgentType,
showActorColumn,
actor,
startDate,
endDate,
isOrgAuditLogs
}: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs( const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
currentWorkspace?.id ?? "",
{ {
eventType, eventType,
userAgentType, userAgentType,
@@ -41,7 +50,8 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
startDate, startDate,
endDate, endDate,
limit: AUDIT_LOG_LIMIT limit: AUDIT_LOG_LIMIT
} },
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null
); );
const isEmpty = !isLoading && !data?.pages?.[0].length; const isEmpty = !isLoading && !data?.pages?.[0].length;
@@ -54,7 +64,8 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
<Tr> <Tr>
<Th>Timestamp</Th> <Th>Timestamp</Th>
<Th>Event</Th> <Th>Event</Th>
<Th>Actor</Th> {isOrgAuditLogs && <Th>Project</Th>}
{showActorColumn && <Th>Actor</Th>}
<Th>Source</Th> <Th>Source</Th>
<Th>Metadata</Th> <Th>Metadata</Th>
</Tr> </Tr>
@@ -64,7 +75,12 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
data?.pages?.map((group, i) => ( data?.pages?.map((group, i) => (
<Fragment key={`auditlog-item-${i + 1}`}> <Fragment key={`auditlog-item-${i + 1}`}>
{group.map((auditLog) => ( {group.map((auditLog) => (
<LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog.id}`} /> <LogsTableRow
showActorColumn={showActorColumn}
isOrgAuditLogs={isOrgAuditLogs}
auditLog={auditLog}
key={`audit-log-${auditLog.id}`}
/>
))} ))}
</Fragment> </Fragment>
))} ))}
@@ -5,9 +5,11 @@ import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
type Props = { type Props = {
auditLog: AuditLog; auditLog: AuditLog;
isOrgAuditLogs?: boolean;
showActorColumn: boolean;
}; };
export const LogsTableRow = ({ auditLog }: Props) => { export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
const renderActor = (actor: Actor) => { const renderActor = (actor: Actor) => {
switch (actor.type) { switch (actor.type) {
case ActorType.USER: case ActorType.USER:
@@ -486,7 +488,8 @@ export const LogsTableRow = ({ auditLog }: Props) => {
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}> <Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
<Td>{formatDate(auditLog.createdAt)}</Td> <Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td> <Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{renderActor(auditLog.actor)} {isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
{showActorColumn && renderActor(auditLog.actor)}
<Td> <Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p> <p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
<p>{auditLog.ipAddress}</p> <p>{auditLog.ipAddress}</p>
@@ -2,24 +2,37 @@ import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl,
Modal,
ModalContent
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context"; import { useOrganization, useWorkspace } from "@app/context";
import { import {
useAddUserToWsE2EE, useAddUsersToOrg,
useAddUserToWsNonE2EE,
useGetOrgUsers, useGetOrgUsers,
useGetUserWsKey, useGetProjectRoles,
useGetWorkspaceUsers useGetWorkspaceUsers
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = z.object({ const addMemberFormSchema = z.object({
orgMembershipId: z.string().trim() orgMembershipIds: z.array(z.string().trim()).min(1),
projectRoleSlugs: z.array(z.string().trim().min(1)).min(1)
}); });
type TAddMemberForm = z.infer<typeof addMemberFormSchema>; type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
@@ -37,48 +50,52 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const { data: wsKey } = useGetUserWsKey(workspaceId);
const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: members } = useGetWorkspaceUsers(workspaceId);
const { data: orgUsers } = useGetOrgUsers(orgId); const { data: orgUsers } = useGetOrgUsers(orgId);
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE(); const { data: roles } = useGetProjectRoles(currentWorkspace?.slug || "");
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
watch,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) }); } = useForm<TAddMemberForm>({
resolver: zodResolver(addMemberFormSchema),
defaultValues: { orgMembershipIds: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
});
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => { const { mutateAsync: addMembersToProject } = useAddUsersToOrg();
const onAddMember = async ({ orgMembershipIds, projectRoleSlugs }: TAddMemberForm) => {
if (!currentWorkspace) return; if (!currentWorkspace) return;
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
// TODO(akhilmhdh): Move to memory storage
const userPrivateKey = localStorage.getItem("PRIVATE_KEY"); const selectedMembers = orgMembershipIds.map((orgMembershipId) =>
if (!userPrivateKey || !wsKey) { orgUsers?.find((orgUser) => orgUser.id === orgMembershipId)
createNotification({ );
text: "Failed to find private key. Try re-login"
}); if (!selectedMembers) return;
return;
}
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
if (!orgUser) return;
try { try {
// TODO: update
if (currentWorkspace.version === ProjectVersion.V1) { if (currentWorkspace.version === ProjectVersion.V1) {
await addUserToWorkspace({ createNotification({
workspaceId, type: "error",
userPrivateKey, text: "Please upgrade your project to invite new members to the project."
decryptKey: wsKey,
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
}); });
} else { } else {
await addUserToWorkspaceNonE2EE({ await addMembersToProject({
projectId: workspaceId, inviteeEmails: selectedMembers.map((member) => member?.user.username!),
usernames: [orgUser.user.username], organizationId: orgId,
orgId organizationRoleSlug: ProjectMembershipRole.Member, // ? This doesn't apply in this case, because we know the users being added are already part of the organization
projects: [
{
slug: currentWorkspace.slug,
id: currentWorkspace.id,
projectRoleSlug: projectRoleSlugs
}
]
}); });
} }
createNotification({ createNotification({
@@ -104,6 +121,9 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username)); return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
}, [orgUsers, members]); }, [orgUsers, members]);
const selectedOrgMembershipIds = watch("orgMembershipIds");
const selectedRoleSlugs = watch("projectRoleSlugs");
return ( return (
<Modal <Modal
isOpen={popUp?.addMember?.isOpen} isOpen={popUp?.addMember?.isOpen}
@@ -115,37 +135,183 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
> >
{filteredOrgUsers.length ? ( {filteredOrgUsers.length ? (
<form onSubmit={handleSubmit(onAddMember)}> <form onSubmit={handleSubmit(onAddMember)}>
<Controller <div className="flex w-full items-center gap-2">
control={control} <div className="w-full">
defaultValue={filteredOrgUsers?.[0]?.user?.username} <Controller
name="orgMembershipId" control={control}
render={({ field, fieldState: { error } }) => ( name="orgMembershipIds"
<FormControl label="Username" isError={Boolean(error)} errorText={error?.message}> render={({ field }) => (
<Select <FormControl label="Invite users to project">
position="popper" <DropdownMenu>
className="w-full" <DropdownMenuTrigger asChild>
defaultValue={filteredOrgUsers?.[0]?.user?.username} {filteredOrgUsers && filteredOrgUsers.length > 0 ? (
value={field.value} <div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
onValueChange={field.onChange} {/* eslint-disable-next-line no-nested-ternary */}
> {selectedOrgMembershipIds.length === 1
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => ( ? filteredOrgUsers.find(
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}> (orgUser) => orgUser.id === selectedOrgMembershipIds[0]
{u?.username} )?.user.username
</SelectItem> : selectedOrgMembershipIds.length === 0
))} ? "No users selected"
</Select> : `${selectedOrgMembershipIds.length} users selected`}
</FormControl> <FontAwesomeIcon icon={faChevronDown} className="text-xs" />
)} </div>
/> ) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No users found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
filteredOrgUsers.map((member) => {
const isSelected = selectedOrgMembershipIds.includes(member.id);
return (
<DropdownMenuItem
onSelect={(event) =>
filteredOrgUsers.length > 1 && event.preventDefault()
}
onClick={() => {
if (selectedOrgMembershipIds.includes(String(member.id))) {
field.onChange(
selectedOrgMembershipIds.filter(
(membershipId: string) =>
membershipId !== String(member.id)
)
);
} else {
field.onChange([
...selectedOrgMembershipIds,
String(member.id)
]);
}
}}
key={`membership-id-${member.id}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{member.user.username}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
<div className="flex min-w-fit justify-end">
<Controller
control={control}
name="projectRoleSlugs"
render={({ field }) => (
<FormControl
label="Select roles"
tooltipText="Select the roles that you wish to assign to the users"
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{roles && roles.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedRoleSlugs.length === 1
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
: selectedRoleSlugs.length === 0
? "Select at least one role"
: `${selectedRoleSlugs.length} roles selected`}
<FontAwesomeIcon
icon={faChevronDown}
className={twMerge("ml-2 text-xs")}
/>
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No roles found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{roles && roles.length > 0 ? (
roles.map((role) => {
const isSelected = selectedRoleSlugs.includes(role.slug);
return (
<DropdownMenuItem
onSelect={(event) => roles.length > 1 && event.preventDefault()}
onClick={() => {
if (selectedRoleSlugs.includes(String(role.slug))) {
field.onChange(
selectedRoleSlugs.filter(
(roleSlug: string) => roleSlug !== String(role.slug)
)
);
} else {
field.onChange([...selectedRoleSlugs, role.slug]);
}
}}
key={`role-slug-${role.slug}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{role.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
</div>
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
size="sm" size="sm"
type="submit" type="submit"
isLoading={isSubmitting} isLoading={isSubmitting}
isDisabled={isSubmitting} isDisabled={
isSubmitting ||
selectedOrgMembershipIds.length === 0 ||
selectedRoleSlugs.length === 0
}
> >
Add Member Add Members
</Button> </Button>
<Button <Button
colorSchema="secondary" colorSchema="secondary"
@@ -222,7 +222,12 @@ export const WorkflowIntegrationTab = () => {
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }} style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start" align="start"
> >
{sortedSlackChannels?.map((slackChannel) => { {sortedSlackChannels?.map((slackChannel) => {
@@ -290,7 +295,12 @@ export const WorkflowIntegrationTab = () => {
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }} style={{
width: "var(--radix-dropdown-menu-trigger-width)",
maxHeight: "350px",
overflowY: "auto"
}}
side="bottom"
align="start" align="start"
> >
{sortedSlackChannels?.map((slackChannel) => { {sortedSlackChannels?.map((slackChannel) => {
+1 -1
View File
@@ -7,4 +7,4 @@ scripts/initialize-standalone-build.sh
cd ../ cd ../
exec node dist/main.mjs exec node --enable-source-maps dist/main.mjs