feat(api): REST API with OpenAPI for other apps, behind Pireon OAuth
A REST adapter next to GraphQL (presenters/http) that calls the same application services, so validation, permissions and domain errors are the same: projects (members, board, stats, activity), issues (search, workflow, assignee, release, links, comments, history), comments, releases and users. Projects and issues can be addressed by id or key (WEB, WEB-12). - Swagger UI at /api/docs, document at /api/docs-json and apps/api/openapi.json (rewritten by `nx serve api`, like schema.gql) for client generation. - Every route but health/auth/webhook needs a Pireon token: the document declares Pireon OAuth2 (authorization code + PKCE, password) through the /api/auth/token proxy, and a bearer scheme for apps with their own client. - configureApp() shares main.ts setup with the e2e, which now also covers REST. - SWAGGER_ENABLED / OPENAPI_FILE config; docs 03/05 and library READMEs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,15 @@ import {
|
||||
Post,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiBody,
|
||||
ApiConsumes,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiProperty,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import {
|
||||
IdentityService,
|
||||
InvalidCredentialsError,
|
||||
@@ -18,10 +27,29 @@ import {
|
||||
import { Public } from '../auth/auth.decorators';
|
||||
import { IssueTokenDto } from './dto/issue-token.dto';
|
||||
|
||||
export class AuthorizationConfigDto {
|
||||
@ApiProperty({ example: 'https://oauth.pireon.dev/oauth/authorize' })
|
||||
authorizeUrl!: string;
|
||||
@ApiProperty({ description: 'Public id of the task-stream OAuth client' })
|
||||
clientId!: string;
|
||||
@ApiProperty({ type: [String], example: ['users:read'] })
|
||||
scopes!: string[];
|
||||
@ApiProperty({ description: 'Whether email + password sign-in is allowed' })
|
||||
passwordGrant!: boolean;
|
||||
}
|
||||
|
||||
export class TokenResponseDto {
|
||||
@ApiProperty() access_token!: string;
|
||||
@ApiProperty({ type: String, nullable: true }) refresh_token!: string | null;
|
||||
@ApiProperty({ description: 'Seconds' }) expires_in!: number;
|
||||
@ApiProperty({ example: 'Bearer' }) token_type!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign-in for browsers. Pireon's /oauth/token has no CORS and the client
|
||||
* secret must stay server-side, so the web app goes through here.
|
||||
*/
|
||||
@ApiTags('Auth')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly identity: IdentityService) {}
|
||||
@@ -29,6 +57,10 @@ export class AuthController {
|
||||
/** What the web app needs to start Authorization Code + PKCE (null = not configured). */
|
||||
@Public()
|
||||
@Get('config')
|
||||
@ApiOperation({
|
||||
summary: 'How to sign in with Pireon (null when not configured)',
|
||||
})
|
||||
@ApiOkResponse({ type: AuthorizationConfigDto })
|
||||
config(): Promise<AuthorizationConfig | null> {
|
||||
return this.identity.authorizationConfig();
|
||||
}
|
||||
@@ -36,6 +68,18 @@ export class AuthController {
|
||||
@Public()
|
||||
@Post('token')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Exchange a grant for Pireon tokens (proxy of Pireon /oauth/token)',
|
||||
description:
|
||||
'For browsers (Pireon has no CORS) and Swagger UI: adds the client secret ' +
|
||||
'server-side. Apps with their own Pireon OAuth client can get tokens from ' +
|
||||
'Pireon directly; any valid Pireon access token is accepted by this API.',
|
||||
})
|
||||
@ApiConsumes('application/json', 'application/x-www-form-urlencoded')
|
||||
@ApiBody({ type: IssueTokenDto })
|
||||
@ApiOkResponse({ type: TokenResponseDto })
|
||||
@ApiUnauthorizedResponse({ description: 'Pireon rejected the grant' })
|
||||
async token(@Body() dto: IssueTokenDto) {
|
||||
try {
|
||||
return toResponse(await this.identity.issueToken(toGrant(dto)));
|
||||
|
||||
Reference in New Issue
Block a user