Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/web/src/app/api/account/avatar/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { db, users, eq } from '@pagespace/db';

const AUTH_OPTIONS = { allow: ['session'] as const, requireCSRF: true };
import { createUserServiceToken, type ServiceScope } from '@pagespace/lib';
import { getActorInfo, logUserActivity } from '@pagespace/lib/monitoring/activity-logger';

// Maximum file size: 5MB
const MAX_FILE_SIZE = 5 * 1024 * 1024;
Expand Down Expand Up @@ -125,6 +126,14 @@ export async function POST(request: NextRequest) {
.set({ image: avatarUrl })
.where(eq(users.id, userId));

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logUserActivity(userId, 'avatar_update', {
targetUserId: userId,
targetUserEmail: actorInfo.actorEmail,
}, actorInfo);
}).catch(() => {});

return NextResponse.json({
success: true,
avatarUrl,
Expand Down Expand Up @@ -176,6 +185,15 @@ export async function DELETE(request: NextRequest) {
.set({ image: null })
.where(eq(users.id, userId));

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logUserActivity(userId, 'avatar_update', {
targetUserId: userId,
targetUserEmail: actorInfo.actorEmail,
updatedFields: ['image'],
}, actorInfo);
}).catch(() => {});

return NextResponse.json({
success: true,
message: 'Avatar deleted successfully'
Expand Down
40 changes: 40 additions & 0 deletions apps/web/src/app/api/calendar/events/[eventId]/attendees/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { loggers, getDriveMemberUserIds } from '@pagespace/lib/server';
import { isUserDriveMember } from '@pagespace/lib';
import { authenticateRequestWithOptions, isAuthError, checkMCPDriveScope } from '@/lib/auth';
import { broadcastCalendarEvent } from '@/lib/websocket/calendar-events';
import { getActorInfo, logActivity } from '@pagespace/lib/monitoring/activity-logger';

const AUTH_OPTIONS_READ = { allow: ['session', 'mcp'] as const, requireCSRF: false };
const AUTH_OPTIONS_WRITE = { allow: ['session', 'mcp'] as const, requireCSRF: true };
Expand Down Expand Up @@ -278,6 +279,19 @@ export async function POST(
attendeeIds: newUserIds,
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'update',
resourceType: 'calendar_event',
resourceId: eventId,
driveId: event.driveId,
metadata: { action: 'attendees_added', addedUserIds: newUserIds, count: newUserIds.length },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json({ attendees });
} catch (error) {
loggers.api.error('Error adding event attendees:', error as Error);
Expand Down Expand Up @@ -381,6 +395,19 @@ export async function PATCH(
attendeeIds: allAttendees.map(a => a.userId),
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'update',
resourceType: 'calendar_event',
resourceId: eventId,
driveId: event.driveId,
metadata: { action: 'rsvp_updated', status, previousStatus: attendee.status },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json(updatedAttendee);
} catch (error) {
loggers.api.error('Error updating RSVP:', error as Error);
Expand Down Expand Up @@ -479,6 +506,19 @@ export async function DELETE(
attendeeIds: [targetUserId],
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'update',
resourceType: 'calendar_event',
resourceId: eventId,
driveId: event.driveId,
metadata: { action: 'attendee_removed', removedUserId: targetUserId },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json({ success: true });
} catch (error) {
loggers.api.error('Error removing event attendee:', error as Error);
Expand Down
30 changes: 30 additions & 0 deletions apps/web/src/app/api/calendar/events/[eventId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { isUserDriveMember, isDriveOwnerOrAdmin } from '@pagespace/lib';
import { broadcastCalendarEvent } from '@/lib/websocket/calendar-events';
import { pushEventUpdateToGoogle, pushEventDeleteToGoogle } from '@/lib/integrations/google-calendar/push-service';
import { isNaiveISODatetime, parseNaiveDatetimeInTimezone } from '@/lib/ai/core/timestamp-utils';
import { getActorInfo, logActivity } from '@pagespace/lib/monitoring/activity-logger';

const AUTH_OPTIONS_READ = { allow: ['session', 'mcp'] as const, requireCSRF: false };
const AUTH_OPTIONS_WRITE = { allow: ['session', 'mcp'] as const, requireCSRF: true };
Expand Down Expand Up @@ -285,6 +286,22 @@ export async function PATCH(
);
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'update',
resourceType: 'calendar_event',
resourceId: eventId,
resourceTitle: updatedEvent.title,
driveId: updatedEvent.driveId,
updatedFields: Object.keys(data).filter(k => (data as Record<string, unknown>)[k] !== undefined),
previousValues: { title: event.title, startAt: event.startAt?.toISOString(), endAt: event.endAt?.toISOString() },
newValues: { title: updatedEvent.title, startAt: updatedEvent.startAt?.toISOString(), endAt: updatedEvent.endAt?.toISOString() },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json(completeEvent);
} catch (error) {
loggers.api.error('Error updating calendar event:', error as Error);
Expand Down Expand Up @@ -372,6 +389,19 @@ export async function DELETE(
attendeeIds: attendees.map(a => a.userId),
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'delete',
resourceType: 'calendar_event',
resourceId: eventId,
resourceTitle: event.title,
driveId: event.driveId,
}).catch(() => {});
}).catch(() => {});

return NextResponse.json({ success: true });
} catch (error) {
loggers.api.error('Error deleting calendar event:', error as Error);
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/app/api/calendar/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { broadcastCalendarEvent } from '@/lib/websocket/calendar-events';
import { pushEventToGoogle } from '@/lib/integrations/google-calendar/push-service';
import { isNaiveISODatetime, parseNaiveDatetimeInTimezone } from '@/lib/ai/core/timestamp-utils';
import { CronExpressionParser } from 'cron-parser';
import { getActorInfo, logActivity } from '@pagespace/lib/monitoring/activity-logger';

const AUTH_OPTIONS_READ = { allow: ['session', 'mcp'] as const, requireCSRF: false };
const AUTH_OPTIONS_WRITE = { allow: ['session', 'mcp'] as const, requireCSRF: true };
Expand Down Expand Up @@ -529,6 +530,25 @@ export async function POST(request: Request) {
);
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'create',
resourceType: 'calendar_event',
resourceId: event.id,
resourceTitle: data.title,
driveId: data.driveId ?? null,
metadata: {
allDay: data.allDay,
visibility: data.visibility,
hasRecurrence: !!data.recurrenceRule,
attendeeCount: (data.attendeeIds?.length ?? 0) + 1,
},
}).catch(() => {});
}).catch(() => {});

