mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 02:27:39 +00:00
Merge pull request #2388 from Infisical/daniel/permission-visualization
feat: user details page audit logs & groups visualization
This commit is contained in:
@@ -122,6 +122,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.merge(
|
.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
project: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
event: z.object({
|
event: z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
metadata: z.any()
|
metadata: z.any()
|
||||||
@@ -138,7 +142,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const auditLogs = await server.services.auditLog.listProjectAuditLogs({
|
const auditLogs = await server.services.auditLog.listAuditLogs({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { AuditLogsSchema, TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
|
|
||||||
@@ -33,23 +33,44 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
.where(
|
.where(
|
||||||
stripUndefinedInWhere({
|
stripUndefinedInWhere({
|
||||||
projectId,
|
projectId,
|
||||||
orgId,
|
[`${TableName.AuditLog}.orgId`]: orgId,
|
||||||
eventType,
|
eventType,
|
||||||
actor,
|
|
||||||
userAgentType
|
userAgentType
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
||||||
|
|
||||||
|
.select(selectAllTableCols(TableName.AuditLog))
|
||||||
|
|
||||||
|
.select(
|
||||||
|
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||||
|
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
||||||
|
)
|
||||||
|
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.orderBy("createdAt", "desc");
|
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||||
|
|
||||||
|
if (actor) {
|
||||||
|
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]);
|
||||||
|
}
|
||||||
|
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
void sqlQuery.where("createdAt", ">=", startDate);
|
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
||||||
}
|
}
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
void sqlQuery.where("createdAt", "<=", endDate);
|
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
|
||||||
}
|
}
|
||||||
const docs = await sqlQuery;
|
const docs = await sqlQuery;
|
||||||
return docs;
|
|
||||||
|
return docs.map((doc) => ({
|
||||||
|
...AuditLogsSchema.parse(doc),
|
||||||
|
project: {
|
||||||
|
name: doc.projectName,
|
||||||
|
slug: doc.projectSlug
|
||||||
|
}
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error });
|
throw new DatabaseError({ error });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||||
@@ -11,7 +12,7 @@ import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-
|
|||||||
|
|
||||||
type TAuditLogServiceFactoryDep = {
|
type TAuditLogServiceFactoryDep = {
|
||||||
auditLogDAL: TAuditLogDALFactory;
|
auditLogDAL: TAuditLogDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
auditLogQueue: TAuditLogQueueServiceFactory;
|
auditLogQueue: TAuditLogQueueServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export const auditLogServiceFactory = ({
|
|||||||
auditLogQueue,
|
auditLogQueue,
|
||||||
permissionService
|
permissionService
|
||||||
}: TAuditLogServiceFactoryDep) => {
|
}: TAuditLogServiceFactoryDep) => {
|
||||||
const listProjectAuditLogs = async ({
|
const listAuditLogs = async ({
|
||||||
userAgentType,
|
userAgentType,
|
||||||
eventType,
|
eventType,
|
||||||
offset,
|
offset,
|
||||||
@@ -36,14 +37,33 @@ export const auditLogServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
auditLogActor
|
auditLogActor
|
||||||
}: TListProjectAuditLogDTO) => {
|
}: TListProjectAuditLogDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
if (projectId) {
|
||||||
actor,
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actorId,
|
actor,
|
||||||
projectId,
|
actorId,
|
||||||
actorAuthMethod,
|
projectId,
|
||||||
actorOrgId
|
actorAuthMethod,
|
||||||
);
|
actorOrgId
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
|
} else {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
||||||
|
* to the organization level
|
||||||
|
*/
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||||
|
|
||||||
const auditLogs = await auditLogDAL.find({
|
const auditLogs = await auditLogDAL.find({
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
@@ -52,8 +72,9 @@ export const auditLogServiceFactory = ({
|
|||||||
eventType,
|
eventType,
|
||||||
userAgentType,
|
userAgentType,
|
||||||
actor: auditLogActor,
|
actor: auditLogActor,
|
||||||
projectId
|
...(projectId ? { projectId } : { orgId: actorOrgId })
|
||||||
});
|
});
|
||||||
|
|
||||||
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
event: { type: logEventType, metadata: eventMetadata },
|
event: { type: logEventType, metadata: eventMetadata },
|
||||||
@@ -76,6 +97,6 @@ export const auditLogServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
createAuditLog,
|
createAuditLog,
|
||||||
listProjectAuditLogs
|
listAuditLogs
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
|||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
auditLogActor?: string;
|
auditLogActor?: string;
|
||||||
projectId: string;
|
projectId?: string;
|
||||||
eventType?: string;
|
eventType?: string;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
userAgentType?: string;
|
userAgentType?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
} & TProjectPermission;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TCreateAuditLogDTO = {
|
export type TCreateAuditLogDTO = {
|
||||||
event: Event;
|
event: Event;
|
||||||
|
|||||||
@@ -473,6 +473,8 @@ export const registerRoutes = async (
|
|||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
orgMembershipDAL,
|
orgMembershipDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
|
permissionService,
|
||||||
|
groupProjectDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectMembershipDAL
|
projectMembershipDAL
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AuditLogsSchema,
|
||||||
GroupsSchema,
|
GroupsSchema,
|
||||||
IncidentContactsSchema,
|
IncidentContactsSchema,
|
||||||
OrganizationsSchema,
|
OrganizationsSchema,
|
||||||
@@ -8,7 +9,9 @@ import {
|
|||||||
OrgRolesSchema,
|
OrgRolesSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
|
import { getLastMidnightDateISO } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -62,6 +65,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/audit-logs",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get all audit logs for an organization",
|
||||||
|
querystring: z.object({
|
||||||
|
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||||
|
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
|
||||||
|
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||||
|
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||||
|
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
||||||
|
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit),
|
||||||
|
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
|
||||||
|
}),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
auditLogs: AuditLogsSchema.omit({
|
||||||
|
eventMetadata: true,
|
||||||
|
eventType: true,
|
||||||
|
actor: true,
|
||||||
|
actorMetadata: true
|
||||||
|
})
|
||||||
|
.merge(
|
||||||
|
z.object({
|
||||||
|
project: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
|
event: z.object({
|
||||||
|
type: z.string(),
|
||||||
|
metadata: z.any()
|
||||||
|
}),
|
||||||
|
actor: z.object({
|
||||||
|
type: z.string(),
|
||||||
|
metadata: z.any()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const auditLogs = await server.services.auditLog.listAuditLogs({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
...req.query,
|
||||||
|
endDate: req.query.endDate,
|
||||||
|
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||||
|
auditLogActor: req.query.actor,
|
||||||
|
actor: req.permission.type
|
||||||
|
});
|
||||||
|
return { auditLogs };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:organizationId/users",
|
url: "/:organizationId/users",
|
||||||
|
|||||||
@@ -134,4 +134,39 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/me/:username/groups",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
username: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
orgId: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const groupMemberships = await server.services.user.listUserGroups({
|
||||||
|
username: req.params.username,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupMemberships;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,6 +95,30 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findByUserId = async (userId: string, orgId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.UserGroupMembership)
|
||||||
|
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||||
|
.join(TableName.Groups, function () {
|
||||||
|
this.on(`${TableName.UserGroupMembership}.groupId`, "=", `${TableName.Groups}.id`).andOn(
|
||||||
|
`${TableName.Groups}.orgId`,
|
||||||
|
"=",
|
||||||
|
db.raw("?", [orgId])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.Groups),
|
||||||
|
db.ref("name").withSchema(TableName.Groups),
|
||||||
|
db.ref("slug").withSchema(TableName.Groups),
|
||||||
|
db.ref("orgId").withSchema(TableName.Groups)
|
||||||
|
);
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByUserId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId).
|
// The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId).
|
||||||
// We need to join the GroupProjectMembership table with the Groups table to get the group name and slug.
|
// We need to join the GroupProjectMembership table with the Groups table to get the group name and slug.
|
||||||
// We also need to join the GroupProjectMembershipRole table to get the role of the group in the project.
|
// We also need to join the GroupProjectMembershipRole table to get the role of the group in the project.
|
||||||
@@ -197,5 +221,5 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
|||||||
return members;
|
return members;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...groupProjectOrm, findByProjectId, findAllProjectGroupMembers };
|
return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
@@ -8,8 +12,10 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
|||||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
|
|
||||||
import { AuthMethod } from "../auth/auth-type";
|
import { AuthMethod } from "../auth/auth-type";
|
||||||
|
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TUserDALFactory } from "./user-dal";
|
import { TUserDALFactory } from "./user-dal";
|
||||||
|
import { TListUserGroupsDTO } from "./user-types";
|
||||||
|
|
||||||
type TUserServiceFactoryDep = {
|
type TUserServiceFactoryDep = {
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
@@ -27,10 +33,12 @@ type TUserServiceFactoryDep = {
|
|||||||
| "delete"
|
| "delete"
|
||||||
>;
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
|
||||||
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "findByUserId">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">;
|
||||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
permissionService: TPermissionServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||||
@@ -40,8 +48,10 @@ export const userServiceFactory = ({
|
|||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
orgMembershipDAL,
|
orgMembershipDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
|
groupProjectDAL,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService
|
smtpService,
|
||||||
|
permissionService
|
||||||
}: TUserServiceFactoryDep) => {
|
}: TUserServiceFactoryDep) => {
|
||||||
const sendEmailVerificationCode = async (username: string) => {
|
const sendEmailVerificationCode = async (username: string) => {
|
||||||
const user = await userDAL.findOne({ username });
|
const user = await userDAL.findOne({ username });
|
||||||
@@ -295,6 +305,27 @@ export const userServiceFactory = ({
|
|||||||
return updatedOrgMembership.projectFavorites;
|
return updatedOrgMembership.projectFavorites;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listUserGroups = async ({ username, actorOrgId, actor, actorId, actorAuthMethod }: TListUserGroupsDTO) => {
|
||||||
|
const user = await userDAL.findOne({
|
||||||
|
username
|
||||||
|
});
|
||||||
|
|
||||||
|
// This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission.
|
||||||
|
if (user.id !== actorId) {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberships = await groupProjectDAL.findByUserId(user.id, actorOrgId);
|
||||||
|
return memberships;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendEmailVerificationCode,
|
sendEmailVerificationCode,
|
||||||
verifyEmailVerificationCode,
|
verifyEmailVerificationCode,
|
||||||
@@ -304,6 +335,7 @@ export const userServiceFactory = ({
|
|||||||
deleteUser,
|
deleteUser,
|
||||||
getMe,
|
getMe,
|
||||||
createUserAction,
|
createUserAction,
|
||||||
|
listUserGroups,
|
||||||
getUserAction,
|
getUserAction,
|
||||||
unlockUser,
|
unlockUser,
|
||||||
getUserPrivateKey,
|
getUserPrivateKey,
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TListUserGroupsDTO = {
|
||||||
|
username: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export enum UserEncryption {
|
export enum UserEncryption {
|
||||||
V1 = 1,
|
V1 = 1,
|
||||||
V2 = 2
|
V2 = 2
|
||||||
|
|||||||
@@ -5,27 +5,29 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { Actor, AuditLog, AuditLogFilters } from "./types";
|
import { Actor, AuditLog, AuditLogFilters } from "./types";
|
||||||
|
|
||||||
export const workspaceKeys = {
|
export const workspaceKeys = {
|
||||||
getAuditLogs: (workspaceId: string, filters: AuditLogFilters) =>
|
getAuditLogs: (filters: AuditLogFilters, workspaceId: string | null) =>
|
||||||
[{ workspaceId, filters }, "audit-logs"] as const,
|
[{ workspaceId, filters }, "audit-logs"] as const,
|
||||||
getAuditLogActorFilterOpts: (workspaceId: string) =>
|
getAuditLogActorFilterOpts: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "audit-log-actor-filters"] as const
|
[{ workspaceId }, "audit-log-actor-filters"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => {
|
export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string | null) => {
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: workspaceKeys.getAuditLogs(workspaceId, filters),
|
queryKey: workspaceKeys.getAuditLogs(filters, workspaceId),
|
||||||
|
enabled: workspaceId !== "",
|
||||||
|
|
||||||
queryFn: async ({ pageParam }) => {
|
queryFn: async ({ pageParam }) => {
|
||||||
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
|
const auditLogEndpoint = workspaceId
|
||||||
`/api/v1/workspace/${workspaceId}/audit-logs`,
|
? `/api/v1/workspace/${workspaceId}/audit-logs`
|
||||||
{
|
: "/api/v1/organization/audit-logs";
|
||||||
params: {
|
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(auditLogEndpoint, {
|
||||||
...filters,
|
params: {
|
||||||
offset: pageParam,
|
...filters,
|
||||||
startDate: filters?.startDate?.toISOString(),
|
offset: pageParam,
|
||||||
endDate: filters?.endDate?.toISOString()
|
startDate: filters?.startDate?.toISOString(),
|
||||||
}
|
endDate: filters?.endDate?.toISOString()
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
return data.auditLogs;
|
return data.auditLogs;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage, pages) =>
|
getNextPageParam: (lastPage, pages) =>
|
||||||
|
|||||||
@@ -851,6 +851,10 @@ export type AuditLog = {
|
|||||||
userAgentType: UserAgentType;
|
userAgentType: UserAgentType;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
project: {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuditLogFilters = {
|
export type AuditLogFilters = {
|
||||||
|
|||||||
@@ -3,128 +3,104 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { organizationKeys } from "../organization/queries";
|
import { organizationKeys } from "../organization/queries";
|
||||||
|
import { userKeys } from "../users/queries";
|
||||||
import { groupKeys } from "./queries";
|
import { groupKeys } from "./queries";
|
||||||
import { TGroup } from "./types";
|
import { TGroup } from "./types";
|
||||||
|
|
||||||
export const useCreateGroup = () => {
|
export const useCreateGroup = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
role
|
role
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { data: group } = await apiRequest.post<TGroup>("/api/v1/groups", {
|
||||||
data: group
|
name,
|
||||||
} = await apiRequest.post<TGroup>("/api/v1/groups", {
|
slug,
|
||||||
name,
|
role
|
||||||
slug,
|
});
|
||||||
role
|
|
||||||
});
|
return group;
|
||||||
|
},
|
||||||
return group;
|
onSuccess: (_, { organizationId }) => {
|
||||||
},
|
queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId));
|
||||||
onSuccess: (_, { organizationId }) => {
|
}
|
||||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId));
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateGroup = () => {
|
export const useUpdateGroup = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
currentSlug,
|
currentSlug,
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
role
|
role
|
||||||
}: {
|
}: {
|
||||||
currentSlug: string;
|
currentSlug: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { data: group } = await apiRequest.patch<TGroup>(`/api/v1/groups/${currentSlug}`, {
|
||||||
data: group
|
name,
|
||||||
} = await apiRequest.patch<TGroup>(`/api/v1/groups/${currentSlug}`, {
|
slug,
|
||||||
name,
|
role
|
||||||
slug,
|
});
|
||||||
role
|
|
||||||
});
|
return group;
|
||||||
|
},
|
||||||
return group;
|
onSuccess: ({ orgId }) => {
|
||||||
},
|
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
|
||||||
onSuccess: ({ orgId }) => {
|
}
|
||||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteGroup = () => {
|
export const useDeleteGroup = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ slug }: { slug: string }) => {
|
||||||
slug
|
const { data: group } = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}`);
|
||||||
}: {
|
|
||||||
slug: string;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
data: group
|
|
||||||
} = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}`);
|
|
||||||
|
|
||||||
return group;
|
return group;
|
||||||
},
|
},
|
||||||
onSuccess: ({ orgId }) => {
|
onSuccess: ({ orgId }) => {
|
||||||
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
|
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAddUserToGroup = () => {
|
export const useAddUserToGroup = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ slug, username }: { slug: string; username: string }) => {
|
||||||
slug,
|
const { data } = await apiRequest.post<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
|
||||||
username
|
|
||||||
}: {
|
|
||||||
slug: string;
|
|
||||||
username: string;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
data
|
|
||||||
} = await apiRequest.post<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { slug }) => {
|
onSuccess: (_, { slug }) => {
|
||||||
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
|
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRemoveUserFromGroup = () => {
|
export const useRemoveUserFromGroup = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({ slug, username }: { slug: string; username: string }) => {
|
||||||
slug,
|
const { data } = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
|
||||||
username
|
|
||||||
}: {
|
|
||||||
slug: string;
|
|
||||||
username: string;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
data
|
|
||||||
} = await apiRequest.delete<TGroup>(`/api/v1/groups/${slug}/users/${username}`);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { slug }) => {
|
onSuccess: (_, { slug, username }) => {
|
||||||
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
|
queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
|
||||||
}
|
queryClient.invalidateQueries(userKeys.listUserGroupMemberships(username));
|
||||||
});
|
}
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TOrgRole } from "../roles/types";
|
|||||||
|
|
||||||
export type TGroupOrgMembership = TGroup & {
|
export type TGroupOrgMembership = TGroup & {
|
||||||
customRole?: TOrgRole;
|
customRole?: TOrgRole;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TGroup = {
|
export type TGroup = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -33,4 +33,11 @@ export type TGroupMembership = {
|
|||||||
}[];
|
}[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGroupWithProjectMemberships = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export {
|
|||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
|
useListUserGroupMemberships,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction,
|
||||||
useRevokeMySessions,
|
useRevokeMySessions,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { SessionStorageKeys } from "@app/const";
|
|||||||
import { setAuthToken } from "@app/reactQuery";
|
import { setAuthToken } from "@app/reactQuery";
|
||||||
|
|
||||||
import { APIKeyDataV2 } from "../apiKeys/types";
|
import { APIKeyDataV2 } from "../apiKeys/types";
|
||||||
|
import { TGroupWithProjectMemberships } from "../groups/types";
|
||||||
import {
|
import {
|
||||||
AddUserToOrgDTO,
|
AddUserToOrgDTO,
|
||||||
APIKeyData,
|
APIKeyData,
|
||||||
@@ -38,6 +39,7 @@ export const userKeys = {
|
|||||||
myAPIKeysV2: ["api-keys-v2"] as const,
|
myAPIKeysV2: ["api-keys-v2"] as const,
|
||||||
mySessions: ["sessions"] as const,
|
mySessions: ["sessions"] as const,
|
||||||
listUsers: ["user-list"] as const,
|
listUsers: ["user-list"] as const,
|
||||||
|
listUserGroupMemberships: (username: string) => ["user-group-memberships", username] as const,
|
||||||
|
|
||||||
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
||||||
};
|
};
|
||||||
@@ -444,3 +446,16 @@ export const fetchMyPrivateKey = async () => {
|
|||||||
|
|
||||||
return privateKey;
|
return privateKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useListUserGroupMemberships = (username: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: userKeys.listUserGroupMemberships(username),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TGroupWithProjectMemberships[]>(
|
||||||
|
`/api/v1/user/me/${username}/groups`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { userKeys } from "../users/queries";
|
||||||
import { workspaceKeys } from "./queries";
|
import { workspaceKeys } from "./queries";
|
||||||
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
|
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
|
||||||
|
|
||||||
@@ -51,14 +52,25 @@ export const useUpdateGroupWorkspaceRole = () => {
|
|||||||
export const useDeleteGroupFromWorkspace = () => {
|
export const useDeleteGroupFromWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ groupSlug, projectSlug }: { groupSlug: string; projectSlug: string }) => {
|
mutationFn: async ({
|
||||||
|
groupSlug,
|
||||||
|
projectSlug
|
||||||
|
}: {
|
||||||
|
groupSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
username?: string;
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
data: { groupMembership }
|
data: { groupMembership }
|
||||||
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`);
|
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`);
|
||||||
return groupMembership;
|
return groupMembership;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectSlug }) => {
|
onSuccess: (_, { projectSlug, username }) => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug));
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
queryClient.invalidateQueries(userKeys.listUserGroupMemberships(username));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronLeft, faEllipsis, faFolder, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -31,6 +31,8 @@ import {
|
|||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
import { TabSections } from "@app/views/Org/Types";
|
import { TabSections } from "@app/views/Org/Types";
|
||||||
|
|
||||||
|
import { UserAuditLogsSection } from "./components/UserProjectsSection/UserAuditLogsSection";
|
||||||
|
import { UserGroupsSection } from "./components/UserProjectsSection/UserGroupsSection";
|
||||||
import { UserDetailsSection, UserOrgMembershipModal, UserProjectsSection } from "./components";
|
import { UserDetailsSection, UserOrgMembershipModal, UserProjectsSection } from "./components";
|
||||||
|
|
||||||
export const UserPage = withPermission(
|
export const UserPage = withPermission(
|
||||||
@@ -241,7 +243,13 @@ export const UserPage = withPermission(
|
|||||||
<div className="mr-4 w-96">
|
<div className="mr-4 w-96">
|
||||||
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
|
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
|
||||||
</div>
|
</div>
|
||||||
<UserProjectsSection membershipId={membershipId} />
|
<div className="w-full space-y-2">
|
||||||
|
<div className="w-full space-y-4">
|
||||||
|
<UserProjectsSection membershipId={membershipId} />
|
||||||
|
<UserGroupsSection orgMembership={membership} />
|
||||||
|
<UserAuditLogsSection orgMembership={membership} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -272,6 +280,7 @@ export const UserPage = withPermission(
|
|||||||
}
|
}
|
||||||
buttonText="Deactivate"
|
buttonText="Deactivate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { EmptyState, IconButton, Tooltip } from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
|
import { OrgUser } from "@app/hooks/api/types";
|
||||||
|
import { LogsSection } from "@app/views/Project/AuditLogsPage/components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
orgMembership: OrgUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserAuditLogsSection = withPermission(
|
||||||
|
({ orgMembership }: Props) => {
|
||||||
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
const { subscription, isLoading } = useSubscription();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
return subscription?.auditLogs ? (
|
||||||
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<p className="text-lg font-semibold text-gray-200">Audit Logs</p>
|
||||||
|
|
||||||
|
<Tooltip content="Show audit log filters">
|
||||||
|
<IconButton
|
||||||
|
colorSchema="primary"
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => setShowFilter(!showFilter)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<p>Filter</p>
|
||||||
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
|
</div>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<LogsSection
|
||||||
|
showFilters={showFilter}
|
||||||
|
filterClassName="bg-mineshaft-900 static"
|
||||||
|
presetActor={orgMembership.user.id}
|
||||||
|
isOrgAuditLogs
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : !isLoading ? (
|
||||||
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<p className="text-lg font-semibold text-gray-200">Audit Logs</p>
|
||||||
|
</div>
|
||||||
|
<EmptyState
|
||||||
|
className="rounded-lg"
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Please{" "}
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
subscription && subscription.slug !== null
|
||||||
|
? `/org/${orgMembership.organization}/billing`
|
||||||
|
: "https://infisical.com/scheduledemo"
|
||||||
|
}
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="cursor-pointer font-medium text-primary-500 transition-all hover:text-primary-600"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
upgrade your subscription
|
||||||
|
</a>
|
||||||
|
</Link>{" "}
|
||||||
|
to view audit logs for this user
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
},
|
||||||
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Member }
|
||||||
|
);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
|
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
import { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
group: TGroupWithProjectMemberships;
|
||||||
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
key={`user-project-membership-${group.id}`}
|
||||||
|
>
|
||||||
|
<Td>{group.name}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Unassign user from group">
|
||||||
|
<IconButton
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("removeUserFromGroup", {
|
||||||
|
groupSlug: group.slug
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { useRemoveUserFromGroup } from "@app/hooks/api";
|
||||||
|
import { OrgUser } from "@app/hooks/api/users/types";
|
||||||
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { UserGroupsTable } from "./UserGroupsTable";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
orgMembership: OrgUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserGroupsSection = ({ orgMembership }: Props) => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"removeUserFromGroup"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup();
|
||||||
|
|
||||||
|
const handleRemoveUserFromGroup = useCallback(async (groupSlug: string) => {
|
||||||
|
try {
|
||||||
|
await removeUserFromGroup({
|
||||||
|
slug: groupSlug,
|
||||||
|
username: orgMembership.user.username
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "User removed from group successfully"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("removeUserFromGroup");
|
||||||
|
} catch (error) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to remove user from group"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">Groups</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UserGroupsTable orgMembership={orgMembership} handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeUserFromGroup.isOpen}
|
||||||
|
title="Are you sure want to unassign user from group?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeUserFromGroup", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() => {
|
||||||
|
const popupData = popUp?.removeUserFromGroup?.data as {
|
||||||
|
groupSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return handleRemoveUserFromGroup(popupData.groupSlug);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { EmptyState, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
|
||||||
|
import { OrgUser } from "@app/hooks/api/types";
|
||||||
|
import { useListUserGroupMemberships } from "@app/hooks/api/users/queries";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { UserGroupsRow } from "./UserGroupsRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
orgMembership: OrgUser;
|
||||||
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
||||||
|
const { data: groups, isLoading } = useListUserGroupMemberships(orgMembership.user.username);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{groups?.map((group) => (
|
||||||
|
<UserGroupsRow
|
||||||
|
key={`user-group-${group.id}`}
|
||||||
|
group={group}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && !groups?.length && (
|
||||||
|
<EmptyState title="This user has not been assigned to any groups" icon={faFolder} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ export const AuditLogsPage = withProjectPermission(
|
|||||||
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
<LogsSection />
|
<LogsSection showFilters />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { Control, Controller, UseFormReset } from "react-hook-form";
|
import { Control, Controller, UseFormReset } from "react-hook-form";
|
||||||
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
|
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
@@ -19,11 +20,13 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
presetActor?: string;
|
||||||
|
className?: string;
|
||||||
control: Control<AuditLogFilterFormData>;
|
control: Control<AuditLogFilterFormData>;
|
||||||
reset: UseFormReset<AuditLogFilterFormData>;
|
reset: UseFormReset<AuditLogFilterFormData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsFilter = ({ control, reset }: Props) => {
|
export const LogsFilter = ({ presetActor, className, control, reset }: Props) => {
|
||||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||||
|
|
||||||
@@ -69,8 +72,13 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-20 z-10 flex items-center justify-between bg-bunker-800">
|
<div
|
||||||
<div className="flex items-center">
|
className={twMerge(
|
||||||
|
"sticky top-20 z-10 flex items-center justify-between bg-bunker-800",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="eventType"
|
name="eventType"
|
||||||
@@ -79,7 +87,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
|||||||
label="Event"
|
label="Event"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mr-4 w-40"
|
className="w-40"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||||
@@ -96,7 +104,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!isLoading && data && data.length > 0 && (
|
{!isLoading && data && data.length > 0 && !presetActor && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="actor"
|
name="actor"
|
||||||
@@ -105,7 +113,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
|||||||
label="Actor"
|
label="Actor"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mr-4 w-40"
|
className="w-40"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||||
@@ -127,7 +135,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
|||||||
label="Source"
|
label="Source"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mr-4 w-40"
|
className="w-40"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||||
@@ -149,12 +157,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl label="Start date" errorText={error?.message} isError={Boolean(error)}>
|
||||||
label="Start date"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="mr-4"
|
|
||||||
>
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={field.value || undefined}
|
value={field.value || undefined}
|
||||||
onChange={(date) => {
|
onChange={(date) => {
|
||||||
@@ -195,26 +198,25 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<Button
|
||||||
<Button
|
isLoading={false}
|
||||||
isLoading={false}
|
colorSchema="primary"
|
||||||
colorSchema="primary"
|
variant="outline_bg"
|
||||||
variant="outline_bg"
|
className="mt-1.5"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
|
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
reset({
|
reset({
|
||||||
eventType: undefined,
|
eventType: undefined,
|
||||||
actor: undefined,
|
actor: presetActor,
|
||||||
userAgentType: undefined,
|
userAgentType: undefined,
|
||||||
startDate: undefined,
|
startDate: undefined,
|
||||||
endDate: undefined
|
endDate: undefined
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Clear filters
|
Clear filters
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,19 @@ import { LogsFilter } from "./LogsFilter";
|
|||||||
import { LogsTable } from "./LogsTable";
|
import { LogsTable } from "./LogsTable";
|
||||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||||
|
|
||||||
export const LogsSection = () => {
|
type Props = {
|
||||||
|
presetActor?: string;
|
||||||
|
showFilters?: boolean;
|
||||||
|
filterClassName?: string;
|
||||||
|
isOrgAuditLogs?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LogsSection = ({
|
||||||
|
presetActor,
|
||||||
|
filterClassName,
|
||||||
|
isOrgAuditLogs,
|
||||||
|
showFilters
|
||||||
|
}: Props) => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -21,6 +33,7 @@ export const LogsSection = () => {
|
|||||||
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
|
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
|
||||||
resolver: yupResolver(auditLogFilterFormSchema),
|
resolver: yupResolver(auditLogFilterFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
actor: presetActor,
|
||||||
page: 1,
|
page: 1,
|
||||||
perPage: 10,
|
perPage: 10,
|
||||||
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
|
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
|
||||||
@@ -43,10 +56,19 @@ export const LogsSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LogsFilter control={control} reset={reset} />
|
{showFilters && (
|
||||||
|
<LogsFilter
|
||||||
|
className={filterClassName}
|
||||||
|
presetActor={presetActor}
|
||||||
|
control={control}
|
||||||
|
reset={reset}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<LogsTable
|
<LogsTable
|
||||||
|
isOrgAuditLogs={isOrgAuditLogs}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
userAgentType={userAgentType}
|
userAgentType={userAgentType}
|
||||||
|
showActorColumn={!presetActor}
|
||||||
actor={actor}
|
actor={actor}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
|
|||||||
@@ -25,15 +25,24 @@ type Props = {
|
|||||||
actor?: string;
|
actor?: string;
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
|
isOrgAuditLogs?: boolean;
|
||||||
|
showActorColumn: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUDIT_LOG_LIMIT = 15;
|
const AUDIT_LOG_LIMIT = 15;
|
||||||
|
|
||||||
export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate }: Props) => {
|
export const LogsTable = ({
|
||||||
|
eventType,
|
||||||
|
userAgentType,
|
||||||
|
showActorColumn,
|
||||||
|
actor,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
isOrgAuditLogs
|
||||||
|
}: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
|
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
|
||||||
currentWorkspace?.id ?? "",
|
|
||||||
{
|
{
|
||||||
eventType,
|
eventType,
|
||||||
userAgentType,
|
userAgentType,
|
||||||
@@ -41,7 +50,8 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
limit: AUDIT_LOG_LIMIT
|
limit: AUDIT_LOG_LIMIT
|
||||||
}
|
},
|
||||||
|
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEmpty = !isLoading && !data?.pages?.[0].length;
|
const isEmpty = !isLoading && !data?.pages?.[0].length;
|
||||||
@@ -54,7 +64,8 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>Timestamp</Th>
|
<Th>Timestamp</Th>
|
||||||
<Th>Event</Th>
|
<Th>Event</Th>
|
||||||
<Th>Actor</Th>
|
{isOrgAuditLogs && <Th>Project</Th>}
|
||||||
|
{showActorColumn && <Th>Actor</Th>}
|
||||||
<Th>Source</Th>
|
<Th>Source</Th>
|
||||||
<Th>Metadata</Th>
|
<Th>Metadata</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
@@ -64,7 +75,12 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
|
|||||||
data?.pages?.map((group, i) => (
|
data?.pages?.map((group, i) => (
|
||||||
<Fragment key={`auditlog-item-${i + 1}`}>
|
<Fragment key={`auditlog-item-${i + 1}`}>
|
||||||
{group.map((auditLog) => (
|
{group.map((auditLog) => (
|
||||||
<LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog.id}`} />
|
<LogsTableRow
|
||||||
|
showActorColumn={showActorColumn}
|
||||||
|
isOrgAuditLogs={isOrgAuditLogs}
|
||||||
|
auditLog={auditLog}
|
||||||
|
key={`audit-log-${auditLog.id}`}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
auditLog: AuditLog;
|
auditLog: AuditLog;
|
||||||
|
isOrgAuditLogs?: boolean;
|
||||||
|
showActorColumn: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsTableRow = ({ auditLog }: Props) => {
|
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
|
||||||
const renderActor = (actor: Actor) => {
|
const renderActor = (actor: Actor) => {
|
||||||
switch (actor.type) {
|
switch (actor.type) {
|
||||||
case ActorType.USER:
|
case ActorType.USER:
|
||||||
@@ -486,7 +488,8 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
|||||||
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
||||||
<Td>{formatDate(auditLog.createdAt)}</Td>
|
<Td>{formatDate(auditLog.createdAt)}</Td>
|
||||||
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
|
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
|
||||||
{renderActor(auditLog.actor)}
|
{isOrgAuditLogs && <Td>{auditLog.project.name}</Td>}
|
||||||
|
{showActorColumn && renderActor(auditLog.actor)}
|
||||||
<Td>
|
<Td>
|
||||||
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
|
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
|
||||||
<p>{auditLog.ipAddress}</p>
|
<p>{auditLog.ipAddress}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user