mirror of
https://github.com/ershisan99/flashcards-api.git
synced 2025-12-16 12:33:17 +00:00
add scalar api reference
This commit is contained in:
@@ -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
2318
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
19
src/infrastructure/common/helpers/api-schema.ts
Normal file
19
src/infrastructure/common/helpers/api-schema.ts
Normal 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
|
||||
}
|
||||
}
|
||||
28
src/main.ts
28
src/main.ts
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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##`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']) {}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user