add scalar api reference

This commit is contained in:
2024-04-12 21:20:28 +02:00
parent 78d77ffd05
commit 616626b4ce
23 changed files with 2428 additions and 23 deletions

View File

@@ -39,6 +39,7 @@
"@nestjs/serve-static": "^4.0.0",
"@nestjs/swagger": "^7.1.16",
"@prisma/client": "4.16.0",
"@scalar/nestjs-api-reference": "^0.2.37",
"@types/passport-local": "^1.0.38",
"axios": "^1.6.7",
"bcrypt": "5.1.0",

2318
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
type Constructor<T = object> = new (...args: any[]) => T
type Wrapper<T = object> = { new (): T & any; prototype: T }
type DecoratorOptions = { name: string }
type ApiSchemaDecorator = <T extends Constructor>(
options: DecoratorOptions
) => (constructor: T) => Wrapper<T>
export const ApiSchema: ApiSchemaDecorator = ({ name }) => {
return constructor => {
const wrapper = class extends constructor {}
Object.defineProperty(wrapper, 'name', {
value: name,
writable: false,
})
return wrapper
}
}

View File

@@ -1,6 +1,7 @@
import { Logger, VersioningType } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
import { apiReference } from '@scalar/nestjs-api-reference'
import * as cookieParser from 'cookie-parser'
import { AppModule } from './app.module'
@@ -22,6 +23,7 @@ async function bootstrap() {
})
const config = new DocumentBuilder()
.setTitle('Flashcards')
.addBearerAuth()
.setDescription('Flashcards API')
.setVersion('1.0')
.addServer('https://api.flashcards.andrii.es')
@@ -36,6 +38,32 @@ async function bootstrap() {
customJs: '/swagger-ui.js',
customCssUrl: '/swagger-themes/dark.css',
})
app.use(
'/reference',
apiReference({
spec: {
content: document,
},
authentication: {
preferredSecurityScheme: 'bearer',
http: {
basic: {
username: 'Basic',
password: 'Basic',
},
bearer: {
token:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJmMmJlOTViOS00ZDA3LTQ3NTEtYTc3NS1iZDYxMmZjOTU1M2EiLCJkYXRlIjoiMjAyMy0wOC0wNVQxMTowMjoxNC42MjFaIiwiaWF0IjoxNjkxMjMzMzM0LCJleHAiOjIwMDY4MDkzMzR9.PGTRcsf34VFaS-Hz7_PUnWR8bBuVK7pdteBWUUYHXfw',
},
},
},
theme: 'deepSpace',
metaData: {
title: 'Flashcards API Reference',
ogTitle: 'Flashcards API Reference',
},
})
)
pipesSetup(app)
app.useGlobalFilters(new HttpExceptionFilter())
await app.listen(process.env.PORT || 3333)

View File

