266 lines
8.7 KiB
TypeScript
266 lines
8.7 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 (!await this.checkPassword(old_password, user.password)) {
|
|
throw new BadRequestException('Неверный старый пароль');
|
|
}
|
|
if (user) {
|
|
const salt = user.salt;
|
|
const password = await bcrypt.hash(new_password, salt);
|
|
await user.updateOne({
|
|
password,
|
|
});
|
|
|
|
return 'ok';
|
|
}
|
|
throw new BadRequestException('Unauthorized request');
|
|
}
|
|
}
|