mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-10-23 10:06:50 -05:00
feat(grpc/account): unstub audit log-specific methods
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
58
src/models/audit-log.ts
Normal 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);
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
26
src/services/grpc/account/v2/utils.ts
Normal file
26
src/services/grpc/account/v2/utils.ts
Normal 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;
|
||||
}
|
||||
25
src/types/mongoose/audit-log.ts
Normal file
25
src/types/mongoose/audit-log.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user