merge: 集成V2.7 企业协作与管理治理
# Conflicts: # apps/server/src/app.module.ts # apps/web/components/layout/Sidebar.tsx # apps/web/lib/permissions.ts # docs/architecture.md # docs/decisions.md # docs/roadmap.md
This commit is contained in:
51
apps/server/src/common/audit/audit.service.spec.ts
Normal file
51
apps/server/src/common/audit/audit.service.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
describe('AuditService V2.7 adapter contract', () => {
|
||||
it('writes an append-only audit event with actor and resource scope', async () => {
|
||||
const prisma = {
|
||||
auditLog: {
|
||||
create: jest.fn().mockResolvedValue({ id: 'audit-1' }),
|
||||
},
|
||||
};
|
||||
const service = new AuditService(prisma as any);
|
||||
|
||||
await service.record({
|
||||
actorId: 'm-admin',
|
||||
action: 'project_member.role_changed',
|
||||
resourceType: 'project_member',
|
||||
resourceId: 'pm-1',
|
||||
projectId: 'project-1',
|
||||
before: { role: 'member' },
|
||||
after: { role: 'admin' },
|
||||
});
|
||||
|
||||
expect(prisma.auditLog.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
actorId: 'm-admin',
|
||||
action: 'project_member.role_changed',
|
||||
resourceType: 'project_member',
|
||||
resourceId: 'pm-1',
|
||||
projectId: 'project-1',
|
||||
before: { role: 'member' },
|
||||
after: { role: 'admin' },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fail the business action when audit storage is unavailable', async () => {
|
||||
const prisma = {
|
||||
auditLog: {
|
||||
create: jest.fn().mockRejectedValue(new Error('database unavailable')),
|
||||
},
|
||||
};
|
||||
const service = new AuditService(prisma as any);
|
||||
(service as any).logger.warn = jest.fn();
|
||||
|
||||
await expect(service.record({
|
||||
actorId: 'm-admin',
|
||||
action: 'comment.created',
|
||||
resourceType: 'comment',
|
||||
resourceId: 'comment-1',
|
||||
})).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
43
apps/server/src/common/audit/audit.service.ts
Normal file
43
apps/server/src/common/audit/audit.service.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
export interface AuditEventInput {
|
||||
actorId?: string | null;
|
||||
action: string;
|
||||
resourceType: string;
|
||||
resourceId: string;
|
||||
productId?: string | null;
|
||||
projectId?: string | null;
|
||||
versionId?: string | null;
|
||||
before?: unknown;
|
||||
after?: unknown;
|
||||
metadata?: unknown;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
private readonly logger = new Logger(AuditService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async record(input: AuditEventInput): Promise<void> {
|
||||
try {
|
||||
await this.prisma.auditLog.create({
|
||||
data: {
|
||||
actorId: input.actorId ?? null,
|
||||
action: input.action,
|
||||
resourceType: input.resourceType,
|
||||
resourceId: input.resourceId,
|
||||
productId: input.productId ?? null,
|
||||
projectId: input.projectId ?? null,
|
||||
versionId: input.versionId ?? null,
|
||||
before: input.before ?? {},
|
||||
after: input.after ?? {},
|
||||
metadata: input.metadata ?? {},
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Audit write failed for ${input.action}: ${error?.message ?? error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user