@@ -19,6 +19,7 @@ import { CommandBus } from '@nestjs/cqrs'
import { FileFieldsInterceptor } from '@nestjs/platform-express'
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiNoContentResponse,
@@ -63,6 +64,7 @@ export class AuthController {
@ApiUnauthorizedResponse({ description: 'Not logged in' })
@ApiBadRequestResponse({ description: 'User not found' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('me')
async getUserData(@Request() req): Promise<UserEntity> {
const userId = req.user.id
@@ -77,6 +79,7 @@ export class AuthController {
@UseInterceptors(FileFieldsInterceptor([{ name: 'avatar', maxCount: 1 }]))
@UseGuards(JwtAuthGuard)
@Patch('me')
@ApiBearerAuth()
async updateUserData(
@Request() req,
@UploadedFiles()
@@ -115,7 +118,7 @@ export class AuthController {
secure: true,
})
return { accessToken: req.user.data.accessToken }
return { accessToken: req.user.data.accessToken, refreshToken: req.user.data.refreshToken }
}
@ApiOperation({ description: 'Create a new user account', summary: 'Create a new user account' })
@@ -155,6 +158,7 @@ export class AuthController {
@HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(JwtAuthGuard)
@Post('logout')
@ApiBearerAuth()
async logout(
@Cookies('accessToken') accessToken: string,
@Res({ passthrough: true }) res: ExpressResponse

View File

@@ -1,5 +1,8 @@
import { IsUUID } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'EmailVerificationRequest' })
export class EmailVerificationDto {
@IsUUID('4')
code: string

View File

@@ -1,5 +1,8 @@
import { IsBoolean, IsEmail, IsOptional, Length } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'LoginRequest' })
export class LoginDto {
@Length(3, 30)
password: string

View File

@@ -1,6 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsEmail, IsOptional, IsString } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'RecoverPasswordRequest' })
export class RecoverPasswordDto {
/** User's email address */
@IsEmail()

View File

@@ -1,17 +1,20 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsBoolean, IsEmail, IsOptional, IsString, Length } from 'class-validator'
export class RegistrationDto {
@Length(3, 30)
@IsOptional()
name?: string
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'RegistrationRequest' })
export class RegistrationDto {
@Length(3, 30)
password: string
@IsEmail()
email: string
@Length(3, 30)
@IsOptional()
name?: string
@ApiProperty({
description: `HTML template to be sent in the email;\n ##name## will be replaced with the user's name; \n ##token## will be replaced with the password recovery token`,
example: `<b>Hello, ##name##!</b><br/>Please confirm your email by clicking on the link below:<br/><a href="http://localhost:3000/confirm-email/##token##">Confirm email</a>. If it doesn't work, copy and paste the following link in your browser:<br/>http://localhost:3000/confirm-email/##token##`,

View File

@@ -1,6 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsOptional, IsString, IsUUID } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'ResendVerificationEmailRequest' })
export class ResendVerificationEmailDto {
@IsUUID()
userId: string

View File

@@ -1,5 +1,8 @@
import { Length } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'ResetPasswordRequest' })
export class ResetPasswordDto {
@Length(3, 30)
password: string

View File

@@ -1,5 +1,8 @@
import { IsNumber, IsString, Max, Min } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'SaveGradeRequest' })
export class SaveGradeDto {
@IsString()
cardId: string

View File

@@ -1,8 +1,10 @@
import { PartialType, PickType } from '@nestjs/swagger'
import { IsOptional } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
import { User } from '../entities/auth.entity'
@ApiSchema({ name: 'UpdateUserRequest' })
export class UpdateUserDataDto extends PartialType(PickType(User, ['name', 'avatar'] as const)) {
@IsOptional()
avatar?: string

View File

@@ -1,5 +1,7 @@
import { ApiProperty, OmitType } from '@nestjs/swagger'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
export class User {
id: string
email: string
@@ -14,6 +16,8 @@ export class User {
export class LoginResponse {
accessToken: string
refreshToken: string
}
@ApiSchema({ name: 'User' })
export class UserEntity extends OmitType(User, ['password']) {}

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { Request } from 'express'
import { Strategy } from 'passport-jwt'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { AppSettings } from '../../../settings/app-settings'
import { UsersService } from '../../users/services/users.service'
@@ -24,7 +24,10 @@ export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh'
private userService: UsersService
) {
super({
jwtFromRequest: cookieExtractor,
jwtFromRequest: ExtractJwt.fromExtractors([
cookieExtractor,
ExtractJwt.fromAuthHeaderAsBearerToken(),
]),
ignoreExpiration: true,
secretOrKey: appSettings.auth.REFRESH_JWT_SECRET_KEY,
})

View File

@@ -5,13 +5,11 @@ import { ExtractJwt, Strategy } from 'passport-jwt'
import { AppSettings } from '../../../settings/app-settings'
import { UsersService } from '../../users/services/users.service'
import { AuthService } from '../auth.service'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@Inject(AppSettings.name) private readonly appSettings: AppSettings,
private authService: AuthService,
private userService: UsersService
) {
super({

View File

@@ -15,6 +15,7 @@ import {
import { CommandBus } from '@nestjs/cqrs'
import { FileFieldsInterceptor } from '@nestjs/platform-express'
import {
ApiBearerAuth,
ApiConsumes,
ApiNoContentResponse,
ApiNotFoundResponse,
@@ -38,6 +39,7 @@ export class CardsController {
@ApiOperation({ summary: 'Get card by id', description: 'Get card by id' })
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiNotFoundResponse({ description: 'Card not found' })
@ApiBearerAuth()
@Get(':id')
findOne(@Param('id') id: string): Promise<CardWithGrade> {
return this.commandBus.execute(new GetDeckByIdCommand(id))
@@ -48,6 +50,7 @@ export class CardsController {
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiNotFoundResponse({ description: 'Card not found' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'questionImg', maxCount: 1 },
@@ -69,6 +72,7 @@ export class CardsController {
@UseGuards(JwtAuthGuard)
@Delete(':id')
@ApiBearerAuth()
@ApiOperation({ summary: 'Delete card by id', description: 'Delete card by id' })
@ApiNoContentResponse({ description: 'New tokens generated successfully' })
@ApiUnauthorizedResponse({ description: 'Unauthorized' })

View File

@@ -1,5 +1,8 @@
import { IsOptional, Length } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'CreateCardRequest' })
export class CreateCardDto {
@Length(3, 500)
question: string

View File

@@ -2,8 +2,11 @@ import { PartialType } from '@nestjs/mapped-types'
import { ApiProperty } from '@nestjs/swagger'
import { IsOptional, Length } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
import { CreateCardDto } from './create-card.dto'
@ApiSchema({ name: 'UpdateCardRequest' })
export class UpdateCardDto extends PartialType(CreateCardDto) {
@IsOptional()
@Length(3, 500)

View File

@@ -19,6 +19,7 @@ import {
import { CommandBus } from '@nestjs/cqrs'
import { FileFieldsInterceptor } from '@nestjs/platform-express'
import {
ApiBearerAuth,
ApiConsumes,
ApiNoContentResponse,
ApiNotFoundResponse,
@@ -61,10 +62,7 @@ import {
@ApiTags('Decks')
@Controller('decks')
export class DecksController {
constructor(
private commandBus: CommandBus,
private decksRepository: DecksRepository
) {}
constructor(private commandBus: CommandBus) {}
@HttpCode(HttpStatus.PARTIAL_CONTENT)
@ApiOperation({
@@ -74,6 +72,7 @@ export class DecksController {
})
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get()
findAllV1(@Query() query: GetAllDecksDto, @Req() req): Promise<PaginatedDecksWithMaxCardsCount> {
const finalQuery = Pagination.getPaginationData(query)
@@ -86,6 +85,7 @@ export class DecksController {
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@UseGuards(JwtAuthGuard)
@Version('2')
@ApiBearerAuth()
@Get()
findAllV2(@Query() query: GetAllDecksDto, @Req() req): Promise<PaginatedDecks> {
const finalQuery = Pagination.getPaginationData(query)
@@ -98,6 +98,7 @@ export class DecksController {
description: 'Retrieve the minimum and maximum amount of cards in a deck.',
summary: 'Minimum and maximum amount of cards in a deck',
})
@ApiBearerAuth()
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@UseGuards(JwtAuthGuard)
@Version('2')
@@ -110,6 +111,7 @@ export class DecksController {
@ApiOperation({ description: 'Create a deck', summary: 'Create a deck' })
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@UseInterceptors(FileFieldsInterceptor([{ name: 'cover', maxCount: 1 }]))
@Post()
create(
@@ -131,6 +133,7 @@ export class DecksController {
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@UseGuards(JwtAuthGuard)
@Get(':id')
@ApiBearerAuth()
findOne(@Param('id') id: string): Promise<DeckWithAuthor> {
return this.commandBus.execute(new GetDeckByIdCommand(id))
}
@@ -142,6 +145,7 @@ export class DecksController {
@UseGuards(JwtAuthGuard)
@UseInterceptors(FileFieldsInterceptor([{ name: 'cover', maxCount: 1 }]))
@Patch(':id')
@ApiBearerAuth()
update(
@Param('id') id: string,
@UploadedFiles()
@@ -162,6 +166,7 @@ export class DecksController {
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiNotFoundResponse({ description: 'Deck not found' })
@Delete(':id')
@ApiBearerAuth()
remove(@Param('id') id: string, @Req() req): Promise<Deck> {
return this.commandBus.execute(new DeleteDeckByIdCommand(id, req.user.id))
}
@@ -170,6 +175,7 @@ export class DecksController {
description: 'Retrieve paginated cards in a deck',
summary: 'Retrieve cards in a deck',
})
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':id/cards')
findCardsInDeck(
@@ -187,6 +193,7 @@ export class DecksController {
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiNotFoundResponse({ description: 'Deck not found' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'questionImg', maxCount: 1 },
@@ -213,6 +220,7 @@ export class DecksController {
description: 'Retrieve a random card in a deck. The cards priority is based on the grade',
summary: 'Retrieve a random card',
})
@ApiBearerAuth()
@Get(':id/learn')
findRandomCardInDeck(
@Param('id') id: string,
@@ -224,6 +232,7 @@ export class DecksController {
)
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiNotFoundResponse({ description: 'Card not found' })

View File

@@ -2,6 +2,9 @@ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'
import { Transform } from 'class-transformer'
import { IsBoolean, IsOptional, Length } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'CreateDeckRequest' })
export class CreateDeckDto {
@Length(3, 30)
name: string

View File

@@ -2,10 +2,12 @@ import { PartialType } from '@nestjs/mapped-types'
import { ApiProperty } from '@nestjs/swagger'
import { IsBoolean } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators'
import { CreateDeckDto } from './create-deck.dto'
@ApiSchema({ name: 'UpdateDeckRequest' })
export class UpdateDeckDto extends PartialType(CreateDeckDto) {
@IsOptionalOrEmptyString()
name?: string

View File

@@ -1,5 +1,8 @@
import { Length, Matches } from 'class-validator'
import { ApiSchema } from '../../../infrastructure/common/helpers/api-schema'
@ApiSchema({ name: 'CreateUserRequest' })
export class CreateUserDto {
@Length(3, 10)
name: string