feat(#37-41): Add domains, ideas, relationships, agents, widgets schema
Schema additions for issues #37-41: New models: - Domain (#37): Life domains (work, marriage, homelab, etc.) - Idea (#38): Brain dumps with pgvector embeddings - Relationship (#39): Generic entity linking (blocks, depends_on) - Agent (#40): ClawdBot agent tracking with metrics - AgentSession (#40): Conversation session tracking - WidgetDefinition (#41): HUD widget registry - UserLayout (#41): Per-user dashboard configuration Updated models: - Task, Event, Project: Added domainId foreign key - User, Workspace: Added new relations New enums: - IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED - RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc. - AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED - EntityType: Added IDEA, DOMAIN Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { Observable } from "rxjs";
|
||||
import { tap } from "rxjs/operators";
|
||||
import { ActivityService } from "../activity.service";
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* Interceptor for automatic activity logging
|
||||
* Logs CREATE, UPDATE, DELETE actions based on HTTP methods
|
||||
*/
|
||||
@Injectable()
|
||||
export class ActivityLoggingInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(ActivityLoggingInterceptor.name);
|
||||
|
||||
constructor(private readonly activityService: ActivityService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const { method, params, body, user, ip, headers } = request;
|
||||
|
||||
// Only log for authenticated requests
|
||||
if (!user) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
// Skip GET requests (read-only)
|
||||
if (method === "GET") {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
return next.handle().pipe(
|
||||
tap(async (result) => {
|
||||
try {
|
||||
const action = this.mapMethodToAction(method);
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract entity information
|
||||
const entityId = params.id || result?.id;
|
||||
const workspaceId = user.workspaceId || body.workspaceId;
|
||||
|
||||
if (!entityId || !workspaceId) {
|
||||
this.logger.warn(
|
||||
"Cannot log activity: missing entityId or workspaceId"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine entity type from controller/handler
|
||||
const controllerName = context.getClass().name;
|
||||
const handlerName = context.getHandler().name;
|
||||
const entityType = this.inferEntityType(controllerName, handlerName);
|
||||
|
||||
// Build activity details with sanitized body
|
||||
const sanitizedBody = this.sanitizeSensitiveData(body);
|
||||
const details: Record<string, any> = {
|
||||
method,
|
||||
controller: controllerName,
|
||||
handler: handlerName,
|
||||
};
|
||||
|
||||
if (method === "POST") {
|
||||
details.data = sanitizedBody;
|
||||
} else if (method === "PATCH" || method === "PUT") {
|
||||
details.changes = sanitizedBody;
|
||||
}
|
||||
|
||||
// Log the activity
|
||||
await this.activityService.logActivity({
|
||||
workspaceId,
|
||||
userId: user.id,
|
||||
action,
|
||||
entityType,
|
||||
entityId,
|
||||
details,
|
||||
ipAddress: ip,
|
||||
userAgent: headers["user-agent"],
|
||||
});
|
||||
} catch (error) {
|
||||
// Don't fail the request if activity logging fails
|
||||
this.logger.error(
|
||||
"Failed to log activity",
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map HTTP method to ActivityAction
|
||||
*/
|
||||
private mapMethodToAction(method: string): ActivityAction | null {
|
||||
switch (method) {
|
||||
case "POST":
|
||||
return ActivityAction.CREATED;
|
||||
case "PATCH":
|
||||
case "PUT":
|
||||
return ActivityAction.UPDATED;
|
||||
case "DELETE":
|
||||
return ActivityAction.DELETED;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer entity type from controller/handler names
|
||||
*/
|
||||
private inferEntityType(
|
||||
controllerName: string,
|
||||
handlerName: string
|
||||
): EntityType {
|
||||
const combined = `${controllerName} ${handlerName}`.toLowerCase();
|
||||
|
||||
if (combined.includes("task")) {
|
||||
return EntityType.TASK;
|
||||
} else if (combined.includes("event")) {
|
||||
return EntityType.EVENT;
|
||||
} else if (combined.includes("project")) {
|
||||
return EntityType.PROJECT;
|
||||
} else if (combined.includes("workspace")) {
|
||||
return EntityType.WORKSPACE;
|
||||
} else if (combined.includes("user")) {
|
||||
return EntityType.USER;
|
||||
}
|
||||
|
||||
// Default to TASK if cannot determine
|
||||
return EntityType.TASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize sensitive data from objects before logging
|
||||
* Redacts common sensitive field names
|
||||
*/
|
||||
private sanitizeSensitiveData(data: any): any {
|
||||
if (!data || typeof data !== "object") {
|
||||
return data;
|
||||
}
|
||||
|
||||
// List of sensitive field names (case-insensitive)
|
||||
const sensitiveFields = [
|
||||
"password",
|
||||
"token",
|
||||
"secret",
|
||||
"apikey",
|
||||
"api_key",
|
||||
"authorization",
|
||||
"creditcard",
|
||||
"credit_card",
|
||||
"cvv",
|
||||
"ssn",
|
||||
"privatekey",
|
||||
"private_key",
|
||||
];
|
||||
|
||||
const sanitize = (obj: any): any => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => sanitize(item));
|
||||
}
|
||||
|
||||
if (obj && typeof obj === "object") {
|
||||
const sanitized: Record<string, any> = {};
|
||||
|
||||
for (const key in obj) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
const isSensitive = sensitiveFields.some((field) =>
|
||||
lowerKey.includes(field)
|
||||
);
|
||||
|
||||
if (isSensitive) {
|
||||
sanitized[key] = "[REDACTED]";
|
||||
} else if (typeof obj[key] === "object") {
|
||||
sanitized[key] = sanitize(obj[key]);
|
||||
} else {
|
||||
sanitized[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
return sanitize(data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user