return NextResponse.json(completeEvent, { status: 201 });
} catch (error) {
loggers.api.error('Error creating calendar event:', error as Error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { authenticateRequestWithOptions, isAuthError } from '@/lib/auth';
import { canUserViewPage } from '@pagespace/lib/server';
import { loggers } from '@pagespace/lib/server';
import { createSignedBroadcastHeaders } from '@pagespace/lib/broadcast-auth';
import { getActorInfo, logActivity } from '@pagespace/lib/monitoring/activity-logger';

const AUTH_OPTIONS = { allow: ['session'] as const, requireCSRF: true };

Expand Down Expand Up @@ -90,6 +91,20 @@ export async function POST(req: Request, { params }: RouteParams) {
}
}

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'create',
resourceType: 'message',
resourceId: messageId,
driveId: null,
pageId,
metadata: { action: 'reaction_added', emoji },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json(reactionWithUser, { status: 201 });
} catch (error) {
// Unique constraint violation - user already reacted with this emoji
Expand Down Expand Up @@ -173,5 +188,19 @@ export async function DELETE(req: Request, { params }: RouteParams) {
}
}

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'delete',
resourceType: 'message',
resourceId: messageId,
driveId: null,
pageId,
metadata: { action: 'reaction_removed', emoji },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json({ success: true });
}
11 changes: 11 additions & 0 deletions apps/web/src/app/api/channels/[pageId]/messages/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { canUserViewPage, canUserEditPage } from '@pagespace/lib/server';
import { loggers } from '@pagespace/lib/server';
import { createSignedBroadcastHeaders } from '@pagespace/lib/broadcast-auth';
import { broadcastInboxEvent } from '@/lib/websocket/socket-utils';
import { getActorInfo, logMessageActivity } from '@pagespace/lib/monitoring/activity-logger';

