Compare commits
10 Commits
3883ea4ad8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f49aa39f5 | |||
| bca8c6cb81 | |||
| 33d5e857b4 | |||
| e09b325181 | |||
| 8352024bb5 | |||
| 621b739dc6 | |||
| af7f4f950b | |||
| af8d15cd12 | |||
| d1d67ba27e | |||
| 370787a355 |
19
http/hooks.http
Normal file
19
http/hooks.http
Normal file
@ -0,0 +1,19 @@
|
||||
GET http://localhost:4001/hooks?key=rgfdfff HTTP/1.1
|
||||
|
||||
###
|
||||
POST http://localhost:4001/hooks?key=rgfdfff HTTP/1.1
|
||||
content-type: application/json
|
||||
|
||||
{
|
||||
"holder": "john",
|
||||
"description": "test token",
|
||||
"author": "john smith",
|
||||
"token": "sdf6sdfhs99-sdf",
|
||||
"rights": {
|
||||
"write": true,
|
||||
"read": true
|
||||
}
|
||||
}
|
||||
|
||||
###
|
||||
DELETE http://localhost:4001/hooks?key=rgfdfff&id=5f4a0347c3a82d5a571f058a HTTP/1.1
|
||||
@ -1,45 +1,51 @@
|
||||
###
|
||||
GET http://localhost:4001/store HTTP/1.1
|
||||
Api-Name: store-service-test
|
||||
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InZpZ2Rvcm92IiwiYWdlbnQiOiJ2c2NvZGUtcmVzdGNsaWVudCIsImlhdCI6MTU5OTQ5OTYyMSwiZXhwIjoxNTk5NDk5OTIxfQ.Ym4xOpoe8UvO626Vxp-XqxspoJSL7JpjZ1rlSW0vIWs
|
||||
|
||||
###
|
||||
GET http://localhost:4001/store/testApi-4345345345 HTTP/1.1
|
||||
Api-Name: store-service-test
|
||||
GET http://localhost:4001/store/rgfdfff HTTP/1.1
|
||||
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InZpZ2Rvcm92IiwiYWdlbnQiOiJ2c2NvZGUtcmVzdGNsaWVudCIsImlhdCI6MTU5OTQ5OTYyMSwiZXhwIjoxNTk5NDk5OTIxfQ.Ym4xOpoe8UvO626Vxp-XqxspoJSL7JpjZ1rlSW0vIWs
|
||||
|
||||
###
|
||||
GET http://localhost:4001/store/testAp2?hook=c8d5f28b-4460-47b9-9369-01caf4951846 HTTP/1.1
|
||||
|
||||
###
|
||||
POST http://localhost:4001/store HTTP/1.1
|
||||
content-type: application/json
|
||||
Api-Name: store-service-test
|
||||
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InN0cmluZyIsImFnZW50IjoidnNjb2RlLXJlc3RjbGllbnQiLCJpYXQiOjE1OTY5MDM0NTQsImV4cCI6MTU5NjkwMzc1NH0.Uos0Ei9Fes1euZ72uh_6E9ixG1orBu79bFYjzzmWRR8
|
||||
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InZpZ2Rvcm92IiwiYWdlbnQiOiJ2c2NvZGUtcmVzdGNsaWVudCIsImlhdCI6MTU5OTQ5NzU5NCwiZXhwIjoxNTk5NDk3ODk0fQ.j8_bKayAiHgVLL-SfiHAH4SnijJLi8dfGWlLx30u8Kc
|
||||
|
||||
{
|
||||
"key": "testAp",
|
||||
"key": "testAp2",
|
||||
"value": {
|
||||
|
||||
},
|
||||
"description": "тестовое апи",
|
||||
"service_name": "test-api",
|
||||
"author": "vigdorov"
|
||||
"author": "vigdorov2",
|
||||
"hide": true
|
||||
}
|
||||
|
||||
### Update Request
|
||||
PUT http://localhost:4001/store HTTP/1.1
|
||||
PUT http://localhost:4001/store?hook=c8d5f28b-4460-47b9-9369-01caf4951846 HTTP/1.1
|
||||
content-type: application/json
|
||||
Api-Name: store-service-test
|
||||
|
||||
{
|
||||
"key": "testApi-4234222",
|
||||
"key": "testAp2",
|
||||
"value": {
|
||||
|
||||
"test": 45,
|
||||
"test2": 33,
|
||||
"test3": 13
|
||||
},
|
||||
"description": "тестовое апи22",
|
||||
"service_name": "test-api",
|
||||
"author": "vigdorov23422"
|
||||
"author": "vigdorov23422",
|
||||
"hide": true
|
||||
}
|
||||
|
||||
###
|
||||
DELETE http://localhost:4001/store/testAp HTTP/1.1
|
||||
Api-Name: store-service-test
|
||||
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InZpZ2Rvcm92IiwiYWdlbnQiOiJ2c2NvZGUtcmVzdGNsaWVudCIsImlhdCI6MTU5OTQ5OTA1MywiZXhwIjoxNTk5NDk5MzUzfQ.xBWvh867VmZb2cREvoul5vQwNA5z9nq6bSXx9ZH5lhc
|
||||
|
||||
|
||||
###
|
||||
GET http://localhost:4001/logs/client HTTP/1.1
|
||||
|
||||
@ -9,6 +9,7 @@ import {LogsService} from './logs/logs.service';
|
||||
import {LogsController} from './logs/logs.controller';
|
||||
import {ClientLog, ClientLogSchema, ServerLog, ServerLogSchema} from './logs/logs.schema';
|
||||
import {AuthService} from './services/auth.service';
|
||||
import {HookTonesController} from './store/hookTokens.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -36,6 +37,7 @@ import {AuthService} from './services/auth.service';
|
||||
controllers: [
|
||||
StoreController,
|
||||
LogsController,
|
||||
HookTonesController,
|
||||
],
|
||||
providers: [
|
||||
StoreService,
|
||||
|
||||
@ -3,6 +3,7 @@ export const DB_TEST_NAME = 'store-service-test';
|
||||
export const DB_LOGGER = 'logger';
|
||||
export const MONGO_URL = 'mongodb://localhost:27017';
|
||||
export const COLLECTION_STORE = 'store';
|
||||
export const COLLECTION_HOOKS = 'hooks';
|
||||
export const COLLECTION_LOGS = 'logs';
|
||||
export const LOG_TYPE = {
|
||||
CLIENT: 'client-logs',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import * as mongoose from 'mongoose';
|
||||
import * as lodash from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
|
||||
import {Injectable, NestInterceptor, ExecutionContext, CallHandler} from '@nestjs/common';
|
||||
import {Observable} from 'rxjs';
|
||||
@ -7,8 +8,8 @@ import {tap, catchError} from 'rxjs/operators';
|
||||
import {LOG_TYPE} from 'src/consts';
|
||||
|
||||
const STATUSES = {
|
||||
OK: 'OK',
|
||||
BAD: 'BAD',
|
||||
OK: 'Успех',
|
||||
BAD: 'Ошибка',
|
||||
};
|
||||
|
||||
const errorSchema = new mongoose.Schema({
|
||||
@ -26,7 +27,7 @@ const saveLog = (
|
||||
response: any,
|
||||
startTime: string,
|
||||
) => {
|
||||
const endTime = new Date().toJSON();
|
||||
const endTime = moment().format();
|
||||
const error = new ErrorModel({
|
||||
type,
|
||||
request: request,
|
||||
@ -40,7 +41,7 @@ const saveLog = (
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const start = new Date().toJSON();
|
||||
const start = moment().format();
|
||||
const args = context.getArgs()?.[0] ?? {};
|
||||
const {headers, url, method, body} = args;
|
||||
const request = {
|
||||
@ -50,9 +51,7 @@ export class LoggingInterceptor implements NestInterceptor {
|
||||
.handle()
|
||||
.pipe(
|
||||
tap((response: any) => {
|
||||
// На текущий момент не будем писать успешные логи, пока не реализуем
|
||||
// пагинацию или кеширование или не научим автоматом чистить базу от старых логов
|
||||
// saveLog(STATUSES.OK, request, response, start);
|
||||
saveLog(STATUSES.OK, request, response, start);
|
||||
}),
|
||||
catchError((err: any) => {
|
||||
saveLog(STATUSES.BAD, request, err, start);
|
||||
|
||||
@ -8,7 +8,7 @@ export class AuthService {
|
||||
) {}
|
||||
|
||||
async checkRequest(request: Request): Promise<boolean> {
|
||||
const {data} = await this.http.post('http://api.auth.vigdorov.ru/auth/check', {
|
||||
const {data} = await this.http.post('https://api.auth.vigdorov.ru/auth/check', {
|
||||
access_token: request.headers.authorization,
|
||||
agent: request.headers['user-agent']
|
||||
}).toPromise();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import * as mongoose from 'mongoose';
|
||||
import * as moment from 'moment';
|
||||
|
||||
import {Logger as DefaultLogger} from '@nestjs/common';
|
||||
import {MONGO_URL, LOG_TYPE} from 'src/consts';
|
||||
@ -23,7 +24,7 @@ const errorSchema = new mongoose.Schema({
|
||||
const ErrorModel = mongoose.model(LOG_TYPE.SERVER, errorSchema);
|
||||
|
||||
const saveError = (type: string, message: string, trace = '') => {
|
||||
const date = new Date().toJSON();
|
||||
const date = moment().format();
|
||||
const error = new ErrorModel({
|
||||
type, message, trace, date
|
||||
});
|
||||
|
||||
86
src/store/hookTokens.controller.ts
Normal file
86
src/store/hookTokens.controller.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import {Controller, Get, Header, Req, Post, Delete, HttpCode, Options, UseInterceptors} from '@nestjs/common';
|
||||
import {ApiTags, ApiQuery, ApiBody, ApiResponse, ApiSecurity} from '@nestjs/swagger';
|
||||
import {StoreService} from './store.service';
|
||||
import {AuthService} from 'src/services/auth.service';
|
||||
import {COLLECTION_HOOKS, ALLOW_ORIGIN_ALL, ALLOW_METHOD, ALLOW_CREDENTIALS, CONTENT_LENGTH, ALLOW_HEADERS} from 'src/consts';
|
||||
import {Request} from 'express';
|
||||
import {makeApiHeader} from './utils';
|
||||
import {HookTokenMap, HookTokenResponse} from './store.schema';
|
||||
import {FIND_ALL_SUCCESS, FIND_ALL_NOT_FOUND, CREATE_TOKEN_SUCCESS, CREATE_TOKEN_NOT_VALID, CREATE_TOKEN_CONFLICT, DELETE_TOKEN_SUCCESS, DELETE_TOKEN_NOT_FOUND} from './hookTokens.responses';
|
||||
import {LoggingInterceptor} from 'src/logs/logging.interceptor';
|
||||
|
||||
@ApiSecurity('apiKey')
|
||||
@UseInterceptors(LoggingInterceptor)
|
||||
@Controller(COLLECTION_HOOKS)
|
||||
@ApiTags(COLLECTION_HOOKS)
|
||||
export class HookTonesController {
|
||||
constructor(
|
||||
private readonly storeService: StoreService,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiQuery({
|
||||
name: 'key',
|
||||
description: 'Ключ хранилища'
|
||||
})
|
||||
@ApiResponse(FIND_ALL_SUCCESS)
|
||||
@ApiResponse(FIND_ALL_NOT_FOUND)
|
||||
async findAll(@Req() request: Request<null, null, null, {key: string}>): Promise<HookTokenResponse[]> {
|
||||
await this.authService.checkRequest(request);
|
||||
const api = makeApiHeader(request);
|
||||
const {key} = request?.query ?? {};
|
||||
return await this.storeService.findApiTokens(api, key, request.headers.authorization);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiQuery({
|
||||
name: 'key',
|
||||
description: 'Ключ хранилища'
|
||||
})
|
||||
@ApiBody({
|
||||
type: HookTokenMap,
|
||||
description: 'Объект создания токена'
|
||||
})
|
||||
@ApiResponse(CREATE_TOKEN_SUCCESS)
|
||||
@ApiResponse(CREATE_TOKEN_NOT_VALID)
|
||||
@ApiResponse(CREATE_TOKEN_CONFLICT)
|
||||
async createToken(@Req() request: Request<null, HookTokenMap, null, {key: string}>): Promise<HookTokenResponse> {
|
||||
await this.authService.checkRequest(request);
|
||||
const api = makeApiHeader(request);
|
||||
const {key} = request?.query ?? {};
|
||||
return await this.storeService.createApiToken(api, key, request?.body, request.headers.authorization);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@ApiQuery({
|
||||
name: 'key',
|
||||
description: 'Ключ хранилища'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'id',
|
||||
description: 'Id токена'
|
||||
})
|
||||
@ApiResponse(DELETE_TOKEN_SUCCESS)
|
||||
@ApiResponse(DELETE_TOKEN_NOT_FOUND)
|
||||
async deleteToken(@Req() request: Request<null, null, null, {key: string, id: string}>): Promise<HookTokenResponse> {
|
||||
await this.authService.checkRequest(request);
|
||||
const api = makeApiHeader(request);
|
||||
const {key, id} = request?.query ?? {};
|
||||
return await this.storeService.deleteApiToken(api, key, id, request.headers.authorization);
|
||||
}
|
||||
|
||||
@Options('')
|
||||
@Header(...ALLOW_ORIGIN_ALL)
|
||||
@Header(...ALLOW_METHOD)
|
||||
@Header(...ALLOW_CREDENTIALS)
|
||||
@Header(...CONTENT_LENGTH)
|
||||
@Header(...ALLOW_HEADERS)
|
||||
@HttpCode(204)
|
||||
async optionsKey(): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
56
src/store/hookTokens.responses.ts
Normal file
56
src/store/hookTokens.responses.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {ApiResponseOptions, ApiProperty} from '@nestjs/swagger';
|
||||
import {HookTokenResponse} from './store.schema';
|
||||
|
||||
class Error {
|
||||
@ApiProperty()
|
||||
statusCode: number;
|
||||
|
||||
@ApiProperty()
|
||||
message: string;
|
||||
|
||||
@ApiProperty()
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const FIND_ALL_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Список всех хуков хранилища',
|
||||
type: HookTokenResponse,
|
||||
isArray: true,
|
||||
};
|
||||
|
||||
export const FIND_ALL_NOT_FOUND: ApiResponseOptions = {
|
||||
status: 404,
|
||||
description: 'Ошибка при попытке получить хуки для несуществующего хранилища',
|
||||
type: Error,
|
||||
};
|
||||
|
||||
export const CREATE_TOKEN_SUCCESS: ApiResponseOptions = {
|
||||
status: 201,
|
||||
description: 'Объект созданного хука',
|
||||
type: HookTokenResponse,
|
||||
};
|
||||
|
||||
export const CREATE_TOKEN_NOT_VALID: ApiResponseOptions = {
|
||||
status: 400,
|
||||
description: 'Ошибка при попытке отправить хук с невалидными полями',
|
||||
type: Error,
|
||||
};
|
||||
|
||||
export const CREATE_TOKEN_CONFLICT: ApiResponseOptions = {
|
||||
status: 409,
|
||||
description: 'Ошибка при попытке создать хук с уже имеющимся именем в базе',
|
||||
type: Error,
|
||||
};
|
||||
|
||||
export const DELETE_TOKEN_SUCCESS: ApiResponseOptions = {
|
||||
status: 200,
|
||||
description: 'Удаляет хук из хранилища',
|
||||
type: HookTokenResponse,
|
||||
};
|
||||
|
||||
export const DELETE_TOKEN_NOT_FOUND: ApiResponseOptions = {
|
||||
status: 404,
|
||||
description: 'Ошибка при попытке удалить несуществующий хук или обратится к несуществующем хранилищу',
|
||||
type: Error,
|
||||
};
|
||||
@ -1,9 +1,9 @@
|
||||
import {Controller, Get, Req, Post, Options, Header, Delete, HttpCode, Put, UseInterceptors} from '@nestjs/common';
|
||||
import {StoreService} from './store.service';
|
||||
import {Store, StoreRequest} from './store.schema';
|
||||
import {ApiResponse, ApiTags, ApiParam, ApiBody, ApiBearerAuth, ApiSecurity} from '@nestjs/swagger';
|
||||
import {ApiResponse, ApiTags, ApiParam, ApiBody, ApiBearerAuth, ApiSecurity, ApiQuery} from '@nestjs/swagger';
|
||||
import {ALLOW_ORIGIN_ALL, ALLOW_METHOD, ALLOW_CREDENTIALS, CONTENT_LENGTH, ALLOW_HEADERS, COLLECTION_STORE} from 'src/consts';
|
||||
import {Request} from 'express';
|
||||
import {Request, response} from 'express';
|
||||
import {LoggingInterceptor} from 'src/logs/logging.interceptor';
|
||||
import {
|
||||
FIND_ALL_SUCCESS,
|
||||
@ -19,18 +19,14 @@ import {
|
||||
REMOVE_NOT_FOUND,
|
||||
} from './store.responses';
|
||||
import {AuthService} from 'src/services/auth.service';
|
||||
import {makeApiHeader} from './utils';
|
||||
|
||||
const prepareStoreToStoreRequest = ({
|
||||
key, value, description, service_name, author
|
||||
key, value, description, service_name, author, hide
|
||||
}: Store): StoreRequest => ({
|
||||
key, value, description, service_name, author,
|
||||
key, value, description, service_name, author, hide: !!hide
|
||||
});
|
||||
|
||||
const makeApiHeader = (request: Request): string => {
|
||||
const apiHeader = request.headers?.['api-name'];
|
||||
return typeof apiHeader === 'string' ? apiHeader : '';
|
||||
};
|
||||
|
||||
@ApiSecurity('apiKey')
|
||||
@UseInterceptors(LoggingInterceptor)
|
||||
@Controller(COLLECTION_STORE)
|
||||
@ -49,7 +45,7 @@ export class StoreController {
|
||||
await this.authService.checkRequest(request);
|
||||
|
||||
const api = makeApiHeader(request);
|
||||
const storeList = await this.storeService.findAll(api);
|
||||
const storeList = await this.storeService.findAll(api, request.headers.authorization);
|
||||
return storeList.map(prepareStoreToStoreRequest);
|
||||
}
|
||||
|
||||
@ -61,12 +57,21 @@ export class StoreController {
|
||||
name: 'key',
|
||||
description: 'Ключ для поиска хранилища',
|
||||
})
|
||||
async findOne(@Req() request: Request<{key: string}>): Promise<StoreRequest> {
|
||||
await this.authService.checkRequest(request);
|
||||
|
||||
const {key} = request.params;
|
||||
@ApiQuery({
|
||||
name: 'hook',
|
||||
description: 'Хук для получения доступа без авторизации',
|
||||
required: false,
|
||||
})
|
||||
async findOne(@Req() request: Request<{key: string}, null, null, {hook: string}>): Promise<StoreRequest> {
|
||||
const api = makeApiHeader(request);
|
||||
const store = await this.storeService.findOne(api, key);
|
||||
const {key} = request?.params ?? {};
|
||||
const {hook} = request?.query ?? {};
|
||||
const isActualHook = await this.storeService.checkHook(api, key, hook, ['read']);
|
||||
if (!isActualHook) {
|
||||
await this.authService.checkRequest(request);
|
||||
}
|
||||
|
||||
const store = await this.storeService.findOneStore(api, key, isActualHook, request.headers.authorization);
|
||||
return prepareStoreToStoreRequest(store);
|
||||
}
|
||||
|
||||
@ -79,7 +84,7 @@ export class StoreController {
|
||||
type: StoreRequest,
|
||||
description: 'Объект для создания хранилища'
|
||||
})
|
||||
async create(@Req() request: Request<null, StoreRequest>): Promise<StoreRequest> {
|
||||
async create(@Req() request: Request<null, null, StoreRequest>): Promise<StoreRequest> {
|
||||
const api = makeApiHeader(request);
|
||||
const store = await this.storeService.create(api, request.body, request.headers.authorization);
|
||||
return prepareStoreToStoreRequest(store);
|
||||
@ -94,9 +99,21 @@ export class StoreController {
|
||||
type: StoreRequest,
|
||||
description: 'Объект для обновления хранилища'
|
||||
})
|
||||
async update(@Req() request: Request<null, StoreRequest>): Promise<StoreRequest> {
|
||||
@ApiQuery({
|
||||
name: 'hook',
|
||||
description: 'Хук для получения доступа без авторизации',
|
||||
required: false,
|
||||
})
|
||||
async update(@Req() request: Request<null, null, StoreRequest, {hook: string}>): Promise<StoreRequest> {
|
||||
const api = makeApiHeader(request);
|
||||
const store = await this.storeService.update(api, request.body);
|
||||
const {hook} = request?.query ?? {};
|
||||
const {key} = request?.body;
|
||||
const isActualHook = await this.storeService.checkHook(api, key, hook, ['write', 'read']);
|
||||
if (!isActualHook) {
|
||||
await this.authService.checkRequest(request);
|
||||
}
|
||||
|
||||
const store = await this.storeService.update(api, request.body, isActualHook, request.headers.authorization);
|
||||
return prepareStoreToStoreRequest(store);
|
||||
}
|
||||
|
||||
@ -111,7 +128,7 @@ export class StoreController {
|
||||
async removeOne(@Req() request: Request<{key: string}>): Promise<StoreRequest> {
|
||||
const {key} = request.params;
|
||||
const api = makeApiHeader(request);
|
||||
const store = await this.storeService.removeOne(api, key);
|
||||
const store = await this.storeService.removeOne(api, key, request.headers.authorization);
|
||||
return prepareStoreToStoreRequest(store);
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,50 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
import {ApiProperty} from '@nestjs/swagger';
|
||||
|
||||
class HookRightMap {
|
||||
@ApiProperty()
|
||||
write: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export type RightType = keyof HookRightMap;
|
||||
|
||||
export class HookTokenResponse {
|
||||
@ApiProperty()
|
||||
holder: string;
|
||||
|
||||
@ApiProperty()
|
||||
description: string;
|
||||
|
||||
@ApiProperty()
|
||||
author: string;
|
||||
|
||||
@ApiProperty()
|
||||
rights: HookRightMap;
|
||||
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class HookTokenMap {
|
||||
@ApiProperty()
|
||||
holder: string;
|
||||
|
||||
@ApiProperty()
|
||||
description: string;
|
||||
|
||||
@ApiProperty()
|
||||
author: string;
|
||||
|
||||
@ApiProperty()
|
||||
token: string;
|
||||
|
||||
@ApiProperty()
|
||||
rights: HookRightMap;
|
||||
}
|
||||
|
||||
export class StoreRequest {
|
||||
@ApiProperty()
|
||||
key: string;
|
||||
@ -17,8 +61,47 @@ export class StoreRequest {
|
||||
|
||||
@ApiProperty()
|
||||
author: string;
|
||||
|
||||
@ApiProperty()
|
||||
hide: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
hook_tokens?: HookTokenResponse[];
|
||||
}
|
||||
|
||||
@Schema()
|
||||
export class HookToken extends Document {
|
||||
@Prop({
|
||||
type: String,
|
||||
})
|
||||
holder: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
})
|
||||
description: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
})
|
||||
author: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
})
|
||||
token: string;
|
||||
|
||||
@Prop({
|
||||
type: {
|
||||
write: Boolean,
|
||||
read: Boolean,
|
||||
},
|
||||
})
|
||||
rights: HookRightMap;
|
||||
}
|
||||
|
||||
const HookTokenSchema = SchemaFactory.createForClass(HookToken);
|
||||
|
||||
@Schema()
|
||||
export class Store extends Document {
|
||||
@Prop({
|
||||
@ -53,6 +136,16 @@ export class Store extends Document {
|
||||
type: String,
|
||||
})
|
||||
author: string;
|
||||
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
})
|
||||
hide: boolean;
|
||||
|
||||
@Prop({
|
||||
type: [HookTokenSchema]
|
||||
})
|
||||
hook_tokens: HookToken[];
|
||||
}
|
||||
|
||||
export const StoreSchema = SchemaFactory.createForClass(Store);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import {Model, Connection} from 'mongoose';
|
||||
import {Injectable, NotFoundException, BadGatewayException, ConflictException, BadRequestException, HttpException, HttpService} from '@nestjs/common';
|
||||
import {Injectable, NotFoundException, BadGatewayException, ConflictException, BadRequestException, HttpException, HttpService, UnauthorizedException} from '@nestjs/common';
|
||||
import {InjectConnection} from '@nestjs/mongoose';
|
||||
import {Store, StoreRequest, StoreSchema} from './store.schema';
|
||||
import {Store, StoreRequest, StoreSchema, HookToken, HookTokenResponse, HookTokenMap, RightType} from './store.schema';
|
||||
import {DB_TEST_NAME, DB_NAME, COLLECTION_STORE} from 'src/consts';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {access} from 'fs';
|
||||
|
||||
interface Token {
|
||||
login: string;
|
||||
@ -12,6 +13,30 @@ interface Token {
|
||||
exp: number;
|
||||
}
|
||||
|
||||
interface User {
|
||||
login: string;
|
||||
avatar: string;
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
const prepareHook = (baseHook: HookToken): HookTokenResponse => ({
|
||||
holder: baseHook.holder,
|
||||
description: baseHook.description,
|
||||
author: baseHook.author,
|
||||
rights: baseHook.rights,
|
||||
id: baseHook._id,
|
||||
});
|
||||
|
||||
const prepareStore = (baseStore: Store): StoreRequest => ({
|
||||
key: baseStore.key,
|
||||
value: baseStore.value,
|
||||
description: baseStore.description,
|
||||
author: baseStore.author,
|
||||
service_name: baseStore.service_name,
|
||||
hook_tokens: baseStore.hook_tokens?.map(prepareHook),
|
||||
hide: !!baseStore.hide,
|
||||
})
|
||||
|
||||
const validateModel = async (store: Store) => {
|
||||
try {
|
||||
await store.validate();
|
||||
@ -35,8 +60,11 @@ export class StoreService {
|
||||
return this.dbConnection.model<Store>(COLLECTION_STORE, StoreSchema);
|
||||
}
|
||||
|
||||
async findAll(api: string): Promise<Store[]> {
|
||||
return this.storeModel(api).find().exec();
|
||||
async findAll(api: string, access_token: string): Promise<Store[]> {
|
||||
const user = await this.loadUserByToken(access_token);
|
||||
return (await this.storeModel(api).find().exec()).filter(store => {
|
||||
return user.is_admin || store.author === user.login || !store.hide;
|
||||
});
|
||||
}
|
||||
|
||||
async create(api: string, store: StoreRequest, access_token: string): Promise<Store> {
|
||||
@ -48,16 +76,17 @@ export class StoreService {
|
||||
|
||||
const {login, agent} = jwt.decode(access_token) as Token;
|
||||
|
||||
const apiPath = 'http://api.auth.vigdorov.ru/users/search/';
|
||||
const apiPath = 'https://api.auth.vigdorov.ru/users/search/';
|
||||
const headers = {
|
||||
Authorization: access_token,
|
||||
'user-agent': agent
|
||||
};
|
||||
|
||||
try {
|
||||
const {data: currentUser} = await this.http.get(`${apiPath}${login}`, {headers}).toPromise();
|
||||
await this.http.get(`${apiPath}${store.author}`, {headers}).toPromise();
|
||||
const author = currentUser.is_admin ? store.author : login;
|
||||
const user = await this.loadUserByToken(access_token);
|
||||
const author = user.is_admin ? store.author : login;
|
||||
await this.http.get(`${apiPath}${author}`, {headers}).toPromise();
|
||||
|
||||
const createdStore = new (this.storeModel(api))({
|
||||
...store,
|
||||
author,
|
||||
@ -86,10 +115,37 @@ export class StoreService {
|
||||
}
|
||||
}
|
||||
|
||||
async update(api: string, {author, ...omitProps}: StoreRequest): Promise<Store> {
|
||||
async loadUserByToken(access_token: string): Promise<User> {
|
||||
const {login, agent} = jwt.decode(access_token) as Token;
|
||||
const apiPath = 'https://api.auth.vigdorov.ru/users/search/';
|
||||
const headers = {
|
||||
Authorization: access_token,
|
||||
'user-agent': agent
|
||||
};
|
||||
try {
|
||||
const {data: currentUser} = await this.http.get<User>(`${apiPath}${login}`, {headers}).toPromise();
|
||||
return currentUser;
|
||||
} catch (e) {
|
||||
if (e?.response?.status === 401) {
|
||||
throw new UnauthorizedException('Доступ запрещен');
|
||||
}
|
||||
throw new BadRequestException(e.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async update(api: string, {author, ...omitProps}: StoreRequest, isActualHook: boolean, access_token: string): Promise<Store> {
|
||||
const searchStore = await this.findOne(api, omitProps.key);
|
||||
|
||||
if (searchStore) {
|
||||
if (!isActualHook) {
|
||||
const user = await this.loadUserByToken(access_token);
|
||||
|
||||
if (!user.is_admin && user.login !== searchStore.author) {
|
||||
throw new NotFoundException(`Not found api key "${omitProps.key}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const store = {
|
||||
...omitProps,
|
||||
author: searchStore.author,
|
||||
@ -123,10 +179,28 @@ export class StoreService {
|
||||
return searchStore;
|
||||
}
|
||||
|
||||
async removeOne(api: string, key: string): Promise<Store> {
|
||||
async findOneStore(api: string, key: string, isActualHook: boolean, access_token: string): Promise<Store> {
|
||||
const store = await this.findOne(api, key);
|
||||
if (isActualHook) {
|
||||
return store;
|
||||
}
|
||||
const user = await this.loadUserByToken(access_token);
|
||||
if (user.is_admin || user.login === store.author || !store.hide) {
|
||||
return store;
|
||||
}
|
||||
throw new NotFoundException(`Not found api key "${key}"`);
|
||||
}
|
||||
|
||||
async removeOne(api: string, key: string, access_token: string): Promise<Store> {
|
||||
const searchStore = await this.findOne(api, key);
|
||||
|
||||
if (searchStore) {
|
||||
const user = await this.loadUserByToken(access_token);
|
||||
|
||||
if (!user.is_admin && user.login !== searchStore.author) {
|
||||
throw new NotFoundException(`Not found api key "${key}"`);
|
||||
}
|
||||
|
||||
await this.storeModel(api).deleteOne({key});
|
||||
|
||||
return searchStore;
|
||||
@ -134,4 +208,100 @@ export class StoreService {
|
||||
|
||||
throw new NotFoundException(`Not found api key "${key}"`);
|
||||
}
|
||||
|
||||
async findApiTokens(api: string, key: string, access_token: string): Promise<HookTokenResponse[]> {
|
||||
const searchStore = await this.storeModel(api).findOne({key});
|
||||
if (searchStore) {
|
||||
const user = await this.loadUserByToken(access_token);
|
||||
if (user.is_admin || user.login === searchStore.author) {
|
||||
return searchStore.hook_tokens?.map(prepareHook);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
throw new NotFoundException(`Not found api key "${key}"`);
|
||||
}
|
||||
|
||||
async createApiToken(api: string, key: string, hook: HookTokenMap, access_token: string): Promise<HookTokenResponse> {
|
||||
const searchStore = await this.storeModel(api).findOne({key});
|
||||
|
||||
if (!searchStore) {
|
||||
throw new NotFoundException(`Not found api key "${key}"`);
|
||||
}
|
||||
|
||||
const user = await this.loadUserByToken(access_token);
|
||||
|
||||
if (!user.is_admin && user.login !== searchStore.author) {
|
||||
throw new BadRequestException(`You don't have access to "${key}"`);
|
||||
}
|
||||
|
||||
const store = {
|
||||
...prepareStore(searchStore),
|
||||
hook_tokens: (searchStore.hook_tokens || []).map(baseHook => {
|
||||
const {id, ...omitHook} = prepareHook(baseHook);
|
||||
return omitHook;
|
||||
}).concat(hook),
|
||||
};
|
||||
|
||||
const updateStore = new (this.storeModel(api))(store);
|
||||
|
||||
try {
|
||||
await validateModel(updateStore);
|
||||
} catch (e) {
|
||||
if (e?.message?.includes('validation failed')) {
|
||||
throw new BadRequestException(e.message);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
await searchStore.updateOne(store);
|
||||
|
||||
const updatedStore = await this.storeModel(api).findOne({key});
|
||||
const oldTokenIds = searchStore.hook_tokens.map(hook => hook._id);
|
||||
const newToken = updatedStore.hook_tokens.find(token => !oldTokenIds.includes(token));
|
||||
|
||||
return prepareHook(newToken);
|
||||
}
|
||||
|
||||
async deleteApiToken(api: string, key: string, id: string, access_token: string): Promise<HookTokenResponse> {
|
||||
const searchStore = await this.storeModel(api).findOne({key});
|
||||
|
||||
if (!searchStore) {
|
||||
throw new NotFoundException(`Not found api key "${key}"`);
|
||||
}
|
||||
|
||||
const user = await this.loadUserByToken(access_token);
|
||||
|
||||
if (!user.is_admin && user.login !== searchStore.author) {
|
||||
throw new BadRequestException(`You don't have access to "${key}"`);
|
||||
}
|
||||
|
||||
const deleteToken = searchStore.hook_tokens?.find(token => token._id.toString() === id);
|
||||
|
||||
if (!deleteToken) {
|
||||
throw new NotFoundException(`Not found token id "${id}" in api key "${key}"`);
|
||||
}
|
||||
|
||||
const store = {
|
||||
...prepareStore(searchStore),
|
||||
hook_tokens: (searchStore.hook_tokens || []).filter(token => token._id.toString() !== id),
|
||||
};
|
||||
|
||||
await searchStore.updateOne(store);
|
||||
|
||||
return prepareHook(deleteToken);
|
||||
}
|
||||
|
||||
async checkHook(api: string, key: string, hook: string, checkObject: RightType[]): Promise<boolean> {
|
||||
const searchStore = await this.storeModel(api).findOne({key});
|
||||
|
||||
if (!searchStore) {
|
||||
throw new NotFoundException(`Not found api key "${key}"`);
|
||||
}
|
||||
|
||||
const token = searchStore.hook_tokens?.find(baseToken => baseToken.token === hook);
|
||||
|
||||
const available = checkObject.every(right => token?.rights[right]);
|
||||
|
||||
return !!token && available;
|
||||
}
|
||||
}
|
||||
8
src/store/utils.ts
Normal file
8
src/store/utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {Request} from 'express';
|
||||
|
||||
const apiName = 'api-name' as const;
|
||||
|
||||
export const makeApiHeader = (request: Request): string => {
|
||||
const apiHeader = request.headers?.[apiName];
|
||||
return typeof apiHeader === 'string' ? apiHeader : '';
|
||||
};
|
||||
Reference in New Issue
Block a user