import { Body, Controller, Get, Headers, HttpCode, HttpStatus, Inject, NotFoundException, Param, Post, UseGuards, ForbiddenException, } from "@nestjs/common"; import { eq, desc, and } from "drizzle-orm"; import { schema, type Database } from "@courtmitra/db"; import { CreateActionPreviewInput, PublicCasePageDto, } from "@courtmitra/contracts"; import { parseOrThrow } from "../common/zod-validation.pipe.js"; import { DB } from "../db/db.module.js"; import { ActionsService } from "./actions.service.js"; import { CurrentUser } from "../auth/current-user.decorator.js"; import { ClerkAuthGuard } from "../auth/clerk-auth.guard.js"; @UseGuards(ClerkAuthGuard) @Controller() export class ActionsController { constructor( @Inject(ActionsService) private readonly actions: ActionsService, @Inject(DB) private readonly db: Database, ) {} /** Verify the action's mission belongs to the requesting user. */ private async assertActionOwnership(actionId: string, userId: string) { const [actionRow] = await this.db .select({ missionId: schema.actions.missionId }) .from(schema.actions) .where(eq(schema.actions.id, actionId)) .limit(1); if (!actionRow) throw new NotFoundException(`Action not found: ${actionId}`); await this.assertMissionOwnership(actionRow.missionId, userId); } /** Verify the mission belongs to the requesting user. */ private async assertMissionOwnership(missionId: string, userId: string) { const [missionRow] = await this.db .select({ userId: schema.missions.userId }) .from(schema.missions) .where(eq(schema.missions.id, missionId)) .limit(1); if (!missionRow) throw new NotFoundException(`Mission not found: ${missionId}`); if (missionRow.userId !== userId) throw new ForbiddenException("You do not own this mission"); } @Post("missions/:missionId/actions/preview") @HttpCode(HttpStatus.CREATED) async preview( @Param("missionId") missionId: string, @Body() body: unknown, @CurrentUser() userId: string, @Headers("idempotency-key") idempotencyKey?: string, ) { await this.assertMissionOwnership(missionId, userId); const input = parseOrThrow(CreateActionPreviewInput, body); return this.actions.previewAction(missionId, input, idempotencyKey); } @Post("actions/:actionId/consent") async grantConsent(@Param("actionId") actionId: string, @CurrentUser() userId: string) { await this.assertActionOwnership(actionId, userId); return this.actions.grantConsent(actionId); } @Post("actions/:actionId/execute") @HttpCode(HttpStatus.ACCEPTED) async execute(@Param("actionId") actionId: string, @CurrentUser() userId: string) { await this.assertActionOwnership(actionId, userId); return this.actions.executeAction(actionId); } @Post("actions/:actionId/confirm-external") @HttpCode(HttpStatus.OK) async confirmExternal( @Param("actionId") actionId: string, @CurrentUser() userId: string, @Body() body: { platform?: string; externalUrl?: string }, ) { await this.assertActionOwnership(actionId, userId); return this.actions.confirmExternalSubmission(actionId, body.platform ?? "web", body.externalUrl); } @Get("missions/:missionId/actions") async listActions(@Param("missionId") missionId: string, @CurrentUser() userId: string) { await this.assertMissionOwnership(missionId, userId); const rows = await this.actions.getActionsForMission(missionId); return { items: rows }; } @Get("actions/:actionId") async get(@Param("actionId") actionId: string, @CurrentUser() userId: string) { await this.assertActionOwnership(actionId, userId); return this.actions.getAction(actionId); } @Get("public-pages/:slug") async getPublicPage(@Param("slug") slug: string) { const [pageRow] = await this.db .select({ id: schema.publicCasePages.id, missionId: schema.publicCasePages.missionId, slug: schema.publicCasePages.slug, visibility: schema.publicCasePages.visibility, redactedSummary: schema.publicCasePages.redactedSummary, createdAt: schema.publicCasePages.createdAt, }) .from(schema.publicCasePages) .where(eq(schema.publicCasePages.slug, slug)) .limit(1); if (!pageRow) throw new NotFoundException(`Public page not found: ${slug}`); const [missionRow] = await this.db .select({ title: schema.missions.title, issueDomain: schema.missions.issueDomain, state: schema.missions.state, }) .from(schema.missions) .where(eq(schema.missions.id, pageRow.missionId)) .limit(1); const latestActionRows = await this.db .select({ id: schema.actions.id, actionType: schema.actions.actionType, title: schema.actions.title, status: schema.actions.status, proofState: schema.actions.proofState, createdAt: schema.actions.createdAt, }) .from(schema.actions) .where(eq(schema.actions.missionId, pageRow.missionId)) .orderBy(desc(schema.actions.createdAt)) .limit(10); const dto: PublicCasePageDto = { id: pageRow.id, missionId: pageRow.missionId, slug: pageRow.slug, visibility: pageRow.visibility, redactedSummary: pageRow.redactedSummary, createdAt: pageRow.createdAt.toISOString(), }; return { page: dto, mission: missionRow ? { title: `User reports / unresolved / alleged issue`, issueDomain: missionRow.issueDomain, state: missionRow.state, } : null, actions: latestActionRows.map((a) => ({ id: a.id, actionType: a.actionType, title: a.title, status: a.status, proofState: a.proofState, createdAt: a.createdAt.toISOString(), })), }; } }