feat(grpc/account): unstub audit log-specific methods

This commit is contained in:
Jonathan Barrow
2026-09-16 12:19:45 -04:00
parent ecd5501175
commit 7fd0f45f31
8 changed files with 234 additions and 21 deletions

8
package-lock.json generated
View File

@@ -12,7 +12,7 @@
"@aws-sdk/client-s3": "^3.657.0",
"@aws-sdk/client-ses": "^3.515.0",
"@inquirer/prompts": "^7.2.0",
"@pretendonetwork/grpc": "^2.6.1",
"@pretendonetwork/grpc": "^2.6.2",
"bcrypt": "^5.0.0",
"buffer-crc32": "^0.2.13",
"colors": "^1.4.0",
@@ -1680,9 +1680,9 @@
}
},
"node_modules/@pretendonetwork/grpc": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.6.1.tgz",
"integrity": "sha512-+HKJ8cTV4AV8PJgUqg2Tocz4EJTajqErY9r/Oul8WIZM2lO+0gRPSf3Mlxi9Ghvjq88ZS9pWWccAKtn77ePrew==",
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.6.2.tgz",
"integrity": "sha512-fwHCejE25X4qmNLlSDLJwtAd52onLebaxhF9oazUIXbM+19OhEkMA9ISXu6hT5rLQ+RMbAXNNisNgr0+kOYyDQ==",
"license": "AGPL-3.0-only",
"dependencies": {
"@bufbuild/protobuf": "^2.2.2",

View File

@@ -28,7 +28,7 @@
"@aws-sdk/client-s3": "^3.657.0",
"@aws-sdk/client-ses": "^3.515.0",
"@inquirer/prompts": "^7.2.0",
"@pretendonetwork/grpc": "^2.6.1",
"@pretendonetwork/grpc": "^2.6.2",
"bcrypt": "^5.0.0",
"buffer-crc32": "^0.2.13",
"colors": "^1.4.0",

58
src/models/audit-log.ts Normal file
View File

@@ -0,0 +1,58 @@
import { Schema, model } from 'mongoose';
import { AuditLogActionType } from '@pretendonetwork/grpc/account/v2/audit_log_action_type';
import type { IAuditLog, IAuditLogComment, IAuditLogMethods, AuditLogModel } from '@/types/mongoose/audit-log';
const AuditLogCommentSchema = new Schema<IAuditLogComment>({
pid: {
type: Number,
required: true
},
created: {
type: Date,
default: Date.now
},
content: {
type: String,
required: true
}
});
const AuditLogSchema = new Schema<IAuditLog, AuditLogModel, IAuditLogMethods>({
action_type: {
type: Number,
enum: Object.values(AuditLogActionType).filter(value => typeof value === 'number'),
required: true,
index: true
},
executed_by_pid: {
type: Number,
required: true,
index: true
},
created: {
type: Date,
default: Date.now,
index: true
},
metadata: { // * This is just arbitrary metadata about the log, which may be context-specific, for example holding the before and after of a user data change
type: String,
default: '{}',
validate: {
validator: (value: string): boolean => {
try {
JSON.parse(value);
return true;
} catch {
return false;
}
},
message: 'metadata must be a valid JSON string'
}
},
comments: {
type: [AuditLogCommentSchema],
default: []
}
});
export const AuditLog = model<IAuditLog, AuditLogModel>('AuditLog', AuditLogSchema);

View File

@@ -1,9 +1,49 @@
import { Status, ServerError } from 'nice-grpc';
import { isValidObjectId, Types } from 'mongoose';
import { AuditLog } from '@/models/audit-log';
import { getExecutorPNID } from '@/services/grpc/account/v2/utils';
import type { CallContext } from 'nice-grpc';
import type { CreateAuditLogCommentRequest, CreateAuditLogCommentResponse } from '@pretendonetwork/grpc/account/v2/create_audit_log_comment_rpc';
export async function createAuditLogComment(_request: CreateAuditLogCommentRequest): Promise<CreateAuditLogCommentResponse> {
throw new ServerError(
Status.UNIMPLEMENTED,
'Stubbed'
);
export async function createAuditLogComment(request: CreateAuditLogCommentRequest, context: CallContext): Promise<CreateAuditLogCommentResponse> {
const pnid = await getExecutorPNID(context);
if (!isValidObjectId(request.id)) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid audit log ID');
}
const content = request.content.trim();
if (content.length === 0) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Comment content cannot be empty');
}
const comment = {
_id: new Types.ObjectId(),
pid: pnid.pid,
created: new Date(),
content
};
const result = await AuditLog.updateOne({
_id: request.id
}, {
$push: {
comments: comment
}
});
if (result.matchedCount === 0) {
throw new ServerError(Status.NOT_FOUND, 'Audit log not found');
}
return {
item: {
parent: request.id,
id: comment._id.toString(),
pid: comment.pid,
createdTimestamp: comment.created,
content: comment.content
}
};
}

View File

