feat: permissions & account paginations
This commit is contained in:
parent
6fca987edf
commit
ff74d5ca74
@ -12,12 +12,3 @@ DB_PORT=27017
|
|||||||
DB_USERNAME=test
|
DB_USERNAME=test
|
||||||
DB_PASSWORD=test
|
DB_PASSWORD=test
|
||||||
DB_NAME=admin
|
DB_NAME=admin
|
||||||
|
|
||||||
# Other
|
|
||||||
PUBLIC_ACCESS_SECRET_KEY= |
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLW1tlHyKC9AG0hGpmkksET2DE
|
|
||||||
r7ojSPemxFWAgFgcPJWQ7x3uNbsdJ3bIZFoA/FClaWKMCZmjnH9tv0bKZtY/CDhM
|
|
||||||
ZEyHpMruRSn6IKrxjtQZWy4uv/w6MzUeyBYG0OvNCiYpdvz5SkAGAUHD5ZNFqn2w
|
|
||||||
KKFD0I2Dr59BFVSGJwIDAQAB
|
|
||||||
-----END PUBLIC KEY-----
|
|
@ -2,37 +2,38 @@ version: "3.3"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
admin:
|
admin:
|
||||||
|
container_name: hub-admin-backend-service
|
||||||
|
tty: true
|
||||||
build:
|
build:
|
||||||
context: ../../.
|
context: ../../.
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- DB_HOST=10.6.0.11
|
- DB_HOST=admin-mongo
|
||||||
- DB_PORT=27017
|
- DB_PORT=27017
|
||||||
- ENVIRONMENT=staging
|
- ENVIRONMENT=staging
|
||||||
- HTTP_HOST=0.0.0.0
|
- HTTP_HOST=0.0.0.0
|
||||||
- HTTP_PORT=8005
|
- HTTP_PORT=8005
|
||||||
- AUTH_SERVICE_HOST=http://pena-auth-service
|
- AUTH_SERVICE_HOST=http://auth
|
||||||
- AUTH_SERVICE_PORT=8000
|
- AUTH_SERVICE_PORT=8000
|
||||||
- DB_USERNAME=test
|
- DB_USERNAME=test
|
||||||
- DB_PASSWORD=test
|
- DB_PASSWORD=test
|
||||||
- DB_NAME=admin
|
- DB_NAME=admin
|
||||||
- PUBLIC_ACCESS_SECRET_KEY= |
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLW1tlHyKC9AG0hGpmkksET2DE
|
|
||||||
r7ojSPemxFWAgFgcPJWQ7x3uNbsdJ3bIZFoA/FClaWKMCZmjnH9tv0bKZtY/CDhM
|
|
||||||
ZEyHpMruRSn6IKrxjtQZWy4uv/w6MzUeyBYG0OvNCiYpdvz5SkAGAUHD5ZNFqn2w
|
|
||||||
KKFD0I2Dr59BFVSGJwIDAQAB
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
networks:
|
networks:
|
||||||
- dev
|
- dev
|
||||||
|
depends_on:
|
||||||
|
- admin-mongo
|
||||||
|
ports:
|
||||||
|
- 8005:8005
|
||||||
|
|
||||||
mongo:
|
admin-mongo:
|
||||||
image: "mongo:6.0.3"
|
image: "mongo:6.0.3"
|
||||||
environment:
|
environment:
|
||||||
MONGO_INITDB_ROOT_USERNAME: test
|
MONGO_INITDB_ROOT_USERNAME: test
|
||||||
MONGO_INITDB_ROOT_PASSWORD: test
|
MONGO_INITDB_ROOT_PASSWORD: test
|
||||||
ports:
|
ports:
|
||||||
- "27017:27017"
|
- 27017:27017
|
||||||
networks:
|
networks:
|
||||||
- dev
|
- dev
|
||||||
|
|
||||||
|
11
package.json
11
package.json
@ -13,8 +13,8 @@
|
|||||||
"test": "jest --coverage",
|
"test": "jest --coverage",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
"build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||||
"compose:start:dev": "docker-compose -f deployments/dev/docker-compose.yaml up -d",
|
"compose:dev:start": "docker-compose -f deployments/dev/docker-compose.yaml up -d",
|
||||||
"compose:stop:dev": "docker-compose -f deployments/dev/docker-compose.yaml down --volumes --rmi local",
|
"compose:dev:stop": "docker-compose -f deployments/dev/docker-compose.yaml down --volumes --rmi local",
|
||||||
"code:check": "prettier --check \"src/**/*.{ts,tsx,js,css,scss,html}\"",
|
"code:check": "prettier --check \"src/**/*.{ts,tsx,js,css,scss,html}\"",
|
||||||
"code:format": "prettier --write \"src/**/*.{ts,tsx,js,css,scss,html}\"",
|
"code:format": "prettier --write \"src/**/*.{ts,tsx,js,css,scss,html}\"",
|
||||||
"code:format:specific-file": "prettier --write",
|
"code:format:specific-file": "prettier --write",
|
||||||
@ -25,6 +25,8 @@
|
|||||||
"@fastify/cookie": "^8.3.0",
|
"@fastify/cookie": "^8.3.0",
|
||||||
"@fastify/cors": "^8.2.0",
|
"@fastify/cors": "^8.2.0",
|
||||||
"@fastify/jwt": "^6.3.3",
|
"@fastify/jwt": "^6.3.3",
|
||||||
|
"@fastify/swagger": "^8.2.1",
|
||||||
|
"@fastify/swagger-ui": "^1.3.0",
|
||||||
"axios": "^1.2.1",
|
"axios": "^1.2.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@ -33,11 +35,9 @@
|
|||||||
"fastify-print-routes": "^2.0.6",
|
"fastify-print-routes": "^2.0.6",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mongoose": "^6.7.2",
|
"mongoose": "^6.7.2",
|
||||||
|
"nodemon": "^2.0.20",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths": "^4.1.0",
|
"tsconfig-paths": "^4.1.0",
|
||||||
"nodemon": "^2.0.20",
|
|
||||||
"@fastify/swagger": "^8.2.1",
|
|
||||||
"@fastify/swagger-ui": "^1.3.0",
|
|
||||||
"typescript-transform-paths": "^3.4.4"
|
"typescript-transform-paths": "^3.4.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -57,6 +57,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
|
"jest-mock-extended": "^3.0.4",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.20",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
|
@ -4,10 +4,12 @@ import { setAccountRoutes } from "@/routes/account.routes";
|
|||||||
import { setPrivilegeRoutes } from "@/routes/privilege.routes";
|
import { setPrivilegeRoutes } from "@/routes/privilege.routes";
|
||||||
import { setRoleRoutes } from "@/routes/role.routes";
|
import { setRoleRoutes } from "@/routes/role.routes";
|
||||||
import { setTariffRoutes } from "@/routes/tariff.routes";
|
import { setTariffRoutes } from "@/routes/tariff.routes";
|
||||||
|
import { setPermissionRoutes } from "@/routes/permission.routes";
|
||||||
|
|
||||||
export const combineRoutes = (router: Router): void => {
|
export const combineRoutes = (router: Router): void => {
|
||||||
router.group("/role", setRoleRoutes);
|
router.group("/role", setRoleRoutes);
|
||||||
router.group("/account", setAccountRoutes);
|
router.group("/account", setAccountRoutes);
|
||||||
router.group("/privilege", setPrivilegeRoutes);
|
router.group("/privilege", setPrivilegeRoutes);
|
||||||
router.group("/tariff", setTariffRoutes);
|
router.group("/tariff", setTariffRoutes);
|
||||||
|
router.group("/permission", setPermissionRoutes);
|
||||||
};
|
};
|
||||||
|
2
src/constants/errors.ts
Normal file
2
src/constants/errors.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const ERROR_NOT_FOUND = new Error("record not found");
|
||||||
|
export const ERROR_INVALID_PARAMS = new Error("invalid params");
|
6
src/constants/http-statuses.ts
Normal file
6
src/constants/http-statuses.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ERROR_INVALID_PARAMS, ERROR_NOT_FOUND } from "./errors";
|
||||||
|
|
||||||
|
export const HTTP_STATUSES = new Map<Error, number>([
|
||||||
|
[ERROR_NOT_FOUND, 404],
|
||||||
|
[ERROR_INVALID_PARAMS, 400],
|
||||||
|
]);
|
15
src/constants/permissions.ts
Normal file
15
src/constants/permissions.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { Permission } from "@/types/models/permission.type";
|
||||||
|
|
||||||
|
const DELETE_ACCOUNT_PERMISSION: Permission = {
|
||||||
|
name: "hub-admin-backend-service.deleteAccount",
|
||||||
|
description: "Разрешение на удаление чужого аккаунта пользователем",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
isDeleted: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PERMISSIONS = {
|
||||||
|
deleteAccount: DELETE_ACCOUNT_PERMISSION.name,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PERMISSION_LIST: Array<Permission> = [DELETE_ACCOUNT_PERMISSION];
|
45
src/handlers/account/helpers.test.ts
Normal file
45
src/handlers/account/helpers.test.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { determinePaginationParameters } from "./helpers";
|
||||||
|
|
||||||
|
import type { PaginationParams } from "./types";
|
||||||
|
|
||||||
|
describe("determinePaginationParameters", () => {
|
||||||
|
const testCases: Array<{
|
||||||
|
input: PaginationParams;
|
||||||
|
result: ReturnType<typeof determinePaginationParameters>;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
page: 1,
|
||||||
|
limit: 2,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
page: 1,
|
||||||
|
limit: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
page: -100,
|
||||||
|
limit: 200,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
page: 1,
|
||||||
|
limit: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
page: 6,
|
||||||
|
limit: -100,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
page: 6,
|
||||||
|
limit: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(testCases)("Успешное определение значений пагинации %j", ({ input, result }) => {
|
||||||
|
expect(determinePaginationParameters(input)).toEqual(result);
|
||||||
|
});
|
||||||
|
});
|
18
src/handlers/account/helpers.ts
Normal file
18
src/handlers/account/helpers.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { ObjectWithRequiredFields } from "@/types/object-with-required-fields";
|
||||||
|
import type { PaginationParams } from "./types";
|
||||||
|
|
||||||
|
const DEFAULT_PAGE = 1;
|
||||||
|
const DEFAULT_LIMIT = 100;
|
||||||
|
|
||||||
|
export const determinePaginationParameters = ({
|
||||||
|
page: optionalPage,
|
||||||
|
limit: optionalLimit,
|
||||||
|
}: PaginationParams): ObjectWithRequiredFields<PaginationParams> => {
|
||||||
|
const page = !optionalPage || optionalPage < 1 ? DEFAULT_PAGE : optionalPage;
|
||||||
|
const limit = !optionalLimit || optionalLimit < 1 ? DEFAULT_LIMIT : optionalLimit;
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
limit: limit > DEFAULT_LIMIT ? DEFAULT_LIMIT : limit,
|
||||||
|
};
|
||||||
|
};
|
@ -2,14 +2,28 @@ import { Types } from "mongoose";
|
|||||||
|
|
||||||
import { AccountModel } from "@/models/account.model";
|
import { AccountModel } from "@/models/account.model";
|
||||||
import { RoleModel } from "@/models/role.model";
|
import { RoleModel } from "@/models/role.model";
|
||||||
|
import { PermissionModule } from "@/services/permission/permission.module";
|
||||||
|
|
||||||
import { getUser } from "@/clients/auth";
|
import { getUser } from "@/clients/auth";
|
||||||
import { validateEmptyFields } from "@/utils/validate-empty-fields";
|
import { validateEmptyFields } from "@/utils/validate-empty-fields";
|
||||||
|
import { determinePaginationParameters } from "./helpers";
|
||||||
|
|
||||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import type { GetAccountRequest, SetAccountRoleRequest } from "./types";
|
import type { Account } from "@/types/models/account.type";
|
||||||
|
import type { GetAccountRequest, SetAccountRoleRequest, GetAccountsRequest, GetAccountsResponse } from "./types";
|
||||||
|
|
||||||
export const getAllAccounts = async () => AccountModel.find({}).lean();
|
export const getAccounts = async (request: GetAccountsRequest): Promise<GetAccountsResponse> => {
|
||||||
|
const { page, limit } = determinePaginationParameters(request?.query || {});
|
||||||
|
|
||||||
|
const accountsCount = await AccountModel.countDocuments();
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(accountsCount / limit);
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const accounts = await AccountModel.find({}).sort({ createdAt: "desc" }).skip(offset).limit(limit).lean();
|
||||||
|
|
||||||
|
return { accounts, totalPages };
|
||||||
|
};
|
||||||
|
|
||||||
export const createAccount = async (request: FastifyRequest, reply: FastifyReply) => {
|
export const createAccount = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
if (!Types.ObjectId.isValid(request.user.id)) {
|
if (!Types.ObjectId.isValid(request.user.id)) {
|
||||||
@ -38,17 +52,17 @@ export const createAccount = async (request: FastifyRequest, reply: FastifyReply
|
|||||||
return createdAccount.save();
|
return createdAccount.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAccountByID = async (request: GetAccountRequest, reply: FastifyReply) => {
|
export const getAccountByID = async (request: GetAccountRequest, reply: FastifyReply): Promise<Account> => {
|
||||||
const [getAccountRequestParams, error] = validateEmptyFields(request.params || {}, ["userId"]);
|
const [getAccountRequestParams, error] = validateEmptyFields(request.params || {}, ["userId"]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
reply.status(400);
|
reply.status(400);
|
||||||
return error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Types.ObjectId.isValid(getAccountRequestParams.userId)) {
|
if (!Types.ObjectId.isValid(getAccountRequestParams.userId)) {
|
||||||
reply.status(400);
|
reply.status(400);
|
||||||
return new Error("invalid user id");
|
throw new Error("invalid user id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await AccountModel.findOne({ userId: getAccountRequestParams.userId }).lean();
|
const account = await AccountModel.findOne({ userId: getAccountRequestParams.userId }).lean();
|
||||||
@ -61,10 +75,10 @@ export const getAccountByID = async (request: GetAccountRequest, reply: FastifyR
|
|||||||
return account;
|
return account;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAccount = async (request: GetAccountRequest, reply: FastifyReply) => {
|
export const getAccount = async (request: GetAccountRequest, reply: FastifyReply): Promise<Account> => {
|
||||||
if (!Types.ObjectId.isValid(request.user.id)) {
|
if (!Types.ObjectId.isValid(request.user.id)) {
|
||||||
reply.status(400);
|
reply.status(400);
|
||||||
return new Error("invalid user id");
|
throw new Error("invalid user id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await AccountModel.findOne({ userId: request.user.id }).lean();
|
const account = await AccountModel.findOne({ userId: request.user.id }).lean();
|
||||||
@ -105,10 +119,10 @@ export const setAccountRole = async (request: SetAccountRoleRequest, reply: Fast
|
|||||||
return account;
|
return account;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeAccount = async (request: FastifyRequest, reply: FastifyReply) => {
|
export const removeAccount = async (request: FastifyRequest, reply: FastifyReply): Promise<Account> => {
|
||||||
if (!Types.ObjectId.isValid(request.user.id)) {
|
if (!Types.ObjectId.isValid(request.user.id)) {
|
||||||
reply.status(400);
|
reply.status(400);
|
||||||
return new Error("invalid user id");
|
throw new Error("invalid user id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await AccountModel.findOneAndUpdate(
|
const account = await AccountModel.findOneAndUpdate(
|
||||||
@ -118,32 +132,92 @@ export const removeAccount = async (request: FastifyRequest, reply: FastifyReply
|
|||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
reply.status(404);
|
reply.status(404);
|
||||||
return new Error("account not found");
|
throw new Error("account not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAccount = async (request: FastifyRequest, reply: FastifyReply) => {
|
export const removeAccountById = async (request: GetAccountRequest, reply: FastifyReply): Promise<Account> => {
|
||||||
|
const [{ userId }, error] = validateEmptyFields(request.params || {}, ["userId"]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reply.status(400);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Types.ObjectId.isValid(userId)) {
|
||||||
|
reply.status(400);
|
||||||
|
throw new Error("invalid user id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await AccountModel.findOne({ userId: request.user.id });
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
reply.status(404);
|
||||||
|
throw new Error("account not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAvailableToDelete = await PermissionModule.deleteAccount(account);
|
||||||
|
|
||||||
|
if (!isAvailableToDelete) {
|
||||||
|
throw new Error("the user doesn't have the permission to delete account");
|
||||||
|
}
|
||||||
|
|
||||||
|
await account.updateOne({ $set: { isDeleted: true, deletedAt: new Date() } });
|
||||||
|
|
||||||
|
return account;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAccount = async (request: FastifyRequest, reply: FastifyReply): Promise<Account> => {
|
||||||
if (!Types.ObjectId.isValid(request.user.id)) {
|
if (!Types.ObjectId.isValid(request.user.id)) {
|
||||||
reply.status(400);
|
reply.status(400);
|
||||||
return new Error("invalid user id");
|
throw new Error("invalid user id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await AccountModel.findByIdAndDelete({ userId: request.user.id }).lean();
|
const account = await AccountModel.findByIdAndDelete({ userId: request.user.id }).lean();
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
reply.status(404);
|
reply.status(404);
|
||||||
return new Error("account not found");
|
throw new Error("account not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restoreAccount = async (request: FastifyRequest, reply: FastifyReply) => {
|
export const deleteAccountById = async (request: GetAccountRequest, reply: FastifyReply): Promise<Account> => {
|
||||||
|
const [{ userId }, error] = validateEmptyFields(request.params || {}, ["userId"]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reply.status(400);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Types.ObjectId.isValid(userId)) {
|
||||||
|
reply.status(400);
|
||||||
|
throw new Error("invalid user id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await AccountModel.findByIdAndDelete({ userId: request.user.id }).lean();
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
reply.status(404);
|
||||||
|
throw new Error("account not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAvailableToDelete = await PermissionModule.deleteAccount(account);
|
||||||
|
|
||||||
|
if (!isAvailableToDelete) {
|
||||||
|
throw new Error("the user doesn't have the permission to delete account");
|
||||||
|
}
|
||||||
|
|
||||||
|
return account;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const restoreAccount = async (request: FastifyRequest, reply: FastifyReply): Promise<Account> => {
|
||||||
if (!Types.ObjectId.isValid(request.user.id)) {
|
if (!Types.ObjectId.isValid(request.user.id)) {
|
||||||
reply.status(400);
|
reply.status(400);
|
||||||
return new Error("invalid user id");
|
throw new Error("invalid user id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await AccountModel.findOneAndUpdate(
|
const account = await AccountModel.findOneAndUpdate(
|
||||||
@ -153,7 +227,7 @@ export const restoreAccount = async (request: FastifyRequest, reply: FastifyRepl
|
|||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
reply.status(404);
|
reply.status(404);
|
||||||
return new Error("account not found");
|
throw new Error("account not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
|
42
src/handlers/account/middleware.ts
Normal file
42
src/handlers/account/middleware.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
import { AccountModel } from "@/models/account.model";
|
||||||
|
import { RoleModel } from "@/models/role.model";
|
||||||
|
|
||||||
|
import { PERMISSIONS } from "@/constants/permissions";
|
||||||
|
|
||||||
|
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction as Done } from "fastify";
|
||||||
|
|
||||||
|
export const determineIsAvailableToDeleteAccount = async (request: FastifyRequest, reply: FastifyReply, done: Done) => {
|
||||||
|
console.info("---------------------------determineIsAvailableToDeleteAccount------------------------------");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!Types.ObjectId.isValid(request.user.id)) {
|
||||||
|
throw new Error("invalid user id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await AccountModel.findOne({ userId: request.user.id }).lean();
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
throw new Error("account not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await RoleModel.findOne({ name: account.role }).lean();
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new Error("role not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("permisson", role.permissions[PERMISSIONS.deleteAccount]);
|
||||||
|
|
||||||
|
if (!role.permissions[PERMISSIONS.deleteAccount]) {
|
||||||
|
throw new Error("the user doesn't have the permission to delete account");
|
||||||
|
}
|
||||||
|
} catch (nativeError) {
|
||||||
|
console.info("determineIsAvailableToDeleteAccount error", (nativeError as Error).message);
|
||||||
|
reply.status(401);
|
||||||
|
return reply.send(nativeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import type { FastifyRequest } from "fastify";
|
import type { FastifyRequest } from "fastify";
|
||||||
|
import type { Account } from "@/types/models/account.type";
|
||||||
|
|
||||||
export type GetAccountRequest = FastifyRequest<{
|
export type GetAccountRequest = FastifyRequest<{
|
||||||
Params?: {
|
Params?: {
|
||||||
@ -12,3 +13,17 @@ export type SetAccountRoleRequest = FastifyRequest<{
|
|||||||
role?: string;
|
role?: string;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type GetAccountsRequest = FastifyRequest<{
|
||||||
|
Querystring?: PaginationParams;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetAccountsResponse = {
|
||||||
|
accounts: Account[];
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaginationParams = {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction as Done } from "fastify";
|
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction as Done } from "fastify";
|
||||||
|
|
||||||
export const verifyUser = async (request: FastifyRequest, reply: FastifyReply, done: Done) => {
|
export const verifyUser = async (request: FastifyRequest, reply: FastifyReply, done: Done) => {
|
||||||
|
console.info("---------------------------verifyUser------------------------------");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { id } = await request.jwtVerify<{ id?: string }>();
|
const { id } = await request.jwtVerify<{ id?: string }>();
|
||||||
|
|
||||||
@ -12,6 +14,7 @@ export const verifyUser = async (request: FastifyRequest, reply: FastifyReply, d
|
|||||||
request.user = { id };
|
request.user = { id };
|
||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
reply.status(401);
|
reply.status(401);
|
||||||
|
console.info("---------------------------verifyUser error------------------------------", nativeError);
|
||||||
return reply.send(nativeError);
|
return reply.send(nativeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
166
src/handlers/permission/index.ts
Normal file
166
src/handlers/permission/index.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
import { PermissionModule } from "@/services/permission/permission.module";
|
||||||
|
import { PermissionModel } from "@/models/permission.model";
|
||||||
|
|
||||||
|
import { validateEmptyFields } from "@/utils/validate-empty-fields";
|
||||||
|
|
||||||
|
import type { FastifyReply } from "fastify";
|
||||||
|
import type { Permission } from "@/types/models/permission.type";
|
||||||
|
import type { CreatePermissionRequest, GetPermissionByIdRequest, UpdatePermissionRequest } from "./types";
|
||||||
|
|
||||||
|
export const getAllPermissions = async (): Promise<Permission[]> => PermissionModule.getAllPermissions();
|
||||||
|
|
||||||
|
export const getPermissionById = async (
|
||||||
|
request: GetPermissionByIdRequest,
|
||||||
|
reply: FastifyReply
|
||||||
|
): Promise<Permission | undefined> => {
|
||||||
|
const [{ permissionId }, validateParamsError] = validateEmptyFields(request.params || {}, ["permissionId"]);
|
||||||
|
|
||||||
|
if (validateParamsError) {
|
||||||
|
reply.status(400);
|
||||||
|
throw validateParamsError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Types.ObjectId.isValid(permissionId)) {
|
||||||
|
reply.status(400);
|
||||||
|
throw new Error("invalid permission id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = await PermissionModel.findOne({
|
||||||
|
_id: new Types.ObjectId(permissionId),
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!permission) {
|
||||||
|
reply.status(404);
|
||||||
|
throw new Error("permission not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPermission = async (request: CreatePermissionRequest, reply: FastifyReply): Promise<Permission> => {
|
||||||
|
const [permission, error] = validateEmptyFields(request.body || {}, ["name", "description"]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reply.status(400);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPermission = new PermissionModel(permission);
|
||||||
|
|
||||||
|
return newPermission.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePermission = async (
|
||||||
|
request: UpdatePermissionRequest,
|
||||||
|
reply: FastifyReply
|
||||||
|
): Promise<Permission | undefined> => {
|
||||||
|
const [permission, validateBodyError] = validateEmptyFields(request.body || {}, ["name", "description"]);
|
||||||
|
const [{ permissionId }, validateParamsError] = validateEmptyFields(request.params || {}, ["permissionId"]);
|
||||||
|
|
||||||
|
if (validateBodyError) {
|
||||||
|
reply.status(400);
|
||||||
|
throw validateBodyError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateParamsError) {
|
||||||
|
reply.status(400);
|
||||||
|
throw validateParamsError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Types.ObjectId.isValid(permissionId)) {
|
||||||
|
reply.status(400);
|
||||||
|
throw new Error("invalid permission id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const findedPermission = await PermissionModel.findByIdAndUpdate(permissionId, {
|
||||||
|
$set: {
|
||||||
|
...permission,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!findedPermission) {
|
||||||
|
reply.status(404);
|
||||||
|
throw new Error("permission not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removePermission = async (
|
||||||
|
request: GetPermissionByIdRequest,
|
||||||
|
reply: FastifyReply
|
||||||
|
): Promise<Permission | undefined> => {
|
||||||
|
const [{ permissionId }, validateParamsError] = validateEmptyFields(request.params || {}, ["permissionId"]);
|
||||||
|
|
||||||
|
if (validateParamsError) {
|
||||||
|
reply.status(400);
|
||||||
|
throw validateParamsError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Types.ObjectId.isValid(permissionId)) {
|
||||||
|
reply.status(400);
|
||||||
|
throw new Error("invalid permission id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = await PermissionModel.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: new Types.ObjectId(permissionId),
|
||||||
|
isDeleted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
deletedAt: new Date(),
|
||||||
|
isDeleted: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!permission) {
|
||||||
|
reply.status(404);
|
||||||
|
throw new Error("permission not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const restorePermission = async (
|
||||||
|
request: GetPermissionByIdRequest,
|
||||||
|
reply: FastifyReply
|
||||||
|
): Promise<Permission | undefined> => {
|
||||||
|
const [{ permissionId }, validateParamsError] = validateEmptyFields(request.params || {}, ["permissionId"]);
|
||||||
|
|
||||||
|
if (validateParamsError) {
|
||||||
|
reply.status(400);
|
||||||
|
throw validateParamsError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Types.ObjectId.isValid(permissionId)) {
|
||||||
|
reply.status(400);
|
||||||
|
throw new Error("invalid permission id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = await PermissionModel.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: new Types.ObjectId(permissionId),
|
||||||
|
isDeleted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
deletedAt: undefined,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
isDeleted: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!permission) {
|
||||||
|
reply.status(404);
|
||||||
|
throw new Error("permission not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission;
|
||||||
|
};
|
20
src/handlers/permission/types.ts
Normal file
20
src/handlers/permission/types.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { FastifyRequest } from "fastify";
|
||||||
|
import type { Permission } from "@/types/models/permission.type";
|
||||||
|
import type { ObjectWithPossibleFields } from "@/types/object-with-possible-fields";
|
||||||
|
|
||||||
|
export type CreatePermissionRequest = FastifyRequest<{
|
||||||
|
Body?: ObjectWithPossibleFields<Permission>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type UpdatePermissionRequest = FastifyRequest<{
|
||||||
|
Body?: ObjectWithPossibleFields<Permission>;
|
||||||
|
Params?: {
|
||||||
|
permissionId?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetPermissionByIdRequest = FastifyRequest<{
|
||||||
|
Params?: {
|
||||||
|
permissionId?: string;
|
||||||
|
};
|
||||||
|
}>;
|
@ -4,6 +4,9 @@ import { Server } from "./server";
|
|||||||
|
|
||||||
import { CONFIGURATION } from "@/constants/configuration";
|
import { CONFIGURATION } from "@/constants/configuration";
|
||||||
|
|
||||||
|
console.info("server configuration: \n", JSON.stringify(CONFIGURATION, null, 2));
|
||||||
|
console.info("env: \n", JSON.stringify(process.env, null, 2));
|
||||||
|
|
||||||
const server = new Server({
|
const server = new Server({
|
||||||
serverOptions: CONFIGURATION.http,
|
serverOptions: CONFIGURATION.http,
|
||||||
databaseOptions: CONFIGURATION.db,
|
databaseOptions: CONFIGURATION.db,
|
||||||
|
@ -8,6 +8,7 @@ const schema: SchemaDefinition<Account> = {
|
|||||||
userId: {
|
userId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
index: { unique: true },
|
||||||
},
|
},
|
||||||
nickname: {
|
nickname: {
|
||||||
type: String,
|
type: String,
|
||||||
|
28
src/models/permission.model.ts
Normal file
28
src/models/permission.model.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Schema, model, SchemaDefinition } from "mongoose";
|
||||||
|
|
||||||
|
import { eloquentSchema } from "./eloquent.schema";
|
||||||
|
|
||||||
|
import type { Permission } from "@/types/models/permission.type";
|
||||||
|
|
||||||
|
const schema: SchemaDefinition<Permission> = {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: { unique: true },
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: "Разрешение на создание ролей",
|
||||||
|
},
|
||||||
|
...eloquentSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
const schemaSettings = {
|
||||||
|
versionKey: false,
|
||||||
|
collection: "permissions",
|
||||||
|
};
|
||||||
|
|
||||||
|
const PermissionSchema = new Schema<Permission>(schema, schemaSettings);
|
||||||
|
|
||||||
|
export const PermissionModel = model("Permission", PermissionSchema);
|
@ -5,9 +5,11 @@ import {
|
|||||||
getAccountByID,
|
getAccountByID,
|
||||||
getAccount,
|
getAccount,
|
||||||
setAccountRole,
|
setAccountRole,
|
||||||
getAllAccounts,
|
getAccounts,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
|
deleteAccountById,
|
||||||
removeAccount,
|
removeAccount,
|
||||||
|
removeAccountById,
|
||||||
restoreAccount,
|
restoreAccount,
|
||||||
} from "@/handlers/account";
|
} from "@/handlers/account";
|
||||||
import { verifyUser } from "@/handlers/auth/middleware";
|
import { verifyUser } from "@/handlers/auth/middleware";
|
||||||
@ -19,17 +21,30 @@ import {
|
|||||||
setAccountRoleSchema,
|
setAccountRoleSchema,
|
||||||
getAccountsSchema,
|
getAccountsSchema,
|
||||||
removeAccountSchema,
|
removeAccountSchema,
|
||||||
|
removeAccountByIdSchema,
|
||||||
restoreAccountSchema,
|
restoreAccountSchema,
|
||||||
deleteAccountSchema,
|
deleteAccountSchema,
|
||||||
|
deleteAccountByIdSchema,
|
||||||
} from "@/swagger/account";
|
} from "@/swagger/account";
|
||||||
|
|
||||||
export const setAccountRoutes = (router: Router): void => {
|
export const setAccountRoutes = (router: Router): void => {
|
||||||
router.get("/all", getAllAccounts, { schema: getAccountsSchema });
|
router.get("/pagination", getAccounts, { schema: getAccountsSchema });
|
||||||
router.get("/:userId", getAccountByID, { schema: getAccountByIdSchema });
|
router.get("/:userId", getAccountByID, { schema: getAccountByIdSchema });
|
||||||
router.get("/", getAccount, { preHandler: [verifyUser], schema: getAccountSchema });
|
router.get("/", getAccount, { preHandler: [verifyUser], schema: getAccountSchema });
|
||||||
|
|
||||||
router.post("/", createAccount, { preHandler: [verifyUser], schema: createAccountSchema });
|
router.post("/", createAccount, { preHandler: [verifyUser], schema: createAccountSchema });
|
||||||
router.post("/restore", restoreAccount, { preHandler: [verifyUser], schema: restoreAccountSchema });
|
router.post("/restore", restoreAccount, { preHandler: [verifyUser], schema: restoreAccountSchema });
|
||||||
|
|
||||||
router.patch("/role", setAccountRole, { preHandler: [verifyUser], schema: setAccountRoleSchema });
|
router.patch("/role", setAccountRole, { preHandler: [verifyUser], schema: setAccountRoleSchema });
|
||||||
|
|
||||||
router.delete("/", removeAccount, { preHandler: [verifyUser], schema: removeAccountSchema });
|
router.delete("/", removeAccount, { preHandler: [verifyUser], schema: removeAccountSchema });
|
||||||
|
router.delete("/:userId", removeAccountById, {
|
||||||
|
preHandler: [verifyUser],
|
||||||
|
schema: removeAccountByIdSchema,
|
||||||
|
});
|
||||||
router.delete("/delete", deleteAccount, { preHandler: [verifyUser], schema: deleteAccountSchema });
|
router.delete("/delete", deleteAccount, { preHandler: [verifyUser], schema: deleteAccountSchema });
|
||||||
|
router.delete("/delete/:userId", deleteAccountById, {
|
||||||
|
preHandler: [verifyUser],
|
||||||
|
schema: deleteAccountByIdSchema,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
31
src/routes/permission.routes.ts
Normal file
31
src/routes/permission.routes.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Router } from "@/server/router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAllPermissions,
|
||||||
|
getPermissionById,
|
||||||
|
createPermission,
|
||||||
|
updatePermission,
|
||||||
|
removePermission,
|
||||||
|
restorePermission,
|
||||||
|
} from "@/handlers/permission";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAllPermissionsSchema,
|
||||||
|
getPermissionByIdSchema,
|
||||||
|
createPermissionSchema,
|
||||||
|
removePermissionSchema,
|
||||||
|
restorePermissionSchema,
|
||||||
|
updatePermissionSchema,
|
||||||
|
} from "@/swagger/permission";
|
||||||
|
|
||||||
|
export const setPermissionRoutes = (router: Router): void => {
|
||||||
|
router.get("/all", getAllPermissions, { schema: getAllPermissionsSchema });
|
||||||
|
router.get("/:permissionId", getPermissionById, { schema: getPermissionByIdSchema });
|
||||||
|
|
||||||
|
router.post("/", createPermission, { schema: createPermissionSchema });
|
||||||
|
router.post("/restore/:permissionId", restorePermission, { schema: restorePermissionSchema });
|
||||||
|
|
||||||
|
router.patch("/:permissionId", updatePermission, { schema: updatePermissionSchema });
|
||||||
|
|
||||||
|
router.delete("/:permissionId", removePermission, { schema: removePermissionSchema });
|
||||||
|
};
|
16
src/services/account/account.service.ts
Normal file
16
src/services/account/account.service.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { RoleModel } from "@/models/role.model";
|
||||||
|
|
||||||
|
import type { Role } from "@/types/models/role.type";
|
||||||
|
import type { Account } from "@/types/models/account.type";
|
||||||
|
|
||||||
|
export class AccountService {
|
||||||
|
public static async determineAccountRole(account: Account): Promise<Role> {
|
||||||
|
const role = await RoleModel.findOne({ name: account.role }).lean();
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new Error("role not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
}
|
6
src/services/permission/permission.interface.ts
Normal file
6
src/services/permission/permission.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { Account } from "@/types/models/account.type";
|
||||||
|
import type { Role } from "@/types/models/role.type";
|
||||||
|
|
||||||
|
export interface AccountService {
|
||||||
|
determineAccountRole(account: Account): Promise<Role>;
|
||||||
|
}
|
6
src/services/permission/permission.module.ts
Normal file
6
src/services/permission/permission.module.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { AccountService } from "@/services/account/account.service";
|
||||||
|
import { PermissionService } from "./permission.service";
|
||||||
|
|
||||||
|
export const PermissionModule = new PermissionService({
|
||||||
|
accountService: AccountService,
|
||||||
|
});
|
78
src/services/permission/permission.service.test.ts
Normal file
78
src/services/permission/permission.service.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { mock, mockReset } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { PermissionService } from "./permission.service";
|
||||||
|
|
||||||
|
import { PERMISSIONS } from "@/constants/permissions";
|
||||||
|
|
||||||
|
import type { AccountService } from "./permission.interface";
|
||||||
|
import type { Account } from "@/types/models/account.type";
|
||||||
|
|
||||||
|
const DEFAULT_ACCOUNT: Account = {
|
||||||
|
userId: "userId",
|
||||||
|
nickname: "nickname",
|
||||||
|
avatar: "/img/avatar.png",
|
||||||
|
role: "admin",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
isDeleted: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("PermissionService", () => {
|
||||||
|
const accountService = mock<AccountService>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReset(accountService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteAccount", () => {
|
||||||
|
test("Успешное определение доступа на удаление аккаунта", async () => {
|
||||||
|
const permissionService = new PermissionService({ accountService });
|
||||||
|
|
||||||
|
accountService.determineAccountRole.mockResolvedValueOnce({
|
||||||
|
name: "admin",
|
||||||
|
permissions: {
|
||||||
|
[PERMISSIONS.deleteAccount]: true,
|
||||||
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAvailableToDeleteAccount = await permissionService.deleteAccount(DEFAULT_ACCOUNT);
|
||||||
|
|
||||||
|
expect(accountService.determineAccountRole).toHaveBeenCalledWith(DEFAULT_ACCOUNT);
|
||||||
|
expect(accountService.determineAccountRole).toHaveBeenCalledTimes(1);
|
||||||
|
expect(isAvailableToDeleteAccount).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("У пользователя нет прав на удаление", async () => {
|
||||||
|
const permissionService = new PermissionService({ accountService });
|
||||||
|
|
||||||
|
accountService.determineAccountRole.mockResolvedValueOnce({
|
||||||
|
name: "admin",
|
||||||
|
permissions: {
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAvailableToDeleteAccount = await permissionService.deleteAccount(DEFAULT_ACCOUNT);
|
||||||
|
|
||||||
|
expect(accountService.determineAccountRole).toHaveBeenCalledWith(DEFAULT_ACCOUNT);
|
||||||
|
expect(accountService.determineAccountRole).toHaveBeenCalledTimes(1);
|
||||||
|
expect(isAvailableToDeleteAccount).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Ошибка должна успешно пробрасываться", async () => {
|
||||||
|
const permissionService = new PermissionService({ accountService });
|
||||||
|
|
||||||
|
accountService.determineAccountRole.mockRejectedValueOnce(new Error("role not found"));
|
||||||
|
|
||||||
|
expect(permissionService.deleteAccount(DEFAULT_ACCOUNT)).resolves.toBe(false);
|
||||||
|
expect(accountService.determineAccountRole).toHaveBeenCalledWith(DEFAULT_ACCOUNT);
|
||||||
|
expect(accountService.determineAccountRole).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
58
src/services/permission/permission.service.ts
Normal file
58
src/services/permission/permission.service.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
import { PermissionModel } from "@/models/permission.model";
|
||||||
|
|
||||||
|
import { PERMISSIONS, PERMISSION_LIST } from "@/constants/permissions";
|
||||||
|
import { ERROR_INVALID_PARAMS, ERROR_NOT_FOUND } from "@/constants/errors";
|
||||||
|
|
||||||
|
import type { Account } from "@/types/models/account.type";
|
||||||
|
import type { AccountService } from "./permission.interface";
|
||||||
|
import type { Permission } from "@/types/models/permission.type";
|
||||||
|
|
||||||
|
type PermissionServiceDeps = {
|
||||||
|
readonly accountService: AccountService;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PermissionService {
|
||||||
|
private accountService: AccountService;
|
||||||
|
|
||||||
|
constructor(deps: PermissionServiceDeps) {
|
||||||
|
this.accountService = deps.accountService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAccount(account: Account): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const role = await this.accountService.determineAccountRole(account);
|
||||||
|
|
||||||
|
if (!role.permissions[PERMISSIONS.deleteAccount]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAllPermissions(): Promise<Permission[]> {
|
||||||
|
const permissions = await PermissionModel.find({}).lean();
|
||||||
|
|
||||||
|
return [...permissions, ...PERMISSION_LIST];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removePermission(id: string, permission: Permission): Promise<Permission> {
|
||||||
|
if (!Types.ObjectId.isValid(id)) {
|
||||||
|
throw ERROR_INVALID_PARAMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findedPermission = await PermissionModel.findByIdAndUpdate(id, {
|
||||||
|
$set: permission,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!findedPermission) {
|
||||||
|
throw ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission;
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,19 @@
|
|||||||
import { getAccountParams, setAccountRoleBody } from "./inputs";
|
import { getAccountParams, setAccountRoleBody, getAccountsQuerystring } from "./inputs";
|
||||||
import {
|
import {
|
||||||
getAccountResponse,
|
getAccountResponse,
|
||||||
createAccountResponse,
|
createAccountResponse,
|
||||||
setAccountRoleResponse,
|
setAccountRoleResponse,
|
||||||
getAccountsResponse,
|
getAccountsResponse,
|
||||||
removeRoleResponse,
|
removeAccountResponse,
|
||||||
} from "./responses";
|
} from "./responses";
|
||||||
|
|
||||||
import type { SwaggerSchema } from "@/types/swagger.type";
|
import type { SwaggerSchema } from "@/types/swagger.type";
|
||||||
|
|
||||||
export const getAccountsSchema: SwaggerSchema = {
|
export const getAccountsSchema: SwaggerSchema = {
|
||||||
summary: "Получение информации об аккаунтах",
|
summary: "Получение информации об аккаунтах",
|
||||||
description: "Получение всех аккаунтов из БД",
|
description: "Получение список аккаунтов с пагинацией из БД",
|
||||||
tags: ["account"],
|
tags: ["account"],
|
||||||
|
querystring: getAccountsQuerystring,
|
||||||
response: getAccountsResponse,
|
response: getAccountsResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -51,7 +52,16 @@ export const removeAccountSchema: SwaggerSchema = {
|
|||||||
summary: "Удаление аккаунта",
|
summary: "Удаление аккаунта",
|
||||||
description: "Помечает аккаунт удалённым, но не удаляет его из БД",
|
description: "Помечает аккаунт удалённым, но не удаляет его из БД",
|
||||||
tags: ["account"],
|
tags: ["account"],
|
||||||
response: removeRoleResponse,
|
response: removeAccountResponse,
|
||||||
|
security: [{ bearer: [] }],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeAccountByIdSchema: SwaggerSchema = {
|
||||||
|
summary: "Удаление аккаунта по ID",
|
||||||
|
description: "Помечает аккаунт удалённым, но не удаляет его из БД",
|
||||||
|
tags: ["account"],
|
||||||
|
params: getAccountParams,
|
||||||
|
response: removeAccountResponse,
|
||||||
security: [{ bearer: [] }],
|
security: [{ bearer: [] }],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,7 +69,16 @@ export const deleteAccountSchema: SwaggerSchema = {
|
|||||||
summary: "Удаление аккаунта",
|
summary: "Удаление аккаунта",
|
||||||
description: "Удаляет аккаунт из БД окончательно",
|
description: "Удаляет аккаунт из БД окончательно",
|
||||||
tags: ["account"],
|
tags: ["account"],
|
||||||
response: removeRoleResponse,
|
response: removeAccountResponse,
|
||||||
|
security: [{ bearer: [] }],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAccountByIdSchema: SwaggerSchema = {
|
||||||
|
summary: "Удаление аккаунта по ID",
|
||||||
|
description: "Удаляет аккаунт из БД окончательно",
|
||||||
|
tags: ["account"],
|
||||||
|
params: getAccountParams,
|
||||||
|
response: removeAccountResponse,
|
||||||
security: [{ bearer: [] }],
|
security: [{ bearer: [] }],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,6 +86,6 @@ export const restoreAccountSchema: SwaggerSchema = {
|
|||||||
summary: "Восстановление аккаунта",
|
summary: "Восстановление аккаунта",
|
||||||
description: "Восстанавливает аккаунт, который не был удалён окончательно",
|
description: "Восстанавливает аккаунт, который не был удалён окончательно",
|
||||||
tags: ["account"],
|
tags: ["account"],
|
||||||
response: removeRoleResponse,
|
response: removeAccountResponse,
|
||||||
security: [{ bearer: [] }],
|
security: [{ bearer: [] }],
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,20 @@ export const getAccountParams: SwaggerMessage = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAccountsQuerystring: SwaggerMessage = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: "number",
|
||||||
|
description: "номер страницы",
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
description: "Лимит количества аккаунтов (больше 100 не обрабатывается)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const setAccountRoleBody: SwaggerMessage = {
|
export const setAccountRoleBody: SwaggerMessage = {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["userId", "role"],
|
required: ["userId", "role"],
|
||||||
|
@ -47,3 +47,44 @@ export const account: SwaggerMessage = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const accounts: SwaggerMessage = {
|
||||||
|
description: "Список аккаунтов",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
accounts: {
|
||||||
|
type: "array",
|
||||||
|
description: "Массив аккаунтов",
|
||||||
|
items: account,
|
||||||
|
},
|
||||||
|
totalPages: { type: "number" },
|
||||||
|
},
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
totalPages: 10,
|
||||||
|
accounts: [
|
||||||
|
{
|
||||||
|
_id: "807f1f77bcf81cd799439011",
|
||||||
|
userId: "507f1f77bcf86cd799439011",
|
||||||
|
nickname: "Ivanov Ivan Ivanovich",
|
||||||
|
avatar: "/media/avatar/default-avatar.jpg",
|
||||||
|
role: "user",
|
||||||
|
isDeleted: false,
|
||||||
|
createdAt: "2017-07-21T17:32:28Z",
|
||||||
|
updatedAt: "2017-07-21T17:32:28Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "807f1f77bcf81cd799439011",
|
||||||
|
userId: "507f1f77bcf86cd799439011",
|
||||||
|
nickname: "Ivanov Ivan Ivanovich",
|
||||||
|
avatar: "/media/avatar/default-avatar.jpg",
|
||||||
|
role: "user",
|
||||||
|
isDeleted: true,
|
||||||
|
createdAt: "2017-07-21T17:32:28Z",
|
||||||
|
updatedAt: "2019-04-14T15:32:15Z",
|
||||||
|
deletedAt: "2021-08-17T13:23:44Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { swaggerError } from "@/utils/swagger-error";
|
import { swaggerError } from "@/utils/swagger-error";
|
||||||
|
|
||||||
import { account } from "./models";
|
import { account, accounts } from "./models";
|
||||||
|
|
||||||
import type { SwaggerMessage } from "@/types/swagger.type";
|
import type { SwaggerMessage } from "@/types/swagger.type";
|
||||||
|
|
||||||
export const getAccountsResponse: Record<string, SwaggerMessage> = {
|
export const getAccountsResponse: Record<string, SwaggerMessage> = {
|
||||||
200: {
|
200: accounts,
|
||||||
type: "array",
|
|
||||||
description: "Массив аккаунтов",
|
|
||||||
items: account,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAccountResponse: Record<string, SwaggerMessage> = {
|
export const getAccountResponse: Record<string, SwaggerMessage> = {
|
||||||
@ -32,7 +28,7 @@ export const setAccountRoleResponse: Record<string, SwaggerMessage> = {
|
|||||||
404: swaggerError(404, "user not found"),
|
404: swaggerError(404, "user not found"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeRoleResponse: Record<string, SwaggerMessage> = {
|
export const removeAccountResponse: Record<string, SwaggerMessage> = {
|
||||||
200: account,
|
200: account,
|
||||||
400: swaggerError(400, "invalid user id"),
|
400: swaggerError(400, "invalid user id"),
|
||||||
401: swaggerError(401, "invalid token"),
|
401: swaggerError(401, "invalid token"),
|
||||||
|
51
src/swagger/permission/index.ts
Normal file
51
src/swagger/permission/index.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { getPermissionParams, createPermissionBody } from "./inputs";
|
||||||
|
import { getPermissionResponse, getAllPermissionsResponse, createPermissionResponse } from "./responses";
|
||||||
|
|
||||||
|
import type { SwaggerSchema } from "@/types/swagger.type";
|
||||||
|
|
||||||
|
export const getAllPermissionsSchema: SwaggerSchema = {
|
||||||
|
summary: "Получение информации об разрешениях",
|
||||||
|
description: "Получение списка всех разрешений из БД",
|
||||||
|
tags: ["permission"],
|
||||||
|
response: getAllPermissionsResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPermissionByIdSchema: SwaggerSchema = {
|
||||||
|
summary: "Получение информации об аккаунте",
|
||||||
|
description: "Получение аккаунта по ID",
|
||||||
|
tags: ["permission"],
|
||||||
|
params: getPermissionParams,
|
||||||
|
response: getPermissionResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPermissionSchema: SwaggerSchema = {
|
||||||
|
summary: "Создание разрешения",
|
||||||
|
tags: ["permission"],
|
||||||
|
body: createPermissionBody,
|
||||||
|
response: createPermissionResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removePermissionSchema: SwaggerSchema = {
|
||||||
|
summary: "Удаление разрешения",
|
||||||
|
description: "Помечает разрешение удалённым, но не удаляет его из БД",
|
||||||
|
tags: ["permission"],
|
||||||
|
params: getPermissionParams,
|
||||||
|
response: getPermissionResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const restorePermissionSchema: SwaggerSchema = {
|
||||||
|
summary: "Восстановление разрешения",
|
||||||
|
description: "Восстанавливает разрешение, которое не было удалёно окончательно",
|
||||||
|
tags: ["permission"],
|
||||||
|
params: getPermissionParams,
|
||||||
|
response: getPermissionResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePermissionSchema: SwaggerSchema = {
|
||||||
|
summary: "Обновление разрешения",
|
||||||
|
description: "Обновляет данные о разрешении",
|
||||||
|
tags: ["permission"],
|
||||||
|
params: getPermissionParams,
|
||||||
|
body: createPermissionBody,
|
||||||
|
response: getPermissionResponse,
|
||||||
|
};
|
41
src/swagger/permission/inputs.ts
Normal file
41
src/swagger/permission/inputs.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { SwaggerMessage } from "@/types/swagger.type";
|
||||||
|
|
||||||
|
export const getPermissionParams: SwaggerMessage = {
|
||||||
|
type: "object",
|
||||||
|
required: ["permissionId"],
|
||||||
|
properties: {
|
||||||
|
permissionId: {
|
||||||
|
type: "string",
|
||||||
|
description: "ID разрешения",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAccountsQuerystring: SwaggerMessage = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: "number",
|
||||||
|
description: "номер страницы",
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
description: "Лимит количества аккаунтов (больше 100 не обрабатывается)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPermissionBody: SwaggerMessage = {
|
||||||
|
type: "object",
|
||||||
|
required: ["name", "description"],
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
description: "имя разрешения",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
description: "описание разрешения",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
42
src/swagger/permission/models.ts
Normal file
42
src/swagger/permission/models.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type { SwaggerMessage } from "@/types/swagger.type";
|
||||||
|
|
||||||
|
export const permission: SwaggerMessage = {
|
||||||
|
description: "Разрешение",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
_id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
description: { type: "string" },
|
||||||
|
isDeleted: { type: "boolean" },
|
||||||
|
createdAt: {
|
||||||
|
type: "string",
|
||||||
|
format: "date-time",
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: "string",
|
||||||
|
format: "date-time",
|
||||||
|
},
|
||||||
|
deletedAt: {
|
||||||
|
type: "string",
|
||||||
|
format: "date-time",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
_id: "807f1f77bcf81cd799439011",
|
||||||
|
name: "hub-admin-backend-service.deleteAccount",
|
||||||
|
nickname: "Разрешение на удаление аккаунта",
|
||||||
|
avatar: "/media/avatar/default-avatar.jpg",
|
||||||
|
role: "user",
|
||||||
|
isDeleted: false,
|
||||||
|
createdAt: "2017-07-21T17:32:28Z",
|
||||||
|
updatedAt: "2017-07-21T17:32:28Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const permissions: SwaggerMessage = {
|
||||||
|
description: "Список разрешений",
|
||||||
|
type: "array",
|
||||||
|
items: permission,
|
||||||
|
};
|
20
src/swagger/permission/responses.ts
Normal file
20
src/swagger/permission/responses.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { swaggerError } from "@/utils/swagger-error";
|
||||||
|
|
||||||
|
import { permission, permissions } from "./models";
|
||||||
|
|
||||||
|
import type { SwaggerMessage } from "@/types/swagger.type";
|
||||||
|
|
||||||
|
export const getAllPermissionsResponse: Record<string, SwaggerMessage> = {
|
||||||
|
200: permissions,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPermissionResponse: Record<string, SwaggerMessage> = {
|
||||||
|
200: permission,
|
||||||
|
400: swaggerError(400, "invalid permission id"),
|
||||||
|
404: swaggerError(400, "permission not found"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPermissionResponse: Record<string, SwaggerMessage> = {
|
||||||
|
200: permission,
|
||||||
|
409: swaggerError(409, "permission already exist"),
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
export type Eloquent = {
|
export type Eloquent = {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
deletedAt: Date;
|
deletedAt?: Date;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
};
|
};
|
||||||
|
6
src/types/models/permission.type.ts
Normal file
6
src/types/models/permission.type.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { Eloquent } from "./eloquent.type";
|
||||||
|
|
||||||
|
export type Permission = Eloquent & {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
16
src/utils/is-error.test.ts
Normal file
16
src/utils/is-error.test.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { isError } from "./is-error";
|
||||||
|
|
||||||
|
describe("isError", () => {
|
||||||
|
test("Должно возвращать значение true, если это ошибка Error", () => {
|
||||||
|
const error: Error = new Error("An error occurred");
|
||||||
|
|
||||||
|
expect(isError(error)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Должно возвращать значение false, если это ошибка Error", () => {
|
||||||
|
expect(isError(undefined)).toBe(false);
|
||||||
|
expect(isError(null)).toBe(false);
|
||||||
|
expect(isError("")).toBe(false);
|
||||||
|
expect(isError({})).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
1
src/utils/is-error.ts
Normal file
1
src/utils/is-error.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const isError = (candidate: unknown): candidate is Error => candidate instanceof Error;
|
12
yarn.lock
12
yarn.lock
@ -3943,6 +3943,13 @@ jest-message-util@^29.3.1:
|
|||||||
slash "^3.0.0"
|
slash "^3.0.0"
|
||||||
stack-utils "^2.0.3"
|
stack-utils "^2.0.3"
|
||||||
|
|
||||||
|
jest-mock-extended@^3.0.4:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-3.0.4.tgz#12a5f993d27aa46232012c439a9b7c54f3b0a1fd"
|
||||||
|
integrity sha512-2ynEZ7IEJNrhrgshklDMhrOdnmW4Nt+PhkyRqZxRgpwMo7JjmFWMzyp0+eSyk+H9KK1QjXI5xTZIw6x7cVDcRg==
|
||||||
|
dependencies:
|
||||||
|
ts-essentials "^7.0.3"
|
||||||
|
|
||||||
jest-mock@^29.3.1:
|
jest-mock@^29.3.1:
|
||||||
version "29.3.1"
|
version "29.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.3.1.tgz#60287d92e5010979d01f218c6b215b688e0f313e"
|
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.3.1.tgz#60287d92e5010979d01f218c6b215b688e0f313e"
|
||||||
@ -5604,6 +5611,11 @@ trim-newlines@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
|
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
|
||||||
integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
|
integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
|
||||||
|
|
||||||
|
ts-essentials@^7.0.3:
|
||||||
|
version "7.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38"
|
||||||
|
integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==
|
||||||
|
|
||||||
ts-jest@^29.0.3:
|
ts-jest@^29.0.3:
|
||||||
version "29.0.3"
|
version "29.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.3.tgz#63ea93c5401ab73595440733cefdba31fcf9cb77"
|
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.3.tgz#63ea93c5401ab73595440733cefdba31fcf9cb77"
|
||||||
|
Loading…
Reference in New Issue
Block a user