diff --git a/api/authentication/src/global/controllers/auth/EmailController.ts b/api/authentication/src/global/controllers/auth/EmailController.ts index 4c445f9..bdff3b5 100644 --- a/api/authentication/src/global/controllers/auth/EmailController.ts +++ b/api/authentication/src/global/controllers/auth/EmailController.ts @@ -1,6 +1,6 @@ import { SwaggerDocsVersion } from '@hikers-book/tsed-common/types'; import { Controller } from '@tsed/di'; -import { BadRequest, Forbidden, NotFound } from '@tsed/exceptions'; +import { BadRequest, Forbidden, NotFound, UnprocessableEntity } from '@tsed/exceptions'; import { Authenticate } from '@tsed/passport'; import { BodyParams } from '@tsed/platform-params'; import { Description, Post, Returns } from '@tsed/schema'; @@ -8,6 +8,7 @@ import { Docs } from '@tsed/swagger'; import { CredentialsAlreadyExist } from '../../exceptions'; import { EmailSendVerificationHandler, EmailVerifyTokenHandler } from '../../handlers'; import { EmailSendVerificationRequest, EmailVerifyTokenRequest } from '../../models'; +import { JWTResponse } from '../../models/auth/email/JWTResponse'; @Description('Email provider controllers.') @Controller('/auth/provider/email') @@ -39,17 +40,20 @@ export class AuthProviderEmailController { @Post('/sign-up') @Description('Sign up user with email and password.') @Authenticate('email-sign-up', { session: false }) - @Returns(200) + @Returns(200, JWTResponse) @Returns(NotFound.STATUS, NotFound) @Returns(Forbidden.STATUS, Forbidden) @Returns(BadRequest.STATUS, BadRequest) + @Returns(UnprocessableEntity.STATUS, UnprocessableEntity) + @Returns(CredentialsAlreadyExist.STATUS, CredentialsAlreadyExist) // istanbul ignore next async signUp() {} @Post('/sign-in') @Description('Sign in user with email and password.') @Authenticate('email-sign-in', { session: false }) - @Returns(200) + @Returns(200, JWTResponse) + @Returns(Forbidden.STATUS, Forbidden) // istanbul ignore next async signIn() {} } diff --git a/api/authentication/src/global/controllers/auth/FacebookController.ts b/api/authentication/src/global/controllers/auth/FacebookController.ts index 27b7bb7..b313fba 100644 --- a/api/authentication/src/global/controllers/auth/FacebookController.ts +++ b/api/authentication/src/global/controllers/auth/FacebookController.ts @@ -3,6 +3,7 @@ import { Controller } from '@tsed/di'; import { Authenticate } from '@tsed/passport'; import { Description, Get } from '@tsed/schema'; import { Docs } from '@tsed/swagger'; +import { OAuth2ReturnStatuses } from '../../decorators'; @Description('Facebook provider controllers.') @Controller('/auth/provider/facebook') @@ -12,12 +13,14 @@ export class AuthProviderFacebookController { @Get('/') @Description('Login with Facebook.') + @OAuth2ReturnStatuses() @Authenticate('facebook', { session: false, scope: ['email'] }) // istanbul ignore next async authenticated() {} @Get('/callback') @Description('Login with Facebook.') + @OAuth2ReturnStatuses() @Authenticate('facebook', { session: false, scope: ['email'] }) // istanbul ignore next async callback() {} diff --git a/api/authentication/src/global/controllers/auth/GithubController.ts b/api/authentication/src/global/controllers/auth/GithubController.ts index 639ecbb..505b91d 100644 --- a/api/authentication/src/global/controllers/auth/GithubController.ts +++ b/api/authentication/src/global/controllers/auth/GithubController.ts @@ -3,6 +3,7 @@ import { Controller } from '@tsed/di'; import { Authenticate } from '@tsed/passport'; import { Description, Get } from '@tsed/schema'; import { Docs } from '@tsed/swagger'; +import { OAuth2ReturnStatuses } from '../../decorators'; @Description('Github provider controllers.') @Controller('/auth/provider/github') @@ -12,12 +13,14 @@ export class AuthProviderGithubController { @Get('/') @Description('Login with Github.') + @OAuth2ReturnStatuses() @Authenticate('github', { session: false, scope: ['user', 'email'] }) // istanbul ignore next async authenticated() {} @Get('/callback') @Description('Login with Github.') + @OAuth2ReturnStatuses() @Authenticate('github', { session: false, scope: ['user', 'email'] }) // istanbul ignore next async callback() {} diff --git a/api/authentication/src/global/controllers/auth/GoogleController.ts b/api/authentication/src/global/controllers/auth/GoogleController.ts index 1abc6cd..f73e361 100644 --- a/api/authentication/src/global/controllers/auth/GoogleController.ts +++ b/api/authentication/src/global/controllers/auth/GoogleController.ts @@ -3,6 +3,7 @@ import { Controller } from '@tsed/di'; import { Authenticate } from '@tsed/passport'; import { Description, Get } from '@tsed/schema'; import { Docs } from '@tsed/swagger'; +import { OAuth2ReturnStatuses } from '../../decorators'; @Description('Google provider controllers.') @Controller('/auth/provider/google') @@ -12,12 +13,14 @@ export class AuthProviderGoogleController { @Get('/') @Description('Login with Google.') + @OAuth2ReturnStatuses() @Authenticate('google', { session: false, scope: ['email', 'profile'] }) // istanbul ignore next async authenticated() {} @Get('/callback') @Description('Login with Google.') + @OAuth2ReturnStatuses() @Authenticate('google', { session: false, scope: ['email', 'profile'] }) // istanbul ignore next async callback() {} diff --git a/api/authentication/src/global/decorators/JWTAuth.ts b/api/authentication/src/global/decorators/JWTAuth.ts index c98dbdc..3c65023 100644 --- a/api/authentication/src/global/decorators/JWTAuth.ts +++ b/api/authentication/src/global/decorators/JWTAuth.ts @@ -1,7 +1,8 @@ +import { SwaggerSecurityScheme } from '@hikers-book/tsed-common/types'; import { useDecorators } from '@tsed/core'; import { Unauthorized } from '@tsed/exceptions'; import { Authenticate, AuthorizeOptions } from '@tsed/passport'; -import { In, Returns, Security } from '@tsed/schema'; +import { Returns, Security } from '@tsed/schema'; /** * Set JWTAuth access on decorated route @@ -10,8 +11,10 @@ import { In, Returns, Security } from '@tsed/schema'; export function JWTAuth(options: AuthorizeOptions = {}) { return useDecorators( Authenticate('jwt-authentication-api', { ...options, session: false }), - Security('jwt'), - Returns(401, Unauthorized).Description('Unauthorized'), - In('header').Name('Authorization').Description('JWT Bearer token').Type(String).Required(false) + Security(SwaggerSecurityScheme.BEARER_JWT), + Returns(401, Unauthorized).Description('Unauthorized') + // Do not set .Required(true), as it will cause Swagger to require Authorization header. + // Properly set securitySchemes will unlock Authorize header which automatically uses Bearer JWT token. + // In('header').Name('Authorization').Description('Bearer JWT token').Type(String) ); } diff --git a/api/authentication/src/global/decorators/OAuth2ReturnStatuses.ts b/api/authentication/src/global/decorators/OAuth2ReturnStatuses.ts new file mode 100644 index 0000000..0d72494 --- /dev/null +++ b/api/authentication/src/global/decorators/OAuth2ReturnStatuses.ts @@ -0,0 +1,17 @@ +import { useDecorators } from '@tsed/core'; +import { Forbidden, UnprocessableEntity } from '@tsed/exceptions'; +import { Returns } from '@tsed/schema'; +import { CredentialsAlreadyExist } from '../exceptions'; + +/** + * Set Swagger return statuses for UAuth2 + * @param options + */ +export function OAuth2ReturnStatuses() { + return useDecorators( + Returns(301), + Returns(UnprocessableEntity.STATUS, UnprocessableEntity), + Returns(Forbidden.STATUS, Forbidden), + Returns(CredentialsAlreadyExist.STATUS, CredentialsAlreadyExist) + ); +} diff --git a/api/authentication/src/global/decorators/index.ts b/api/authentication/src/global/decorators/index.ts index f148012..6ea258b 100644 --- a/api/authentication/src/global/decorators/index.ts +++ b/api/authentication/src/global/decorators/index.ts @@ -3,3 +3,4 @@ */ export * from './JWTAuth'; +export * from './OAuth2ReturnStatuses'; diff --git a/api/authentication/src/global/models/auth/email/JWTResponse.ts b/api/authentication/src/global/models/auth/email/JWTResponse.ts new file mode 100644 index 0000000..c1a94af --- /dev/null +++ b/api/authentication/src/global/models/auth/email/JWTResponse.ts @@ -0,0 +1,14 @@ +import { Description, Required, Schema, Title } from '@tsed/schema'; + +@Schema({ additionalProperties: false }) +export class JWTResponse { + @Title('jwt') + @Description('JWT token.') + @Required() + jwt!: string; + + @Title('refresh') + @Description('JWT refresh token.') + @Required() + refresh!: string; +} diff --git a/api/authentication/src/global/models/index.ts b/api/authentication/src/global/models/index.ts index 2bd71db..daef4c8 100644 --- a/api/authentication/src/global/models/index.ts +++ b/api/authentication/src/global/models/index.ts @@ -10,6 +10,7 @@ export * from './auth/email/EmailSendVerificationRequest'; export * from './auth/email/EmailSignInRequest'; export * from './auth/email/EmailSignUpRequest'; export * from './auth/email/EmailVerifyTokenRequest'; +export * from './auth/email/JWTResponse'; export * from './config/AuthModel'; export * from './config/FrontendConfigModel'; export * from './config/JWTConfigModel'; diff --git a/api/authentication/src/global/services/ProtocolAuthService.spec.ts b/api/authentication/src/global/services/ProtocolAuthService.spec.ts index ee1a544..8d60bb9 100644 --- a/api/authentication/src/global/services/ProtocolAuthService.spec.ts +++ b/api/authentication/src/global/services/ProtocolAuthService.spec.ts @@ -15,7 +15,7 @@ import { import { AuthProviderEnum } from '../enums'; import { CredentialsAlreadyExist } from '../exceptions'; import { CredentialsMapper } from '../mappers/CredentialsMapper'; -import { Credentials, EmailSignInRequest, EmailSignUpRequest, User } from '../models'; +import { Credentials, EmailSignInRequest, EmailSignUpRequest, JWTResponse, User } from '../models'; import { ProviderGithubPair } from '../types'; import { CryptographyUtils } from '../utils'; import { JWTService } from './JWTService'; @@ -523,12 +523,13 @@ describe('ProtocolAuthService', () => { }); it('Should return jwt', async () => { - expect.assertions(1); + expect.assertions(2); // @ts-expect-error private const jwt = await service.createJWT(CredentialsStubPopulated); - expect(jwt).toStrictEqual({ jwt: 'jwt', refresh: 'refresh' }); + expect(jwt).toBeInstanceOf(JWTResponse); + expect(jwt).toEqual({ jwt: 'jwt', refresh: 'refresh' }); }); it('Should throw 422', async () => { diff --git a/api/authentication/src/global/services/ProtocolAuthService.ts b/api/authentication/src/global/services/ProtocolAuthService.ts index b038e09..7845fe0 100644 --- a/api/authentication/src/global/services/ProtocolAuthService.ts +++ b/api/authentication/src/global/services/ProtocolAuthService.ts @@ -8,7 +8,8 @@ import { AuthProviderEnum } from '../enums'; import { CredentialsAlreadyExist } from '../exceptions'; import { CredentialsMapper } from '../mappers/CredentialsMapper'; import { Credentials, EmailSignInRequest, EmailSignUpRequest, User } from '../models'; -import { AuthProviderPair, JWTResponse, OAuth2ProviderPair } from '../types'; +import { JWTResponse } from '../models/auth/email/JWTResponse'; +import { AuthProviderPair, OAuth2ProviderPair } from '../types'; import { CryptographyUtils } from '../utils/CryptographyUtils'; import { JWTService } from './JWTService'; import { CredentialsMongoService } from './mongo/CredentialsMongoService'; @@ -122,10 +123,10 @@ export class ProtocolAuthService { true ); - return { + return CommonUtils.buildModel(JWTResponse, { jwt, refresh - }; + }); } private getEmailFromAuthProfile(data: AuthProviderPair): string { diff --git a/api/authentication/src/global/types/ProtocolAuthService.ts b/api/authentication/src/global/types/ProtocolAuthService.ts index 51ddbd9..aad9f46 100644 --- a/api/authentication/src/global/types/ProtocolAuthService.ts +++ b/api/authentication/src/global/types/ProtocolAuthService.ts @@ -11,8 +11,3 @@ export type ProviderGooglePair = { provider: AuthProviderEnum.GOOGLE; profile: G export type OAuth2ProviderPair = ProviderFacebookPair | ProviderGithubPair | ProviderGooglePair; export type AuthProviderPair = ProviderEmailPair | OAuth2ProviderPair; - -export type JWTResponse = { - jwt: string; - refresh: string; -}; diff --git a/api/authentication/src/test/stubs/Auth.ts b/api/authentication/src/test/stubs/Auth.ts index 903dd19..220d9f1 100644 --- a/api/authentication/src/test/stubs/Auth.ts +++ b/api/authentication/src/test/stubs/Auth.ts @@ -1,8 +1,8 @@ +import { CommonUtils } from '@hikers-book/tsed-common/utils'; import { Profile as FacebookProfile } from 'passport-facebook'; import { Profile as GithubProfile } from 'passport-github2'; import { Profile as GoogleProfile } from 'passport-google-oauth20'; -import { EmailSignUpRequest } from '../../global/models'; -import { JWTResponse } from '../../global/types'; +import { EmailSignUpRequest, JWTResponse } from '../../global/models'; export const ProfileFacebookStub: FacebookProfile = { id: 'id', @@ -40,7 +40,7 @@ export const ProfileEmailStub: EmailSignUpRequest = { full_name: 'Tester' }; -export const JWTStub: JWTResponse = { +export const JWTStub = CommonUtils.buildModel(JWTResponse, { jwt: 'jwt', refresh: 'refresh' -}; +}); diff --git a/api/trips/src/v1/controllers/TripsController.ts b/api/trips/src/v1/controllers/TripsController.ts index d2d43b8..df93b49 100644 --- a/api/trips/src/v1/controllers/TripsController.ts +++ b/api/trips/src/v1/controllers/TripsController.ts @@ -15,7 +15,7 @@ export class TripsController { @Get('/') @Description('Returns list of trips.') - @Returns(200, [Trip]) + @Returns(200, Array).Of(Trip) async get(): Promise { return this.getTripsHandler.execute(); } diff --git a/packages/tsed-common/src/decorators/JWTAuth.ts b/packages/tsed-common/src/decorators/JWTAuth.ts index b488e12..f54cd86 100644 --- a/packages/tsed-common/src/decorators/JWTAuth.ts +++ b/packages/tsed-common/src/decorators/JWTAuth.ts @@ -1,7 +1,8 @@ import { useDecorators } from '@tsed/core'; import { Unauthorized } from '@tsed/exceptions'; import { Authenticate, AuthorizeOptions } from '@tsed/passport'; -import { In, Returns, Security } from '@tsed/schema'; +import { Returns, Security } from '@tsed/schema'; +import { SwaggerSecurityScheme } from '../types'; /** * Set JWTAuth access on decorated route @@ -10,8 +11,10 @@ import { In, Returns, Security } from '@tsed/schema'; export function JWTAuth(options: AuthorizeOptions = {}) { return useDecorators( Authenticate('jwt', { ...options, session: false }), - Security('bearerAuth'), - Returns(401, Unauthorized).Description('Unauthorized'), - In('header').Name('Authorization').Description('JWT Bearer token').Type(String).Required(true) + Security(SwaggerSecurityScheme.BEARER_JWT), + Returns(401, Unauthorized).Description('Unauthorized') + // Do not set .Required(true), as it will cause Swagger to require Authorization header. + // Properly set securitySchemes will unlock Authorize header which automatically uses Bearer JWT token. + // In('header').Name('Authorization').Description('Bearer JWT token').Type(String) ); } diff --git a/packages/tsed-common/src/server/ConfigSwagger.ts b/packages/tsed-common/src/server/ConfigSwagger.ts index acf7b9f..6502432 100644 --- a/packages/tsed-common/src/server/ConfigSwagger.ts +++ b/packages/tsed-common/src/server/ConfigSwagger.ts @@ -1,5 +1,6 @@ import { OpenSpec3, OpenSpecInfo } from '@tsed/openspec'; import { SwaggerSettings } from '@tsed/swagger'; +import { SwaggerSecurityScheme } from '../types'; import { SwaggerDocsVersion } from '../types/SwaggerDocsVersion.enum'; export class ConfigSwagger { @@ -26,7 +27,10 @@ export class ConfigSwagger { return { path: `/${docsVersion}/docs`, doc: docsVersion, - specVersion: '3.0.1', + specVersion: '3.0.3', + options: { + requestSnippets: true + }, spec: >{ info: { title, @@ -35,10 +39,11 @@ export class ConfigSwagger { }, components: { securitySchemes: { - bearerAuth: { + [SwaggerSecurityScheme.BEARER_JWT]: { type: 'http', scheme: 'bearer', - bearerFormat: 'JWT' + bearerFormat: 'JWT', + description: 'Bearer JWT token' } } } diff --git a/packages/tsed-common/src/types/SwaggerSecurityScheme.enum.ts b/packages/tsed-common/src/types/SwaggerSecurityScheme.enum.ts new file mode 100644 index 0000000..ecaaece --- /dev/null +++ b/packages/tsed-common/src/types/SwaggerSecurityScheme.enum.ts @@ -0,0 +1,3 @@ +export enum SwaggerSecurityScheme { + BEARER_JWT = 'BEARER_JWT' +} diff --git a/packages/tsed-common/src/types/index.ts b/packages/tsed-common/src/types/index.ts index 5947415..8a21cfe 100644 --- a/packages/tsed-common/src/types/index.ts +++ b/packages/tsed-common/src/types/index.ts @@ -4,4 +4,5 @@ export * from './JWTPayload'; export * from './SwaggerDocsVersion.enum'; +export * from './SwaggerSecurityScheme.enum'; export * from './mongo';