@@ -1,9 +1,28 @@
import { Status, ServerError } from 'nice-grpc';
import { isValidObjectId } from 'mongoose';
import { AuditLog } from '@/models/audit-log';
import type { ListAuditLogCommentsRequest, ListAuditLogCommentsResponse } from '@pretendonetwork/grpc/account/v2/list_audit_log_comments_rpc';
export async function listAuditLogComments(_request: ListAuditLogCommentsRequest): Promise<ListAuditLogCommentsResponse> {
throw new ServerError(
Status.UNIMPLEMENTED,
'Stubbed'
);
export async function listAuditLogComments(request: ListAuditLogCommentsRequest): Promise<ListAuditLogCommentsResponse> {
if (!isValidObjectId(request.id)) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid audit log ID');
}
const auditLog = await AuditLog.findById(request.id, {
comments: 1
});
if (!auditLog) {
throw new ServerError(Status.NOT_FOUND, 'Audit log not found');
}
return {
items: auditLog.comments.map(comment => ({
parent: auditLog._id.toString(),
id: comment._id.toString(),
pid: comment.pid,
createdTimestamp: comment.created,
content: comment.content
}))
};
}

View File

@@ -1,9 +1,54 @@
import { Status, ServerError } from 'nice-grpc';
import { AuditLog } from '@/models/audit-log';
import type { FilterQuery } from 'mongoose';
import type { ListAuditLogsRequest, ListAuditLogsResponse } from '@pretendonetwork/grpc/account/v2/list_audit_logs_rpc';
import type { IAuditLog } from '@/types/mongoose/audit-log';
export async function listAuditLogs(_request: ListAuditLogsRequest): Promise<ListAuditLogsResponse> {
throw new ServerError(
Status.UNIMPLEMENTED,
'Stubbed'
);
// * These seem sensible to me
const DEFAULT_PAGE_LIMIT = 50;
const MAX_PAGE_LIMIT = 500;
export async function listAuditLogs(request: ListAuditLogsRequest): Promise<ListAuditLogsResponse> {
const limit = Math.min(request.page?.limit || DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT);
const offset = request.page?.offset ?? 0;
const filter: FilterQuery<IAuditLog> = {};
if (request.filter) {
if (request.filter.executedByPid.length !== 0) {
filter.executed_by_pid = {
$in: request.filter.executedByPid
};
}
if (request.filter.actionType.length !== 0) {
filter.action_type = {
$in: request.filter.actionType
};
}
// * extra_json is currently unused. See https://github.com/PretendoNetwork/grpc/pull/10#discussion_r2637508248
}
const [total, auditLogs] = await Promise.all([
AuditLog.countDocuments(filter),
AuditLog.find(filter).sort({
created: -1,
_id: -1
}).skip(offset).limit(limit)
]);
return {
page: {
total,
offset,
itemCount: auditLogs.length
},
items: auditLogs.map(auditLog => ({
id: auditLog._id.toString(),
actionType: auditLog.action_type,
executedByPid: auditLog.executed_by_pid,
createdTimestamp: BigInt(auditLog.created.getTime()),
commentCount: auditLog.comments.length,
metadataJson: auditLog.metadata
}))
};
}

View File

@@ -0,0 +1,26 @@
import { Status, ServerError } from 'nice-grpc';
import { getPNIDByPID } from '@/database';
import type { CallContext } from 'nice-grpc';
import type { PNIDPermissionFlag } from '@/types/common/permission-flags';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
// TODO - The API doesn't have a field for this itself, and this service wasn't made to have a token sent to it because of non-user system use. Should we port the token auth from the API service here anyway instead?
export async function getExecutorPNID(context: CallContext, permission?: PNIDPermissionFlag): Promise<HydratedPNIDDocument> {
const executorPID = Number(context.metadata.get('X-Executor-PID')?.trim());
if (!Number.isInteger(executorPID) || executorPID <= 0) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Missing or invalid X-Executor-PID metadata');
}
const pnid = await getPNIDByPID(executorPID);
if (!pnid) {
throw new ServerError(Status.NOT_FOUND, 'Executor PNID not found');
}
if (permission !== undefined && !pnid.hasPermission(permission)) {
throw new ServerError(Status.PERMISSION_DENIED, 'Executor PNID lacks the required permission');
}
return pnid;
}

View File

@@ -0,0 +1,25 @@
import type { Model, HydratedDocument, Types } from 'mongoose';
import type { AuditLogActionType } from '@pretendonetwork/grpc/account/v2/audit_log_action_type';
export interface IAuditLogComment {
_id: Types.ObjectId;
pid: number;
created: Date;
content: string;
}
export interface IAuditLog {
action_type: AuditLogActionType;
executed_by_pid: number;
created: Date;
metadata: string;
comments: Types.DocumentArray<IAuditLogComment>;
}
export interface IAuditLogMethods {}
interface IAuditLogQueryHelpers {}
export interface AuditLogModel extends Model<IAuditLog, IAuditLogQueryHelpers, IAuditLogMethods> {}
export type HydratedAuditLogDocument = HydratedDocument<IAuditLog, IAuditLogMethods>;