263 lines
8.6 KiB
TypeScript
263 lines
8.6 KiB
TypeScript
import {Model, Connection, Document} from 'mongoose';
|
||
import {Injectable, NotFoundException, BadGatewayException, ConflictException, BadRequestException, NotAcceptableException} from '@nestjs/common';
|
||
import {InjectConnection} from '@nestjs/mongoose';
|
||
import {DB_NAME, USERS_CONTROLLER, SECRET_JWT_ACCESS_KEY, SECRET_JWT_REFRESH_KEY} from 'src/consts';
|
||
import {User, UserSchema, CreateUserRequest, UserResponse, UserModel, UpdateUserRequest, TokenResponse, UpdateUserSelf} from './users.schema';
|
||
import * as bcrypt from 'bcrypt';
|
||
import * as jwt from 'jsonwebtoken';
|
||
import {Token} from './types';
|
||
|
||
const validateModel = async (user: Document) => {
|
||
try {
|
||
await user.validate();
|
||
} catch (e) {
|
||
throw new BadGatewayException(e);
|
||
}
|
||
};
|
||
|
||
const prepareUserToUserModel = (user: User): UserModel => ({
|
||
login: user.login,
|
||
avatar: user.avatar,
|
||
password: user.password,
|
||
is_admin: user.is_admin,
|
||
});
|
||
|
||
const prepareUserToUserResponse = (user: User): UserResponse => ({
|
||
login: user.login,
|
||
avatar: user.avatar,
|
||
is_admin: user.is_admin,
|
||
});
|
||
|
||
@Injectable()
|
||
export class UserService {
|
||
constructor(
|
||
@InjectConnection(DB_NAME) private dbConnection: Connection,
|
||
) {}
|
||
|
||
userModel(): Model<User> {
|
||
return this.dbConnection.model<User>(USERS_CONTROLLER, UserSchema);
|
||
}
|
||
|
||
async findUser(login: string): Promise<UserModel> {
|
||
const user = await this.userModel().findOne({login});
|
||
return user ? prepareUserToUserModel(user) : user;
|
||
}
|
||
|
||
async findOne(login: string): Promise<UserResponse> {
|
||
const user = await this.userModel().findOne({login});
|
||
if (!user) {
|
||
throw new NotFoundException(`Not found user "${login}"`);
|
||
}
|
||
return prepareUserToUserResponse(user);
|
||
}
|
||
|
||
async findAll(): Promise<UserResponse[]> {
|
||
const users = await this.userModel().find().exec();
|
||
return users.map(prepareUserToUserResponse);
|
||
}
|
||
|
||
async create(user: CreateUserRequest, requesterLogin: string): Promise<UserResponse> {
|
||
const requester = await this.findUser(requesterLogin);
|
||
|
||
if (!requester.is_admin) {
|
||
throw new NotAcceptableException(`Действие запрещено`);
|
||
}
|
||
|
||
const searchUser = await this.findUser(user.login);
|
||
|
||
if (searchUser) {
|
||
throw new ConflictException(`User login "${user.login}" is already in use`);
|
||
}
|
||
|
||
const Model = await this.userModel();
|
||
|
||
try {
|
||
const checkUser = new Model(user);
|
||
await validateModel(checkUser);
|
||
} catch (e) {
|
||
if (e?.message?.includes('validation failed')) {
|
||
throw new BadRequestException(e.message);
|
||
}
|
||
throw e;
|
||
}
|
||
|
||
const salt = await bcrypt.genSalt(10);
|
||
const password = await bcrypt.hash(user.password, salt);
|
||
|
||
const createUser = new Model({
|
||
...user,
|
||
salt,
|
||
password,
|
||
});
|
||
|
||
const savedUser = await createUser.save();
|
||
|
||
return prepareUserToUserResponse(savedUser);
|
||
}
|
||
|
||
async update(user: UpdateUserRequest): Promise<UserResponse> {
|
||
if (user.login === 'admin') {
|
||
throw new BadRequestException('Запрещено менять пользователя admin');
|
||
}
|
||
|
||
const searchUser = await this.userModel().findOne({login: user.login});
|
||
|
||
if (!searchUser) {
|
||
throw new NotFoundException(`Not found user login "${user.login}"`)
|
||
}
|
||
|
||
const Model = await this.userModel();
|
||
const updateUser = new Model({
|
||
...user,
|
||
password: searchUser.password,
|
||
});
|
||
|
||
try {
|
||
await validateModel(updateUser);
|
||
} catch (e) {
|
||
if (e?.message?.includes('validation failed')) {
|
||
throw new BadRequestException(e.message);
|
||
}
|
||
throw e;
|
||
}
|
||
|
||
await searchUser.updateOne({
|
||
is_admin: user.is_admin,
|
||
avatar: user.avatar,
|
||
});
|
||
|
||
return prepareUserToUserResponse(updateUser);
|
||
}
|
||
|
||
async removeOne(login: string, requesterLogin: string): Promise<UserResponse> {
|
||
const requester = await this.findUser(requesterLogin);
|
||
|
||
if (!requester.is_admin) {
|
||
throw new NotAcceptableException(`Действие запрещено`);
|
||
}
|
||
|
||
if (login === 'admin') {
|
||
throw new BadRequestException('Запрещено удалять пользователя admin');
|
||
}
|
||
|
||
const searchUser = await this.userModel().findOne({login});
|
||
|
||
if (!searchUser) {
|
||
throw new NotFoundException(`Not found user login "${login}"`);
|
||
}
|
||
|
||
await this.userModel().deleteOne({login});
|
||
|
||
return prepareUserToUserResponse(searchUser);
|
||
}
|
||
|
||
checkPassword(password: string, hash: string): Promise<boolean> {
|
||
return bcrypt.compare(password, hash);
|
||
}
|
||
|
||
generateTokens(login: string, agent: string): TokenResponse {
|
||
const access_token = jwt.sign({
|
||
login,
|
||
agent,
|
||
iat: Math.floor(Date.now() / 1000),
|
||
exp: Math.floor(Date.now() / 1000) + (60 * 5),
|
||
}, SECRET_JWT_ACCESS_KEY);
|
||
const refresh_token = jwt.sign({
|
||
login,
|
||
agent,
|
||
iat: Math.floor(Date.now() / 1000),
|
||
exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 3),
|
||
}, SECRET_JWT_REFRESH_KEY);
|
||
return {
|
||
access_token,
|
||
refresh_token,
|
||
};
|
||
}
|
||
|
||
async authUser(login: string, password: string, agent: string): Promise<TokenResponse> {
|
||
const searchUser = await this.findUser(login);
|
||
if (searchUser && await this.checkPassword(password, searchUser.password)) {
|
||
return this.generateTokens(login, agent);
|
||
}
|
||
|
||
throw new BadRequestException('Не верный логин или пароль');
|
||
}
|
||
|
||
verifyToken(token: string, secret: string): void {
|
||
try {
|
||
jwt.verify(token, secret);
|
||
} catch (e) {
|
||
throw new BadRequestException('Authorization is outdated');
|
||
}
|
||
}
|
||
|
||
checkToken(token: Token, agent: string): boolean {
|
||
return token.agent === agent;
|
||
}
|
||
|
||
async refreshAuth(refresh_token: string, agent: string): Promise<TokenResponse> {
|
||
this.verifyToken(refresh_token, SECRET_JWT_REFRESH_KEY);
|
||
const token = jwt.decode(refresh_token) as Token;
|
||
const searchUser = await this.findUser(token.login);
|
||
if (searchUser && this.checkToken(token, agent)) {
|
||
return this.generateTokens(token.login, agent);
|
||
}
|
||
throw new BadRequestException('Unauthorized request');
|
||
}
|
||
|
||
async checkAccessToken(access_token: string, agent: string): Promise<boolean> {
|
||
try {
|
||
this.verifyToken(access_token, SECRET_JWT_ACCESS_KEY);
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
|
||
const token = jwt.decode(access_token) as Token;
|
||
const searchUser = await this.findUser(token.login);
|
||
return searchUser && this.checkToken(token, agent);
|
||
}
|
||
|
||
async findMe(access_token: string): Promise<UserResponse> {
|
||
const token = jwt.decode(access_token) as Token;
|
||
return await this.findOne(token.login);
|
||
}
|
||
|
||
async updateUser(user: UpdateUserRequest, requesterLogin: string): Promise<UserResponse> {
|
||
const requester = await this.findUser(requesterLogin);
|
||
|
||
if (!requester.is_admin) {
|
||
throw new NotAcceptableException(`Действие запрещено`);
|
||
}
|
||
|
||
return await this.update(user);
|
||
}
|
||
|
||
async updateSelf(access_token: string, {avatar}: UpdateUserSelf): Promise<UserResponse> {
|
||
const {login} = jwt.decode(access_token) as Token;
|
||
const requester = await this.findUser(login);
|
||
return await this.update({
|
||
avatar,
|
||
login,
|
||
is_admin: requester.is_admin,
|
||
});
|
||
}
|
||
|
||
async changePassword(access_token: string, old_password: string, new_password: string): Promise<string> {
|
||
const {login} = jwt.decode(access_token) as Token;
|
||
if (login === 'admin') {
|
||
throw new BadRequestException('Запрещено менять пароль пользователя admin');
|
||
}
|
||
const user = await this.userModel().findOne({login});
|
||
if (user && await this.checkPassword(old_password, user.password)) {
|
||
const salt = user.salt;
|
||
const password = await bcrypt.hash(new_password, salt);
|
||
await user.updateOne({
|
||
password,
|
||
});
|
||
|
||
return 'ok';
|
||
}
|
||
throw new BadRequestException('Unauthorized request');
|
||
}
|
||
}
|