import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards, Req, ForbiddenException, } from "@nestjs/common"; import { ApiTags, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiBearerAuth, } from "@nestjs/swagger"; import { Throttle } from "@nestjs/throttler"; import { BetterAuthGuard } from "../auth/better-auth.guard"; import { PodcastAccessGuard } from "../common/guards/podcast-access.guard"; import { NetworkService } from "./network.service"; import { CreateGuestPublicProfileDto, UpdateGuestPublicProfileDto, GuestNetworkSearchDto, CreateNetworkBookingRequestDto, RespondToBookingRequestDto, CreateNetworkBookmarkDto, UpdateNetworkBookmarkDto, CreateHostReviewDto, TrackProfileViewDto, } from "./network.dto"; @ApiTags("Network") @Controller("network") export class NetworkController { constructor(private readonly networkService: NetworkService) {} // ─── Public Endpoints ──────────────────────────────── @Get("guests/search") @Throttle({ default: { limit: 30, ttl: 60000 } }) @ApiOperation({ summary: "Search the guest network" }) @ApiResponse({ status: 200, description: "Search results" }) async searchGuests(@Query() query: GuestNetworkSearchDto) { return this.networkService.searchProfiles(query as any); } @Get("guests/trending") @ApiOperation({ summary: "Get trending guest voices" }) @ApiQuery({ name: "region", required: false, example: "east-africa" }) @ApiQuery({ name: "limit", required: false, example: 10 }) async getTrending( @Query("region") region?: string, @Query("limit") limit?: number, ) { return this.networkService.getTrending(region, limit || 10); } @Get("guests/:vanityUrl") @ApiOperation({ summary: "Get public guest profile by vanity URL" }) @ApiParam({ name: "vanityUrl", example: "john-nduati" }) async getProfile(@Param("vanityUrl") vanityUrl: string) { return this.networkService.getProfileByVanityUrl(vanityUrl); } @Post("views") @ApiOperation({ summary: "Track a profile view" }) async trackView(@Body() body: TrackProfileViewDto) { return this.networkService.trackView(body as any); } // ─── Protected Endpoints ───────────────────────────── @Post("profiles") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Create your public guest profile" }) @ApiResponse({ status: 201, description: "Profile created" }) @ApiResponse({ status: 409, description: "Profile already exists" }) async createProfile( @Req() req: any, @Body() body: CreateGuestPublicProfileDto, ) { return this.networkService.createProfile(req.userId, body as any); } @Put("profiles/me") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Update your public guest profile" }) async updateMyProfile( @Req() req: any, @Body() body: UpdateGuestPublicProfileDto, ) { return this.networkService.updateProfile(req.userId, body as any); } @Get("profiles/me") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Get your public profile with analytics" }) async getMyProfile(@Req() req: any) { return this.networkService.getMyProfile(req.userId); } @Get("profiles/:id/views") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Get profile view statistics" }) @ApiParam({ name: "id" }) @ApiQuery({ name: "days", required: false, example: 30 }) async getViewStats( @Req() req: any, @Param("id") id: string, @Query("days") days?: number, ) { // SECURITY: `req.userId` is set by BetterAuthGuard from the verified session. // Never trust client-supplied identity headers (e.g. `x-user-id`) for // authorization decisions. getProfileViewStats enforces ownership // server-side. return this.networkService.getProfileViewStats(req.userId, id, days || 30); } // ─── Bookmarks ─────────────────────────────────────── @Post("bookmarks") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "Bookmark a guest profile" }) async createBookmark( @Body() body: CreateNetworkBookmarkDto, @Req() req: any, ) { return this.networkService.createBookmark(body as any, req.userId); } @Put("bookmarks/:id") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "Update a bookmark" }) async updateBookmark( @Req() req: any, @Param("id") id: string, @Body() body: UpdateNetworkBookmarkDto, ) { return this.networkService.updateBookmark(req.userId, id, body as any); } @Delete("bookmarks/:id") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "Remove a bookmark" }) async removeBookmark( @Req() req: any, @Param("id") id: string, ) { return this.networkService.removeBookmark(req.userId, id); } @Get("bookmarks") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "List bookmarked guest profiles" }) @ApiQuery({ name: "podcastId", required: true }) @ApiQuery({ name: "status", required: false }) async listBookmarks( @Req() req: any, @Query("podcastId") podcastId: string, @Query("status") status?: string, ) { return this.networkService.listBookmarks(podcastId, req.userId, status); } // ─── Booking Requests ──────────────────────────────── @Post("booking-requests") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "Send a booking request to a guest" }) @ApiResponse({ status: 201, description: "Booking request created" }) async createBookingRequest( @Body() body: CreateNetworkBookingRequestDto, @Req() req: any, ) { return this.networkService.createBookingRequest(body as any, req.userId); } @Put("booking-requests/:id/respond") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "Accept or decline a booking request" }) @ApiParam({ name: "id" }) async respondToBookingRequest( @Req() req: any, @Param("id") id: string, @Body() body: RespondToBookingRequestDto, ) { return this.networkService.respondToBookingRequest( id, req.userId, body.status, ); } @Get("booking-requests/received") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "List received booking requests" }) @ApiQuery({ name: "profileId", required: true }) @ApiQuery({ name: "status", required: false }) async listBookingRequests( @Req() req: any, @Query("profileId") profileId: string, @Query("status") status?: string, ) { return this.networkService.listBookingRequests(req.userId, profileId, status); } @Get("booking-requests/sent") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "List sent booking requests" }) @ApiQuery({ name: "podcastId", required: true }) @ApiQuery({ name: "status", required: false }) async listSentBookingRequests( @Req() req: any, @Query("podcastId") podcastId: string, @Query("status") status?: string, ) { return this.networkService.listSentBookingRequests(req.userId, podcastId, status); } // ─── Reviews ───────────────────────────────────────── @Post("reviews") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "Submit a host review for a guest" }) async submitReview( @Req() req: any, @Body() body: CreateHostReviewDto, ) { return this.networkService.submitReview(req.userId, body as any); } // ─── Reputation ────────────────────────────────────── @Get("profiles/:id/reputation") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Get reputation insights for a profile" }) @ApiParam({ name: "id" }) async getReputationInsights( @Req() req: any, @Param("id") id: string, ) { return this.networkService.getReputationInsights(req.userId, id); } @Post("profiles/:id/reputation/recalculate") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Recalculate reputation score for a profile" }) @ApiParam({ name: "id" }) async recalculateReputation( @Req() req: any, @Param("id") id: string, ) { return this.networkService.recalculateReputation(req.userId, id); } // ─── Recommendations ───────────────────────────────── @Get("profiles/:id/recommendations") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Get guest recommendations based on network graph" }) @ApiParam({ name: "id" }) @ApiQuery({ name: "limit", required: false, example: 5 }) async getRecommendations( @Req() req: any, @Param("id") id: string, @Query("limit") limit?: number, ) { return this.networkService.getRecommendations(req.userId, id, limit || 5); } @Get("profiles/:id/network-stats") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Get social network statistics for a profile" }) @ApiParam({ name: "id" }) async getProfileNetworkStats( @Req() req: any, @Param("id") id: string, ) { return this.networkService.getProfileNetworkStats(req.userId, id); } // ─── Podcast Network Stats ─────────────────────────── @Get("stats/:podcastId") @UseGuards(BetterAuthGuard, PodcastAccessGuard) @ApiBearerAuth() @ApiOperation({ summary: "Get network usage statistics for a podcast" }) @ApiParam({ name: "podcastId" }) async getPodcastNetworkStats( @Param("podcastId") podcastId: string, @Req() req: any, ) { return this.networkService.getPodcastNetworkStats(podcastId, req.userId); } // ─── Admin ─────────────────────────────────────────── @Put("admin/verify/:profileId") @UseGuards(BetterAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: "Verify a guest profile (admin)" }) @ApiParam({ name: "profileId" }) async verifyProfile( @Req() req: any, @Param("profileId") profileId: string, @Body() body: { status: "basic" | "verified" | "featured" }, ) { // SECURITY: `req.userId` is set by BetterAuthGuard from the verified session. // Using the client-controlled `x-user-id` header here would let any // authenticated user impersonate an admin for this endpoint. return this.networkService.verifyProfile(req.userId, profileId, body.status); } }