diff --git a/codegen.mts b/codegen.mts index afe669c0ac..05af8add95 100644 --- a/codegen.mts +++ b/codegen.mts @@ -84,6 +84,31 @@ const config: CodegenConfig = { ID: 'string', }, mappers: { + AuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + UserInvitedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + UserJoinedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + UserRemovedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + OrganizationSettingsUpdatedAuditLog: + '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + OrganizationTransferredAuditLog: + '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + ProjectCreatedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + ProjectSettingsUpdatedAuditLog: + '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + ProjectDeletedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + TargetCreatedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + TargetSettingsUpdatedAuditLog: + '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + TargetDeletedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + SchemaPolicySettingsUpdatedAuditLog: + '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + SchemaCheckedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + SchemaPublishAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + SchemaDeletedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + RoleCreatedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + RoleAssignedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + RoleDeletedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', + RoleUpdatedAuditLog: '../modules/audit-logs/module.graphql.mappers#AuditLogMapper', SchemaChange: '../modules/schema/module.graphql.mappers#SchemaChangeMapper', SchemaChangeApproval: '../modules/schema/module.graphql.mappers#SchemaChangeApprovalMapper', diff --git a/packages/libraries/core/src/client/agent.ts b/packages/libraries/core/src/client/agent.ts index 385da554e8..d4a3873aab 100644 --- a/packages/libraries/core/src/client/agent.ts +++ b/packages/libraries/core/src/client/agent.ts @@ -110,7 +110,6 @@ export function createAgent( const promise = captureAsync(event); inProgressCaptures.push(promise); void promise.finally(() => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises inProgressCaptures = inProgressCaptures.filter(p => p !== promise); }); } else { diff --git a/packages/migrations/src/clickhouse-actions/011-audit-logs.ts b/packages/migrations/src/clickhouse-actions/011-audit-logs.ts new file mode 100644 index 0000000000..e6c3764502 --- /dev/null +++ b/packages/migrations/src/clickhouse-actions/011-audit-logs.ts @@ -0,0 +1,20 @@ +import type { Action } from '../clickhouse'; + +export const action: Action = async exec => { + await exec(` + CREATE TABLE IF NOT EXISTS "audit_log" + ( + "id" UUID DEFAULT generateUUIDv4() CODEC(ZSTD(1)), + "event_time" DateTime CODEC(ZSTD(1)), + "user_id" String CODEC(ZSTD(1)), + "user_email" String CODEC(ZSTD(1)), + "organization_id" String CODEC(ZSTD(1)), + "event_action" String CODEC(ZSTD(1)), + "metadata" String CODEC(ZSTD(1)) + ) + ENGINE = ReplacingMergeTree + ORDER BY ("event_time", "user_id") + TTL event_time + INTERVAL 1 YEAR + SETTINGS index_granularity = 8192 + `); +}; diff --git a/packages/migrations/src/clickhouse.ts b/packages/migrations/src/clickhouse.ts index d0b1e1f5b1..3e311b8351 100644 --- a/packages/migrations/src/clickhouse.ts +++ b/packages/migrations/src/clickhouse.ts @@ -166,6 +166,7 @@ export async function migrateClickHouse( import('./clickhouse-actions/008-daily-operations-log'), import('./clickhouse-actions/009-ttl-1-year'), import('./clickhouse-actions/010-app-deployment-operations'), + import('./clickhouse-actions/011-audit-logs'), ]); async function actionRunner(action: Action, index: number) { diff --git a/packages/services/api/src/create.ts b/packages/services/api/src/create.ts index faf2a1cf2a..f00b5d2ac9 100644 --- a/packages/services/api/src/create.ts +++ b/packages/services/api/src/create.ts @@ -5,6 +5,7 @@ import { alertsModule } from './modules/alerts'; import { WEBHOOKS_CONFIG, WebhooksConfig } from './modules/alerts/providers/tokens'; import { appDeploymentsModule } from './modules/app-deployments'; import { APP_DEPLOYMENTS_ENABLED } from './modules/app-deployments/providers/app-deployments-enabled-token'; +import { auditLogsModule } from './modules/audit-logs'; import { authModule } from './modules/auth'; import { billingModule } from './modules/billing'; import { BILLING_CONFIG, BillingConfig } from './modules/billing/providers/tokens'; @@ -82,6 +83,7 @@ const modules = [ integrationsModule, alertsModule, feedbackModule, + auditLogsModule, cdnModule, adminModule, usageEstimationModule, diff --git a/packages/services/api/src/modules/audit-logs/index.ts b/packages/services/api/src/modules/audit-logs/index.ts new file mode 100644 index 0000000000..e2c2b875a7 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/index.ts @@ -0,0 +1,13 @@ +import { createModule } from 'graphql-modules'; +import { ClickHouse } from '../operations/providers/clickhouse-client'; +import { AuditLogManager } from './providers/audit-logs-manager'; +import { resolvers } from './resolvers.generated'; +import { typeDefs } from './module.graphql'; + +export const auditLogsModule = createModule({ + id: 'audit-logs', + dirname: __dirname, + typeDefs, + resolvers, + providers: [AuditLogManager, ClickHouse], +}); diff --git a/packages/services/api/src/modules/audit-logs/module.graphql.mappers.ts b/packages/services/api/src/modules/audit-logs/module.graphql.mappers.ts new file mode 100644 index 0000000000..9f39c4856d --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/module.graphql.mappers.ts @@ -0,0 +1,3 @@ +import { AuditLogModel } from './providers/audit-logs-manager'; + +export type AuditLogMapper = AuditLogModel; diff --git a/packages/services/api/src/modules/audit-logs/module.graphql.ts b/packages/services/api/src/modules/audit-logs/module.graphql.ts new file mode 100644 index 0000000000..21636bfb9f --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/module.graphql.ts @@ -0,0 +1,238 @@ +import { gql } from 'graphql-modules'; + +export const typeDefs = gql` + scalar DateTime + scalar Date + interface AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + } + + type AuditLogUserRecord { + userId: ID! + userEmail: String! + user: User # this one is nullable because it can be deleted! + } + + type AuditLogConnection { + nodes: [AuditLog]! + total: Int! + } + + type AuditLogFileExport { + id: ID! + url: String! + validUntil: DateTime! + createdAt: DateTime! + } + + type ExportResultEdge { + node: AuditLogFileExport! + cursor: String! + } + + type ExportResultConnection { + edges: [ExportResultEdge!]! + pageInfo: PageInfo! + } + + input AuditLogFilter { + date: Date + userId: String + organizationId: String + eventType: String + } + + extend type Query { + auditLogs(filter: AuditLogFilter, limit: Int, offset: Int): AuditLogConnection! + auditLogExports(limit: Int, offset: Int, filter: AuditLogFilter): ExportResultConnection! + } + + extend type Mutation { + exportAuditLogsToFile(filter: AuditLogFilter!): AuditLogFileExport! + } + + type ModifyAuditLogError implements Error { + message: String! + } + + type UserInvitedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + inviteeId: String! + inviteeEmail: String! + } + + type UserJoinedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + inviteeId: String! + inviteeEmail: String! + } + type UserRemovedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + removedUserId: String! + removedUserEmail: String! + } + + type OrganizationSettingsUpdatedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + updatedFields: JSON! + } + + type OrganizationTransferredAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + newOwnerId: String! + newOwnerEmail: String! + } + + type ProjectCreatedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + projectName: String! + } + + type ProjectSettingsUpdatedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + updatedFields: JSON! + } + + type ProjectDeletedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + projectName: String! + } + + type TargetCreatedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + targetId: String! + targetName: String! + } + + type TargetSettingsUpdatedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + targetId: String! + updatedFields: JSON! + } + + type TargetDeletedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + targetId: String! + targetName: String! + } + + type SchemaPolicySettingsUpdatedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + updatedFields: JSON! + } + + type SchemaCheckedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + targetId: String! + schemaSdl: String! + } + + type SchemaPublishAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + targetId: String! + schemaName: String! + } + + type SchemaDeletedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + projectId: String! + targetId: String! + schemaName: String! + } + + type RoleCreatedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + roleId: String! + roleName: String! + } + + type RoleAssignedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + roleId: String! + roleName: String! + userIdAssigned: String! + userEmailAssigned: String! + } + + type RoleDeletedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + roleId: String! + roleName: String! + } + + type RoleUpdatedAuditLog implements AuditLog { + id: ID! + eventTime: DateTime! + user: AuditLogUserRecord! + organizationId: String! + roleId: String! + roleName: String! + updatedFields: JSON! + } +`; diff --git a/packages/services/api/src/modules/audit-logs/providers/audit-logs-manager.ts b/packages/services/api/src/modules/audit-logs/providers/audit-logs-manager.ts new file mode 100644 index 0000000000..20b819a1aa --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/providers/audit-logs-manager.ts @@ -0,0 +1,114 @@ +import { Injectable, Scope } from 'graphql-modules'; +import { z } from 'zod'; +import { QueryAuditLogsArgs } from '../../../__generated__/types'; +import { ClickHouse, sql } from '../../operations/providers/clickhouse-client'; +import { Logger } from '../../shared/providers/logger'; +import { AuditLogEvent, auditLogSchema } from './audit-logs-types'; + +const auditLogDbObject = z.object({ + id: z.string(), + event_time: z.string(), + user_id: z.string(), + user_email: z.string(), + organization_id: z.string(), + event_action: z.string(), + metadata: z.string(), +}); +export type AuditLogModel = z.infer; + +const auditLogCount = z.object({ + 'COUNT()': z.string(), +}); + +@Injectable({ + scope: Scope.Operation, + global: true, +}) +export class AuditLogManager { + private logger: Logger; + + constructor( + logger: Logger, + private clickHouse: ClickHouse, + ) { + this.logger = logger.child({ source: 'AuditLogsManager' }); + } + + async createLogAuditEvent(event: AuditLogEvent): Promise { + const { eventType, organizationId, user } = event; + this.logger.info('Creating a log audit event (event=%o)', event); + + const parsedEvent = auditLogSchema.parse(event); + const metadata = { + user: user.user, + ...parsedEvent, + }; + + const eventMetadata = JSON.stringify(metadata); + const eventTime = new Date(); + + const values = [ + eventTime, + user.userId, + user.userEmail, + organizationId, + eventType, + eventMetadata, + ]; + + await this.clickHouse.insert({ + query: sql` + INSERT INTO audit_log + (event_time, user_id, user_email, organization_id, event_action, metadata) + FORMAT CSV`, + data: [values], + timeout: 5000, + queryId: 'create-audit-log', + }); + } + + async getPaginatedAuditLogs(props: QueryAuditLogsArgs): Promise { + this.logger.info( + 'Getting paginated audit logs (limit=%s, offset=%s)', + props.limit, + props.offset, + ); + + const limit = props.limit ?? 25; // Default to 25 if limit is undefined + const sqlLimit = sql.raw(limit.toString()); + const offset = props.offset ?? 0; // Default to 0 if offset is undefined + const sqlOffset = sql.raw(offset.toString()); + + const result = await this.clickHouse.query({ + query: sql` + SELECT * + FROM audit_log + ORDER BY event_time DESC + LIMIT ${sqlLimit} + OFFSET ${sqlOffset} + `, + queryId: 'get-audit-logs', + timeout: 5000, + }); + + const model = z.array(auditLogDbObject); + return model.parse(result.data); + } + + async getAuditLogsCount(): Promise { + this.logger.info('Getting audit logs count'); + + const result = await this.clickHouse.query({ + query: sql` + SELECT COUNT(*) + FROM audit_log + `, + queryId: 'get-audit-logs-count', + timeout: 5000, + }); + + const parsed = auditLogCount.parse(result.data[0]); + const resultParseInt = parseInt(parsed['COUNT()']); + return resultParseInt; + } +} diff --git a/packages/services/api/src/modules/audit-logs/providers/audit-logs-types.ts b/packages/services/api/src/modules/audit-logs/providers/audit-logs-types.ts new file mode 100644 index 0000000000..7bf9cd3ec8 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/providers/audit-logs-types.ts @@ -0,0 +1,193 @@ +import { z } from 'zod'; +import type { User } from '../../../shared/entities'; + +const userInvitedAuditLogSchema = z.object({ + inviteeId: z.string(), + inviteeEmail: z.string(), +}); +const userJoinedAuditLogSchema = z.object({ + inviteeId: z.string(), + inviteeEmail: z.string(), +}); + +const userRemovedAuditLogSchema = z.object({ + removedUserId: z.string(), + removedUserEmail: z.string(), +}); + +const organizationSettingsUpdatedAuditLogSchema = z.object({ + updatedFields: z.string(), +}); + +const organizationTransferredAuditLogSchema = z.object({ + newOwnerId: z.string(), + newOwnerEmail: z.string(), +}); + +const projectCreatedAuditLogSchema = z.object({ + projectId: z.string(), + projectName: z.string(), +}); + +const projectSettingsUpdatedAuditLogSchema = z.object({ + projectId: z.string(), + updatedFields: z.string(), +}); + +const projectDeletedAuditLogSchema = z.object({ + projectId: z.string(), + projectName: z.string(), +}); + +const targetCreatedAuditLogSchema = z.object({ + projectId: z.string(), + targetId: z.string(), + targetName: z.string(), +}); + +const targetSettingsUpdatedAuditLogSchema = z.object({ + projectId: z.string(), + targetId: z.string(), + updatedFields: z.string(), +}); + +const targetDeletedAuditLogSchema = z.object({ + projectId: z.string(), + targetId: z.string(), + targetName: z.string(), +}); + +const schemaPolicySettingsUpdatedAuditLogSchema = z.object({ + projectId: z.string(), + updatedFields: z.string(), +}); + +const schemaCheckedAuditLogSchema = z.object({ + projectId: z.string(), + targetId: z.string(), + schemaSdl: z.string(), +}); + +const schemaPublishAuditLogSchema = z.object({ + projectId: z.string(), + targetId: z.string(), + schemaName: z.string(), + schemaSdl: z.string(), +}); + +const schemaDeletedAuditLogSchema = z.object({ + projectId: z.string(), + targetId: z.string(), + schemaName: z.string(), +}); + +const roleCreatedAuditLogSchema = z.object({ + roleId: z.string(), + roleName: z.string(), +}); + +const roleAssignedAuditLogSchema = z.object({ + roleId: z.string(), + roleName: z.string(), + userIdAssigned: z.string(), + userEmailAssigned: z.string(), +}); + +const roleDeletedAuditLogSchema = z.object({ + roleId: z.string(), + roleName: z.string(), +}); + +const roleUpdatedAuditLogSchema = z.object({ + roleId: z.string(), + roleName: z.string(), + updatedFields: z.string(), +}); + +export const auditLogSchema = z.discriminatedUnion('eventType', [ + z.object({ + eventType: z.literal('USER_INVITED'), + userInvitedAuditLogSchema, + }), + z.object({ + eventType: z.literal('USER_JOINED'), + userJoinedAuditLogSchema, + }), + z.object({ + eventType: z.literal('USER_REMOVED'), + userRemovedAuditLogSchema, + }), + z.object({ + eventType: z.literal('ORGANIZATION_SETTINGS_UPDATED'), + organizationSettingsUpdatedAuditLogSchema, + }), + z.object({ + eventType: z.literal('ORGANIZATION_TRANSFERRED'), + organizationTransferredAuditLogSchema, + }), + z.object({ + eventType: z.literal('PROJECT_CREATED'), + projectCreatedAuditLogSchema, + }), + z.object({ + eventType: z.literal('PROJECT_SETTINGS_UPDATED'), + projectSettingsUpdatedAuditLogSchema, + }), + z.object({ + eventType: z.literal('PROJECT_DELETED'), + projectDeletedAuditLogSchema, + }), + z.object({ + eventType: z.literal('TARGET_CREATED'), + targetCreatedAuditLogSchema, + }), + z.object({ + eventType: z.literal('TARGET_SETTINGS_UPDATED'), + targetSettingsUpdatedAuditLogSchema, + }), + z.object({ + eventType: z.literal('TARGET_DELETED'), + targetDeletedAuditLogSchema, + }), + z.object({ + eventType: z.literal('SCHEMA_POLICY_SETTINGS_UPDATED'), + schemaPolicySettingsUpdatedAuditLogSchema, + }), + z.object({ + eventType: z.literal('SCHEMA_CHECKED'), + schemaCheckedAuditLogSchema, + }), + z.object({ + eventType: z.literal('SCHEMA_PUBLISH'), + schemaPublishAuditLogSchema, + }), + z.object({ + eventType: z.literal('SCHEMA_DELETED'), + schemaDeletedAuditLogSchema, + }), + z.object({ + eventType: z.literal('ROLE_CREATED'), + roleCreatedAuditLogSchema, + }), + z.object({ + eventType: z.literal('ROLE_ASSIGNED'), + roleAssignedAuditLogSchema, + }), + z.object({ + eventType: z.literal('ROLE_DELETED'), + roleDeletedAuditLogSchema, + }), + z.object({ + eventType: z.literal('ROLE_UPDATED'), + roleUpdatedAuditLogSchema, + }), +]); + +export type AuditLogEvent = z.infer & { + user: { + userId: string; + userEmail: string; + user: (User & { isAdmin: boolean }) | null; + }; + organizationId: string; +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/Mutation/exportAuditLogsToFile.ts b/packages/services/api/src/modules/audit-logs/resolvers/Mutation/exportAuditLogsToFile.ts new file mode 100644 index 0000000000..36caf1cce2 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/Mutation/exportAuditLogsToFile.ts @@ -0,0 +1,15 @@ +import type { MutationResolvers } from './../../../../__generated__/types.next'; + +// TODO: Export audit logs to a file +export const exportAuditLogsToFile: NonNullable< + MutationResolvers['exportAuditLogsToFile'] +> = async (_parent, _arg, _ctx) => { + /* Implement Mutation.exportAuditLogsToFile resolver logic here */ + return { + createdAt: '2021-09-01T00:00:00Z', + id: 'id', + url: 'url', + validUntil: '2021-09-01T00:00:00Z', + __typename: 'AuditLogFileExport', + }; +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/OrganizationSettingsUpdatedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/OrganizationSettingsUpdatedAuditLog.ts new file mode 100644 index 0000000000..ca5527ab74 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/OrganizationSettingsUpdatedAuditLog.ts @@ -0,0 +1,28 @@ +import type { OrganizationSettingsUpdatedAuditLogResolvers } from './../../../__generated__/types'; + +export const OrganizationSettingsUpdatedAuditLog: OrganizationSettingsUpdatedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'ORGANIZATION_SETTINGS_UPDATED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + updatedFields: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'ORGANIZATION_SETTINGS_UPDATED' && + parsedMetadata.typeFields.eventType === 'ORGANIZATION_SETTINGS_UPDATED' + ) { + return parsedMetadata.typeFields.OrganizationSettingsUpdatedAuditLogSchema.updatedFields; + } + throw new Error('Invalid eventType'); + }, + organizationId: e => e.organization_id, + user: e => { + return { + userEmail: e.user_email, + userId: e.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/OrganizationTransferredAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/OrganizationTransferredAuditLog.ts new file mode 100644 index 0000000000..76bdaeff97 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/OrganizationTransferredAuditLog.ts @@ -0,0 +1,38 @@ +import type { OrganizationTransferredAuditLogResolvers } from './../../../__generated__/types'; + +export const OrganizationTransferredAuditLog: OrganizationTransferredAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'ORGANIZATION_TRANSFERRED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + newOwnerEmail: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'ORGANIZATION_TRANSFERRED' && + parsedMetadata.eventType === 'ORGANIZATION_TRANSFERRED' + ) { + return parsedMetadata.typeFields.OrganizationTransferredAuditLogSchema.newOwnerEmail; + } + throw new Error('Invalid eventType'); + }, + newOwnerId: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'ORGANIZATION_TRANSFERRED' && + parsedMetadata.typeFields.eventType === 'ORGANIZATION_TRANSFERRED' + ) { + return parsedMetadata.typeFields.OrganizationTransferredAuditLogSchema.newOwnerId; + } + throw new Error('Invalid eventType'); + }, + organizationId: e => e.organization_id, + user: e => { + return { + userEmail: e.user_email, + userId: e.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/ProjectCreatedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/ProjectCreatedAuditLog.ts new file mode 100644 index 0000000000..852a0505a3 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/ProjectCreatedAuditLog.ts @@ -0,0 +1,38 @@ +import type { ProjectCreatedAuditLogResolvers } from './../../../__generated__/types'; + +export const ProjectCreatedAuditLog: ProjectCreatedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'PROJECT_CREATED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'PROJECT_CREATED' && + parsedMetadata.typeFields.eventType === 'PROJECT_CREATED' + ) { + return parsedMetadata.typeFields.ProjectCreatedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + projectName: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'PROJECT_CREATED' && + parsedMetadata.typeFields.eventType === 'PROJECT_CREATED' + ) { + return parsedMetadata.typeFields.ProjectCreatedAuditLogSchema.projectName; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/ProjectDeletedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/ProjectDeletedAuditLog.ts new file mode 100644 index 0000000000..e73a74b7c4 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/ProjectDeletedAuditLog.ts @@ -0,0 +1,38 @@ +import type { ProjectDeletedAuditLogResolvers } from './../../../__generated__/types'; + +export const ProjectDeletedAuditLog: ProjectDeletedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'PROJECT_DELETED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'PROJECT_DELETED' && + parsedMetadata.typeFields.eventType === 'PROJECT_DELETED' + ) { + return parsedMetadata.typeFields.ProjectDeletedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + projectName: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'PROJECT_DELETED' && + parsedMetadata.typeFields.eventType === 'PROJECT_DELETED' + ) { + return parsedMetadata.typeFields.ProjectDeletedAuditLogSchema.projectName; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/ProjectSettingsUpdatedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/ProjectSettingsUpdatedAuditLog.ts new file mode 100644 index 0000000000..200d39a2db --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/ProjectSettingsUpdatedAuditLog.ts @@ -0,0 +1,38 @@ +import type { ProjectSettingsUpdatedAuditLogResolvers } from './../../../__generated__/types'; + +export const ProjectSettingsUpdatedAuditLog: ProjectSettingsUpdatedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'PROJECT_SETTINGS_UPDATED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'PROJECT_SETTINGS_UPDATED' && + parsedMetadata.typeFields.eventType === 'PROJECT_SETTINGS_UPDATED' + ) { + return parsedMetadata.typeFields.ProjectSettingsUpdatedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + updatedFields: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'PROJECT_SETTINGS_UPDATED' && + parsedMetadata.typeFields.eventType === 'PROJECT_SETTINGS_UPDATED' + ) { + return parsedMetadata.typeFields.ProjectSettingsUpdatedAuditLogSchema.updatedFields; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/Query/auditLogExports.ts b/packages/services/api/src/modules/audit-logs/resolvers/Query/auditLogExports.ts new file mode 100644 index 0000000000..b0828cb25d --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/Query/auditLogExports.ts @@ -0,0 +1,31 @@ +import type { QueryResolvers } from './../../../../__generated__/types.next'; + +// TODO: Implement Query.auditLogExports resolver +export const auditLogExports: NonNullable = async ( + _parent, + _arg, + _ctx, +) => { + return { + edges: [ + { + cursor: 'cursor', + node: { + createdAt: '2021-09-01T00:00:00Z', + id: 'id', + url: 'url', + validUntil: '2021-09-01T00:00:00Z', + __typename: 'AuditLogFileExport', + }, + __typename: 'ExportResultEdge', + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: 'startCursor', + endCursor: 'endCursor', + }, + __typename: 'ExportResultConnection', + }; +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/Query/auditLogs.ts b/packages/services/api/src/modules/audit-logs/resolvers/Query/auditLogs.ts new file mode 100644 index 0000000000..325d4ce8f3 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/Query/auditLogs.ts @@ -0,0 +1,26 @@ +import { AuditLogManager } from '../../providers/audit-logs-manager'; +import type { QueryResolvers } from './../../../../__generated__/types'; + +export const auditLogs: NonNullable = async (_parent, arg, ctx) => { + const countAuditLogs = await ctx.injector.get(AuditLogManager).getAuditLogsCount(); + if (countAuditLogs === 0) { + return { + nodes: [], + total: 0, + __typename: 'AuditLogConnection', + }; + } + + const { limit, offset, filter } = arg; + const auditLogs = await ctx.injector.get(AuditLogManager).getPaginatedAuditLogs({ + filter, + limit, + offset, + }); + + return { + nodes: auditLogs, + total: countAuditLogs, + __typename: 'AuditLogConnection', + }; +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/RoleAssignedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/RoleAssignedAuditLog.ts new file mode 100644 index 0000000000..371153e346 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/RoleAssignedAuditLog.ts @@ -0,0 +1,58 @@ +import type { RoleAssignedAuditLogResolvers } from './../../../__generated__/types'; + +export const RoleAssignedAuditLog: RoleAssignedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'ROLE_ASSIGNED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + roleId: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'ROLE_ASSIGNED' && + parsedMetadata.typeFields.eventType === 'ROLE_ASSIGNED' + ) { + return parsedMetadata.typeFields.RoleAssignedAuditLogSchema.roleId; + } + throw new Error('Invalid eventType'); + }, + roleName: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'ROLE_ASSIGNED' && + parsedMetadata.typeFields.eventType === 'ROLE_ASSIGNED' + ) { + return parsedMetadata.typeFields.RoleAssignedAuditLogSchema.roleName; + } + throw new Error('Invalid eventType'); + }, + userEmailAssigned: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'ROLE_ASSIGNED' && + parsedMetadata.typeFields.eventType === 'ROLE_ASSIGNED' + ) { + return parsedMetadata.typeFields.RoleAssignedAuditLogSchema.userEmailAssigned; + } + throw new Error('Invalid eventType'); + }, + userIdAssigned: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'ROLE_ASSIGNED' && + parsedMetadata.typeFields.eventType === 'ROLE_ASSIGNED' + ) { + return parsedMetadata.typeFields.RoleAssignedAuditLogSchema.userIdAssigned; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/RoleCreatedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/RoleCreatedAuditLog.ts new file mode 100644 index 0000000000..ceca7c81cd --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/RoleCreatedAuditLog.ts @@ -0,0 +1,39 @@ +import type { RoleCreatedAuditLogResolvers } from './../../../__generated__/types'; + +export const RoleCreatedAuditLog: RoleCreatedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'ROLE_CREATED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + roleId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'ROLE_CREATED' && + parsedMetadata.typeFields.eventType === 'ROLE_CREATED' + ) { + return parsedMetadata.typeFields.RoleCreatedAuditLogSchema.roleId; + } + throw new Error('Invalid eventType'); + }, + roleName: e => { + const parsedMetadata = JSON.parse(e.metadata); + if ( + e.event_action === 'ROLE_CREATED' && + parsedMetadata.typeFields.eventType === 'ROLE_CREATED' + ) { + return parsedMetadata.typeFields.RoleCreatedAuditLogSchema.roleName; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/RoleDeletedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/RoleDeletedAuditLog.ts new file mode 100644 index 0000000000..53ad33f2fc --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/RoleDeletedAuditLog.ts @@ -0,0 +1,40 @@ +import type { RoleDeletedAuditLogResolvers } from './../../../__generated__/types'; + +export const RoleDeletedAuditLog: RoleDeletedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'ROLE_DELETED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + roleId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'ROLE_DELETED' && + parsedMetadata.typeFields.eventType === 'ROLE_DELETED' + ) { + return parsedMetadata.typeFields.RoleDeletedAuditLogSchema.roleId; + } + throw new Error('Invalid eventType'); + }, + roleName: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'ROLE_DELETED' && + parsedMetadata.typeFields.eventType === 'ROLE_DELETED' + ) { + return parsedMetadata.typeFields.RoleDeletedAuditLogSchema.roleName; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/RoleUpdatedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/RoleUpdatedAuditLog.ts new file mode 100644 index 0000000000..64eee2852d --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/RoleUpdatedAuditLog.ts @@ -0,0 +1,51 @@ +import type { RoleUpdatedAuditLogResolvers } from './../../../__generated__/types'; + +export const RoleUpdatedAuditLog: RoleUpdatedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'ROLE_UPDATED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + roleId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'ROLE_UPDATED' && + parsedMetadata.typeFields.eventType === 'ROLE_UPDATED' + ) { + return parsedMetadata.typeFields.RoleUpdatedAuditLogSchema.roleId; + } + throw new Error('Invalid eventType'); + }, + updatedFields: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'ROLE_UPDATED' && + parsedMetadata.typeFields.eventType === 'ROLE_UPDATED' + ) { + return parsedMetadata.typeFields.RoleUpdatedAuditLogSchema.updatedFields; + } + throw new Error('Invalid eventType'); + }, + roleName: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'ROLE_UPDATED' && + parsedMetadata.typeFields.eventType === 'ROLE_UPDATED' + ) { + return parsedMetadata.typeFields.RoleUpdatedAuditLogSchema.roleName; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/SchemaCheckedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/SchemaCheckedAuditLog.ts new file mode 100644 index 0000000000..60aa0ef631 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/SchemaCheckedAuditLog.ts @@ -0,0 +1,51 @@ +import type { SchemaCheckedAuditLogResolvers } from './../../../__generated__/types'; + +export const SchemaCheckedAuditLog: SchemaCheckedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'SCHEMA_CHECKED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_CHECKED' && + parsedMetadata.typeFields.eventType === 'SCHEMA_CHECKED' + ) { + return parsedMetadata.typeFields.SchemaCheckedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + schemaSdl: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_CHECKED' && + parsedMetadata.typeFields.eventType === 'SCHEMA_CHECKED' + ) { + return parsedMetadata.typeFields.SchemaCheckedAuditLogSchema.schemaSdl; + } + throw new Error('Invalid eventType'); + }, + targetId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_CHECKED' && + parsedMetadata.typeFields.eventType === 'SCHEMA_CHECKED' + ) { + return parsedMetadata.typeFields.SchemaCheckedAuditLogSchema.targetId; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/SchemaDeletedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/SchemaDeletedAuditLog.ts new file mode 100644 index 0000000000..7d09a36be8 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/SchemaDeletedAuditLog.ts @@ -0,0 +1,51 @@ +import type { SchemaDeletedAuditLogResolvers } from './../../../__generated__/types'; + +export const SchemaDeletedAuditLog: SchemaDeletedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'SCHEMA_DELETED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_DELETED' && + parsedMetadata.typeFields.eventType === 'SCHEMA_DELETED' + ) { + return parsedMetadata.typeFields.SchemaDeletedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + schemaName: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_DELETED' && + parsedMetadata.typeFields.eventType === 'SCHEMA_DELETED' + ) { + return parsedMetadata.typeFields.SchemaDeletedAuditLogSchema.schemaName; + } + throw new Error('Invalid eventType '); + }, + targetId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_DELETED' && + parsedMetadata.typeFields.eventType === 'SCHEMA_DELETED' + ) { + return parsedMetadata.typeFields.SchemaDeletedAuditLogSchema.targetId; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/SchemaPolicySettingsUpdatedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/SchemaPolicySettingsUpdatedAuditLog.ts new file mode 100644 index 0000000000..6d16b0e748 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/SchemaPolicySettingsUpdatedAuditLog.ts @@ -0,0 +1,40 @@ +import type { SchemaPolicySettingsUpdatedAuditLogResolvers } from './../../../__generated__/types'; + +export const SchemaPolicySettingsUpdatedAuditLog: SchemaPolicySettingsUpdatedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'SCHEMA_POLICY_SETTINGS_UPDATED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_POLICY_SETTINGS_UPDATED' && + parsedMetadata.typeFields.eventType === 'SCHEMA_POLICY_SETTINGS_UPDATED' + ) { + return parsedMetadata.typeFields.SchemaPolicySettingsUpdatedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + updatedFields: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_POLICY_SETTINGS_UPDATED' && + parsedMetadata.typeFields.eventType === 'SCHEMA_POLICY_SETTINGS_UPDATED' + ) { + return parsedMetadata.typeFields.SchemaPolicySettingsUpdatedAuditLogSchema.updatedFields; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/SchemaPublishAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/SchemaPublishAuditLog.ts new file mode 100644 index 0000000000..b6843d5d1c --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/SchemaPublishAuditLog.ts @@ -0,0 +1,51 @@ +import type { SchemaPublishAuditLogResolvers } from './../../../__generated__/types'; + +export const SchemaPublishAuditLog: SchemaPublishAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'SCHEMA_PUBLISH', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_PUBLISH' && + parsedMetadata.typeFields.eventType === 'SCHEMA_PUBLISH' + ) { + return parsedMetadata.typeFields.SchemaPublishAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + targetId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_PUBLISH' && + parsedMetadata.typeFields.eventType === 'SCHEMA_PUBLISH' + ) { + return parsedMetadata.typeFields.SchemaPublishAuditLogSchema.targetId; + } + throw new Error('Invalid eventType'); + }, + schemaName: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'SCHEMA_PUBLISH' && + parsedMetadata.typeFields.eventType === 'SCHEMA_PUBLISH' + ) { + return parsedMetadata.typeFields.SchemaPublishAuditLogSchema.schemaName; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/TargetCreatedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/TargetCreatedAuditLog.ts new file mode 100644 index 0000000000..24dc00ab01 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/TargetCreatedAuditLog.ts @@ -0,0 +1,51 @@ +import type { TargetCreatedAuditLogResolvers } from './../../../__generated__/types'; + +export const TargetCreatedAuditLog: TargetCreatedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'TARGET_CREATED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_CREATED' && + parsedMetadata.typeFields.eventType === 'TARGET_CREATED' + ) { + return parsedMetadata.typeFields.TargetCreatedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + targetId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_CREATED' && + parsedMetadata.typeFields.eventType === 'TARGET_CREATED' + ) { + return parsedMetadata.typeFields.TargetCreatedAuditLogSchema.targetId; + } + throw new Error('Invalid eventType'); + }, + targetName: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_CREATED' && + parsedMetadata.typeFields.eventType === 'TARGET_CREATED' + ) { + return parsedMetadata.typeFields.TargetCreatedAuditLogSchema.targetName; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/TargetDeletedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/TargetDeletedAuditLog.ts new file mode 100644 index 0000000000..677da4b4a6 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/TargetDeletedAuditLog.ts @@ -0,0 +1,51 @@ +import type { TargetDeletedAuditLogResolvers } from './../../../__generated__/types'; + +export const TargetDeletedAuditLog: TargetDeletedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'TARGET_DELETED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_DELETED' && + parsedMetadata.typeFields.eventType === 'TARGET_DELETED' + ) { + return parsedMetadata.typeFields.TargetDeletedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + targetId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_DELETED' && + parsedMetadata.typeFields.eventType === 'TARGET_DELETED' + ) { + return parsedMetadata.typeFields.TargetDeletedAuditLogSchema.targetId; + } + throw new Error('Invalid eventType'); + }, + targetName: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_DELETED' && + parsedMetadata.typeFields.eventType === 'TARGET_DELETED' + ) { + return parsedMetadata.typeFields.TargetDeletedAuditLogSchema.targetName; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/TargetSettingsUpdatedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/TargetSettingsUpdatedAuditLog.ts new file mode 100644 index 0000000000..477bb410d0 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/TargetSettingsUpdatedAuditLog.ts @@ -0,0 +1,51 @@ +import type { TargetSettingsUpdatedAuditLogResolvers } from './../../../__generated__/types'; + +export const TargetSettingsUpdatedAuditLog: TargetSettingsUpdatedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'TARGET_SETTINGS_UPDATED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + projectId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_SETTINGS_UPDATED' && + parsedMetadata.typeFields.eventType === 'TARGET_SETTINGS_UPDATED' + ) { + return parsedMetadata.typeFields.TargetSettingsUpdatedAuditLogSchema.projectId; + } + throw new Error('Invalid eventType'); + }, + targetId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_SETTINGS_UPDATED' && + parsedMetadata.typeFields.eventType === 'TARGET_SETTINGS_UPDATED' + ) { + return parsedMetadata.typeFields.TargetSettingsUpdatedAuditLogSchema.targetId; + } + throw new Error('Invalid eventType'); + }, + updatedFields: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'TARGET_SETTINGS_UPDATED' && + parsedMetadata.typeFields.eventType === 'TARGET_SETTINGS_UPDATED' + ) { + return parsedMetadata.typeFields.TargetSettingsUpdatedAuditLogSchema.updatedFields; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/UserInvitedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/UserInvitedAuditLog.ts new file mode 100644 index 0000000000..9e1228f540 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/UserInvitedAuditLog.ts @@ -0,0 +1,40 @@ +import type { UserInvitedAuditLogResolvers } from './../../../__generated__/types'; + +export const UserInvitedAuditLog: UserInvitedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'USER_INVITED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + inviteeEmail: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'USER_INVITED' && + parsedMetadata.typeFields.eventType === 'USER_INVITED' + ) { + return parsedMetadata.typeFields.UserInvitedAuditLogSchema.inviteeEmail; + } + throw new Error('Invalid eventType'); + }, + inviteeId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'USER_INVITED' && + parsedMetadata.typeFields.eventType === 'USER_INVITED' + ) { + return parsedMetadata.typeFields.UserInvitedAuditLogSchema.inviteeId; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/UserJoinedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/UserJoinedAuditLog.ts new file mode 100644 index 0000000000..a1be8fc453 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/UserJoinedAuditLog.ts @@ -0,0 +1,34 @@ +import type { UserJoinedAuditLogResolvers } from './../../../__generated__/types'; + +export const UserJoinedAuditLog: UserJoinedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'USER_JOINED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + inviteeEmail: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if (e.event_action === 'USER_JOINED' && parsedMetadata.typeFields.eventType === 'USER_JOINED') { + return parsedMetadata.typeFields.UserJoinedAuditLogSchema.inviteeEmail; + } + throw new Error('Invalid eventType'); + }, + inviteeId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if (e.event_action === 'USER_JOINED' && parsedMetadata.typeFields.eventType === 'USER_JOINED') { + return parsedMetadata.typeFields.UserJoinedAuditLogSchema.inviteeId; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/audit-logs/resolvers/UserRemovedAuditLog.ts b/packages/services/api/src/modules/audit-logs/resolvers/UserRemovedAuditLog.ts new file mode 100644 index 0000000000..9be54028c4 --- /dev/null +++ b/packages/services/api/src/modules/audit-logs/resolvers/UserRemovedAuditLog.ts @@ -0,0 +1,40 @@ +import type { UserRemovedAuditLogResolvers } from './../../../__generated__/types'; + +export const UserRemovedAuditLog: UserRemovedAuditLogResolvers = { + __isTypeOf: e => e.event_action === 'USER_REMOVED', + eventTime: e => { + const eventTime = new Date(e.event_time); + return eventTime.toISOString(); + }, + id: e => e.id, + organizationId: e => e.organization_id, + removedUserEmail: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'USER_REMOVED' && + parsedMetadata.typeFields.eventType === 'USER_REMOVED' + ) { + return parsedMetadata.typeFields.UserRemovedAuditLogSchema.removedUserEmail; + } + throw new Error('Invalid eventType'); + }, + removedUserId: e => { + const parsedMetadata = JSON.parse(e.metadata); + + if ( + e.event_action === 'USER_REMOVED' && + parsedMetadata.typeFields.eventType === 'USER_REMOVED' + ) { + return parsedMetadata.typeFields.UserRemovedAuditLogSchema.removedUserId; + } + throw new Error('Invalid eventType'); + }, + user: async parent => { + return { + userEmail: parent.user_email, + userId: parent.user_id, + __typename: 'AuditLogUserRecord', + }; + }, +}; diff --git a/packages/services/api/src/modules/project/resolvers/Mutation/createProject.ts b/packages/services/api/src/modules/project/resolvers/Mutation/createProject.ts index ccb89814fe..de9e242b23 100644 --- a/packages/services/api/src/modules/project/resolvers/Mutation/createProject.ts +++ b/packages/services/api/src/modules/project/resolvers/Mutation/createProject.ts @@ -1,4 +1,6 @@ import { z } from 'zod'; +import { AuditLogManager } from '../../../audit-logs/providers/audit-logs-manager'; +import { AuthManager } from '../../../auth/providers/auth-manager'; import { OrganizationManager } from '../../../organization/providers/organization-manager'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { TargetManager } from '../../../target/providers/target-manager'; @@ -63,6 +65,26 @@ export const createProject: NonNullable = as }), ]); + // Audit Log Event + try { + const currentUser = await injector.get(AuthManager).getCurrentUser(); + await injector.get(AuditLogManager).createLogAuditEvent({ + eventType: 'PROJECT_CREATED', + organizationId: organizationId, + user: { + userId: currentUser.id, + userEmail: currentUser.email, + user: currentUser, + }, + projectCreatedAuditLogSchema: { + projectId: project.id, + projectName: project.name, + }, + }); + } catch (error) { + console.error('Failed to create audit log event', error); + } + return { ok: { createdProject: project, diff --git a/packages/services/api/src/modules/project/resolvers/Mutation/deleteProject.ts b/packages/services/api/src/modules/project/resolvers/Mutation/deleteProject.ts index 27970f54f7..c8fc9f58cd 100644 --- a/packages/services/api/src/modules/project/resolvers/Mutation/deleteProject.ts +++ b/packages/services/api/src/modules/project/resolvers/Mutation/deleteProject.ts @@ -1,3 +1,5 @@ +import { AuditLogManager } from '../../../audit-logs/providers/audit-logs-manager'; +import { AuthManager } from '../../../auth/providers/auth-manager'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { ProjectManager } from '../../providers/project-manager'; import type { MutationResolvers } from './../../../../__generated__/types.next'; @@ -21,6 +23,27 @@ export const deleteProject: NonNullable = as organization: organizationId, project: projectId, }); + + // Audit Log Event + try { + const currentUser = await injector.get(AuthManager).getCurrentUser(); + await injector.get(AuditLogManager).createLogAuditEvent({ + eventType: 'PROJECT_DELETED', + organizationId: organizationId, + user: { + userId: currentUser.id, + userEmail: currentUser.email, + user: currentUser, + }, + projectDeletedAuditLogSchema: { + projectId: projectId, + projectName: deletedProject.name, + }, + }); + } catch (error) { + console.error('Failed to create audit log event', error); + } + return { selector: { organization: organizationId, diff --git a/packages/services/api/src/modules/project/resolvers/Mutation/updateProjectName.ts b/packages/services/api/src/modules/project/resolvers/Mutation/updateProjectName.ts index 595d280952..5e218ea747 100644 --- a/packages/services/api/src/modules/project/resolvers/Mutation/updateProjectName.ts +++ b/packages/services/api/src/modules/project/resolvers/Mutation/updateProjectName.ts @@ -1,4 +1,6 @@ import { z } from 'zod'; +import { AuditLogManager } from '../../../audit-logs/providers/audit-logs-manager'; +import { AuthManager } from '../../../auth/providers/auth-manager'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { ProjectManager } from '../../providers/project-manager'; import { ProjectNameModel } from '../../validation'; @@ -35,6 +37,29 @@ export const updateProjectName: NonNullable = asyn project, name: input.name, }); + + // Audit Log Event + try { + const currentUser = await injector.get(AuthManager).getCurrentUser(); + await injector.get(AuditLogManager).createLogAuditEvent({ + eventType: 'TARGET_CREATED', + organizationId: target.orgId, + user: { + userId: currentUser.id, + userEmail: currentUser.email, + user: currentUser, + }, + targetCreatedAuditLogSchema: { + projectId: target.projectId, + targetId: target.id, + targetName: target.name, + }, + }); + } catch (error) { + console.error('Failed to create audit log event', error); + } + return { ok: { selector: { diff --git a/packages/services/api/src/modules/target/resolvers/Mutation/deleteTarget.ts b/packages/services/api/src/modules/target/resolvers/Mutation/deleteTarget.ts index fa04e6d095..4fe9f8d3a6 100644 --- a/packages/services/api/src/modules/target/resolvers/Mutation/deleteTarget.ts +++ b/packages/services/api/src/modules/target/resolvers/Mutation/deleteTarget.ts @@ -1,3 +1,5 @@ +import { AuditLogManager } from '../../../audit-logs/providers/audit-logs-manager'; +import { AuthManager } from '../../../auth/providers/auth-manager'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { TargetManager } from '../../providers/target-manager'; import type { MutationResolvers } from './../../../../__generated__/types.next'; @@ -27,6 +29,28 @@ export const deleteTarget: NonNullable = asyn project: projectId, target: targetId, }); + + // Audit Log Event + try { + const currentUser = await injector.get(AuthManager).getCurrentUser(); + await injector.get(AuditLogManager).createLogAuditEvent({ + eventType: 'TARGET_DELETED', + organizationId: target.orgId, + user: { + userId: currentUser.id, + userEmail: currentUser.email, + user: currentUser, + }, + targetDeletedAuditLogSchema: { + projectId: target.projectId, + targetId: target.id, + targetName: target.name, + }, + }); + } catch (error) { + console.error('Failed to create audit log event', error); + } + return { selector: { organization: organizationId, diff --git a/packages/services/api/src/modules/target/resolvers/Mutation/experimental__updateTargetSchemaComposition.ts b/packages/services/api/src/modules/target/resolvers/Mutation/experimental__updateTargetSchemaComposition.ts index 6e3beb25ba..b8947b31b7 100644 --- a/packages/services/api/src/modules/target/resolvers/Mutation/experimental__updateTargetSchemaComposition.ts +++ b/packages/services/api/src/modules/target/resolvers/Mutation/experimental__updateTargetSchemaComposition.ts @@ -1,3 +1,5 @@ +import { AuditLogManager } from '../../../audit-logs/providers/audit-logs-manager'; +import { AuthManager } from '../../../auth/providers/auth-manager'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { TargetManager } from '../../providers/target-manager'; import type { MutationResolvers } from './../../../../__generated__/types.next'; @@ -12,10 +14,36 @@ export const experimental__updateTargetSchemaComposition: NonNullable< translator.translateTargetId(input), ]); - return injector.get(TargetManager).updateTargetSchemaComposition({ + const result = await injector.get(TargetManager).updateTargetSchemaComposition({ organizationId, projectId, targetId, nativeComposition: input.nativeComposition, }); + + // Audit Log Event + try { + const currentUser = await injector.get(AuthManager).getCurrentUser(); + + await injector.get(AuditLogManager).createLogAuditEvent({ + eventType: 'TARGET_SETTINGS_UPDATED', + organizationId: organizationId, + user: { + userId: currentUser.id, + userEmail: currentUser.email, + user: currentUser, + }, + targetSettingsUpdatedAuditLogSchema: { + projectId: projectId, + targetId: targetId, + updatedFields: JSON.stringify({ + nativeComposition: input.nativeComposition, + }), + }, + }); + } catch (error) { + console.error('Failed to create audit log event', error); + } + + return result; }; diff --git a/packages/services/api/src/modules/target/resolvers/Mutation/setTargetValidation.ts b/packages/services/api/src/modules/target/resolvers/Mutation/setTargetValidation.ts index 8dddfc24c8..d61f725849 100644 --- a/packages/services/api/src/modules/target/resolvers/Mutation/setTargetValidation.ts +++ b/packages/services/api/src/modules/target/resolvers/Mutation/setTargetValidation.ts @@ -1,3 +1,5 @@ +import { AuditLogManager } from '../../../audit-logs/providers/audit-logs-manager'; +import { AuthManager } from '../../../auth/providers/auth-manager'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { TargetManager } from '../../providers/target-manager'; import type { MutationResolvers } from './../../../../__generated__/types.next'; @@ -22,9 +24,37 @@ export const setTargetValidation: NonNullable