mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
resolve merge conflict
This commit is contained in:
@@ -122,6 +122,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
@@ -138,7 +142,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const auditLogs = await server.services.auditLog.listProjectAuditLogs({
|
||||
const auditLogs = await server.services.auditLog.listAuditLogs({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
|
||||
@@ -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
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
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 { ormify, stripUndefinedInWhere } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
|
||||
@@ -33,23 +33,44 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
orgId,
|
||||
[`${TableName.AuditLog}.orgId`]: orgId,
|
||||
eventType,
|
||||
actor,
|
||||
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)
|
||||
.offset(offset)
|
||||
.orderBy("createdAt", "desc");
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||
|
||||
if (actor) {
|
||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
void sqlQuery.where("createdAt", ">=", startDate);
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
void sqlQuery.where("createdAt", "<=", endDate);
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
|
||||
}
|
||||
const docs = await sqlQuery;
|
||||
return docs;
|
||||
|
||||
return docs.map((doc) => ({
|
||||
...AuditLogsSchema.parse(doc),
|
||||
project: {
|
||||
name: doc.projectName,
|
||||
slug: doc.projectSlug
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error });
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||
@@ -11,7 +12,7 @@ import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-
|
||||
|
||||
type TAuditLogServiceFactoryDep = {
|
||||
auditLogDAL: TAuditLogDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
auditLogQueue: TAuditLogQueueServiceFactory;
|
||||
};
|
||||
|
||||
@@ -22,7 +23,7 @@ export const auditLogServiceFactory = ({
|
||||
auditLogQueue,
|
||||
permissionService
|
||||
}: TAuditLogServiceFactoryDep) => {
|
||||
const listProjectAuditLogs = async ({
|
||||
const listAuditLogs = async ({
|
||||
userAgentType,
|
||||
eventType,
|
||||
offset,
|
||||
@@ -36,14 +37,33 @@ export const auditLogServiceFactory = ({
|
||||
projectId,
|
||||
auditLogActor
|
||||
}: TListProjectAuditLogDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
if (projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
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({
|
||||
startDate,
|
||||
endDate,
|
||||
@@ -52,8 +72,9 @@ export const auditLogServiceFactory = ({
|
||||
eventType,
|
||||
userAgentType,
|
||||
actor: auditLogActor,
|
||||
projectId
|
||||
...(projectId ? { projectId } : { orgId: actorOrgId })
|
||||
});
|
||||
|
||||
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
||||
...el,
|
||||
event: { type: logEventType, metadata: eventMetadata },
|
||||
@@ -76,6 +97,6 @@ export const auditLogServiceFactory = ({
|
||||
|
||||
return {
|
||||
createAuditLog,
|
||||
listProjectAuditLogs
|
||||
listAuditLogs
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,14 +6,14 @@ import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
|
||||
export type TListProjectAuditLogDTO = {
|
||||
auditLogActor?: string;
|
||||
projectId: string;
|
||||
projectId?: string;
|
||||
eventType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
userAgentType?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} & TProjectPermission;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateAuditLogDTO = {
|
||||
event: Event;
|
||||
|
||||
@@ -452,7 +452,9 @@ export const PROJECT_USERS = {
|
||||
INVITE_MEMBER: {
|
||||
projectId: "The ID of the project to invite the member to.",
|
||||
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: {
|
||||
projectId: "The ID of the project to remove the member from.",
|
||||
|
||||
@@ -473,6 +473,8 @@ export const registerRoutes = async (
|
||||
userAliasDAL,
|
||||
orgMembershipDAL,
|
||||
tokenService,
|
||||
permissionService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
projectMembershipDAL
|
||||
});
|
||||
@@ -497,6 +499,7 @@ export const registerRoutes = async (
|
||||
tokenService,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectRoleDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
orgMembershipDAL,
|
||||
@@ -504,8 +507,7 @@ export const registerRoutes = async (
|
||||
smtpService,
|
||||
userDAL,
|
||||
groupDAL,
|
||||
orgBotDAL,
|
||||
projectRoleDAL
|
||||
orgBotDAL
|
||||
});
|
||||
const signupService = authSignupServiceFactory({
|
||||
tokenService,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AuditLogsSchema,
|
||||
GroupsSchema,
|
||||
IncidentContactsSchema,
|
||||
OrganizationsSchema,
|
||||
@@ -8,7 +9,9 @@ import {
|
||||
OrgRolesSchema,
|
||||
UsersSchema
|
||||
} 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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
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({
|
||||
method: "GET",
|
||||
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({
|
||||
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: {
|
||||
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).
|
||||
// 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.
|
||||
@@ -197,5 +221,5 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
return members;
|
||||
};
|
||||
|
||||
return { ...groupProjectOrm, findByProjectId, findAllProjectGroupMembers };
|
||||
return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers };
|
||||
};
|
||||
|
||||
@@ -153,7 +153,6 @@ export const orgAdminServiceFactory = ({
|
||||
members: [
|
||||
{
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin,
|
||||
userPublicKey: userEncryptionKey.publicKey
|
||||
}
|
||||
]
|
||||
|
||||
@@ -114,11 +114,11 @@ export const orgServiceFactory = ({
|
||||
tokenService,
|
||||
orgBotDAL,
|
||||
licenseService,
|
||||
projectRoleDAL,
|
||||
samlConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectRoleDAL
|
||||
projectUserMembershipRoleDAL
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
/*
|
||||
* 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">;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "findOne">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||
|
||||
@@ -53,4 +53,5 @@ export type TAddUsersToWorkspaceNonE2EEDTO = {
|
||||
sendEmails?: boolean;
|
||||
emails: string[];
|
||||
usernames: string[];
|
||||
roleSlugs?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -16,7 +16,7 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK
|
||||
privateKey: userPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => {
|
||||
const newWsMembers = members.map(({ orgMembershipId, userPublicKey }) => {
|
||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
|
||||
plaintextProjectKey,
|
||||
userPublicKey,
|
||||
@@ -25,7 +25,6 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK
|
||||
|
||||
return {
|
||||
orgMembershipId,
|
||||
projectRole: projectMembershipRole,
|
||||
workspaceEncryptedKey: inviteeCipherText,
|
||||
workspaceEncryptedNonce: inviteeNonce
|
||||
};
|
||||
|
||||
@@ -300,8 +300,7 @@ export const projectQueueFactory = ({
|
||||
members: [
|
||||
{
|
||||
userPublicKey: user.publicKey,
|
||||
orgMembershipId: orgMembership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin
|
||||
orgMembershipId: orgMembership.id
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -277,8 +277,7 @@ export const projectServiceFactory = ({
|
||||
members: [
|
||||
{
|
||||
userPublicKey: user.publicKey,
|
||||
orgMembershipId: orgMembership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin
|
||||
orgMembershipId: orgMembership.id
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -292,7 +291,7 @@ export const projectServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.create(
|
||||
{ projectMembershipId: userProjectMembership.id, role: projectAdmin.projectRole },
|
||||
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
|
||||
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 { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@@ -88,7 +88,6 @@ export type AddUserToWsDTO = {
|
||||
userPrivateKey: string;
|
||||
members: {
|
||||
orgMembershipId: string;
|
||||
projectMembershipRole: ProjectMembershipRole;
|
||||
userPublicKey: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
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 { BadRequestError } from "@app/lib/errors";
|
||||
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 { AuthMethod } from "../auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TUserDALFactory } from "./user-dal";
|
||||
import { TListUserGroupsDTO } from "./user-types";
|
||||
|
||||
type TUserServiceFactoryDep = {
|
||||
userDAL: Pick<
|
||||
@@ -27,10 +33,12 @@ type TUserServiceFactoryDep = {
|
||||
| "delete"
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "findByUserId">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||
@@ -40,8 +48,10 @@ export const userServiceFactory = ({
|
||||
userAliasDAL,
|
||||
orgMembershipDAL,
|
||||
projectMembershipDAL,
|
||||
groupProjectDAL,
|
||||
tokenService,
|
||||
smtpService
|
||||
smtpService,
|
||||
permissionService
|
||||
}: TUserServiceFactoryDep) => {
|
||||
const sendEmailVerificationCode = async (username: string) => {
|
||||
const user = await userDAL.findOne({ username });
|
||||
@@ -295,6 +305,27 @@ export const userServiceFactory = ({
|
||||
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 {
|
||||
sendEmailVerificationCode,
|
||||
verifyEmailVerificationCode,
|
||||
@@ -304,6 +335,7 @@ export const userServiceFactory = ({
|
||||
deleteUser,
|
||||
getMe,
|
||||
createUserAction,
|
||||
listUserGroups,
|
||||
getUserAction,
|
||||
unlockUser,
|
||||
getUserPrivateKey,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TListUserGroupsDTO = {
|
||||
username: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export enum UserEncryption {
|
||||
V1 = 1,
|
||||
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.
|
||||
|
||||
<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.
|
||||

|
||||
</Step>
|
||||
<Step title="Install Slack app to workspace">
|
||||
Press "Add" and select "Slack" as the platform.
|
||||

|
||||
|
||||
Give your Slack integration a descriptive alias. You will use this to select the Slack integration for your project.
|
||||

|
||||
|
||||
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**.
|
||||

|
||||
|
||||
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.
|
||||

|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Configure project to use Slack workflow integration
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
||||

|
||||
</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.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure the Slack notification settings for the project and click Save.">
|
||||

|
||||
<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">
|
||||
## Configure admin settings
|
||||
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>
|
||||
|
||||
## Create Slack workflow integration
|
||||
## Create Slack workflow integration
|
||||
|
||||
<Steps>
|
||||
<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>
|
||||
|
||||
## Configure project to use Slack workflow integration
|
||||
## Configure project to use Slack workflow integration
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
||||

|
||||
</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.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure the Slack notification settings for the project and click Save.">
|
||||

|
||||
<Info>
|
||||
To enable notifications in private Slack channels, you need to invite your Slack bot to join those channels.
|
||||
</Info>
|
||||
You now have a working native integration with Slack!
|
||||
<Steps>
|
||||
<Step title="Navigate to the Workflow Integrations tab in the project settings">
|
||||

|
||||
</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.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure the Slack notification settings for the project and click Save.">
|
||||

|
||||
<Info>
|
||||
To enable notifications in private Slack channels, you need to invite your Slack bot to join those channels.
|
||||
</Info>
|
||||
You now have a working native integration with Slack!
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</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 (!emailCheckBool) {
|
||||
try {
|
||||
await mutateAsync({ email });
|
||||
await mutateAsync({ email: email.toLowerCase() });
|
||||
setEmail(email.toLowerCase())
|
||||
incrementStep();
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
placeholder="email@example.com, email2@example.com..."
|
||||
/>
|
||||
</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
|
||||
onClick={() => {
|
||||
if (serverDetails?.emailConfigured) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { userKeys } from "../users/queries";
|
||||
import { userKeys } from "../users";
|
||||
import {
|
||||
APIKeyDataV2,
|
||||
CreateAPIKeyDataV2DTO,
|
||||
|
||||
@@ -4,28 +4,28 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { Actor, AuditLog, AuditLogFilters } from "./types";
|
||||
|
||||
export const workspaceKeys = {
|
||||
getAuditLogs: (workspaceId: string, filters: AuditLogFilters) =>
|
||||
export const auditLogKeys = {
|
||||
getAuditLogs: (workspaceId: string | null, filters: AuditLogFilters) =>
|
||||
[{ workspaceId, filters }, "audit-logs"] as const,
|
||||
getAuditLogActorFilterOpts: (workspaceId: string) =>
|
||||
[{ workspaceId }, "audit-log-actor-filters"] as const
|
||||
};
|
||||
|
||||
export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => {
|
||||
export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string | null) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: workspaceKeys.getAuditLogs(workspaceId, filters),
|
||||
queryKey: auditLogKeys.getAuditLogs(workspaceId, filters),
|
||||
queryFn: async ({ pageParam }) => {
|
||||
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
|
||||
`/api/v1/workspace/${workspaceId}/audit-logs`,
|
||||
{
|
||||
params: {
|
||||
...filters,
|
||||
offset: pageParam,
|
||||
startDate: filters?.startDate?.toISOString(),
|
||||
endDate: filters?.endDate?.toISOString()
|
||||
}
|
||||
const auditLogEndpoint = workspaceId
|
||||
? `/api/v1/workspace/${workspaceId}/audit-logs`
|
||||
: "/api/v1/organization/audit-logs";
|
||||
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(auditLogEndpoint, {
|
||||
params: {
|
||||
...filters,
|
||||
offset: pageParam,
|
||||
startDate: filters?.startDate?.toISOString(),
|
||||
endDate: filters?.endDate?.toISOString()
|
||||
}
|
||||
);
|
||||
});
|
||||
return data.auditLogs;
|
||||
},
|
||||
getNextPageParam: (lastPage, pages) =>
|
||||
@@ -35,7 +35,7 @@ export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) =
|
||||
|
||||
export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getAuditLogActorFilterOpts(workspaceId),
|
||||
queryKey: auditLogKeys.getAuditLogActorFilterOpts(workspaceId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ actors: Actor[] }>(
|
||||
`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`
|
||||
|
||||
@@ -851,6 +851,10 @@ export type AuditLog = {
|
||||
userAgentType: UserAgentType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
project: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AuditLogFilters = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { apiRequest } from "@app/config/request";
|
||||
import { setAuthToken } from "@app/reactQuery";
|
||||
|
||||
import { organizationKeys } from "../organization/queries";
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import {
|
||||
ChangePasswordDTO,
|
||||
CompleteAccountDTO,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { caKeys } from "./queries";
|
||||
import {
|
||||
TCertificateAuthority,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { caKeys } from "../ca/queries";
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { certTemplateKeys } from "./queries";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
@@ -54,7 +54,9 @@ export const useDeleteCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplate, {}, TDeleteCertificateTemplateDTO>({
|
||||
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;
|
||||
},
|
||||
onSuccess: ({ caId }, { projectId, id }) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
||||
|
||||
export const useDeleteCert = () => {
|
||||
|
||||
@@ -3,128 +3,104 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { organizationKeys } from "../organization/queries";
|
||||
import { userKeys } from "../users/query-keys";
|
||||
import { groupKeys } from "./queries";
|
||||
import { TGroup } from "./types";
|
||||
|
||||
export const useCreateGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
name,
|
||||
slug,
|
||||
role
|
||||
}: {
|
||||
name: string;
|
||||
slug: string;
|
||||
organizationId: string;
|
||||
role?: string;
|
||||
}) => {
|
||||
const {
|
||||
data: group
|
||||
} = await apiRequest.post<TGroup>("/api/v1/groups", {
|
||||
name,
|
||||
slug,
|
||||
role
|
||||
});
|
||||
|
||||
return group;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId));
|
||||
}
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
name,
|
||||
slug,
|
||||
role
|
||||
}: {
|
||||
name: string;
|
||||
slug: string;
|
||||
organizationId: string;
|
||||
role?: string;
|
||||
}) => {
|
||||
const { data: group } = await apiRequest.post<TGroup>("/api/v1/groups", {
|
||||
name,
|
||||
slug,
|
||||
role
|
||||
});
|
||||
|
||||
return group;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
currentSlug,
|
||||
name,
|
||||
slug,
|
||||
role
|
||||
}: {
|
||||
currentSlug: string;
|
||||
name?: string;
|
||||
slug?: string;
|
||||
role?: string;
|
||||
}) => {
|
||||
const {
|
||||
data: group
|
||||
} = await apiRequest.patch<TGroup>(`/api/v1/groups/${currentSlug}`, {
|
||||
name,
|
||||
slug,
|
||||
role
|
||||
});
|
||||
|
||||
return group;
|
||||
},
|
||||
onSuccess: ({ orgId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
|
||||
}
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
currentSlug,
|
||||
name,
|
||||
slug,
|
||||
role
|
||||
}: {
|
||||
currentSlug: string;
|
||||
name?: string;
|
||||
slug?: string;
|
||||
role?: string;
|
||||
}) => {
|
||||
const { data: group } = await apiRequest.patch<TGroup>(`/api/v1/groups/${currentSlug}`, {
|
||||
name,
|
||||
slug,
|
||||
role
|
||||
});
|
||||
|
||||
return group;
|
||||
},
|
||||
onSuccess: ({ orgId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
slug
|
||||
}: {
|
||||
slug: string;
|
||||
}) => {
|
||||
const {
|
||||
data: group
|
||||
} = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}`);
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ slug }: { slug: string }) => {
|
||||
const { data: group } = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}`);
|
||||
|
||||
return group;
|
||||
},
|
||||
onSuccess: ({ orgId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
|
||||
}
|
||||
});
|
||||
return group;
|
||||
},
|
||||
onSuccess: ({ orgId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddUserToGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
slug,
|
||||
username
|
||||
}: {
|
||||
slug: string;
|
||||
username: string;
|
||||
}) => {
|
||||
const {
|
||||
data
|
||||
} = await apiRequest.post<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ slug, username }: { slug: string; username: string }) => {
|
||||
const { data } = await apiRequest.post<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { slug }) => {
|
||||
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { slug }) => {
|
||||
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRemoveUserFromGroup = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
slug,
|
||||
username
|
||||
}: {
|
||||
slug: string;
|
||||
username: string;
|
||||
}) => {
|
||||
const {
|
||||
data
|
||||
} = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ slug, username }: { slug: string; username: string }) => {
|
||||
const { data } = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { slug }) => {
|
||||
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
|
||||
}
|
||||
});
|
||||
};
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { slug, username }) => {
|
||||
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
|
||||
queryClient.invalidateQueries(userKeys.listUserGroupMemberships(username));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TOrgRole } from "../roles/types";
|
||||
|
||||
export type TGroupOrgMembership = TGroup & {
|
||||
customRole?: TOrgRole;
|
||||
}
|
||||
};
|
||||
|
||||
export type TGroup = {
|
||||
id: string;
|
||||
@@ -33,4 +33,11 @@ export type TGroupMembership = {
|
||||
}[];
|
||||
createdAt: 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 { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import {
|
||||
App,
|
||||
BitBucketWorkspace,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { TCloudIntegration } from "./types";
|
||||
|
||||
export const integrationQueryKeys = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { pkiAlertKeys } from "./queries";
|
||||
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 { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { pkiCollectionKeys } from "./queries";
|
||||
import {
|
||||
TAddItemToPkiCollectionDTO,
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
TPkiCollection,
|
||||
TPkiCollectionItem,
|
||||
TRemoveItemFromPkiCollectionDTO,
|
||||
TUpdatePkiCollectionTO} from "./types";
|
||||
TUpdatePkiCollectionTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreatePkiCollection = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
useGetOrgUsers,
|
||||
useGetUser,
|
||||
useGetUserAction,
|
||||
useListUserGroupMemberships,
|
||||
useLogoutUser,
|
||||
useRegisterUserAction,
|
||||
useRevokeMySessions,
|
||||
@@ -28,3 +29,4 @@ export {
|
||||
useUpdateOrgMembership,
|
||||
useUpdateUserAuthMethods
|
||||
} from "./queries";
|
||||
export { userKeys } from "./query-keys";
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { userKeys } from "./queries";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { userKeys } from "./query-keys";
|
||||
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
|
||||
|
||||
export const useAddUserToWsE2EE = () => {
|
||||
@@ -51,9 +51,10 @@ export const useAddUserToWsNonE2EE = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
|
||||
mutationFn: async ({ projectId, usernames }) => {
|
||||
mutationFn: async ({ projectId, usernames, roleSlugs }) => {
|
||||
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
|
||||
usernames
|
||||
usernames,
|
||||
roleSlugs
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@ import { SessionStorageKeys } from "@app/const";
|
||||
import { setAuthToken } from "@app/reactQuery";
|
||||
|
||||
import { APIKeyDataV2 } from "../apiKeys/types";
|
||||
import { TGroupWithProjectMemberships } from "../groups/types";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { userKeys } from "./query-keys";
|
||||
import {
|
||||
AddUserToOrgDTO,
|
||||
APIKeyData,
|
||||
@@ -20,28 +23,6 @@ import {
|
||||
UserEnc
|
||||
} 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 () => {
|
||||
const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user");
|
||||
|
||||
@@ -173,8 +154,15 @@ export const useAddUsersToOrg = () => {
|
||||
mutationFn: (dto) => {
|
||||
return apiRequest.post("/api/v1/invite-org/signup", dto);
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
onSuccess: (_, { organizationId, projects }) => {
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
21
frontend/src/hooks/api/users/query-keys.tsx
Normal file
21
frontend/src/hooks/api/users/query-keys.tsx
Normal file
@@ -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
|
||||
};
|
||||
@@ -133,6 +133,7 @@ export type AddUserToWsDTOE2EE = {
|
||||
export type AddUserToWsDTONonE2EE = {
|
||||
projectId: string;
|
||||
usernames: string[];
|
||||
roleSlugs?: string[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
@@ -150,9 +151,11 @@ export type DeletOrgMembershipDTO = {
|
||||
|
||||
export type AddUserToOrgDTO = {
|
||||
inviteeEmails: string[];
|
||||
projects?: { id: string; projectRoleSlug: string[] }[];
|
||||
organizationRoleSlug: 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 = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { workspaceKeys } from "../workspace/query-keys";
|
||||
import { workflowIntegrationKeys } from "./queries";
|
||||
import {
|
||||
TDeleteSlackIntegrationDTO,
|
||||
|
||||
@@ -38,3 +38,4 @@ export {
|
||||
useUpdateWsEnvironment,
|
||||
useUpgradeProject
|
||||
} from "./queries";
|
||||
export { workspaceKeys } from "./query-keys";
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
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";
|
||||
|
||||
export const useAddGroupToWorkspace = () => {
|
||||
@@ -51,14 +52,25 @@ export const useUpdateGroupWorkspaceRole = () => {
|
||||
export const useDeleteGroupFromWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ groupSlug, projectSlug }: { groupSlug: string; projectSlug: string }) => {
|
||||
mutationFn: async ({
|
||||
groupSlug,
|
||||
projectSlug
|
||||
}: {
|
||||
groupSlug: string;
|
||||
projectSlug: string;
|
||||
username?: string;
|
||||
}) => {
|
||||
const {
|
||||
data: { groupMembership }
|
||||
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`);
|
||||
return groupMembership;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
onSuccess: (_, { projectSlug, username }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
|
||||
|
||||
if (username) {
|
||||
queryClient.invalidateQueries(userKeys.listUserGroupMemberships(username));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,9 +15,10 @@ import { TIntegration } from "../integrations/types";
|
||||
import { TPkiAlert } from "../pkiAlerts/types";
|
||||
import { TPkiCollection } from "../pkiCollections/types";
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import { userKeys } from "../users/queries";
|
||||
import { userKeys } from "../users/query-keys";
|
||||
import { TWorkspaceUser } from "../users/types";
|
||||
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
||||
import { workspaceKeys } from "./query-keys";
|
||||
import {
|
||||
CreateEnvironmentDTO,
|
||||
CreateWorkspaceDTO,
|
||||
@@ -37,55 +38,6 @@ import {
|
||||
Workspace
|
||||
} 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 { data } = await apiRequest.get<{ workspace: Workspace }>(
|
||||
`/api/v1/workspace/${workspaceId}`
|
||||
|
||||
52
frontend/src/hooks/api/workspace/query-keys.tsx
Normal file
52
frontend/src/hooks/api/workspace/query-keys.tsx
Normal file
@@ -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
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { NextRouter, useRouter } from "next/router";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { useSelectOrganization } from "@app/hooks/api";
|
||||
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";
|
||||
|
||||
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
|
||||
@@ -43,6 +43,11 @@ export const useNavigateToSelectOrganization = () => {
|
||||
await navigateUserToOrg(router, config.defaultAuthOrgId);
|
||||
}
|
||||
|
||||
const localOrgId = localStorage.getItem("orgData.id")
|
||||
if(!cliCallbackPort && localOrgId != null){
|
||||
await navigateUserToOrg(router, localOrgId);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries(userKeys.getUser);
|
||||
let redirectTo = "/login/select-organization";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
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 { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
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";
|
||||
|
||||
export const UserPage = withPermission(
|
||||
@@ -241,7 +243,13 @@ export const UserPage = withPermission(
|
||||
<div className="mr-4 w-96">
|
||||
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</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>
|
||||
)}
|
||||
@@ -272,6 +280,7 @@ export const UserPage = withPermission(
|
||||
}
|
||||
buttonText="Deactivate"
|
||||
/>
|
||||
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.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>
|
||||
<div />
|
||||
</div>
|
||||
<LogsSection />
|
||||
<LogsSection showFilters />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { Control, Controller, UseFormReset } from "react-hook-form";
|
||||
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
@@ -19,11 +20,13 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
presetActor?: string;
|
||||
className?: string;
|
||||
control: Control<AuditLogFilterFormData>;
|
||||
reset: UseFormReset<AuditLogFilterFormData>;
|
||||
};
|
||||
|
||||
export const LogsFilter = ({ control, reset }: Props) => {
|
||||
export const LogsFilter = ({ presetActor, className, control, reset }: Props) => {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
|
||||
@@ -69,8 +72,13 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sticky top-20 z-10 flex items-center justify-between bg-bunker-800">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
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
|
||||
control={control}
|
||||
name="eventType"
|
||||
@@ -79,7 +87,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
label="Event"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-4 w-40"
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||
@@ -96,7 +104,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isLoading && data && data.length > 0 && (
|
||||
{!isLoading && data && data.length > 0 && !presetActor && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="actor"
|
||||
@@ -105,7 +113,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
label="Actor"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-4 w-40"
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||
@@ -127,7 +135,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
label="Source"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-4 w-40"
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||
@@ -149,12 +157,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Start date"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-4"
|
||||
>
|
||||
<FormControl label="Start date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(date) => {
|
||||
@@ -195,26 +198,25 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
isLoading={false}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
|
||||
onClick={() =>
|
||||
reset({
|
||||
eventType: undefined,
|
||||
actor: undefined,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined
|
||||
})
|
||||
}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={false}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-1.5"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
||||
onClick={() =>
|
||||
reset({
|
||||
eventType: undefined,
|
||||
actor: presetActor,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined
|
||||
})
|
||||
}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,19 @@ import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable } from "./LogsTable";
|
||||
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 router = useRouter();
|
||||
|
||||
@@ -21,6 +33,7 @@ export const LogsSection = () => {
|
||||
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
|
||||
resolver: yupResolver(auditLogFilterFormSchema),
|
||||
defaultValues: {
|
||||
actor: presetActor,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
|
||||
@@ -43,10 +56,19 @@ export const LogsSection = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LogsFilter control={control} reset={reset} />
|
||||
{showFilters && (
|
||||
<LogsFilter
|
||||
className={filterClassName}
|
||||
presetActor={presetActor}
|
||||
control={control}
|
||||
reset={reset}
|
||||
/>
|
||||
)}
|
||||
<LogsTable
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
eventType={eventType}
|
||||
userAgentType={userAgentType}
|
||||
showActorColumn={!presetActor}
|
||||
actor={actor}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
|
||||
@@ -25,15 +25,24 @@ type Props = {
|
||||
actor?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
};
|
||||
|
||||
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 { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
|
||||
currentWorkspace?.id ?? "",
|
||||
{
|
||||
eventType,
|
||||
userAgentType,
|
||||
@@ -41,7 +50,8 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
|
||||
startDate,
|
||||
endDate,
|
||||
limit: AUDIT_LOG_LIMIT
|
||||
}
|
||||
},
|
||||
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null
|
||||
);
|
||||
|
||||
const isEmpty = !isLoading && !data?.pages?.[0].length;
|
||||
@@ -54,7 +64,8 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
|
||||
<Tr>
|
||||
<Th>Timestamp</Th>
|
||||
<Th>Event</Th>
|
||||
<Th>Actor</Th>
|
||||
{isOrgAuditLogs && <Th>Project</Th>}
|
||||
{showActorColumn && <Th>Actor</Th>}
|
||||
<Th>Source</Th>
|
||||
<Th>Metadata</Th>
|
||||
</Tr>
|
||||
@@ -64,7 +75,12 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
|
||||
data?.pages?.map((group, i) => (
|
||||
<Fragment key={`auditlog-item-${i + 1}`}>
|
||||
{group.map((auditLog) => (
|
||||
<LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog.id}`} />
|
||||
<LogsTableRow
|
||||
showActorColumn={showActorColumn}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
auditLog={auditLog}
|
||||
key={`audit-log-${auditLog.id}`}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
@@ -5,9 +5,11 @@ import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
type Props = {
|
||||
auditLog: AuditLog;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
};
|
||||
|
||||
export const LogsTableRow = ({ auditLog }: Props) => {
|
||||
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
|
||||
const renderActor = (actor: Actor) => {
|
||||
switch (actor.type) {
|
||||
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`}>
|
||||
<Td>{formatDate(auditLog.createdAt)}</Td>
|
||||
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
|
||||
{renderActor(auditLog.actor)}
|
||||
{isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
|
||||
{showActorColumn && renderActor(auditLog.actor)}
|
||||
<Td>
|
||||
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
|
||||
<p>{auditLog.ipAddress}</p>
|
||||
|
||||
@@ -2,24 +2,37 @@ import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
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 {
|
||||
useAddUserToWsE2EE,
|
||||
useAddUserToWsNonE2EE,
|
||||
useAddUsersToOrg,
|
||||
useGetOrgUsers,
|
||||
useGetUserWsKey,
|
||||
useGetProjectRoles,
|
||||
useGetWorkspaceUsers
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
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>;
|
||||
@@ -37,48 +50,52 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const orgId = currentOrg?.id || "";
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
|
||||
const { data: wsKey } = useGetUserWsKey(workspaceId);
|
||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||
|
||||
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
|
||||
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
|
||||
const { data: roles } = useGetProjectRoles(currentWorkspace?.slug || "");
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
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 (!currentOrg?.id) return;
|
||||
// TODO(akhilmhdh): Move to memory storage
|
||||
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
|
||||
if (!userPrivateKey || !wsKey) {
|
||||
createNotification({
|
||||
text: "Failed to find private key. Try re-login"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
|
||||
if (!orgUser) return;
|
||||
|
||||
const selectedMembers = orgMembershipIds.map((orgMembershipId) =>
|
||||
orgUsers?.find((orgUser) => orgUser.id === orgMembershipId)
|
||||
);
|
||||
|
||||
if (!selectedMembers) return;
|
||||
|
||||
try {
|
||||
// TODO: update
|
||||
if (currentWorkspace.version === ProjectVersion.V1) {
|
||||
await addUserToWorkspace({
|
||||
workspaceId,
|
||||
userPrivateKey,
|
||||
decryptKey: wsKey,
|
||||
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Please upgrade your project to invite new members to the project."
|
||||
});
|
||||
} else {
|
||||
await addUserToWorkspaceNonE2EE({
|
||||
projectId: workspaceId,
|
||||
usernames: [orgUser.user.username],
|
||||
orgId
|
||||
await addMembersToProject({
|
||||
inviteeEmails: selectedMembers.map((member) => member?.user.username!),
|
||||
organizationId: 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({
|
||||
@@ -104,6 +121,9 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username));
|
||||
}, [orgUsers, members]);
|
||||
|
||||
const selectedOrgMembershipIds = watch("orgMembershipIds");
|
||||
const selectedRoleSlugs = watch("projectRoleSlugs");
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addMember?.isOpen}
|
||||
@@ -115,37 +135,183 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
>
|
||||
{filteredOrgUsers.length ? (
|
||||
<form onSubmit={handleSubmit(onAddMember)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={filteredOrgUsers?.[0]?.user?.username}
|
||||
name="orgMembershipId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Username" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Select
|
||||
position="popper"
|
||||
className="w-full"
|
||||
defaultValue={filteredOrgUsers?.[0]?.user?.username}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
|
||||
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
|
||||
{u?.username}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="orgMembershipIds"
|
||||
render={({ field }) => (
|
||||
<FormControl label="Invite users to project">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{filteredOrgUsers && filteredOrgUsers.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 */}
|
||||
{selectedOrgMembershipIds.length === 1
|
||||
? filteredOrgUsers.find(
|
||||
(orgUser) => orgUser.id === selectedOrgMembershipIds[0]
|
||||
)?.user.username
|
||||
: selectedOrgMembershipIds.length === 0
|
||||
? "No users selected"
|
||||
: `${selectedOrgMembershipIds.length} users selected`}
|
||||
<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">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
isDisabled={
|
||||
isSubmitting ||
|
||||
selectedOrgMembershipIds.length === 0 ||
|
||||
selectedRoleSlugs.length === 0
|
||||
}
|
||||
>
|
||||
Add Member
|
||||
Add Members
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
|
||||
@@ -222,7 +222,12 @@ export const WorkflowIntegrationTab = () => {
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<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"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
@@ -290,7 +295,12 @@ export const WorkflowIntegrationTab = () => {
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<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"
|
||||
>
|
||||
{sortedSlackChannels?.map((slackChannel) => {
|
||||
|
||||
@@ -7,4 +7,4 @@ scripts/initialize-standalone-build.sh
|
||||
|
||||
cd ../
|
||||
|
||||
exec node dist/main.mjs
|
||||
exec node --enable-source-maps dist/main.mjs
|
||||
|
||||
Reference in New Issue
Block a user