import { Body, Controller, Delete, Get, HttpCode, NotFoundException, Param, Put, Query, Res, } from '@nestjs/common'; import { ApiNoContentResponse, ApiOkResponse, ApiOperation, ApiParam, ApiTags, } from '@nestjs/swagger'; import { User } from '@task-stream/api/identity/domain'; import { ApiPireonAuth, CurrentUser, Public, } from '@task-stream/api/identity/presenters'; import { ApiCommandErrors, ApiReadErrors, } from '@task-stream/api/shared/presenters'; import { SharingService } from '@task-stream/api/tracker/application'; import type { Response } from 'express'; import { ShareDashboardDto } from './dto/requests.dto'; import { DashboardShareDto, PublicReleaseNotesDto, ReleaseNotesShareDto, SharedDashboardDto, } from './dto/responses.dto'; import { Refs } from './refs'; const PROJECT = ApiParam({ name: 'project', description: 'Project id or key', example: 'WEB', }); /** * The project's dashboard for clients: a secret link (/share/{token}) that * shows the blocks its owners and admins chose, without an account. */ @ApiTags('Projects') @Controller() export class SharingController { constructor( private readonly sharing: SharingService, private readonly refs: Refs, ) {} @Get('projects/:project/share') @ApiPireonAuth() @PROJECT @ApiOperation({ summary: "The project's link for clients, if any" }) @ApiOkResponse({ type: DashboardShareDto }) @ApiNoContentResponse({ description: 'It has none' }) @ApiReadErrors() async share( @CurrentUser() me: User, @Param('project') project: string, @Res({ passthrough: true }) response: Response, ) { const share = await this.sharing.share( me.id, await this.refs.projectId(me.id, project), ); if (!share) response.status(204); return share ?? undefined; } @Put('projects/:project/share') @ApiPireonAuth() @PROJECT @ApiOperation({ summary: 'Turn the link on or change its blocks (renew: a new secret)', }) @ApiOkResponse({ type: DashboardShareDto }) @ApiCommandErrors() async save( @CurrentUser() me: User, @Param('project') project: string, @Body() body: ShareDashboardDto, ) { return this.sharing.save( me.id, await this.refs.projectId(me.id, project), body.blocks, body.renew ?? false, ); } @Delete('projects/:project/share') @HttpCode(204) @ApiPireonAuth() @PROJECT @ApiOperation({ summary: 'The link stops working' }) @ApiNoContentResponse() @ApiCommandErrors() async remove(@CurrentUser() me: User, @Param('project') project: string) { await this.sharing.remove(me.id, await this.refs.projectId(me.id, project)); } @Get('releases/:release/public-notes') @ApiPireonAuth() @ApiParam({ name: 'release', format: 'uuid' }) @ApiOperation({ summary: "A release's public notes link, if any (owners and admins)", }) @ApiOkResponse({ type: ReleaseNotesShareDto }) @ApiReadErrors() async releaseNotesShare( @CurrentUser() me: User, @Param('release') release: string, @Res({ passthrough: true }) res: Response, ) { const share = await this.sharing.releaseNotesShare(me.id, release); if (!share) res.status(204); return share ?? undefined; } @Put('releases/:release/public-notes') @ApiPireonAuth() @ApiParam({ name: 'release', format: 'uuid' }) @ApiOperation({ summary: "Turn a release's public notes page on (owners and admins)", description: 'The page shows the notes written, or the ones drawn from its finished tickets. ?renew=true makes a new link.', }) @ApiOkResponse({ type: ReleaseNotesShareDto }) @ApiCommandErrors() shareReleaseNotes( @CurrentUser() me: User, @Param('release') release: string, @Query('renew') renew?: string, ) { return this.sharing.shareReleaseNotes(me.id, release, renew === 'true'); } @Delete('releases/:release/public-notes') @HttpCode(204) @ApiPireonAuth() @ApiParam({ name: 'release', format: 'uuid' }) @ApiOperation({ summary: "A release's public notes link stops working" }) @ApiNoContentResponse() @ApiCommandErrors() async unshareReleaseNotes( @CurrentUser() me: User, @Param('release') release: string, ) { await this.sharing.unshareReleaseNotes(me.id, release); } @Get('shared/release-notes/:token') @Public() @ApiOperation({ summary: "A release's notes, for anyone with its link" }) @ApiOkResponse({ type: PublicReleaseNotesDto }) async publicReleaseNotes(@Param('token') token: string) { const notes = /^[\w-]{20,100}$/.test(token) ? await this.sharing.publicReleaseNotes(token) : null; if (!notes) throw new NotFoundException('Unknown link'); return notes; } @Get('shared/dashboards/:token') @Public() @ApiOperation({ summary: 'What a link for clients shows (no session)', description: 'Only the chosen blocks: tickets per status, the sprint under way, release forecasts, open epics and activity per day. The others are null.', }) @ApiOkResponse({ type: SharedDashboardDto }) async shared(@Param('token') token: string) { const dashboard = /^[\w-]{20,100}$/.test(token) ? await this.sharing.shared(token) : null; if (!dashboard) throw new NotFoundException('Unknown link'); return dashboard; } }