apps/api: add swagger

This commit is contained in:
vchikalkin 2023-11-01 14:12:17 +03:00
parent 5d99d2bbbc
commit 0cf879b383
13 changed files with 209 additions and 38 deletions

View File

@ -1,5 +1,20 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
"sourceRoot": "src",
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/swagger/plugin",
"options": {
"dtoFileNameSuffix": [".entity.ts", ".dto.ts"],
"controllerFileNameSuffix": [".controller.ts"],
"classValidatorShim": true,
"dtoKeyOfComment": "description",
"controllerKeyOfComment": "description",
"introspectComments": true
}
}
]
}
}

View File

@ -23,6 +23,7 @@
},
"dependencies": {
"@fastify/cookie": "^9.1.0",
"@fastify/static": "^6.12.0",
"@nestjs/cache-manager": "^2.1.0",
"@nestjs/cli": "^10.1.18",
"@nestjs/common": "^10.2.7",
@ -33,6 +34,7 @@
"@nestjs/mongoose": "^10.0.1",
"@nestjs/platform-express": "^10.2.7",
"@nestjs/platform-fastify": "^10.2.7",
"@nestjs/swagger": "^7.1.14",
"bcrypt": "^5.1.1",
"cache-manager": "^5.2.4",
"cache-manager-ioredis": "^2.1.0",

View File

@ -10,17 +10,21 @@ import {
Get,
HttpException,
HttpStatus,
Param,
Post,
Query,
Req,
Res,
UnauthorizedException,
} from '@nestjs/common';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { FastifyReply, FastifyRequest } from 'fastify';
import { cookieOptions } from 'src/config/cookie';
import { env } from 'src/config/env';
import { Credentials } from 'src/dto/credentials';
import { Account } from 'src/schemas/account.schema';
@Controller('account')
@ApiTags('account')
export class AccountController {
constructor(private readonly accountService: AccountService) {}
@ -35,11 +39,15 @@ export class AccountController {
}
@Post('/create')
@ApiResponse({
status: HttpStatus.CREATED,
type: Account,
})
async create(@Body() createAccountDto: CreateAccountDto, @Res() reply: FastifyReply) {
try {
const createdAccount = await this.accountService.create(createAccountDto);
return reply.send(createdAccount);
return reply.status(HttpStatus.CREATED).send(createdAccount);
} catch (error) {
throw new HttpException(error, HttpStatus.BAD_REQUEST);
}
@ -50,8 +58,13 @@ export class AccountController {
return this.accountService.findAll();
}
@Delete('/delete/:login')
async delete(@Param('login') username: CreateAccountDto['username']) {
@Delete('/delete')
@ApiResponse({
status: HttpStatus.OK,
type: Account,
})
// @ApiQuery({ name: 'username', type: CreateAccountDto['username'] })
async delete(@Query('username') username: string) {
return this.accountService.delete(username);
}
@ -76,8 +89,10 @@ export class AccountController {
@Get('/get-user')
async getUser(@Req() req: FastifyRequest, @Res() reply: FastifyReply) {
const token = req.cookies[env.COOKIE_TOKEN_NAME];
if (!token) throw new UnauthorizedException();
const account = await this.accountService.getUser(token);
if (!account) throw new UnauthorizedException('Account not found');
return reply.send(account);
}

View File

@ -58,6 +58,10 @@ export class AccountService {
}
public async getUser(token: string) {
return this.jwtService.decode(token);
try {
return this.jwtService.verify(token);
} catch {
throw new UnauthorizedException('Invalid token');
}
}
}

View File

@ -1,14 +1,17 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
export class CreateAccountDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
readonly username: string;
public readonly username: string;
@ApiPropertyOptional({})
@IsString()
@MinLength(10)
@IsOptional()
readonly password: string;
public readonly password: string;
readonly [key: string]: unknown;
}

View File

@ -1,9 +1,11 @@
import { AppService } from './app.service';
import { env } from './config/env';
import { Controller, Get, HttpStatus, Req, Res } from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import { FastifyReply, FastifyRequest } from 'fastify';
@Controller()
@ApiExcludeController()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('auth')

View File

@ -1,10 +1,13 @@
import { IsNotEmpty, IsString } from "class-validator";
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class Credentials {
@ApiProperty()
@IsString()
@IsNotEmpty()
readonly login: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
readonly password: string;

View File

@ -4,12 +4,25 @@
import { Credentials } from '../dto/credentials';
import { LdapService } from './ldap.service';
import type { CookieSerializeOptions } from '@fastify/cookie';
import { Body, Controller, Get, HttpException, HttpStatus, Post, Req, Res } from '@nestjs/common';
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post,
Req,
Res,
UnauthorizedException,
} from '@nestjs/common';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { FastifyReply, FastifyRequest } from 'fastify';
import { cookieOptions } from 'src/config/cookie';
import { env } from 'src/config/env';
import { User } from 'src/utils/ldap';
@Controller('ldap')
@ApiTags('ldap')
export class LdapController {
cookieOptions: CookieSerializeOptions;
constructor(private readonly ldapService: LdapService) {}
@ -25,6 +38,9 @@ export class LdapController {
}
@Post('/signin')
@ApiResponse({
status: HttpStatus.OK,
})
async login(@Body() credentials: Credentials, @Res() reply: FastifyReply) {
try {
const token = await this.ldapService.login(credentials);
@ -46,11 +62,19 @@ export class LdapController {
}
@Get('/get-user')
@ApiResponse({
status: HttpStatus.OK,
type: User,
})
async getUser(@Req() req: FastifyRequest, @Res() reply: FastifyReply) {
const token = req.cookies[env.COOKIE_TOKEN_NAME];
if (!token) throw new UnauthorizedException();
const user = await this.ldapService.getUser(token);
if (!user) throw new UnauthorizedException('User not found');
return reply.send(user);
}
}

View File

@ -1,6 +1,6 @@
import type { DecodedToken, TokenPayload } from './types/jwt';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Cache } from 'cache-manager';
import { env } from 'src/config/env';
@ -37,18 +37,22 @@ export class LdapService {
}
public async getUser(token: string) {
const { username } = this.jwtService.decode(token) as DecodedToken;
try {
const { username } = this.jwtService.verify(token) as DecodedToken;
const cachedUser = await this.cacheManager.get(username);
const cachedUser = (await this.cacheManager.get(username)) as ldap.User;
if (!cachedUser) {
const user = await ldap.authenticate(username);
if (!cachedUser) {
const user = await ldap.authenticate(username);
await this.cacheManager.set(username, user);
await this.cacheManager.set(username, user);
return user;
return user;
}
return cachedUser;
} catch {
throw new UnauthorizedException('Invalid token');
}
return cachedUser;
}
}

View File

@ -3,10 +3,22 @@
import { AppModule } from './app.module';
import { env } from './config/env';
import { fastifyCookie } from '@fastify/cookie';
import type { INestApplication } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
function setupOpenApi(app: INestApplication) {
const config = new DocumentBuilder()
.setTitle('Evo.Auth')
.setVersion('1.0')
// .addTag('api')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('swagger', app, document, { useGlobalPrefix: true });
}
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
@ -22,6 +34,8 @@ async function bootstrap() {
app.useGlobalPipes(new ValidationPipe({ stopAtFirstError: true }));
setupOpenApi(app);
await app.listen(env.API_PORT, '0.0.0.0');
}

View File

@ -1,5 +1,6 @@
/* eslint-disable @babel/no-invalid-this */
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
import * as bcrypt from 'bcrypt';
import type { HydratedDocument } from 'mongoose';
@ -7,9 +8,13 @@ export type UserDocument = HydratedDocument<Account>;
@Schema({ strict: false })
export class Account {
@ApiResponseProperty()
@ApiProperty()
@Prop({ index: { unique: true }, required: true })
username: string;
@ApiResponseProperty()
@ApiProperty()
@Prop({ required: true })
password: string;
}

View File

@ -1,16 +1,24 @@
import { ApiResponseProperty } from '@nestjs/swagger';
import type { AuthenticationOptions } from 'ldap-authentication';
import * as ldap from 'ldap-authentication';
import { env } from 'src/config/env';
export type User = {
department: string;
displayName: string;
domain: string;
domainName: string;
mail: string;
position: string;
username: string;
};
export class User {
@ApiResponseProperty()
public department: string;
@ApiResponseProperty()
public displayName: string;
@ApiResponseProperty()
public domain: string;
@ApiResponseProperty()
public domainName: string;
@ApiResponseProperty()
public mail: string;
@ApiResponseProperty()
public position: string;
@ApiResponseProperty()
public username: string;
}
export type LdapUser = {
accountExpires: string;

View File

@ -457,6 +457,11 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.51.0.tgz#6d419c240cfb2b66da37df230f7e7eef801c32fa"
integrity sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==
"@fastify/accept-negotiator@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz#c1c66b3b771c09742a54dd5bc87c582f6b0630ff"
integrity sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==
"@fastify/ajv-compiler@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz#459bff00fefbf86c96ec30e62e933d2379e46670"
@ -517,6 +522,29 @@
path-to-regexp "^6.1.0"
reusify "^1.0.4"
"@fastify/send@^2.0.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@fastify/send/-/send-2.1.0.tgz#1aa269ccb4b0940a2dadd1f844443b15d8224ea0"
integrity sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==
dependencies:
"@lukeed/ms" "^2.0.1"
escape-html "~1.0.3"
fast-decode-uri-component "^1.0.1"
http-errors "2.0.0"
mime "^3.0.0"
"@fastify/static@^6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@fastify/static/-/static-6.12.0.tgz#f3d55dda201c072bae0593e5d45dde8fd235c288"
integrity sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==
dependencies:
"@fastify/accept-negotiator" "^1.0.0"
"@fastify/send" "^2.0.0"
content-disposition "^0.5.3"
fastify-plugin "^4.0.0"
glob "^8.0.1"
p-limit "^3.1.0"
"@fontsource/montserrat@^5.0.13":
version "5.0.13"
resolved "https://registry.yarnpkg.com/@fontsource/montserrat/-/montserrat-5.0.13.tgz#e9e78a726e4a452b820c8c0bdcddf4444c922db6"
@ -1027,6 +1055,11 @@
resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe"
integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==
"@lukeed/ms@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@lukeed/ms/-/ms-2.0.1.tgz#3c2bbc258affd9cc0e0cc7828477383c73afa6ee"
integrity sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==
"@mapbox/node-pre-gyp@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa"
@ -1121,7 +1154,7 @@
"@types/jsonwebtoken" "9.0.2"
jsonwebtoken "9.0.0"
"@nestjs/mapped-types@*":
"@nestjs/mapped-types@*", "@nestjs/mapped-types@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz#c8a090a8d22145b85ed977414c158534210f2e4f"
integrity sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==
@ -1166,6 +1199,17 @@
jsonc-parser "3.2.0"
pluralize "8.0.0"
"@nestjs/swagger@^7.1.14":
version "7.1.14"
resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-7.1.14.tgz#492b3816308264472b3619f5c0336f378f1c9995"
integrity sha512-2Ol4S6qHeYVVmkshkWBM8E/qkmEqEOUj2QIewr0jLSyo30H7f3v81pJyks6pTLy4PK0LGUXojMvIfFIE3mmGQQ==
dependencies:
"@nestjs/mapped-types" "2.0.2"
js-yaml "4.1.0"
lodash "4.17.21"
path-to-regexp "3.2.0"
swagger-ui-dist "5.9.0"
"@nestjs/testing@^10.2.7":
version "10.2.7"
resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.2.7.tgz#50408ccb4c809d216a12d60ac7932fd6ad7fedf4"
@ -2846,7 +2890,7 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0:
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
content-disposition@0.5.4:
content-disposition@0.5.4, content-disposition@^0.5.3:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
@ -4472,6 +4516,17 @@ glob@^7.0.0, glob@^7.1.3, glob@^7.1.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^8.0.1:
version "8.1.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^5.0.1"
once "^1.3.0"
glob@^9.2.0:
version "9.3.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21"
@ -5580,6 +5635,13 @@ js-types@^1.0.0:
resolved "https://registry.yarnpkg.com/js-types/-/js-types-1.0.0.tgz#d242e6494ed572ad3c92809fc8bed7f7687cbf03"
integrity sha512-bfwqBW9cC/Lp7xcRpug7YrXm0IVw+T9e3g4mCYnv0Pjr3zIzU9PCQElYU9oSGAWzXlbdl9X5SAMPejO9sxkeUw==
js-yaml@4.1.0, js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
js-yaml@^3.13.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
@ -5588,13 +5650,6 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
jsdoc-type-pratt-parser@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz#136f0571a99c184d84ec84662c45c29ceff71114"
@ -6042,6 +6097,11 @@ mime@2.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
mime@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -6071,6 +6131,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimatch@^8.0.2:
version "8.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229"
@ -7777,6 +7844,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swagger-ui-dist@5.9.0:
version "5.9.0"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.9.0.tgz#d52b6cf52fd0a8e6930866c402aaa793fe4e3f76"
integrity sha512-NUHSYoe5XRTk/Are8jPJ6phzBh3l9l33nEyXosM17QInoV95/jng8+PuSGtbD407QoPf93MH3Bkh773OgesJpA==
symbol-observable@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"