apps/api: add users functional

This commit is contained in:
vchikalkin 2023-11-01 12:29:48 +03:00
parent 9c76654406
commit 724d8ccf28
12 changed files with 150 additions and 30 deletions

View File

@ -36,8 +36,11 @@
"bcrypt": "^5.1.1",
"cache-manager": "^5.2.4",
"cache-manager-ioredis": "^2.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"ldap-authentication": "2.3.1",
"mongoose": "^7.6.3",
"radash": "^11.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",

View File

@ -1,6 +1,7 @@
import type { DecodedToken } from './ldap/types/jwt';
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { omit } from 'radash';
@Injectable()
export class AppService {
@ -11,9 +12,8 @@ export class AppService {
}
public refreshToken(token: string) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { exp, iat, ...payload } = this.jwtService.decode(token) as DecodedToken;
const payload = this.jwtService.decode(token) as DecodedToken;
return this.jwtService.sign(payload);
return this.jwtService.sign(omit(payload, ['iat', 'exp']));
}
}

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from "class-validator";
export class Credentials {
@IsString()
@IsNotEmpty()
readonly login: string;
@IsString()
@IsNotEmpty()
readonly password: string;
}

View File

@ -1,4 +0,0 @@
export class Credentials {
readonly login: string;
readonly password: string;
}

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
/* eslint-disable class-methods-use-this */
/* eslint-disable import/no-extraneous-dependencies */
import { Credentials } from './dto/credentials';
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';
@ -26,10 +26,8 @@ export class LdapController {
@Post('/signin')
async login(@Body() credentials: Credentials, @Res() reply: FastifyReply) {
const { login, password } = credentials;
try {
const token = await this.ldapService.login(login, password);
const token = await this.ldapService.login(credentials);
return reply.setCookie(env.COOKIE_TOKEN_NAME, token, cookieOptions).status(200).send();
} catch {

View File

@ -4,6 +4,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Cache } from 'cache-manager';
import { env } from 'src/config/env';
import type { Credentials } from 'src/dto/credentials';
import * as ldap from 'src/utils/ldap';
@Injectable()
@ -13,7 +14,7 @@ export class LdapService {
private readonly jwtService: JwtService
) {}
public async login(login: string, password: string) {
public async login({ login, password }: Credentials) {
const user = await ldap.authenticate(login, password);
const { username } = user;

View File

@ -1,5 +1,5 @@
export type TokenPayload = {
domain: string;
[key: string]: unknown;
username: string;
};

View File

@ -1,7 +1,9 @@
/* eslint-disable import/no-duplicates */
/* eslint-disable unicorn/prefer-top-level-await */
import { AppModule } from './app.module';
import { env } from './config/env';
import { fastifyCookie } from '@fastify/cookie';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
import { FastifyAdapter } from '@nestjs/platform-fastify';
@ -18,6 +20,8 @@ async function bootstrap() {
secret: env.API_SECRET,
});
app.useGlobalPipes(new ValidationPipe({ stopAtFirstError: true }));
await app.listen(env.API_PORT, '0.0.0.0');
}

View File

@ -1,5 +1,14 @@
import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
readonly username: string;
@IsString()
@MinLength(10)
@IsOptional()
readonly password: string;
readonly [key: string]: unknown;
}

View File

@ -3,30 +3,82 @@
/* eslint-disable import/no-extraneous-dependencies */
import { CreateUserDto } from './dto/create-user.dto';
import { UsersService } from './users.service';
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { User } from 'src/schemas/user.schema';
import {
Body,
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Param,
Post,
Req,
Res,
} from '@nestjs/common';
import { FastifyReply, FastifyRequest } from 'fastify';
import { cookieOptions } from 'src/config/cookie';
import { env } from 'src/config/env';
import { Credentials } from 'src/dto/credentials';
@Controller()
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post('/users/create-user')
async create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
private clearCookies(req, reply) {
if (req.cookies) {
Object.keys(req.cookies).forEach((cookieName) => {
reply.clearCookie(cookieName, {
path: '/',
});
});
}
}
@Get('/users')
@Post('/create')
async create(@Body() createUserDto: CreateUserDto, @Res() reply: FastifyReply) {
try {
const createdUser = await this.usersService.create(createUserDto);
return reply.send(createdUser);
} catch (error) {
throw new HttpException(error, HttpStatus.BAD_REQUEST);
}
}
@Get()
async findAll() {
return this.usersService.findAll();
}
@Delete('/users/delete/:username')
async delete(@Param('username') username: string) {
@Delete('/delete/:login')
async delete(@Param('login') username: CreateUserDto['username']) {
return this.usersService.delete(username);
}
@Post('/users/check')
async check(@Body() user: User) {
return this.usersService.check(user);
@Post('/signin')
async login(@Body() credentials: Credentials, @Res() reply: FastifyReply) {
try {
const token = await this.usersService.login(credentials);
return reply.setCookie(env.COOKIE_TOKEN_NAME, token, cookieOptions).status(200).send();
} catch {
throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
}
}
@Get('/logout')
async logout(@Req() req: FastifyRequest, @Res() reply: FastifyReply) {
this.clearCookies(req, reply);
return reply.status(302).redirect('/login');
}
@Get('/get-user')
async getUser(@Req() req: FastifyRequest, @Res() reply: FastifyReply) {
const token = req.cookies[env.COOKIE_TOKEN_NAME];
const user = await this.usersService.getUser(token);
return reply.send(user);
}
}

View File

@ -1,9 +1,12 @@
import type { CreateUserDto } from './dto/create-user.dto';
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/mongoose';
import * as bcrypt from 'bcrypt';
import { Model } from 'mongoose';
import { omit } from 'radash';
import type { Credentials } from 'src/dto/credentials';
import type { TokenPayload } from 'src/ldap/types/jwt';
import { User } from 'src/schemas/user.schema';
import { generatePassword } from 'src/utils/password';
@ -32,20 +35,29 @@ export class UsersService {
return this.userModel.findOneAndDelete({ username }).exec();
}
public async check({ username, password }: User) {
public async login({ login, password }: Credentials) {
try {
const user = await this.userModel.findOne({ username });
const user = await this.userModel.findOne({ username: login });
if (!user) {
throw new NotFoundException('User not found');
throw new UnauthorizedException('User not found');
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
throw new UnauthorizedException('Invalid login credentials');
}
return passwordMatch;
const payload: TokenPayload = {
username: login,
...omit(user.toJSON(), ['password']),
};
return this.jwtService.sign(payload);
} catch (error) {
throw new UnauthorizedException(error);
}
}
public async getUser(token: string) {
return this.jwtService.decode(token);
}
}

View File

@ -1578,6 +1578,11 @@
dependencies:
"@types/superagent" "*"
"@types/validator@^13.7.10":
version "13.11.5"
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.11.5.tgz#1911964fd5556b08d3479d1ded977c06f89a44a7"
integrity sha512-xW4qsT4UIYILu+7ZrBnfQdBYniZrMLYYK3wN9M/NdeIHgBN5pZI2/8Q7UfdWIcr5RLJv/OGENsx91JIpUUoC7Q==
"@types/webidl-conversions@*":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.2.tgz#d703e2bf61d8b77a7669adcd8fdf98108155d594"
@ -2668,6 +2673,20 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107"
integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==
class-transformer@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336"
integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==
class-validator@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.0.tgz#40ed0ecf3c83b2a8a6a320f4edb607be0f0df159"
integrity sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==
dependencies:
"@types/validator" "^13.7.10"
libphonenumber-js "^1.10.14"
validator "^13.7.0"
clean-regexp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7"
@ -5774,6 +5793,11 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
libphonenumber-js@^1.10.14:
version "1.10.49"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz#c871661c62452348d228c96425f75ddf7e10f05a"
integrity sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ==
light-my-request@5.11.0, light-my-request@^5.9.1:
version "5.11.0"
resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-5.11.0.tgz#90e446c303b3a47b59df38406d5f5c2cf224f2d1"
@ -6829,6 +6853,11 @@ quick-format-unescaped@^4.0.3:
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
radash@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/radash/-/radash-11.0.0.tgz#925698e94b554336fc159c253ac33a9a49881835"
integrity sha512-CRWxTFTDff0IELGJ/zz58yY4BDgyI14qSM5OLNKbCItJrff7m7dXbVF0kWYVCXQtPb3SXIVhXvAImH6eT7VLSg==
rambda@^7.4.0:
version "7.5.0"
resolved "https://registry.yarnpkg.com/rambda/-/rambda-7.5.0.tgz#1865044c59bc0b16f63026c6e5a97e4b1bbe98fe"
@ -8231,6 +8260,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
validator@^13.7.0:
version "13.11.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b"
integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==
value-or-promise@^1.0.11, value-or-promise@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.12.tgz#0e5abfeec70148c78460a849f6b003ea7986f15c"