// Type for attachment metadata stored in the database
interface AttachmentMeta {
Expand Down Expand Up @@ -167,6 +168,16 @@ export async function POST(req: Request, { params }: { params: Promise<{ pageId:
set: { lastReadAt: new Date() },
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logMessageActivity(userId, 'create', {
id: createdMessage.id,
pageId,
driveId: null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Attach drive ID to channel activity entries

This new audit entry is recorded with driveId: null, so channel message activity is detached from its actual drive. Drive-scoped activity queries filter on activityLogs.driveId (apps/web/src/app/api/activities/route.ts uses eq(activityLogs.driveId, params.driveId) in drive context), which means these records are omitted from drive audit/history views; the same driveId: null pattern added in the reactions route has the same effect. It also suppresses event-triggered workflows because emitWorkflowEvent returns early when event.driveId is missing (apps/web/src/lib/workflows/event-trigger.ts).

Useful? React with 👍 / 👎.

conversationType: 'channel',
}, actorInfo);
}).catch(() => {});
Comment on lines +171 to +179

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pass the real drive context in message audit logs.

At Line 176, driveId is hardcoded to null even for channel messages. This drops drive-level context for audit queries and indexing. Please pass the actual channel/page driveId instead.

💡 Suggested fix
-  // Audit logging (fire-and-forget)
+  // Audit logging (fire-and-forget) - include actual drive context
   getActorInfo(userId).then(actorInfo => {
     logMessageActivity(userId, 'create', {
       id: createdMessage.id,
       pageId,
-      driveId: null,
+      driveId: channel?.driveId ?? null,
       conversationType: 'channel',
     }, actorInfo);
   }).catch(() => {});

If channel is only loaded later, move this block to run after the existing channel lookup (Line 237+) or do a small pages lookup before logging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/channels/`[pageId]/messages/route.ts around lines 171 -
179, The audit log currently hardcodes driveId: null when calling
logMessageActivity after getActorInfo; change this to pass the real drive
context by ensuring you have the channel/page driveId before logging—either move
the getActorInfo.then(...) block to after the channel lookup (where
channel.driveId is available) or perform a small pages lookup to fetch the
page's driveId and use that value instead of null when constructing the payload
for logMessageActivity (referencing createdMessage.id, pageId, and the resolved
driveId).


const newMessage = await db.query.channelMessages.findFirst({
where: eq(channelMessages.id, createdMessage.id),
with: {
Expand Down
48 changes: 48 additions & 0 deletions apps/web/src/app/api/pages/[pageId]/tasks/statuses/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DEFAULT_TASK_STATUSES } from '@pagespace/db';
import { authenticateRequestWithOptions, isAuthError, checkMCPPageScope } from '@/lib/auth';
import { canUserEditPage, canUserViewPage } from '@pagespace/lib/server';
import { broadcastTaskEvent } from '@/lib/websocket';
import { getActorInfo, logActivity } from '@pagespace/lib/monitoring/activity-logger';

const AUTH_OPTIONS_READ = { allow: ['session', 'mcp'] as const, requireCSRF: false };
const AUTH_OPTIONS_WRITE = { allow: ['session', 'mcp'] as const, requireCSRF: true };
Expand Down Expand Up @@ -175,6 +176,20 @@ export async function POST(
data: { statusConfigAdded: newConfig },
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'create',
resourceType: 'page',
resourceId: pageId,
driveId: null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve drive context in task status audit logs

These task-status audit logs are also written with driveId: null, which makes them invisible in drive-level audit/history endpoints that filter strictly by drive ID (apps/web/src/app/api/activities/route.ts drive context). Because workflow event dispatch drops events without a drive (apps/web/src/lib/workflows/event-trigger.ts), these operations also cannot trigger any drive-scoped event workflows, even though they are page changes inside a drive.

Useful? React with 👍 / 👎.

pageId,
metadata: { featureType: 'task_status', statusName: newConfig.name, statusSlug: newConfig.slug },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json(newConfig, { status: 201 });
}

Expand Down Expand Up @@ -262,6 +277,20 @@ export async function PUT(
data: { statusConfigsUpdated: updatedConfigs },
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'update',
resourceType: 'page',
resourceId: pageId,
driveId: null,
pageId,
metadata: { featureType: 'task_status', statusCount: statuses.length },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json({ statusConfigs: updatedConfigs });
}

Expand Down Expand Up @@ -388,5 +417,24 @@ export async function DELETE(
},
});

// Audit logging (fire-and-forget)
getActorInfo(userId).then(actorInfo => {
logActivity({
userId,
...actorInfo,
operation: 'delete',
resourceType: 'page',
resourceId: pageId,
driveId: null,
pageId,
metadata: {
featureType: 'task_status',
deletedStatus: statusToDelete.slug,
migratedTo: migrateToSlug,
migratedCount: tasksWithStatus.length,
},
}).catch(() => {});
}).catch(() => {});

return NextResponse.json({ success: true });
}
29 changes: 29 additions & 0 deletions apps/web/src/app/api/workflows/[workflowId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { authenticateRequestWithOptions, isAuthError } from '@/lib/auth';
import { checkDriveAccess } from '@pagespace/lib/server';
import { db, workflows, pages, eq, and } from '@pagespace/db';
import { validateCronExpression, validateTimezone, getNextRunDate } from '@/lib/workflows/cron-utils';
import { getActorInfo, logActivity } from '@pagespace/lib/monitoring/activity-logger';

const AUTH_OPTIONS_READ = { allow: ['session'] as const, requireCSRF: false };
const AUTH_OPTIONS_WRITE = { allow: ['session'] as const, requireCSRF: true };
Expand Down Expand Up @@ -147,6 +148,21 @@ export async function PATCH(
.where(eq(workflows.id, workflowId))
.returning();

// Audit logging (fire-and-forget)
getActorInfo(auth.userId).then(actorInfo => {
logActivity({
userId: auth.userId,
...actorInfo,
operation: 'update',
resourceType: 'workflow',
resourceId: workflowId,
resourceTitle: updated.name,
driveId: workflow.driveId,
updatedFields: Object.keys(data).filter(k => (data as Record<string, unknown>)[k] !== undefined),
metadata: { triggerType: updated.triggerType },
}).catch(() => {});
}).catch(() => {});

return NextResponse.json(updated);
}

Expand All @@ -164,5 +180,18 @@ export async function DELETE(

await db.delete(workflows).where(eq(workflows.id, workflowId));

// Audit logging (fire-and-forget)
getActorInfo(auth.userId).then(actorInfo => {
logActivity({
userId: auth.userId,
...actorInfo,
operation: 'delete',
resourceType: 'workflow',
resourceId: workflowId,
resourceTitle: result.workflow.name,
driveId: result.workflow.driveId,
}).catch(() => {});
}).catch(() => {});

return NextResponse.json({ success: true });
}
Loading