Files
gitmost/apps/server/src/common/events/audit-events.ts
T
claude code agent 227 ec542a924b feat(comment): store suggestedText + POST /comments/apply-suggestion (#315 phase 4)
Server side of agent comment suggestions.

- CreateCommentDto gains optional suggestedText (<=2000). CommentService.create
  accepts it ONLY for a top-level inline comment with a non-empty selection,
  requires it be non-empty and differ from selection (else BadRequest), and
  stores it.
- POST /comments/apply-suggestion (ApplySuggestionDto { commentId }): authorizes
  with validateCanEdit (applying edits page text) BEFORE any structural check or
  mutation, then CommentService.applySuggestion:
  - runs the phase-3 collab event applyCommentSuggestion on `page.<pageId>` to
    atomically check-and-replace the marked text, returning { applied, currentText };
  - applied → stamp suggestion_applied_at/by, auto-resolve the thread, ws
    commentUpdated, audit COMMENT_SUGGESTION_APPLIED;
  - already-applied (DB) → idempotent success (no re-apply), self-healing the
    resolve if it was missed — satisfies the issue's double-click / two-user
    race requirement;
  - collab verdict applied:false && currentText===suggestedText → idempotent
    success (crash between doc mutation and DB write);
  - text changed → 409 ConflictException carrying currentText;
  - gateway undefined/throw → hard error, never a silent success.
- audit-events: COMMENT_SUGGESTION_APPLIED.

Tests: create validation (reply/no-selection/equal-to-selection rejected;
valid stored) + applySuggestion verdict branches incl. both idempotent paths.
jest src/core/comment: 33 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:09:23 +03:00

159 lines
4.5 KiB
TypeScript

export const AuditEvent = {
// Workspace
WORKSPACE_CREATED: 'workspace.created',
WORKSPACE_UPDATED: 'workspace.updated',
WORKSPACE_INVITE_CREATED: 'workspace.invite_created',
WORKSPACE_INVITE_RESENT: 'workspace.invite_resent',
WORKSPACE_INVITE_REVOKED: 'workspace.invite_revoked',
// User
USER_CREATED: 'user.created',
USER_DELETED: 'user.deleted',
USER_LOGIN: 'user.login',
USER_LOGOUT: 'user.logout',
USER_ROLE_CHANGED: 'user.role_changed',
USER_PASSWORD_CHANGED: 'user.password_changed',
USER_PASSWORD_RESET: 'user.password_reset',
USER_UPDATED: 'user.updated',
USER_DEACTIVATED: 'user.deactivated',
USER_ACTIVATED: 'user.activated',
// API Keys
API_KEY_CREATED: 'api_key.created',
API_KEY_UPDATED: 'api_key.updated',
API_KEY_DELETED: 'api_key.deleted',
// SCIM Tokens
SCIM_TOKEN_CREATED: 'scim_token.created',
SCIM_TOKEN_UPDATED: 'scim_token.updated',
SCIM_TOKEN_DELETED: 'scim_token.deleted',
// Space
SPACE_CREATED: 'space.created',
SPACE_UPDATED: 'space.updated',
SPACE_DELETED: 'space.deleted',
SPACE_MEMBER_ADDED: 'space.member_added',
SPACE_MEMBER_REMOVED: 'space.member_removed',
SPACE_MEMBER_ROLE_CHANGED: 'space.member_role_changed',
// Group
GROUP_CREATED: 'group.created',
GROUP_UPDATED: 'group.updated',
GROUP_DELETED: 'group.deleted',
GROUP_MEMBER_ADDED: 'group.member_added',
GROUP_MEMBER_REMOVED: 'group.member_removed',
// Comment
COMMENT_CREATED: 'comment.created',
COMMENT_DELETED: 'comment.deleted',
// Comment updates / resolve
COMMENT_UPDATED: 'comment.updated',
COMMENT_RESOLVED: 'comment.resolved',
COMMENT_REOPENED: 'comment.reopened',
COMMENT_SUGGESTION_APPLIED: 'comment.suggestion_applied',
// Page
PAGE_CREATED: 'page.created',
PAGE_TRASHED: 'page.trashed',
PAGE_DELETED: 'page.deleted',
PAGE_RESTORED: 'page.restored',
PAGE_MOVED_TO_SPACE: 'page.moved_to_space',
PAGE_DUPLICATED: 'page.duplicated',
// Page permission
PAGE_RESTRICTED: 'page.restricted',
PAGE_RESTRICTION_REMOVED: 'page.restriction_removed',
PAGE_PERMISSION_ADDED: 'page.permission_added',
PAGE_PERMISSION_REMOVED: 'page.permission_removed',
// Page verification
PAGE_VERIFICATION_CREATED: 'page.verification_created',
PAGE_VERIFICATION_UPDATED: 'page.verification_updated',
PAGE_VERIFICATION_REMOVED: 'page.verification_removed',
PAGE_VERIFIED: 'page.verified',
PAGE_APPROVAL_REQUESTED: 'page.approval_requested',
PAGE_APPROVAL_REJECTED: 'page.approval_rejected',
PAGE_MARKED_OBSOLETE: 'page.marked_obsolete',
// Share
SHARE_CREATED: 'share.created',
SHARE_DELETED: 'share.deleted',
// Import / Export
PAGE_IMPORTED: 'page.imported',
PAGE_EXPORTED: 'page.exported',
SPACE_EXPORTED: 'space.exported',
// SSO provider management
SSO_PROVIDER_CREATED: 'sso.provider_created',
SSO_PROVIDER_UPDATED: 'sso.provider_updated',
SSO_PROVIDER_DELETED: 'sso.provider_deleted',
// MFA
USER_MFA_ENABLED: 'user.mfa_enabled',
USER_MFA_DISABLED: 'user.mfa_disabled',
USER_MFA_BACKUP_CODE_GENERATED: 'user.mfa_backup_code_generated',
// License
LICENSE_ACTIVATED: 'license.activated',
LICENSE_REMOVED: 'license.removed',
// Attachment
ATTACHMENT_UPLOADED: 'attachment.uploaded',
// ATTACHMENT_DELETED: 'attachment.deleted',
} as const;
export type AuditEventType = (typeof AuditEvent)[keyof typeof AuditEvent];
export const EXCLUDED_AUDIT_EVENTS: Set<string> = new Set([
AuditEvent.PAGE_CREATED,
AuditEvent.PAGE_MOVED_TO_SPACE,
AuditEvent.PAGE_DUPLICATED,
AuditEvent.COMMENT_CREATED,
AuditEvent.COMMENT_UPDATED,
AuditEvent.COMMENT_RESOLVED,
AuditEvent.COMMENT_REOPENED,
AuditEvent.ATTACHMENT_UPLOADED
]);
export const AuditResource = {
WORKSPACE: 'workspace',
USER: 'user',
PAGE: 'page',
SPACE: 'space',
SPACE_MEMBER: 'space_member',
GROUP: 'group',
COMMENT: 'comment',
SHARE: 'share',
API_KEY: 'api_key',
SCIM_TOKEN: 'scim_token',
SSO_PROVIDER: 'sso_provider',
WORKSPACE_INVITATION: 'workspace_invitation',
ATTACHMENT: 'attachment',
LICENSE: 'license',
} as const;
export type AuditResourceType =
(typeof AuditResource)[keyof typeof AuditResource];
export type ActorType = 'user' | 'system' | 'api_key';
export interface AuditLogPayload {
event: AuditEventType;
resourceType: AuditResourceType;
resourceId?: string;
spaceId?: string;
changes?: {
before?: Record<string, any>;
after?: Record<string, any>;
};
metadata?: Record<string, any>;
}
export interface AuditLogData extends AuditLogPayload {
workspaceId: string;
actorId?: string;
actorType: ActorType;
ipAddress?: string;
userAgent?: string;
}