Compare commits
10 Commits
1bb5bd88bc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| de0fc07b5e | |||
| cb09e7839e | |||
| 5dd2646d1a | |||
| 6e3f003cd4 | |||
| a33d901c39 | |||
| 850cfb7ec0 | |||
| f0100ba084 | |||
| 221368874e | |||
| d4f6032232 | |||
| 96d3232f9f |
@ -1,5 +1,7 @@
|
||||
import {Injectable, UnauthorizedException, HttpService} from '@nestjs/common';
|
||||
import {Request} from 'express';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {Token} from 'src/users/types';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -7,8 +9,8 @@ export class AuthService {
|
||||
private http: HttpService
|
||||
) {}
|
||||
|
||||
async checkRequest(request: Request): Promise<boolean> {
|
||||
const {data} = await this.http.post('http://api.auth.vigdorov.ru/auth/check', {
|
||||
async checkRequest(request: Request): Promise<Token> {
|
||||
const {data} = await this.http.post('https://api.auth.vigdorov.ru/auth/check', {
|
||||
access_token: request.headers.authorization,
|
||||
agent: request.headers['user-agent']
|
||||
}).toPromise();
|
||||
@ -16,6 +18,7 @@ export class AuthService {
|
||||
if (!data) {
|
||||
throw new UnauthorizedException('Доступ запрещен');
|
||||
}
|
||||
return data;
|
||||
|
||||
return jwt.decode(request.headers.authorization) as Token;
|
||||
}
|
||||
}
|
||||
|
||||
33
src/main.ts
33
src/main.ts
@ -1,12 +1,43 @@
|
||||
import {NestFactory} from '@nestjs/core';
|
||||
import {AppModule} from './app.module';
|
||||
import {SwaggerModule, DocumentBuilder} from '@nestjs/swagger';
|
||||
import {USERS_CONTROLLER, AUTH_CONTROLLER} from './consts';
|
||||
import {USERS_CONTROLLER, AUTH_CONTROLLER, MONGO_URL, DB_NAME} from './consts';
|
||||
import * as mongoose from 'mongoose';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
const initAdmin = async () => {
|
||||
mongoose.connect(`${MONGO_URL}/${DB_NAME}`, {useNewUrlParser: true});
|
||||
const schema = new mongoose.Schema({
|
||||
login: String,
|
||||
avatar: String,
|
||||
password: String,
|
||||
is_admin: Boolean,
|
||||
});
|
||||
|
||||
const Model = mongoose.model('users', schema);
|
||||
await Model.deleteOne({login: 'admin'});
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const password = await bcrypt.hash('Monawko900', salt);
|
||||
const admin = new Model({
|
||||
login: 'admin',
|
||||
avatar: 'https://s.starladder.com/uploads/team_logo/4/3/5/e/meta_tag_7e51261a8844f9636aec079a0cab756f.png',
|
||||
password,
|
||||
is_admin: true,
|
||||
});
|
||||
admin.save();
|
||||
};
|
||||
initAdmin();
|
||||
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const options = new DocumentBuilder()
|
||||
.addSecurity('apiKey', {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'Authorization',
|
||||
})
|
||||
.setTitle('Auth API')
|
||||
.setDescription('API для авторизации приложений и работы с пользователями')
|
||||
.setVersion('1.0.0')
|
||||
|
||||
6
src/users/types.ts
Normal file
6
src/users/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Token {
|
||||
login: string;
|
||||
agent: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
@ -1,37 +1,43 @@
|
||||
import {Controller, Get, Req, Post, Options, Header, Delete, HttpCode, Put} from '@nestjs/common';
|
||||
|
||||
import { ApiResponse, ApiTags, ApiParam, ApiBody } from '@nestjs/swagger';
|
||||
import {ApiResponse, ApiTags, ApiParam, ApiBody, ApiSecurity} from '@nestjs/swagger';
|
||||
import {
|
||||
ALLOW_ORIGIN_ALL,
|
||||
ALLOW_METHOD,
|
||||
ALLOW_CREDENTIALS,
|
||||
CONTENT_LENGTH,
|
||||
ALLOW_HEADERS,
|
||||
USERS_CONTROLLER,
|
||||
ALLOW_ORIGIN_ALL,
|
||||
ALLOW_METHOD,
|
||||
ALLOW_CREDENTIALS,
|
||||
CONTENT_LENGTH,
|
||||
ALLOW_HEADERS,
|
||||
USERS_CONTROLLER,
|
||||
} from '../consts';
|
||||
import { UserService } from './users.service';
|
||||
import {UserService} from './users.service';
|
||||
import {
|
||||
UserResponse,
|
||||
CreateUserRequest,
|
||||
UpdateUserRequest,
|
||||
UpdateUserSelf,
|
||||
UserResponse,
|
||||
CreateUserRequest,
|
||||
UpdateUserRequest,
|
||||
UpdateUserSelf,
|
||||
ChangePasswordRequest,
|
||||
} from './users.schema';
|
||||
import { Request } from 'express';
|
||||
import {Request} from 'express';
|
||||
import {
|
||||
FIND_ALL_SUCCESS,
|
||||
FIND_ONE_SUCCESS,
|
||||
FIND_ONE_NOT_FOUND,
|
||||
CREATE_SUCCESS,
|
||||
CREATE_CONFLICT,
|
||||
CREATE_NOT_VALID,
|
||||
UPDATE_SUCCESS,
|
||||
UPDATE_NOT_FOUND,
|
||||
UPDATE_NOT_VALID,
|
||||
REMOVE_SUCCESS,
|
||||
REMOVE_NOT_FOUND,
|
||||
FIND_ALL_SUCCESS,
|
||||
FIND_ONE_SUCCESS,
|
||||
FIND_ONE_NOT_FOUND,
|
||||
CREATE_SUCCESS,
|
||||
CREATE_CONFLICT,
|
||||
CREATE_NOT_VALID,
|
||||
UPDATE_SUCCESS,
|
||||
UPDATE_NOT_FOUND,
|
||||
UPDATE_NOT_VALID,
|
||||
REMOVE_SUCCESS,
|
||||
REMOVE_NOT_FOUND,
|
||||
EDIT_ME_SUCCESS,
|
||||
EDIT_ME_NOT_VALID,
|
||||
CHANGE_PASSWORD_SUCCESS,
|
||||
CHANGE_PASSWORD_NOT_VALID,
|
||||
} from './users.responses';
|
||||
import {AuthService} from 'src/auth/auth.service';
|
||||
|
||||
@ApiSecurity('apiKey')
|
||||
@Controller(USERS_CONTROLLER)
|
||||
@ApiTags(USERS_CONTROLLER)
|
||||
export class UsersController {
|
||||
@ -65,6 +71,7 @@ export class UsersController {
|
||||
|
||||
@Get('me')
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(FIND_ONE_SUCCESS)
|
||||
async findMe(@Req() request: Request): Promise<UserResponse> {
|
||||
await this.authService.checkRequest(request);
|
||||
|
||||
@ -73,6 +80,12 @@ export class UsersController {
|
||||
|
||||
@Post('edit-me')
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(EDIT_ME_SUCCESS)
|
||||
@ApiResponse(EDIT_ME_NOT_VALID)
|
||||
@ApiBody({
|
||||
type: UpdateUserSelf,
|
||||
description: 'Объект обновления пользователя',
|
||||
})
|
||||
async findEdit(@Req() request: Request<null, UpdateUserSelf>): Promise<UserResponse> {
|
||||
await this.authService.checkRequest(request);
|
||||
|
||||
@ -89,9 +102,9 @@ export class UsersController {
|
||||
description: 'Объект для создания пользователя'
|
||||
})
|
||||
async createUser(@Req() request: Request<null, CreateUserRequest>): Promise<UserResponse> {
|
||||
await this.authService.checkRequest(request);
|
||||
const {login} = await this.authService.checkRequest(request);
|
||||
|
||||
return await this.userService.create(request.body);
|
||||
return await this.userService.create(request.body, login);
|
||||
}
|
||||
|
||||
@Put()
|
||||
@ -104,9 +117,9 @@ export class UsersController {
|
||||
description: 'Объект обновления данных пользователя'
|
||||
})
|
||||
async updateUser(@Req() request: Request<null, UpdateUserRequest>): Promise<UserResponse> {
|
||||
await this.authService.checkRequest(request);
|
||||
const {login} = await this.authService.checkRequest(request);
|
||||
|
||||
return await this.userService.update(request.body);
|
||||
return await this.userService.updateUser(request.body, login);
|
||||
}
|
||||
|
||||
@Delete(':login')
|
||||
@ -118,9 +131,24 @@ export class UsersController {
|
||||
description: 'Логин пользователя',
|
||||
})
|
||||
async removeUser(@Req() request: Request<{login: string}>): Promise<UpdateUserRequest> {
|
||||
const {login} = await this.authService.checkRequest(request);
|
||||
|
||||
return await this.userService.removeOne(request.params.login, login);
|
||||
}
|
||||
|
||||
@Post('change-password')
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiResponse(CHANGE_PASSWORD_SUCCESS)
|
||||
@ApiResponse(CHANGE_PASSWORD_NOT_VALID)
|
||||
@ApiBody({
|
||||
type: ChangePasswordRequest,
|
||||
description: 'Объект изменения пароля',
|
||||
})
|
||||
async changePassword(@Req() request: Request<null, {old_password: string, new_password: string}>): Promise<string> {
|
||||
await this.authService.checkRequest(request);
|
||||
|
||||
return await this.userService.removeOne(request.params.login);
|
||||
const {headers, body} = request;
|
||||
return await this.userService.changePassword(headers.authorization, body.old_password, body.new_password);
|
||||
}
|
||||
|
||||
@Options([
|
||||
|
||||
@ -39,13 +39,13 @@ export const CREATE_SUCCESS: ApiResponseOptions = {
|
||||
|
||||
export const CREATE_CONFLICT: ApiResponseOptions = {
|
||||
status: 409,
|
||||
description: 'Объект вновь созданного пользователя',
|
||||
description: 'Ошибка при попытке создания пользователя с уже существующим логином',
|
||||
type: Error,
|
||||
};
|
||||
|
||||
export const CREATE_NOT_VALID: ApiResponseOptions = {
|
||||
status: 400,
|
||||
description: 'Ошибка при попытке создания пользователя с уже существующим логином',
|
||||
description: 'Ошибка при попытке создания пользователя с не валидными полями',
|
||||
type: Error,
|
||||
};
|
||||
|
||||
@ -78,3 +78,33 @@ export const REMOVE_NOT_FOUND: ApiResponseOptions = {
|
||||
description: 'Ошибка при попытке удалить пользователя с несуществующим логином',
|
||||
type: Error,
|
||||
};
|
||||
|
||||
export const FIND_ME_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Объект пользователя, который сделал запрос',
|
||||
type: UserResponse,
|
||||
};
|
||||
|
||||
export const EDIT_ME_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Обновленный объект пользователя, который сделал запрос',
|
||||
type: UserResponse,
|
||||
};
|
||||
|
||||
export const EDIT_ME_NOT_VALID: ApiResponseOptions = {
|
||||
status: 400,
|
||||
description: 'Ошибка при попытке обновить пользователя с невалидными полями',
|
||||
type: Error,
|
||||
};
|
||||
|
||||
export const CHANGE_PASSWORD_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Возвращает "ok" при успешном изменении пароля',
|
||||
type: String,
|
||||
};
|
||||
|
||||
export const CHANGE_PASSWORD_NOT_VALID: ApiResponseOptions = {
|
||||
status: 400,
|
||||
description: 'Ошибка при не верном вводе старого пароля',
|
||||
type: Error,
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import {Prop, Schema, SchemaFactory} from '@nestjs/mongoose';
|
||||
import {ApiProperty} from '@nestjs/swagger';
|
||||
import { Document } from 'mongoose';
|
||||
import {Document} from 'mongoose';
|
||||
|
||||
export class CreateUserRequest {
|
||||
@ApiProperty()
|
||||
@ -19,6 +19,9 @@ export class UpdateUserRequest {
|
||||
|
||||
@ApiProperty()
|
||||
avatar: string;
|
||||
|
||||
@ApiProperty()
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
export class UpdateUserSelf {
|
||||
@ -67,6 +70,14 @@ export class CheckAuthTokenRequest {
|
||||
agent: string;
|
||||
}
|
||||
|
||||
export class ChangePasswordRequest {
|
||||
@ApiProperty()
|
||||
old_password: string;
|
||||
|
||||
@ApiProperty()
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export class RefreshAuthRequest {
|
||||
@ApiProperty()
|
||||
refresh_token: string;
|
||||
@ -107,6 +118,11 @@ export class User extends Document {
|
||||
type: Boolean,
|
||||
})
|
||||
is_admin: boolean;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
})
|
||||
salt: string;
|
||||
}
|
||||
|
||||
@Schema()
|
||||
|
||||
@ -1,17 +1,11 @@
|
||||
import {Model, Connection, Document} from 'mongoose';
|
||||
import {Injectable, NotFoundException, BadGatewayException, ConflictException, BadRequestException} from '@nestjs/common';
|
||||
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';
|
||||
|
||||
interface Token {
|
||||
login: string;
|
||||
agent: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
import {Token} from './types';
|
||||
|
||||
const validateModel = async (user: Document) => {
|
||||
try {
|
||||
@ -62,7 +56,13 @@ export class UserService {
|
||||
return users.map(prepareUserToUserResponse);
|
||||
}
|
||||
|
||||
async create(user: CreateUserRequest): Promise<UserResponse> {
|
||||
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) {
|
||||
@ -96,6 +96,10 @@ export class UserService {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -118,15 +122,24 @@ export class UserService {
|
||||
}
|
||||
|
||||
await searchUser.updateOne({
|
||||
...{
|
||||
avatar: user.avatar,
|
||||
},
|
||||
is_admin: user.is_admin,
|
||||
avatar: user.avatar,
|
||||
});
|
||||
|
||||
return prepareUserToUserResponse(updateUser);
|
||||
}
|
||||
|
||||
async removeOne(login: string): Promise<UserResponse> {
|
||||
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) {
|
||||
@ -147,13 +160,13 @@ export class UserService {
|
||||
login,
|
||||
agent,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (20), // секунды * минуты // выставить 60 * 2
|
||||
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 * 5), // секунды * минуты * часы * дни // потом выставить 60 * 60 * 24 * 1
|
||||
exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 3),
|
||||
}, SECRET_JWT_REFRESH_KEY);
|
||||
return {
|
||||
access_token,
|
||||
@ -167,7 +180,7 @@ export class UserService {
|
||||
return this.generateTokens(login, agent);
|
||||
}
|
||||
|
||||
throw new BadRequestException('Не верный логин или пароль');
|
||||
throw new BadRequestException('Неверный логин или пароль');
|
||||
}
|
||||
|
||||
verifyToken(token: string, secret: string): void {
|
||||
@ -198,7 +211,7 @@ export class UserService {
|
||||
} 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);
|
||||
@ -209,11 +222,44 @@ export class UserService {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
14
users.http
14
users.http
@ -9,7 +9,7 @@ POST http://localhost:4002/users HTTP/1.1
|
||||
content-type: application/json
|
||||
|
||||
{
|
||||
"login": "gfhHfgDHDU89",
|
||||
"login": "string",
|
||||
"avatar": "string",
|
||||
"password": "string"
|
||||
}
|
||||
@ -62,4 +62,14 @@ Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InN0cmluZyIsImFn
|
||||
|
||||
{
|
||||
"avatar": "hui"
|
||||
}
|
||||
}
|
||||
|
||||
###
|
||||
POST http://localhost:4002/users/change-password HTTP/1.1
|
||||
content-type: application/json
|
||||
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InN0cmluZyIsImFnZW50IjoidnNjb2RlLXJlc3RjbGllbnQiLCJpYXQiOjE1OTY4OTE3NjIsImV4cCI6MTU5Njg5MTc4Mn0.u_sYoVdCPjioimDZ-m7j3wAvgvaiw-pAl-OL5ei87K8
|
||||
|
||||
{
|
||||
"old_password": "string32",
|
||||
"new_password": "string"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user