Compare commits

...

76 Commits

Author SHA1 Message Date
123be7da57 add amount field to privileges - fix missing
Some checks failed
Lint / Lint (push) Failing after 1m6s
Deploy / DeployService (push) Has been cancelled
Deploy / CreateImage (push) Has been cancelled
2025-01-24 01:26:29 +03:00
30075d4043 add json tags to privilege model. front contract needs
Some checks failed
Lint / Lint (push) Failing after 59s
Deploy / CreateImage (push) Successful in 1m46s
Deploy / DeployService (push) Successful in 24s
2025-01-23 01:13:41 +03:00
534339533f ci: change inter service communication to domain routing
Some checks failed
Lint / Lint (push) Failing after 1m6s
Deploy / CreateImage (push) Successful in 1m44s
Deploy / DeployService (push) Successful in 24s
2025-01-11 14:09:44 +03:00
6701653964 configs
All checks were successful
Deploy / CreateImage (push) Successful in 1m42s
Deploy / DeployService (push) Successful in 24s
2025-01-10 01:39:46 +03:00
19ce38986c add lit rules
Some checks failed
Lint / Lint (push) Failing after 57s
2025-01-05 02:14:18 +03:00
680db6bbec ci: deployment rules WIP
Some checks failed
Lint / Lint (push) Failing after 35s
2025-01-05 02:11:15 +03:00
8ae0a721fb applied renamer 2025-01-02 01:20:08 +03:00
ff68956fb5 added tariff rest to openapi docs 2024-07-26 16:57:56 +03:00
5070cc9f50 added method restore 2024-07-26 15:12:08 +03:00
302802946c rename validate func for privilege 2024-07-26 14:44:37 +03:00
bb81299c25 added method for updste tariff 2024-07-26 14:38:54 +03:00
27d09e02a7 prepare for update method 2024-07-25 18:47:03 +03:00
e49ec0e1cc after test 2024-07-25 18:08:32 +03:00
9de4b4d894 added method for soft delete tariff 2024-07-25 17:35:12 +03:00
91b67483b8 added methods for pagination tariffs 2024-07-25 17:10:01 +03:00
1324c917ce added create methods for tariff users and admmins 2024-07-25 15:43:43 +03:00
c3d9e2a0d3 added type middleware 2024-07-25 14:49:50 +03:00
a1ec819de7 added getting controllers for tariffs 2024-07-25 14:06:49 +03:00
c8f2b77b93 added jwt for tariffs 2024-07-25 13:51:27 +03:00
0ac712e69d prepare logic for tarif rework on go 2024-07-23 14:27:56 +03:00
9b416ec3f3 add openapi 2024-06-09 16:58:43 +03:00
b589f416e8 fix bugs after testing 2024-06-04 19:51:14 +03:00
e2b4a699b2 add another privilege methods, need test 2024-06-04 16:12:15 +03:00
1df10f9e33 add method for create many privileges 2024-06-04 14:53:48 +03:00
b3a689e447 add get by id and get by service key privilege methods 2024-06-04 13:15:09 +03:00
70fd6535f7 add delete privilege method by privilege id 2024-06-04 12:42:40 +03:00
bd2e1372f4 replace amount from pj 2024-05-29 20:32:46 +03:00
71fd522c46 add repo method for update privilege 2024-05-29 20:27:25 +03:00
dc25cc41df add repo method for create privilege 2024-05-29 20:06:28 +03:00
a2baad87d7 add req bodies for create update privileges 2024-05-29 19:05:04 +03:00
d80378df68 add logic for to conntrollers, svc privileges map and get all privileges 2024-05-29 18:55:17 +03:00
b23e8876a0 update mod and local sepate success 2024-05-29 17:51:48 +03:00
846282e1c6 add initialize controllers and repos, build app 2024-05-29 17:31:33 +03:00
148e8de6be init bones go pj 2024-05-29 16:02:06 +03:00
481fe2bcfc ci: remove from external interface 2024-04-21 20:59:46 +03:00
c60c3b1125 feat: preparing fro production 2024-01-27 06:10:42 +03:00
72709b90c8 Merge branch 'dev' into 'staging'
Update docker-compose.yaml

See merge request pena-services/hub_admin_backend_service!69
2024-01-17 15:21:39 +00:00
b2eec34ed0 Update docker-compose.yaml 2024-01-17 15:21:14 +00:00
41e27e28ff Merge branch 'dev' into 'staging'
Update docker-compose.yaml

See merge request pena-services/hub_admin_backend_service!68
2024-01-11 00:04:40 +00:00
0971ac8ef6 Update docker-compose.yaml 2024-01-11 00:03:23 +00:00
d738c3f594 Merge branch 'dev' into 'staging'
Update docker-compose.yaml

See merge request pena-services/hub_admin_backend_service!67
2024-01-10 22:52:33 +00:00
d711c20db0 Update docker-compose.yaml 2024-01-10 22:52:06 +00:00
8250fbe713 Merge branch 'dev' into 'staging'
Update .gitlab-ci.yml file

See merge request pena-services/hub_admin_backend_service!66
2024-01-10 22:46:59 +00:00
397e72170c Update .gitlab-ci.yml file 2024-01-10 22:46:15 +00:00
acfb364a67 fix: only active tariffs in response 2023-12-20 19:53:41 +03:00
adcb57f157 fix: update tariff privilege check remove 2023-12-16 00:17:14 +03:00
4355698db8 feat: tariff ordering support 2023-12-15 23:01:09 +03:00
e08c9246e2 Merge branch 'dev' into 'staging'
feat: add description to tariff

See merge request pena-services/hub_admin_backend_service!65
2023-12-15 15:20:27 +00:00
6875e4d4e4 feat: add description to tariff 2023-12-15 18:16:14 +03:00
4d52ea8364 Merge branch 'dev' into 'staging'
Update index.ts

See merge request pena-services/hub_admin_backend_service!63
2023-09-16 17:48:12 +00:00
ee00fc3ac7 Update index.ts 2023-09-16 17:47:26 +00:00
5c94e5264f Merge branch 'dev' into 'staging'
Dev

See merge request pena-services/hub_admin_backend_service!62
2023-09-15 21:18:09 +00:00
3c7a95705e Merge branch 'naming' into 'dev'
ci(test): local testing

See merge request pena-services/hub_admin_backend_service!61
2023-09-15 21:12:03 +00:00
kroyzen
98b7451157 ci(test): local testing 2023-08-23 17:59:45 +03:00
928b368b8b Merge branch 'naming' into 'dev'
style: privilegies => privileges

See merge request pena-services/hub_admin_backend_service!60
2023-08-22 23:15:11 +00:00
kroyzen
c25f72a185 style: privilegies => privileges 2023-08-22 11:14:53 +03:00
62d3038a1e Merge branch 'dev' into 'staging'
Dev

See merge request pena-services/hub_admin_backend_service!59
2023-07-03 16:48:49 +00:00
b30590dbfb Merge branch 'staging' into 'dev'
# Conflicts:
#   src/handlers/privilege/index.ts
2023-07-03 16:48:37 +00:00
c7cc06aa61 Update file index.ts 2023-07-03 16:43:05 +00:00
Kirill
1990809cec Merge branch 'dev' into 'staging'
feat: price not required for privilege

See merge request pena-services/hub_admin_backend_service!58
2023-06-24 12:12:46 +00:00
Kirill
4f34378010 feat: price not required for privilege 2023-06-24 12:12:46 +00:00
Kirill
651f99d2c5 Merge branch 'dev' into 'staging'
feat: private tariff & remove duplicate bag

See merge request pena-services/hub_admin_backend_service!57
2023-06-22 13:29:48 +00:00
Kirill
c3fb5177bc feat: private tariff & remove duplicate bag 2023-06-22 13:29:48 +00:00
08e15f5108 Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!56
2023-06-17 17:45:02 +00:00
6be82fa7d5 Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!55
2023-06-17 17:36:55 +00:00
c5a6704961 Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!54
2023-06-17 17:06:09 +00:00
f1e30336dc Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!53
2023-06-17 16:59:52 +00:00
65a694f351 Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!52
2023-06-17 16:53:00 +00:00
0ec6a755ab Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!51
2023-06-17 16:46:47 +00:00
08f08b8779 Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!50
2023-06-17 15:56:12 +00:00
c694f5fe1c Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!49
2023-06-17 15:28:26 +00:00
4adccd374b Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!48
2023-06-17 15:07:26 +00:00
013fe877f2 Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!47
2023-06-17 14:03:08 +00:00
5b1d664649 Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!46
2023-06-16 21:02:25 +00:00
24dda5ecd4 Merge branch 'dev' into 'staging'
Update file index.ts

See merge request pena-services/hub_admin_backend_service!45
2023-06-16 20:52:37 +00:00
93bbcc3251 Merge branch 'dev' into 'staging'
Dev

See merge request pena-services/hub_admin_backend_service!44
2023-06-16 19:50:59 +00:00
143 changed files with 2894 additions and 10072 deletions

@ -1,6 +0,0 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [2, "always", ["feat", "fix", "docs", "style", "refactor", "revert", "chore", "build", "ci", "test"]]
}
}

@ -1 +0,0 @@
jest.config.js

145
.eslintrc

@ -1,145 +0,0 @@
{
"root": true,
"env": {
"browser": true,
"es2022": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier", "import"],
"extends": [
"prettier",
"eslint:recommended",
"eslint-config-prettier",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/eslint-recommended"
],
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".ts"],
"paths": ["./src"]
},
"typescript": {
"alwaysTryTypes": true
}
},
"import/parsers": {
"@typescript-eslint/parser": [".ts"]
},
"extensions": [".js", ".ts"],
"import/ignore": ["node_modules"]
},
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": ["./tsconfig.json"],
"ecmaFeatures": {
"jsx": false,
"arrowFunctions": true
}
},
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/quotes": ["error", "double"],
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/lines-between-class-members": ["error", { "exceptAfterOverload": true }],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "default",
"format": ["camelCase", "UPPER_CASE", "PascalCase", "snake_case"],
"leadingUnderscore": "allow"
},
{
"selector": "variable",
"format": ["camelCase", "UPPER_CASE", "PascalCase", "snake_case"],
"leadingUnderscore": "allow"
},
{
"selector": "typeLike",
"format": ["camelCase", "UPPER_CASE", "PascalCase", "snake_case"],
"leadingUnderscore": "allow"
},
{
"selector": "enumMember",
"format": ["camelCase", "UPPER_CASE", "PascalCase", "snake_case"],
"leadingUnderscore": "allow"
},
{
"selector": "objectLiteralProperty",
"format": [],
"leadingUnderscore": "allow"
}
],
"import/no-named-default": 0,
"import/prefer-default-export": "off",
"import/no-dynamic-require": "off",
"import/no-duplicates": "off",
"import/no-unresolved": "error",
"import/no-cycle": "off",
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
],
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"import/order": [
"error",
{
"newlines-between": "always-and-inside-groups",
"groups": [["builtin", "external"], ["internal", "sibling"], "type"]
}
],
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
],
"comma-dangle": "off",
"class-methods-use-this": "off",
"func-names": "off",
"function-paren-newline": "off",
"global-require": "off",
"implicit-arrow-linebreak": 0,
"no-use-before-define": "off",
"no-inner-declarations": "off",
"no-console": ["warn", { "allow": ["error", "info"] }],
"consistent-return": "off",
"no-alert": "error",
"no-process-exit": "error",
"no-shadow": "error",
"no-param-reassign": [
"error",
{
"props": true,
"ignorePropertyModificationsFor": ["accamulator", "request"]
}
],
"object-shorthand": "off",
"quotes": ["error", "double"]
},
"globals": {
"window": false,
"FormData": false,
"Blob": true,
"document": false
}
}

@ -0,0 +1,24 @@
name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry
on:
push:
branches:
- 'main'
- 'staging'
jobs:
CreateImage:
runs-on: [hubstaging]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with:
runner: hubstaging
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
runs-on: [hubstaging]
needs: CreateImage
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
with:
runner: hubstaging

14
.gitea/workflows/lint.yml Normal file

@ -0,0 +1,14 @@
name: Lint
run-name: ${{ gitea.actor }} produce linting
on:
push:
branches:
- 'dev'
jobs:
Lint:
runs-on: [hubstaging]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/lint.yml@v1.1.2
with:
runner: hubstaging

1
.gitignore vendored

@ -1,4 +1,5 @@
/node_modules
nohup.out
.env
main
/.idea

@ -1,3 +0,0 @@
include:
- project: "devops/pena-continuous-integration"
file: "/nodejs/docker/nodejs.gitlab-ci.yml"

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn run code:format && yarn run lint && yarn run code:check && git add .

@ -1,12 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "auto",
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false
}

@ -1,37 +1,13 @@
FROM node:19.1-alpine AS dev
RUN apk update && rm -rf /var/cache/apk/*
WORKDIR /usr/app
# Install node dependencies - done in a separate step so Docker can cache it.
COPY yarn.lock .
COPY package.json .
COPY tsconfig.json .
RUN yarn install --ignore-scripts --non-interactive --frozen-lockfile && yarn cache clean
COPY . .
RUN ls
RUN yarn build
FROM node:19.1-alpine AS production
RUN apk update && rm -rf /var/cache/apk/*
FROM gitea.pena/penadevops/container-images/golang:main as build
WORKDIR /app
ENV GOPRIVATE=gitea.pena/PenaSide/common,gitea.pena/PenaSide/linters-golang,gitea.pena/PenaSide/customer,gitea.pena/PenaSide/trashlog,gitea.pena/PenaSide/hlog
ENV GOINSECURE=gitea.pena/PenaSide/common,gitea.pena/PenaSide/linters-golang,gitea.pena/PenaSide/customer,gitea.pena/PenaSide/trashlog,gitea.pena/PenaSide/hlog
COPY . /app
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app ./cmd/main.go
COPY --from=dev /usr/app/dist /app
COPY --from=dev /usr/app/package.json /app/
COPY --from=dev /usr/app/yarn.lock /app/
COPY --from=dev /usr/app/tsconfig.json .
FROM gitea.pena/penadevops/container-images/alpine:main
RUN apk add --no-cache ca-certificates
COPY --from=build app/app .
CMD ["/app"]
RUN chown -R node: .
USER node
RUN yarn install --non-interactive --frozen-lockfile --production && yarn cache clean
CMD ["yarn", "start:prod"]

10
Taskfile.dist.yml Normal file

@ -0,0 +1,10 @@
version: "3"
tasks:
update-linter:
cmds:
- go get -u gitea.pena/PenaSide/linters-golang
lint:
cmds:
- task: update-linter
- cmd: golangci-lint run -v -c $(go list -f '{{"{{"}}.Dir{{"}}"}}' -m gitea.pena/PenaSide/linters-golang)/.golangci.yml

33
cmd/main.go Normal file

@ -0,0 +1,33 @@
package main
import (
"context"
"fmt"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/app"
"gitea.pena/PenaSide/tariffs/internal/initialize"
"os"
"os/signal"
"syscall"
_ "gitea.pena/PenaSide/linters-golang/pkg/dummy"
)
func main() {
logger, err := zap.NewProduction()
if err != nil {
fmt.Printf("Failed to initialize logger: %v\n", err)
os.Exit(1)
}
config, err := initialize.LoadConfig()
if err != nil {
logger.Fatal("Failed to load config", zap.Error(err))
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err = app.Run(ctx, *config, logger); err != nil {
logger.Fatal("App exited with error", zap.Error(err))
}
}

@ -0,0 +1,24 @@
version: "3.3"
services:
admin:
container_name: hub-admin-backend-service
restart: unless-stopped
tty: true
image: $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
ports:
- 10.8.0.8:59303:8005
networks:
- default
environment:
- DB_HOST=10.8.0.8
- DB_PORT=27017
- ENVIRONMENT=staging
- HTTP_HOST=0.0.0.0
- HTTP_PORT=8005
- AUTH_SERVICE_HOST=http://10.8.0.8
- AUTH_SERVICE_PORT=59300
- DB_USERNAME=$DB_USERNAME-prod
- DB_PASSWORD=$DB_PASSWORD
- DB_NAME=administrator
- PUBLIC_ACCESS_SECRET_KEY=$PUBLIC_ACCESS_SECRET_KEY

@ -1,28 +1,22 @@
version: "3.3"
services:
admin:
container_name: hub-admin-backend-service
tariffs:
restart: unless-stopped
tty: true
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
expose:
- 8005
networks:
- marketplace_penahub_frontend
- default
image: gitea.pena:3000/penaside/tariffs/staging:$GITHUB_RUN_NUMBER
ports:
- 10.7.0.6:59303:8005
- 10.7.0.6:59304:8006
environment:
- DB_HOST=10.6.0.11
- DB_PORT=27017
- ENVIRONMENT=staging
- HTTP_HOST=0.0.0.0
- HTTP_PORT=8005
- AUTH_SERVICE_HOST=http://pena-auth-service
- AUTH_SERVICE_PORT=8080
- DB_USERNAME=$DB_USERNAME
- DB_PASSWORD=$DB_PASSWORD
- DB_NAME=administrator
- PUBLIC_ACCESS_SECRET_KEY=$PUBLIC_ACCESS_SECRET_KEY
networks:
marketplace_penahub_frontend:
external: true
MONGO_URL: mongodb://administrator:64143ffdd9304865586e5cf1@mongodb.pena:27017/administrator?authSource=administrator
MONGO_DB_NAME: administrator
DB_HOST: 10.7.0.6
DB_PORT: 27017
ENVIRONMENT: staging
INTERNAL_HTTP_ADDRESS: 0.0.0.0:8006
EXTERNAL_HTTP_ADDRESS: 0.0.0.0:8005
AUTH_SERVICE_HOST: http://10.7.0.4
AUTH_SERVICE_PORT: 59300
JWT_PUBLIC_KEY: "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLW1tlHyKC9AG0hGpmkksET2DE\nr7ojSPemxFWAgFgcPJWQ7x3uNbsdJ3bIZFoA/FClaWKMCZmjnH9tv0bKZtY/CDhM\nZEyHpMruRSn6IKrxjtQZWy4uv/w6MzUeyBYG0OvNCiYpdvz5SkAGAUHD5ZNFqn2w\nKKFD0I2Dr59BFVSGJwIDAQAB\n-----END PUBLIC KEY-----"
PUBLIC_ACCESS_SECRET_KEY: "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLW1tlHyKC9AG0hGpmkksET2DE\nr7ojSPemxFWAgFgcPJWQ7x3uNbsdJ3bIZFoA/FClaWKMCZmjnH9tv0bKZtY/CDhM\nZEyHpMruRSn6IKrxjtQZWy4uv/w6MzUeyBYG0OvNCiYpdvz5SkAGAUHD5ZNFqn2w\nKKFD0I2Dr59BFVSGJwIDAQAB\n-----END PUBLIC KEY-----"

@ -0,0 +1,8 @@
PUBLIC_ACCESS_SECRET_KEY="-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHgnvr7O2tiApjJfid1orFnIGm69
80fZp+Lpbjo+NC/0whMFga2Biw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6B
dA4TS2kB9Kf0wn0+7wSlyikHoKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y
+3GyaOY536H47qyXAgMBAAE=
-----END PUBLIC KEY-----"
SALT=10

@ -0,0 +1,61 @@
version: "3.3"
services:
hub-admin-backend-service:
container_name: hub-admin-backend-service
tty: true
build:
context: ../../.
dockerfile: Dockerfile
target: test
env_file:
- .env.test
environment:
- HTTP_HOST=0.0.0.0
- HTTP_PORT=8000
- DB_HOST=hub-admin-backend-db
- DB_PORT=27017
- DB_USERNAME=test
- DB_PASSWORD=test
- DB_NAME=admin
- ENVIRONMENT=staging
- AUTH_SERVICE_HOST=http://pena-auth-service
- AUTH_SERVICE_PORT=8000
depends_on:
- hub-admin-backend-migration
- hub-admin-backend-db
ports:
- 8001:8000
networks:
- test
hub-admin-backend-migration:
container_name: hub-admin-backend-migration
build:
context: ../../.
dockerfile: Dockerfile
target: test
command:
[
"sh",
"-c",
'migrate -source file://migrations -database "mongodb://test:test@hub-admin-backend-db:27017/admin?authSource=admin" up',
]
depends_on:
- hub-admin-backend-db
networks:
- test
hub-admin-backend-db:
container_name: hub-admin-backend-db
image: "mongo:6.0.3"
environment:
MONGO_INITDB_ROOT_USERNAME: test
MONGO_INITDB_ROOT_PASSWORD: test
ports:
- "27021:27017"
networks:
- test
networks:
test:

641
docs/openapi.yaml Normal file

@ -0,0 +1,641 @@
openapi: 3.0.0
info:
title: hub_admin_backend_service
description: hub_admin_backend_service
version: 1.0.0
tags:
- name : PrivilegeExternal
description: Внешний сервер
- name : PrivilegeInternal
description: Внутренний сервер
- name: TariffExternal
description: Внешний сервер
- name: TariffInternal
description: Внутренний сервер
paths:
/privilege/service:
get:
summary: Получить все привилегии поделенные по сервисам в ассоциативном массиве
tags:
- PrivilegeExternal
responses:
'200':
description: Успех. Получен ассоциативный массив, ключ - serviceKey, значение - массив привилегий этого сервиса.
content:
application/json:
schema:
$ref: "#/components/schemas/ServicePrivilegesMap"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
/privilege:
get:
summary: Получить все привилегии
tags:
- PrivilegeInternal
responses:
'200':
description: Успех. Возвращается список привилегий.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Privilege"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
post:
summary: Создать новую привилегию
tags:
- PrivilegeInternal
requestBody:
description: Данные для создания привилегии
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUpdateReq"
responses:
'200':
description: Привилегия успешно создана
content:
application/json:
schema:
$ref: "#/components/schemas/Privilege"
'400':
$ref: "#/components/responses/400"
'409':
$ref: "#/components/responses/409"
'500':
$ref: "#/components/responses/500"
put:
summary: Обновить существующую привилегию
tags:
- PrivilegeInternal
requestBody:
description: Данные для обновления привилегии
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUpdateReq"
responses:
'200':
description: Привилегия успешно обновлена
content:
application/json:
schema:
$ref: "#/components/schemas/Privilege"
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
delete:
summary: Удалить привилегию
tags:
- PrivilegeInternal
requestBody:
description: Данные для удвления привилегии
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DeleteRestoreReq"
responses:
'200':
description: Привилегия успешно удалена
content:
application/json:
schema:
$ref: "#/components/schemas/Privilege"
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
/privilege/{privilegeId}:
get:
summary: Получить привилегию по ID
tags:
- PrivilegeInternal
parameters:
- name: privilegeId
in: path
required: true
description: Идентификатор привилегии
schema:
type: string
responses:
'200':
description: Привилегия успешно получена
content:
application/json:
schema:
$ref: "#/components/schemas/Privilege"
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
/privilege/service/{serviceKey}:
get:
summary: Получить привилегии по ключу сервиса
tags:
- PrivilegeInternal
parameters:
- name: serviceKey
in: path
required: true
description: Ключ сервиса
schema:
type: string
responses:
'200':
description: Успешный ответ
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Privilege"
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
/privilege/many:
post:
summary: Создать несколько привилегий
tags:
- PrivilegeInternal
requestBody:
description: Данные для создания привилегий
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PostPutMany"
responses:
'200':
description: Успешный ответ
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Privilege"
'400':
$ref: "#/components/responses/400"
'409':
$ref: "#/components/responses/409"
'500':
$ref: "#/components/responses/500"
put:
summary: Обновить несколько привилегий
tags:
- PrivilegeInternal
requestBody:
description: Данные для обновления привилегий
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PostPutMany"
responses:
'200':
description: Успешный ответ
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Privilege"
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
/privilege/restore:
post:
summary: Восстановить удалённые привилегии
tags:
- PrivilegeInternal
requestBody:
description: Данные для восстановления привилегии
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DeleteRestoreReq"
responses:
'200':
description: Привилегия успешно восстановлена
content:
application/json:
schema:
$ref: "#/components/schemas/Privilege"
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
/tariff/{id}:
get:
summary: получить тарифф по id
tags:
- TariffExternal
- TariffInternal
responses:
'200':
description: Тарифф успешно получен
content:
application/json:
schema:
$ref: "#/components/schemas/Tariff"
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
put:
summary: обновить тарифф по id
tags:
- TariffInternal
parameters:
- name: id
in: path
required: true
schema:
type: string
description: id тарифа для обновления
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Tariff'
responses:
'200':
description: Тариф успешно обновлен
content:
application/json:
schema:
$ref: '#/components/schemas/Tariff'
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
/tariff/getList:
get:
summary: получить тариффы с пагинацией
tags:
- TariffExternal
- TariffInternal
parameters:
- name: page
in: query
description: Номер страницы для пагинации
required: false
schema:
type: integer
default: 1
- name: limit
in: query
description: Количество элементов на странице
required: false
schema:
type: integer
default: 25
responses:
'200':
description: Успешный ответ с тариффами
content:
application/json:
schema:
$ref: '#/components/schemas/TariffPagination'
'401':
$ref: "#/components/responses/401"
'500':
$ref: "#/components/responses/500"
/tariff/:
post:
summary: создать тарифф
tags:
- TariffExternal
- TariffInternal
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Tariff'
responses:
'200':
description: Тариф успешно создан
content:
application/json:
schema:
$ref: '#/components/schemas/Tariff'
'400':
$ref: "#/components/responses/400"
'401':
$ref: "#/components/responses/401"
'500':
$ref: "#/components/responses/500"
delete:
summary: метод мягкого удаления тариффа
tags:
- TariffInternal
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: string
description: id тарифа для удаления
required:
- id
responses:
'200':
description: Тариф успешно удален
content:
application/json:
schema:
$ref: '#/components/schemas/Tariff'
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
/tariff/restore:
post:
summary: метод восстановления тариффа из мягко удаленных
tags:
- TariffInternal
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: string
description: id тарифа для восстановления
required:
- id
responses:
'200':
description: Тариф успешно восстановлен
content:
application/json:
schema:
$ref: '#/components/schemas/Tariff'
'400':
$ref: "#/components/responses/400"
'404':
$ref: "#/components/responses/404"
'500':
$ref: "#/components/responses/500"
components:
schemas:
Privilege:
type: object
properties:
id:
type: string
description: Уникальный идентификатор привилегии
name:
type: string
description: Имя привилегии
privilegeId:
type: string
description: Идентификатор привилегии
serviceKey:
type: string
description: Ключ сервиса, к которому относится привилегия
description:
type: string
description: Описание привилегии
type:
type: string
description: Тип привилегии
value:
type: string
description: Значение привилегии
price:
type: number
format: float
description: Цена привилегии
createdAt:
type: string
format: date-time
description: Дата создания привилегии
updatedAt:
type: string
format: date-time
description: Дата последнего обновления привилегии
isDeleted:
type: boolean
description: Флаг удаления привилегии
deletedAt:
type: string
format: date-time
description: Дата удаления привилегии (если была удалена)
nullable: true
ServicePrivilegesMap:
type: object
description: Ассоциативный массив, где ключ - serviceKey, значение - массив привилегий этого сервиса.
additionalProperties:
type: array
items:
$ref: "#/components/schemas/Privilege"
CreateUpdateReq:
type: object
description: Данные для создания или обновления привилегии
properties:
name:
type: string
description: Имя привилегии
privilegeId:
type: string
description: Уникальный идентификатор привилегии
serviceKey:
type: string
description: Ключ сервиса, к которому относится привилегия
description:
type: string
description: Описание привилегии
type:
type: string
description: Тип привилегии
value:
type: string
description: Значение привилегии
price:
type: number
format: float
description: Цена привилегии
DeleteRestoreReq:
type: object
description: Данные для удаления, восстановления привилегии
properties:
privilegeId:
type: string
description: Идентификатор привилегии, которую нужно удалить
PostPutMany:
type: object
description: Данные для создания, обновления массива привилегий
properties:
privileges:
type: array
description: Массив объектов для создания или обновления привилегий
items:
$ref: "#/components/schemas/CreateUpdateReq"
Tariff:
type: object
properties:
_id:
type: string
format: objectId
description: id тарифа objectID в mongo
name:
type: string
description: Название тариффа
userID:
type: string
description: id пользователя котторому принадлежит тариф, либо пустое если тарифф создаваля админом
description:
type: string
description: Описание тариффа
price:
type: integer
description: цена тарифа
order:
type: integer
description:
isCustom:
type: boolean
description: Флаг состояни кастомного тариффа
privileges:
type: array
items:
$ref: '#/components/schemas/Privilege'
description: Привилегии входящие в тарифф
isDeleted:
type: boolean
description: Флаг состояния удаления
createdAt:
type: string
format: date-time
description: Время создания
updatedAt:
type: string
format: date-time
description: Последнее время удаления
deletedAt:
type: string
format: date-time
description: Время удаления
TariffPagination:
type: object
properties:
totalPages:
type: integer
description: количество страниц на количесво элементов
tariffs:
type: array
items:
$ref: '#/components/schemas/Tariff'
responses:
'200':
description: Success
content:
application/json:
schema:
type: string
description: Success
'201':
description: Created
content:
application/json:
schema:
type: string
description: Created
'204':
description: No content
content:
application/json:
schema:
type: string
description: No content
'400':
description: Bad Request
content:
application/json:
schema:
type: string
description: Bad Request
'401':
description: Unauthorized
content:
application/json:
schema:
type: string
description: Unauthorized
'403':
description: Forbidden
content:
application/json:
schema:
type: string
description: Forbidden
'404':
description: Not Found
content:
application/json:
schema:
type: string
description: Not Found
'409':
description: Conflict
content:
application/json:
schema:
type: string
description: Already exist
'500':
description: Internal Server Error
content:
application/json:
schema:
type: string
description: Internal Server Error

40
go.mod Normal file

@ -0,0 +1,40 @@
module gitea.pena/PenaSide/tariffs
go 1.23.2
toolchain go1.23.4
require (
gitea.pena/PenaSide/common v0.0.0-20241231090536-2454377ad2a0
github.com/caarlos0/env/v8 v8.0.0
github.com/gofiber/fiber/v2 v2.52.4
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joho/godotenv v1.5.1
go.mongodb.org/mongo-driver v1.15.0
go.uber.org/zap v1.27.0
)
require (
gitea.pena/PenaSide/linters-golang v0.0.0-20241207122018-933207374735 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
)

99
go.sum Normal file

@ -0,0 +1,99 @@
gitea.pena/PenaSide/common v0.0.0-20241231090536-2454377ad2a0 h1:B4+DAND6gJnCsc9DZY6XyMVLzD23nI8whk8xI7XKGrM=
gitea.pena/PenaSide/common v0.0.0-20241231090536-2454377ad2a0/go.mod h1:U7QFuvkrIWyb/m/SOyrsroS7DJntjcr9k7kNy3vtPdU=
gitea.pena/PenaSide/linters-golang v0.0.0-20241207122018-933207374735 h1:jDVeUhGBTXBibmW5dmtJg2m2+z5z2Rf6J4G0LpjVoJ0=
gitea.pena/PenaSide/linters-golang v0.0.0-20241207122018-933207374735/go.mod h1:gdd+vOT6up9STkEbxa2qESLIMZFjCmRbkcheFQCVgZU=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0=
github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc=
go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

105
internal/app/app.go Normal file

@ -0,0 +1,105 @@
package app
import (
"context"
"errors"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/controller/middleware"
"gitea.pena/PenaSide/tariffs/internal/initialize"
"gitea.pena/PenaSide/tariffs/internal/models"
"gitea.pena/PenaSide/tariffs/internal/server/http"
"gitea.pena/PenaSide/tariffs/pkg/closer"
"time"
)
func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
defer func() {
if r := recover(); r != nil {
logger.Error("Recovered from a panic", zap.Any("error", r))
}
}()
logger.Info("Starting application", zap.Any("AppCFG", cfg))
ctx, cancel := context.WithCancel(ctx)
defer cancel()
shutdownGroup := closer.NewCloserGroup()
mdb, err := initialize.MongoDB(ctx, cfg)
if err != nil {
logger.Error("Failed to initialize MongoDB", zap.Error(err))
return err
}
mw := middleware.NewMiddleware(logger)
repositories := initialize.NewRepository(initialize.RepositoryDeps{
Logger: logger,
Mdb: mdb,
})
controllers := initialize.NewControllers(initialize.ControllerDeps{
Logger: logger,
Repos: repositories,
MW: mw,
})
internalSrv := http.NewServer(http.ServerConfig{
Logger: logger,
Controllers: []http.Controller{controllers.PrivilegeInternal, controllers.TariffInternal},
JWTConfig: &models.JWTConfiguration{
PrivateKey: cfg.PrivateKey,
PublicKey: cfg.PublicKey,
Issuer: cfg.Issuer,
Audience: cfg.Audience,
},
})
externalSrv := http.NewServer(http.ServerConfig{
Logger: logger,
Controllers: []http.Controller{controllers.PrivilegeExternal, controllers.TariffExternal},
JWTConfig: &models.JWTConfiguration{
PrivateKey: cfg.PrivateKey,
PublicKey: cfg.PublicKey,
Issuer: cfg.Issuer,
Audience: cfg.Audience,
},
})
go func() {
if err := internalSrv.Start(cfg.InternalHttpAddress); err != nil {
logger.Error("Server startup error", zap.Error(err))
cancel()
}
}()
go func() {
if err := externalSrv.Start(cfg.ExternalHttpAddress); err != nil {
logger.Error("Server startup error", zap.Error(err))
cancel()
}
}()
internalSrv.ListRoutes()
externalSrv.ListRoutes()
shutdownGroup.Add(closer.CloserFunc(internalSrv.Shutdown))
shutdownGroup.Add(closer.CloserFunc(externalSrv.Shutdown))
shutdownGroup.Add(closer.CloserFunc(mdb.Client().Disconnect))
<-ctx.Done()
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer timeoutCancel()
if err := shutdownGroup.Call(timeoutCtx); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
logger.Error("Shutdown timed out", zap.Error(err))
} else {
logger.Error("Failed to shutdown services gracefully", zap.Error(err))
}
return err
}
logger.Info("Application has stopped")
return nil
}

@ -0,0 +1,44 @@
package middleware
import (
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/models"
"strconv"
)
type MiddleWare struct {
logger *zap.Logger
}
func NewMiddleware(logger *zap.Logger) *MiddleWare {
return &MiddleWare{
logger: logger,
}
}
func (mw *MiddleWare) ExtractUserID(ctx *fiber.Ctx) (string, bool) {
id, ok := ctx.Context().UserValue(models.AuthJWTDecodedUserIDKey).(string)
return id, ok
}
func (mw *MiddleWare) ExtractToken(ctx *fiber.Ctx) (string, bool) {
token, ok := ctx.Context().UserValue(models.AuthJWTDecodedAccessTokenKey).(string)
return token, ok
}
func (mw *MiddleWare) GetPaginationParameters(ctx *fiber.Ctx) (int64, int64) {
page := int64(1)
limit := int64(25)
if p := ctx.Query("page"); p != "" {
if parPage, err := strconv.ParseInt(p, 10, 64); err == nil {
page = parPage
}
}
if l := ctx.Query("limit"); l != "" {
if parLimit, err := strconv.ParseInt(l, 10, 64); err == nil {
limit = parLimit
}
}
return page, limit
}

@ -0,0 +1,41 @@
package privilege_external
import (
"errors"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
our_errors "gitea.pena/PenaSide/tariffs/internal/errors"
"gitea.pena/PenaSide/tariffs/internal/repository/privilege"
"gitea.pena/PenaSide/tariffs/internal/tools"
)
type Deps struct {
Repo *privilege.Privilege
Logger *zap.Logger
}
type PrivilegeExternal struct {
repo *privilege.Privilege
logger *zap.Logger
}
func NewPrivilegeExternal(deps Deps) *PrivilegeExternal {
return &PrivilegeExternal{
repo: deps.Repo,
logger: deps.Logger,
}
}
// хаб нода getAllPrivilegesMap
func (p *PrivilegeExternal) GetAllService(c *fiber.Ctx) error {
privileges, err := p.repo.GetAllPrivileges(c.Context())
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Privileges not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(tools.ConvertPrivilegesToMap(privileges))
}

@ -0,0 +1,13 @@
package privilege_external
import (
"github.com/gofiber/fiber/v2"
)
func (p *PrivilegeExternal) Register(router fiber.Router) {
router.Get("/service", p.GetAllService)
}
func (p *PrivilegeExternal) Name() string {
return "privilege"
}

@ -0,0 +1,244 @@
package privilege_internal
import (
"errors"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
our_errors "gitea.pena/PenaSide/tariffs/internal/errors"
"gitea.pena/PenaSide/tariffs/internal/models"
"gitea.pena/PenaSide/tariffs/internal/repository/privilege"
"gitea.pena/PenaSide/tariffs/internal/tools"
)
type Deps struct {
Repo *privilege.Privilege
Logger *zap.Logger
}
type PrivilegeInternal struct {
repo *privilege.Privilege
logger *zap.Logger
}
func NewPrivilegeInternal(deps Deps) *PrivilegeInternal {
return &PrivilegeInternal{
repo: deps.Repo,
logger: deps.Logger,
}
}
// хаб нода getAllPrivileges
func (p *PrivilegeInternal) Get(c *fiber.Ctx) error {
privileges, err := p.repo.GetAllPrivileges(c.Context())
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Privileges not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(privileges)
}
// хаб нода registerPrivilege
func (p *PrivilegeInternal) Create(c *fiber.Ctx) error {
var req models.CreateUpdateReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if !tools.ValidatePrivilege(req) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing required fields"})
}
result, err := p.repo.Create(c.Context(), req)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrAlreadyExist):
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "Privilege already exist"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(result)
}
// хаб нода replacePrivilege
func (p *PrivilegeInternal) Update(c *fiber.Ctx) error {
var req models.CreateUpdateReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if !tools.ValidatePrivilege(req) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing required fields"})
}
result, err := p.repo.Update(c.Context(), req)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Privilege not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(result)
}
// хаб нода removePrivilege
func (p *PrivilegeInternal) Delete(c *fiber.Ctx) error {
var req struct {
ID string `json:"privilegeId"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.ID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "PrivilegeID is required"})
}
result, err := p.repo.Delete(c.Context(), req.ID)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Privilege not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(result)
}
// хаб нода getPrivilege
func (p *PrivilegeInternal) GetByID(c *fiber.Ctx) error {
privilegeID := c.Params("privilegeId")
if privilegeID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Privilege ID is required"})
}
result, err := p.repo.GetByID(c.Context(), privilegeID)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Privilege not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(result)
}
// хаб нода getServicePrivileges
func (p *PrivilegeInternal) GetByService(c *fiber.Ctx) error {
serviceKey := c.Params("serviceKey")
if serviceKey == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Service key is required"})
}
result, err := p.repo.GetByServiceKey(c.Context(), serviceKey)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Privileges not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(result)
}
// хаб нода registerPrivileges
func (p *PrivilegeInternal) PostMany(c *fiber.Ctx) error {
var req models.ManyCreateUpdate
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if len(req.Privileges) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "len array dont be 0"})
}
for _, priv := range req.Privileges {
if !tools.ValidatePrivilege(priv) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing required fields"})
}
}
result, err := p.repo.PostMany(c.Context(), req)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrAlreadyExist):
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "One is privilege already exist"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(result)
}
// хаб нода replacePrivileges
func (p *PrivilegeInternal) UpdateMany(c *fiber.Ctx) error {
var req models.ManyCreateUpdate
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if len(req.Privileges) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "len array dont be 0"})
}
for _, priv := range req.Privileges {
if !tools.ValidatePrivilege(priv) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing required fields"})
}
}
result, err := p.repo.UpdateMany(c.Context(), req)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "One is privilege not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(result)
}
// хаб нода restorePrivilege
func (p *PrivilegeInternal) Restore(c *fiber.Ctx) error {
var req struct {
ID string `json:"privilegeId"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.ID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "PrivilegeID is required"})
}
result, err := p.repo.RestorePrivilege(c.Context(), req.ID)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Privilege not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return c.Status(fiber.StatusOK).JSON(result)
}

@ -0,0 +1,19 @@
package privilege_internal
import "github.com/gofiber/fiber/v2"
func (p *PrivilegeInternal) Register(router fiber.Router) {
router.Get("/", p.Get)
router.Post("/", p.Create)
router.Put("/", p.Update)
router.Delete("/", p.Delete)
router.Get("/:privilegeId", p.GetByID)
router.Get("/service/:serviceKey", p.GetByService)
router.Post("/many", p.PostMany)
router.Put("/many", p.UpdateMany)
router.Post("/restore", p.Restore)
}
func (p *PrivilegeInternal) Name() string {
return "privilege"
}

@ -0,0 +1,98 @@
package tariff_external
import (
"errors"
"github.com/gofiber/fiber/v2"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/controller/middleware"
our_errors "gitea.pena/PenaSide/tariffs/internal/errors"
"gitea.pena/PenaSide/tariffs/internal/models"
"gitea.pena/PenaSide/tariffs/internal/repository/tariff"
"gitea.pena/PenaSide/tariffs/internal/tools"
)
type Deps struct {
Repo *tariff.Tariff
Logger *zap.Logger
MiddleWare *middleware.MiddleWare
}
type TariffExternal struct {
repo *tariff.Tariff
logger *zap.Logger
middleWare *middleware.MiddleWare
}
func NewTariffExternal(deps Deps) *TariffExternal {
return &TariffExternal{
repo: deps.Repo,
logger: deps.Logger,
middleWare: deps.MiddleWare,
}
}
func (t *TariffExternal) Get(ctx *fiber.Ctx) error {
tariffID := ctx.Params("id")
if tariffID == "" {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url field id don't be empty"})
}
objID, err := primitive.ObjectIDFromHex(tariffID)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "no valid object tariff id"})
}
result, err := t.repo.GetByID(ctx.Context(), objID)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tariff not found"})
default:
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return ctx.Status(fiber.StatusOK).JSON(result)
}
func (t *TariffExternal) GetList(ctx *fiber.Ctx) error {
page, limit := t.middleWare.GetPaginationParameters(ctx)
userID, ok := t.middleWare.ExtractUserID(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No auth"})
}
result, err := t.repo.GetList(ctx.Context(), page, limit, userID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return ctx.Status(fiber.StatusOK).JSON(result)
}
func (t *TariffExternal) Create(ctx *fiber.Ctx) error {
userID, ok := t.middleWare.ExtractUserID(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No auth"})
}
var req models.Tariff
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
req.UserID = userID
err := tools.ValidateTariff(req)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
result, err := t.repo.Create(ctx.Context(), req)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return ctx.Status(fiber.StatusOK).JSON(result)
}

@ -0,0 +1,13 @@
package tariff_external
import "github.com/gofiber/fiber/v2"
func (t *TariffExternal) Register(router fiber.Router) {
router.Get("/getList", t.GetList)
router.Get("/:id", t.Get)
router.Post("/", t.Create)
}
func (t *TariffExternal) Name() string {
return "tariff"
}

@ -0,0 +1,179 @@
package tariff_internal
import (
"errors"
"github.com/gofiber/fiber/v2"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/controller/middleware"
our_errors "gitea.pena/PenaSide/tariffs/internal/errors"
"gitea.pena/PenaSide/tariffs/internal/models"
"gitea.pena/PenaSide/tariffs/internal/repository/tariff"
"gitea.pena/PenaSide/tariffs/internal/tools"
)
// todo middleware jwt
type Deps struct {
Repo *tariff.Tariff
Logger *zap.Logger
MiddleWare *middleware.MiddleWare
}
type TariffInternal struct {
repo *tariff.Tariff
logger *zap.Logger
middleWare *middleware.MiddleWare
}
func NewTariffInternal(deps Deps) *TariffInternal {
return &TariffInternal{
repo: deps.Repo,
logger: deps.Logger,
middleWare: deps.MiddleWare,
}
}
func (t *TariffInternal) Get(ctx *fiber.Ctx) error {
tariffID := ctx.Params("id")
if tariffID == "" {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url field id don't be empty"})
}
objID, err := primitive.ObjectIDFromHex(tariffID)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "no valid object tariff id"})
}
result, err := t.repo.GetByID(ctx.Context(), objID)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tariff not found"})
default:
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return ctx.Status(fiber.StatusOK).JSON(result)
}
func (t *TariffInternal) GetList(ctx *fiber.Ctx) error {
page, limit := t.middleWare.GetPaginationParameters(ctx)
result, err := t.repo.GetList(ctx.Context(), page, limit, "")
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return ctx.Status(fiber.StatusOK).JSON(result)
}
func (t *TariffInternal) Create(ctx *fiber.Ctx) error {
var req models.Tariff
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
err := tools.ValidateTariff(req)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
result, err := t.repo.Create(ctx.Context(), req)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return ctx.Status(fiber.StatusOK).JSON(result)
}
func (t *TariffInternal) Delete(ctx *fiber.Ctx) error {
var req struct {
ID string `json:"id"`
}
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.ID == "" {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Tariff id don't be nil"})
}
objID, err := primitive.ObjectIDFromHex(req.ID)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "no valid object tariff id"})
}
result, err := t.repo.SoftDelete(ctx.Context(), objID)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tariff not found"})
default:
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return ctx.Status(fiber.StatusOK).JSON(result)
}
func (t *TariffInternal) Update(ctx *fiber.Ctx) error {
var req models.Tariff
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
err := tools.ValidateTariff(req)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
id := ctx.Params("id")
objID, err := primitive.ObjectIDFromHex(id)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "no valid object tariff id"})
}
result, err := t.repo.Update(ctx.Context(), objID, req)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tariff not found"})
default:
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return ctx.Status(fiber.StatusOK).JSON(result)
}
func (t *TariffInternal) Restore(ctx *fiber.Ctx) error {
var req struct {
ID string `json:"id"`
}
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.ID == "" {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id don`t be empty"})
}
objID, err := primitive.ObjectIDFromHex(req.ID)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "no valid object tariff id"})
}
result, err := t.repo.Restore(ctx.Context(), objID)
if err != nil {
switch {
case errors.Is(err, our_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Tariff not found"})
default:
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
}
return ctx.Status(fiber.StatusOK).JSON(result)
}

@ -0,0 +1,16 @@
package tariff_internal
import "github.com/gofiber/fiber/v2"
func (t *TariffInternal) Register(router fiber.Router) {
router.Get("/:id", t.Get)
router.Get("/getList", t.GetList)
router.Post("/", t.Create)
router.Delete("/", t.Delete)
router.Put("/:id", t.Update)
router.Post("/restore", t.Restore)
}
func (t *TariffInternal) Name() string {
return "tariff"
}

@ -0,0 +1,8 @@
package errors
import "errors"
var (
ErrAlreadyExist = errors.New("already exist")
ErrNotFound = errors.New("not found")
)

@ -0,0 +1,34 @@
package initialize
import (
"gitea.pena/PenaSide/common/mongo"
"github.com/caarlos0/env/v8"
"github.com/joho/godotenv"
"log"
)
type Config struct {
AppName string `env:"APP_NAME" envDefault:"hub-admin-backend-service"`
InternalHttpAddress string `env:"INTERNAL_HTTP_ADDRESS" envDefault:"localhost:8000"`
ExternalHttpAddress string `env:"EXTERNAL_HTTP_ADDRESS" envDefault:"localhost:8001"`
PrivateKey string `env:"JWT_PRIVATE_KEY"`
PublicKey string `env:"JWT_PUBLIC_KEY"`
Issuer string `env:"JWT_ISSUER"`
Audience string `env:"JWT_AUDIENCE"`
ExternalCfg ExternalCfg
}
type ExternalCfg struct {
Database mongo.Configuration
}
func LoadConfig() (*Config, error) {
if err := godotenv.Load(); err != nil {
log.Print("No .env file found")
}
var config Config
if err := env.Parse(&config); err != nil {
return nil, err
}
return &config, nil
}

@ -0,0 +1,46 @@
package initialize
import (
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/controller/middleware"
"gitea.pena/PenaSide/tariffs/internal/controller/privilege_external"
"gitea.pena/PenaSide/tariffs/internal/controller/privilege_internal"
"gitea.pena/PenaSide/tariffs/internal/controller/tariff_external"
"gitea.pena/PenaSide/tariffs/internal/controller/tariff_internal"
)
type ControllerDeps struct {
Logger *zap.Logger
Repos *Repository
MW *middleware.MiddleWare
}
type Controller struct {
PrivilegeInternal *privilege_internal.PrivilegeInternal
PrivilegeExternal *privilege_external.PrivilegeExternal
TariffInternal *tariff_internal.TariffInternal
TariffExternal *tariff_external.TariffExternal
}
func NewControllers(deps ControllerDeps) *Controller {
return &Controller{
PrivilegeInternal: privilege_internal.NewPrivilegeInternal(privilege_internal.Deps{
Repo: deps.Repos.PrivilegeRepo,
Logger: deps.Logger,
}),
PrivilegeExternal: privilege_external.NewPrivilegeExternal(privilege_external.Deps{
Repo: deps.Repos.PrivilegeRepo,
Logger: deps.Logger,
}),
TariffInternal: tariff_internal.NewTariffInternal(tariff_internal.Deps{
Logger: deps.Logger,
Repo: deps.Repos.TariffRepo,
MiddleWare: deps.MW,
}),
TariffExternal: tariff_external.NewTariffExternal(tariff_external.Deps{
Logger: deps.Logger,
Repo: deps.Repos.TariffRepo,
MiddleWare: deps.MW,
}),
}
}

@ -0,0 +1,35 @@
package initialize
import (
"context"
mdb "gitea.pena/PenaSide/common/mongo"
"go.mongodb.org/mongo-driver/mongo"
"time"
)
func MongoDB(ctx context.Context, cfg Config) (*mongo.Database, error) {
dbConfig := &mdb.Configuration{
URL: cfg.ExternalCfg.Database.URL,
DatabaseName: cfg.ExternalCfg.Database.DatabaseName,
}
newCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
mongoDeps := &mdb.ConnectDeps{
Configuration: dbConfig,
Timeout: 10 * time.Second,
}
db, err := mdb.Connect(newCtx, mongoDeps)
if err != nil {
return nil, err
}
err = db.Client().Ping(newCtx, nil)
if err != nil {
return nil, err
}
return db, nil
}

@ -0,0 +1,32 @@
package initialize
import (
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/repository/privilege"
"gitea.pena/PenaSide/tariffs/internal/repository/tariff"
)
type RepositoryDeps struct {
Logger *zap.Logger
Mdb *mongo.Database
}
type Repository struct {
PrivilegeRepo *privilege.Privilege
TariffRepo *tariff.Tariff
}
func NewRepository(deps RepositoryDeps) *Repository {
return &Repository{
PrivilegeRepo: privilege.NewPrivilegeRepo(privilege.Deps{
Mdb: deps.Mdb.Collection("privileges"),
Logger: deps.Logger,
}),
TariffRepo: tariff.NewTariffRepo(tariff.Deps{
Mdb: deps.Mdb.Collection("tariffs"),
Logger: deps.Logger,
PrivilegeDB: deps.Mdb.Collection("privileges"),
}),
}
}

18
internal/models/jwt.go Normal file

@ -0,0 +1,18 @@
package models
import (
"github.com/golang-jwt/jwt/v5"
"time"
)
type JWTConfiguration struct {
PrivateKey string
PublicKey string
Issuer string
Audience string
Algorithm jwt.SigningMethodRSA
ExpiresIn time.Duration
}
const AuthJWTDecodedUserIDKey = "userID"
const AuthJWTDecodedAccessTokenKey = "access-token"

@ -0,0 +1,24 @@
package models
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"time"
)
type Privilege struct {
ID primitive.ObjectID `bson:"_id" json:"_id"`
Name string `bson:"name" json:"name"`
PrivilegeID string `bson:"privilegeId" json:"privilegeId"`
ServiceKey string `bson:"serviceKey" json:"serviceKey"`
Description string `bson:"description" json:"description"`
Type string `bson:"type" json:"type"`
Value string `bson:"value" json:"value"`
Price float64 `bson:"price" json:"price"`
Amount float64 `bson:"amount" json:"amount"`
CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
IsDeleted bool `bson:"isDeleted" json:"isDeleted"`
DeletedAt *time.Time `bson:"deletedAt" json:"deletedAt"`
}

@ -0,0 +1,20 @@
package models
type CreateUpdateReq struct {
Name string `json:"name"`
PrivilegeId string `json:"privilegeId"`
ServiceKey string `json:"serviceKey"`
Description string `json:"description"`
Type string `json:"type"`
Value string `json:"value"`
Price float64 `json:"price"`
}
type ManyCreateUpdate struct {
Privileges []CreateUpdateReq `json:"privileges"`
}
type TariffPagination struct {
TotalPages int `json:"totalPages"`
Tariffs []Tariff `json:"tariffs"`
}

21
internal/models/tariff.go Normal file

@ -0,0 +1,21 @@
package models
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"time"
)
type Tariff struct {
ID primitive.ObjectID `json:"_id" bson:"_id"`
Name string `json:"name" bson:"name"`
UserID string `json:"userID" bson:"userID"`
Description string `json:"description" bson:"description"`
Price int `json:"price" bson:"price"`
Order int `json:"order" bson:"order"`
IsCustom bool `json:"isCustom" bson:"isCustom"`
Privileges []Privilege `json:"privileges" bson:"privileges"`
IsDeleted bool `json:"isDeleted" bson:"isDeleted"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
DeletedAt time.Time `json:"deletedAt" bson:"deletedAt"`
}

@ -0,0 +1,297 @@
package privilege
import (
"context"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/errors"
"gitea.pena/PenaSide/tariffs/internal/models"
"time"
)
type Deps struct {
Mdb *mongo.Collection
Logger *zap.Logger
}
type Privilege struct {
mdb *mongo.Collection
logger *zap.Logger
}
func NewPrivilegeRepo(deps Deps) *Privilege {
return &Privilege{
mdb: deps.Mdb,
logger: deps.Logger,
}
}
func (p *Privilege) GetAllPrivileges(ctx context.Context) ([]models.Privilege, error) {
filter := bson.M{"isDeleted": false}
opts := options.Find()
cursor, err := p.mdb.Find(ctx, filter, opts)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.ErrNotFound
}
p.logger.Error("failed to find privileges", zap.Error(err))
return nil, err
}
defer cursor.Close(ctx)
var privileges []models.Privilege
if err = cursor.All(ctx, &privileges); err != nil {
p.logger.Error("failed to decode privileges", zap.Error(err))
return nil, err
}
return privileges, nil
}
func (p *Privilege) Create(ctx context.Context, req models.CreateUpdateReq) (models.Privilege, error) {
exist := models.Privilege{}
err := p.mdb.FindOne(ctx, bson.M{"privilegeId": req.PrivilegeId}).Decode(&exist)
if err == nil {
return models.Privilege{}, errors.ErrAlreadyExist
} else if err != mongo.ErrNoDocuments {
p.logger.Error("Failed to check existing privilege", zap.Error(err))
return models.Privilege{}, err
}
privilege := models.Privilege{
ID: primitive.NewObjectID(),
Name: req.Name,
PrivilegeID: req.PrivilegeId,
ServiceKey: req.ServiceKey,
Description: req.Description,
Type: req.Type,
Value: req.Value,
Price: req.Price,
CreatedAt: time.Now(),
IsDeleted: false,
}
_, err = p.mdb.InsertOne(ctx, privilege)
if err != nil {
p.logger.Error("Failed to create privilege", zap.Error(err))
return models.Privilege{}, err
}
return privilege, nil
}
func (p *Privilege) Update(ctx context.Context, req models.CreateUpdateReq) (models.Privilege, error) {
exist := models.Privilege{}
err := p.mdb.FindOne(ctx, bson.M{"privilegeId": req.PrivilegeId}).Decode(&exist)
if err != nil {
if err == mongo.ErrNoDocuments {
return models.Privilege{}, errors.ErrNotFound
}
p.logger.Error("Failed to find privilege", zap.Error(err))
return models.Privilege{}, err
}
currTime := time.Now()
update := bson.M{
"$set": bson.M{
"name": req.Name,
"serviceKey": req.ServiceKey,
"description": req.Description,
"type": req.Type,
"value": req.Value,
"price": req.Price,
"updatedAt": currTime,
},
}
_, err = p.mdb.UpdateOne(ctx, bson.M{"privilegeId": req.PrivilegeId}, update)
if err != nil {
p.logger.Error("Failed to update privilege", zap.Error(err))
return models.Privilege{}, err
}
exist.Name = req.Name
exist.ServiceKey = req.ServiceKey
exist.Description = req.Description
exist.Type = req.Type
exist.Value = req.Value
exist.Price = req.Price
exist.UpdatedAt = currTime
return exist, nil
}
func (p *Privilege) Delete(ctx context.Context, id string) (models.Privilege, error) {
var privilege models.Privilege
filter := bson.M{"privilegeId": id}
update := bson.M{
"$set": bson.M{
"isDeleted": true,
"deletedAt": time.Now(),
},
}
opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
err := p.mdb.FindOneAndUpdate(ctx, filter, update, opts).Decode(&privilege)
if err != nil {
if err == mongo.ErrNoDocuments {
return models.Privilege{}, errors.ErrNotFound
}
p.logger.Error("failed to delete privilege", zap.Error(err))
return models.Privilege{}, err
}
return privilege, nil
}
func (p *Privilege) GetByID(ctx context.Context, id string) (models.Privilege, error) {
var privilege models.Privilege
filter := bson.M{
"privilegeId": id,
"isDeleted": false,
}
err := p.mdb.FindOne(ctx, filter).Decode(&privilege)
if err != nil {
if err == mongo.ErrNoDocuments {
return models.Privilege{}, errors.ErrNotFound
}
p.logger.Error("failed to get privilege by privilege id", zap.Error(err))
return models.Privilege{}, err
}
return privilege, nil
}
func (p *Privilege) GetByServiceKey(ctx context.Context, serviceKey string) ([]models.Privilege, error) {
filter := bson.M{
"serviceKey": serviceKey,
"isDeleted": false,
}
opts := options.Find()
cursor, err := p.mdb.Find(ctx, filter, opts)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.ErrNotFound
}
p.logger.Error("failed to find privileges by service key", zap.Error(err))
return nil, err
}
defer cursor.Close(ctx)
var privileges []models.Privilege
if err = cursor.All(ctx, &privileges); err != nil {
p.logger.Error("failed to decode privileges", zap.Error(err))
return nil, err
}
return privileges, nil
}
func (p *Privilege) PostMany(ctx context.Context, req models.ManyCreateUpdate) ([]models.Privilege, error) {
var privileges []models.Privilege
for _, r := range req.Privileges {
exist := models.Privilege{}
err := p.mdb.FindOne(ctx, bson.M{"privilegeId": r.PrivilegeId}).Decode(&exist)
if err == nil {
p.logger.Error("privilege already exists", zap.String("privilegeId", r.PrivilegeId))
return nil, errors.ErrAlreadyExist
} else if err != mongo.ErrNoDocuments {
p.logger.Error("failed to check existing privilege", zap.Error(err))
return nil, err
}
privileges = append(privileges, models.Privilege{
ID: primitive.NewObjectID(),
Name: r.Name,
PrivilegeID: r.PrivilegeId,
ServiceKey: r.ServiceKey,
Description: r.Description,
Type: r.Type,
Value: r.Value,
Price: r.Price,
CreatedAt: time.Now(),
IsDeleted: false,
})
}
noneType := make([]interface{}, len(privileges))
for i, v := range privileges {
noneType[i] = v
}
_, err := p.mdb.InsertMany(ctx, noneType)
if err != nil {
p.logger.Error("failed to insert privileges", zap.Error(err))
return nil, err
}
return privileges, nil
}
func (p *Privilege) UpdateMany(ctx context.Context, req models.ManyCreateUpdate) ([]models.Privilege, error) {
var updated []models.Privilege
for _, r := range req.Privileges {
filter := bson.M{"privilegeId": r.PrivilegeId}
update := bson.M{
"$set": bson.M{
"name": r.Name,
"serviceKey": r.ServiceKey,
"description": r.Description,
"type": r.Type,
"value": r.Value,
"price": r.Price,
"updatedAt": time.Now(),
},
}
var updatedPrivilege models.Privilege
opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
err := p.mdb.FindOneAndUpdate(ctx, filter, update, opts).Decode(&updatedPrivilege)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.ErrNotFound
}
p.logger.Error("failed to update privilege", zap.Error(err))
return nil, err
}
updated = append(updated, updatedPrivilege)
}
return updated, nil
}
func (p *Privilege) RestorePrivilege(ctx context.Context, id string) (models.Privilege, error) {
exist := models.Privilege{}
err := p.mdb.FindOne(ctx, bson.M{"privilegeId": id}).Decode(&exist)
if err != nil {
if err == mongo.ErrNoDocuments {
return models.Privilege{}, errors.ErrNotFound
}
p.logger.Error("Failed to find privilege", zap.Error(err))
return models.Privilege{}, err
}
currTime := time.Now()
update := bson.M{
"$set": bson.M{
"isDeleted": false,
"updatedAt": currTime,
},
}
_, err = p.mdb.UpdateOne(ctx, bson.M{"privilegeId": id}, update)
if err != nil {
p.logger.Error("Failed to restore privilege", zap.Error(err))
return models.Privilege{}, err
}
exist.IsDeleted = false
exist.UpdatedAt = currTime
return exist, nil
}

@ -0,0 +1,210 @@
package tariff
import (
"context"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/errors"
"gitea.pena/PenaSide/tariffs/internal/models"
"math"
"time"
)
type Deps struct {
Mdb *mongo.Collection
Logger *zap.Logger
PrivilegeDB *mongo.Collection
}
type Tariff struct {
mdb *mongo.Collection
logger *zap.Logger
privilegeDB *mongo.Collection
}
func NewTariffRepo(deps Deps) *Tariff {
return &Tariff{
mdb: deps.Mdb,
logger: deps.Logger,
privilegeDB: deps.PrivilegeDB,
}
}
func (t *Tariff) GetByID(ctx context.Context, id primitive.ObjectID) (models.Tariff, error) {
var tariff models.Tariff
filter := bson.M{
"_id": id,
"isDeleted": false,
}
err := t.mdb.FindOne(ctx, filter).Decode(&tariff)
if err != nil {
if err == mongo.ErrNoDocuments {
return tariff, errors.ErrNotFound
}
t.logger.Error("failed to get tariff by object id", zap.Error(err))
return tariff, err
}
return tariff, nil
}
func (t *Tariff) Create(ctx context.Context, req models.Tariff) (models.Tariff, error) {
req.ID = primitive.NewObjectID()
req.CreatedAt = time.Now()
req.IsDeleted = false
req.UpdatedAt = time.Now()
_, err := t.mdb.InsertOne(ctx, req)
if err != nil {
t.logger.Error("failed insert tariff", zap.Error(err))
return req, err
}
return req, nil
}
func (t *Tariff) GetList(ctx context.Context, page, limit int64, userID string) (models.TariffPagination, error) {
var result models.TariffPagination
filter := bson.M{
"$or": []bson.M{
{"isCustom": false, "isDeleted": false},
},
}
if userID != "" {
filter["$or"] = append(filter["$or"].([]bson.M), bson.M{"isCustom": true, "userID": userID})
}
count, err := t.mdb.CountDocuments(ctx, filter)
if err != nil {
t.logger.Error("failed count tariffs", zap.Error(err))
return result, err
}
totalPages := int(math.Ceil(float64(count) / float64(limit)))
offset := (page - 1) * limit
findOptions := options.Find().SetSort(bson.D{{"order", 1}}).SetSkip(offset).SetLimit(limit)
cursor, err := t.mdb.Find(ctx, filter, findOptions)
if err != nil {
t.logger.Error("failed find tariffs", zap.Error(err))
return result, err
}
defer cursor.Close(ctx)
var tariffs []models.Tariff
if err = cursor.All(ctx, &tariffs); err != nil {
t.logger.Error("failed decode tariffs", zap.Error(err))
return result, err
}
result.TotalPages = totalPages
result.Tariffs = tariffs
return result, nil
}
func (t *Tariff) SoftDelete(ctx context.Context, tariffID primitive.ObjectID) (models.Tariff, error) {
var tariff models.Tariff
filter := bson.M{"_id": tariffID}
update := bson.M{"$set": bson.M{"isDeleted": true, "deletedAt": time.Now()}}
err := t.mdb.FindOneAndUpdate(ctx, filter, update, options.FindOneAndUpdate().SetReturnDocument(options.After)).Decode(&tariff)
if err != nil {
if err == mongo.ErrNoDocuments {
return tariff, errors.ErrNotFound
}
t.logger.Error("failed soft delete tariff", zap.Error(err))
return tariff, err
}
return tariff, nil
}
func (t *Tariff) Update(ctx context.Context, tariffID primitive.ObjectID, req models.Tariff) (models.Tariff, error) {
var tariff models.Tariff
err := t.mdb.FindOne(ctx, bson.M{"_id": tariffID}).Decode(&tariff)
if err != nil {
if err == mongo.ErrNoDocuments {
return tariff, errors.ErrNotFound
}
t.logger.Error("failed find tariff", zap.Error(err))
return tariff, err
}
privilegeIDs := make([]string, len(req.Privileges))
for i, privilege := range req.Privileges {
privilegeIDs[i] = privilege.PrivilegeID
}
cursor, err := t.privilegeDB.Find(ctx, bson.M{"privilegeId": bson.M{"$in": privilegeIDs}})
if err != nil {
t.logger.Error("failed find privileges", zap.Error(err))
return tariff, err
}
defer cursor.Close(ctx)
privilegeMap := make(map[string]models.Privilege)
for cursor.Next(ctx) {
var privilege models.Privilege
if err := cursor.Decode(&privilege); err != nil {
t.logger.Error("failed decode privilege", zap.Error(err))
return tariff, err
}
privilegeMap[privilege.PrivilegeID] = privilege
}
clean := make([]models.Privilege, len(req.Privileges))
for i, privilege := range req.Privileges {
origPrivilege := privilegeMap[privilege.PrivilegeID]
clean[i] = models.Privilege{
Name: origPrivilege.Name,
PrivilegeID: origPrivilege.PrivilegeID,
ServiceKey: origPrivilege.ServiceKey,
Description: origPrivilege.Description,
Type: origPrivilege.Type,
Value: origPrivilege.Value,
Price: origPrivilege.Price,
}
}
update := bson.M{
"$set": bson.M{
"order": req.Order,
"name": req.Name,
"price": req.Price,
"isCustom": req.IsCustom,
"privileges": clean,
},
}
err = t.mdb.FindOneAndUpdate(ctx, bson.M{"_id": tariffID}, update).Decode(&tariff)
if err != nil {
if err == mongo.ErrNoDocuments {
return tariff, errors.ErrNotFound
}
t.logger.Error("failed update tariff", zap.Error(err))
return tariff, err
}
return tariff, nil
}
func (t *Tariff) Restore(ctx context.Context, tariffID primitive.ObjectID) (models.Tariff, error) {
var tariff models.Tariff
filter := bson.M{"_id": tariffID}
update := bson.M{"$set": bson.M{"isDeleted": false}}
err := t.mdb.FindOneAndUpdate(ctx, filter, update, options.FindOneAndUpdate().SetReturnDocument(options.After)).Decode(&tariff)
if err != nil {
if err == mongo.ErrNoDocuments {
return tariff, errors.ErrNotFound
}
t.logger.Error("failed restore tariff", zap.Error(err))
return tariff, err
}
return tariff, nil
}

@ -0,0 +1,75 @@
package http
import (
"context"
"fmt"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"gitea.pena/PenaSide/tariffs/internal/models"
"gitea.pena/PenaSide/tariffs/internal/utils"
)
type ServerConfig struct {
Logger *zap.Logger
Controllers []Controller
JWTConfig *models.JWTConfiguration
}
type Server struct {
Logger *zap.Logger
Controllers []Controller
app *fiber.App
}
func NewServer(config ServerConfig) *Server {
app := fiber.New()
jwtUtil := utils.NewJWT(config.JWTConfig)
app.Use("/tariff", utils.NewAuthenticator(jwtUtil))
app.Use("/privilege", func(c *fiber.Ctx) error {
return c.Next()
})
s := &Server{
Logger: config.Logger,
Controllers: config.Controllers,
app: app,
}
s.registerRoutes()
return s
}
func (s *Server) Start(addr string) error {
if err := s.app.Listen(addr); err != nil {
s.Logger.Error("Failed to start server", zap.Error(err))
return err
}
return nil
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.app.Shutdown()
}
func (s *Server) registerRoutes() {
for _, c := range s.Controllers {
router := s.app.Group(c.Name())
c.Register(router)
}
}
type Controller interface {
Register(router fiber.Router)
Name() string
}
func (s *Server) ListRoutes() {
fmt.Println("Registered routes:")
for _, stack := range s.app.Stack() {
for _, route := range stack {
fmt.Printf("%s %s\n", route.Method, route.Path)
}
}
}

@ -0,0 +1,17 @@
package tools
import "gitea.pena/PenaSide/tariffs/internal/models"
func ConvertPrivilegesToMap(privileges []models.Privilege) map[string][]models.Privilege {
resultMap := make(map[string][]models.Privilege)
for _, privilege := range privileges {
svcKey := privilege.ServiceKey
if _, ok := resultMap[svcKey]; !ok {
resultMap[svcKey] = []models.Privilege{}
}
resultMap[svcKey] = append(resultMap[svcKey], privilege)
}
return resultMap
}

@ -0,0 +1,32 @@
package tools
import (
"errors"
"gitea.pena/PenaSide/tariffs/internal/models"
)
func ValidatePrivilege(req models.CreateUpdateReq) bool {
if req.Name == "" || req.PrivilegeId == "" || req.ServiceKey == "" || req.Type == "" || req.Value == "" || req.Description == "" {
return false
}
return true
}
func ValidateTariff(tariff models.Tariff) error {
if tariff.Name == "" {
return errors.New("name is required")
}
if tariff.Price < 0 {
return errors.New("invalid price value")
}
if len(tariff.Privileges) == 0 {
return errors.New("privileges are required")
}
for _, privilege := range tariff.Privileges {
if privilege.PrivilegeID == "" {
return errors.New("privilegeID is required in privileges")
}
}
return nil
}

@ -0,0 +1,59 @@
package utils
import (
"errors"
"github.com/gofiber/fiber/v2"
"gitea.pena/PenaSide/tariffs/internal/models"
"strings"
)
const (
prefix = "Bearer "
)
func NewAuthenticator(jwtUtil *JWT) fiber.Handler {
return func(c *fiber.Ctx) error {
if jwtUtil == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Invalid arguments")
}
err := authenticate(jwtUtil, c)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
}
return c.Next()
}
}
func authenticate(jwtUtil *JWT, c *fiber.Ctx) error {
jws, err := parseJWSFromRequest(c)
if err != nil {
return err
}
userID, validateErr := jwtUtil.Validate(jws)
if validateErr != nil {
return validateErr
}
c.Locals(models.AuthJWTDecodedUserIDKey, userID)
c.Locals(models.AuthJWTDecodedAccessTokenKey, jws)
return nil
}
func parseJWSFromRequest(c *fiber.Ctx) (string, error) {
header := c.Get("Authorization")
if header != "" && strings.HasPrefix(header, prefix) {
return strings.TrimPrefix(header, prefix), nil
}
token := c.Query("Authorization")
if token == "" {
return "", errors.New("failed to parse jws from request: no valid token found")
}
return token, nil
}

89
internal/utils/jwt.go Normal file

@ -0,0 +1,89 @@
package utils
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"gitea.pena/PenaSide/tariffs/internal/models"
"time"
)
type JWT struct {
privateKey []byte
publicKey []byte
algorithm *jwt.SigningMethodRSA
expiresIn time.Duration
issuer string
audience string
}
func NewJWT(configuration *models.JWTConfiguration) *JWT {
return &JWT{
privateKey: []byte(configuration.PrivateKey),
publicKey: []byte(configuration.PublicKey),
algorithm: jwt.SigningMethodRS256,
expiresIn: 15 * time.Minute,
issuer: configuration.Issuer,
audience: configuration.Audience,
}
}
func (receiver *JWT) Create(id string) (string, error) {
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(receiver.privateKey)
if err != nil {
return "", fmt.Errorf("failed to parse private key on <Create> of <JWT>: %w", err)
}
now := time.Now().UTC()
claims := jwt.MapClaims{
"id": id, // Our userID
"exp": now.Add(receiver.expiresIn).Unix(), // The expiration time after which the token must be disregarded.
"aud": receiver.audience, // Audience
"iss": receiver.issuer, // Issuer
}
token, err := jwt.NewWithClaims(receiver.algorithm, claims).SignedString(privateKey)
if err != nil {
return "", fmt.Errorf("failed to sing on <Create> of <JWT>: %w", err)
}
return token, nil
}
func (receiver *JWT) Validate(tokenString string) (string, error) {
key, err := jwt.ParseRSAPublicKeyFromPEM(receiver.publicKey)
if err != nil {
return "", fmt.Errorf("failed to parse rsa public key on <Validate> of <JWT>: %w", err)
}
parseCallback := func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %s", token.Header["alg"])
}
return key, nil
}
token, err := jwt.Parse(
tokenString,
parseCallback,
jwt.WithAudience(receiver.audience),
jwt.WithIssuer(receiver.issuer),
)
if err != nil {
return "", fmt.Errorf("failed to parse jwt token on <Validate> of <JWT>: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return "", errors.New("token is invalid on <Validate> of <JWT>")
}
data, ok := claims["id"].(string)
if !ok {
return "", errors.New("data is empty or not a string on <Validate> of <JWT>")
}
return data, nil
}

@ -1,13 +0,0 @@
import type { Config } from "jest";
const config: Config = {
preset: "ts-jest",
testEnvironment: "node",
moduleDirectories: ["node_modules", "<rootDir>"],
moduleNameMapper: {
"@/(.*)": "<rootDir>/src/$1",
},
testTimeout: 15000,
};
export default config;

@ -0,0 +1 @@
[{ "delete": "tariffs", "deletes": [{ "q": {} }] }]

@ -0,0 +1,43 @@
[
{
"insert": "tariffs",
"ordered": true,
"documents": [
{
"_id": {
"$oid": "64e6105384368b75221a5c3e"
},
"name": "100 генераций",
"price": 5000,
"userId": "64e6106eab16496587435cbf",
"isCustom": false,
"privileges": [
{
"name": "100 генераций",
"privilegeId": "64e60d28eac51324f2296753",
"serviceKey": "templategen-key",
"description": "",
"amount": 1,
"type": "count",
"value": "100",
"price": 5000
},
{
"name": "100 генераций",
"privilegeId": "64e60e9094732821e3142eb8",
"serviceKey": "shortlink-key",
"description": "",
"amount": 1,
"type": "count",
"value": "100",
"price": 1000
}
],
"isDeleted": false,
"createdAt": "2023-06-16T08:15:30.336Z",
"updatedAt": "2023-06-16T08:15:30.336Z",
"deletedAt": "2023-06-16T08:15:30.336Z"
}
]
}
]

@ -0,0 +1 @@
[{ "delete": "privileges", "deletes": [{ "q": {} }] }]

@ -0,0 +1,42 @@
[
{
"insert": "privileges",
"ordered": true,
"documents": [
{
"_id": {
"$oid": "64e60d28eac51324f2296753"
},
"name": "100 генераций",
"privilegeId": "64e60d28eac51324f2296753",
"serviceKey": "templategen-key",
"description": "",
"amount": 1,
"type": "count",
"value": "100",
"price": 5000,
"isDeleted": false,
"createdAt": "2023-06-16T08:15:30.336Z",
"updatedAt": "2023-06-16T08:15:30.336Z",
"deletedAt": "2023-06-16T08:15:30.336Z"
},
{
"_id": {
"$oid": "64e60e9094732821e3142eb8"
},
"name": "100 генераций",
"privilegeId": "64e60e9094732821e3142eb8",
"serviceKey": "shortlink-key",
"description": "",
"amount": 1,
"type": "count",
"value": "100",
"price": 1000,
"isDeleted": false,
"createdAt": "2023-06-16T08:15:30.336Z",
"updatedAt": "2023-06-16T08:15:30.336Z",
"deletedAt": "2023-06-16T08:15:30.336Z"
}
]
}
]

@ -1,9 +0,0 @@
{
"ignore": ["test", ".git", "node_modules"],
"watch": ["src"],
"exec": "npm run start",
"ext": "ts",
"env": {
"ENVIRONMENT": "development"
}
}

@ -1,72 +0,0 @@
{
"name": "auth-server",
"version": "1.1.0",
"description": "",
"main": "index.js",
"author": "DevMArt",
"license": "ISC",
"scripts": {
"setup": "yarn & husky install",
"start": "ts-node ./src/index.ts",
"start:prod": "node -r ts-node/register/transpile-only -r tsconfig-paths/register ./src/index.js",
"dev": "nodemon",
"test": "jest --coverage",
"test:watch": "jest --watch",
"build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
"compose:dev:start": "docker-compose -f deployments/dev/docker-compose.yaml up -d",
"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:format": "prettier --write \"src/**/*.{ts,tsx,js,css,scss,html}\"",
"code:format:specific-file": "prettier --write",
"lint": "eslint .",
"lint:fix": "eslint --fix ."
},
"dependencies": {
"@fastify/cookie": "^8.3.0",
"@fastify/jwt": "^6.3.3",
"@fastify/swagger": "^8.2.1",
"@fastify/swagger-ui": "^1.3.0",
"axios": "^1.2.1",
"bcryptjs": "^2.4.3",
"dotenv": "^16.0.3",
"fastify": "^4.9.2",
"fastify-plugin": "^4.3.0",
"fastify-print-routes": "^2.0.6",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.7.2",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript-transform-paths": "^3.4.4"
},
"devDependencies": {
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@types/bcryptjs": "^2.4.2",
"@types/jest": "^29.2.3",
"@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"eslint": "^8.23.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^7.0.4",
"jest": "^29.3.1",
"jest-mock-extended": "^3.0.4",
"nodemon": "^2.0.20",
"prettier": "^2.7.1",
"ts-jest": "^29.0.3",
"tsc-alias": "^1.8.3",
"typescript": "^4.9.3"
},
"lint-staged": {
"./src/**/*.{ts,js,jsx,tsx}": [
"eslint --ignore-path .gitignore --fix",
"prettier --ignore-path .gitignore --write"
]
}
}

37
pkg/closer/closer.go Normal file

@ -0,0 +1,37 @@
package closer
import (
"context"
)
type Closer interface {
Close(ctx context.Context) error
}
type CloserFunc func(ctx context.Context) error
func (cf CloserFunc) Close(ctx context.Context) error {
return cf(ctx)
}
type CloserGroup struct {
closers []Closer
}
func NewCloserGroup() *CloserGroup {
return &CloserGroup{}
}
func (cg *CloserGroup) Add(c Closer) {
cg.closers = append(cg.closers, c)
}
func (cg *CloserGroup) Call(ctx context.Context) error {
var closeErr error
for i := len(cg.closers) - 1; i >= 0; i-- {
if err := cg.closers[i].Close(ctx); err != nil && closeErr == nil {
closeErr = err
}
}
return closeErr
}

@ -1,16 +0,0 @@
import { authService } from "./instance";
import type { User } from "@/types/models/user.type";
import type { GetUserRequest } from "./types";
export const getUser = async (request: GetUserRequest): Promise<User | null> => {
try {
const { data } = await authService.get<User>(`/user/${request.id}`);
return data;
} catch (nativeError) {
console.error(nativeError);
}
return null;
};

@ -1,7 +0,0 @@
import axios from "axios";
import { CONFIGURATION } from "@/constants/configuration";
export const authService = axios.create({
baseURL: `${CONFIGURATION.authService.host}:${CONFIGURATION.authService.port}`,
});

@ -1,3 +0,0 @@
export type GetUserRequest = {
id: string;
};

@ -1,15 +0,0 @@
import { Router } from "@/server/router";
import { setAccountRoutes } from "@/routes/account.routes";
import { setPrivilegeRoutes } from "@/routes/privilege.routes";
import { setRoleRoutes } from "@/routes/role.routes";
import { setTariffRoutes } from "@/routes/tariff.routes";
import { setPermissionRoutes } from "@/routes/permission.routes";
export const combineRoutes = (router: Router): void => {
router.group("/role", setRoleRoutes);
router.group("/account", setAccountRoutes);
router.group("/privilege", setPrivilegeRoutes);
router.group("/tariff", setTariffRoutes);
router.group("/permission", setPermissionRoutes);
};

@ -1,19 +0,0 @@
import path from "path";
import * as dotenv from "dotenv";
import { defineEnvironment } from "./define-environment";
export const configureENV = () => {
const environment = defineEnvironment();
if (environment === "development") {
return dotenv.config({
debug: true,
path: path.resolve(process.env.PWD || "", ".env.example"),
});
}
return dotenv.config();
};
configureENV();

@ -1,7 +0,0 @@
import type { DatabaseOptions } from "@/types/configuration/database-options";
export const constituteMongoURI = ({ username, password, host, port, database }: DatabaseOptions) => {
const dbConnection = `mongodb://${username}:${password}@${host}:${port}`;
return database ? `${dbConnection}/${database}` : dbConnection;
};

@ -1,12 +0,0 @@
import type { Environment } from "@/types/environment";
export const defineEnvironment = (): Environment => {
const environments: Environment[] = ["development", "staging", "production"];
const environment = process.env.ENVIRONMENT as Environment | undefined;
if (environment && environments.includes(environment)) {
return environment;
}
return "development";
};

@ -1,21 +0,0 @@
import cookie from "@fastify/cookie";
import jwt from "@fastify/jwt";
import swagger from "@fastify/swagger";
import swaggerUI from "@fastify/swagger-ui";
import printRoutes from "@/plugins/print-routes";
import { DEFAULT } from "@/constants/default";
import type { FastifyInstance } from "fastify";
import type { PluginsOptions } from "@/types/configuration/plugins-options";
export const registerFastifyPlugins = (fastify: FastifyInstance, options: PluginsOptions) => {
fastify.register(cookie, options.cookie);
fastify.register(jwt, options.jwt);
fastify.register(swagger, DEFAULT.swaggerOptions);
fastify.register(swaggerUI, DEFAULT.swaggerUIOptions);
fastify.register(printRoutes);
return fastify;
};

@ -1,21 +0,0 @@
export const CONFIGURATION = {
http: {
host: process.env.HTTP_HOST || "",
port: Number(process.env.HTTP_PORT) || 8080,
},
db: {
host: process.env.DB_HOST || "",
port: Number(process.env.DB_PORT) || 3000,
username: process.env.DB_USERNAME || "admin",
password: process.env.DB_PASSWORD || "admin",
database: process.env.DB_NAME || "database",
},
service: {
publicAccessSecretKey: process.env.PUBLIC_ACCESS_SECRET_KEY || "",
salt: Number(process.env.SALT) || 10,
},
authService: {
host: process.env.AUTH_SERVICE_HOST || "",
port: Number(process.env.AUTH_SERVICE_PORT) || 8081,
},
} as const;

@ -1,46 +0,0 @@
import type { FastifyServerOptions } from "fastify";
import type { FastifyDynamicSwaggerOptions } from "@fastify/swagger";
import type { FastifySwaggerUiOptions } from "@fastify/swagger-ui";
type Default = {
readonly fastifyOptions: FastifyServerOptions;
readonly swaggerOptions: FastifyDynamicSwaggerOptions;
readonly swaggerUIOptions: FastifySwaggerUiOptions;
};
export const DEFAULT: Default = {
fastifyOptions: {
logger: true,
ajv: {
customOptions: { removeAdditional: "failing" },
},
bodyLimit: 30 * 1024 * 1024,
},
swaggerOptions: {
openapi: {
info: {
title: "Hub Admin Backend",
description: "Тестирование сервиса админ панели хаба",
version: "0.1.0",
},
components: {
securitySchemes: {
bearer: {
description: "Authorization header token, sample: Bearer <token>",
type: "http",
bearerFormat: "JWT",
scheme: "bearer",
},
},
},
},
hideUntagged: true,
},
swaggerUIOptions: {
routePrefix: "/swagger",
uiConfig: {
docExpansion: "full",
deepLinking: false,
},
},
};

@ -1,15 +0,0 @@
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];

@ -1,234 +0,0 @@
import { Types } from "mongoose";
import { AccountModel } from "@/models/account.model";
import { RoleModel } from "@/models/role.model";
import { PermissionModule } from "@/services/permission/permission.module";
import { getUser } from "@/clients/auth";
import { validateEmptyFields } from "@/utils/validate-empty-fields";
import { determinePaginationParameters } from "@/utils/determine-pagination-parameters";
import type { FastifyReply, FastifyRequest } from "fastify";
import type { Account } from "@/types/models/account.type";
import type { GetAccountRequest, SetAccountRoleRequest, GetAccountsRequest, GetAccountsResponse } from "./types";
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) => {
if (!Types.ObjectId.isValid(request.user.id)) {
void reply.status(400);
return new Error("invalid user id");
}
const account = await AccountModel.findOne({ userId: request.user.id }).lean();
if (account) {
void reply.status(409);
return new Error("account already exist");
}
const user = await getUser({ id: request.user.id });
if (!user) {
void reply.status(404);
return new Error("user not found");
}
const createdAccount = new AccountModel({
userId: user._id,
});
return createdAccount.save();
};
export const getAccountByID = async (request: GetAccountRequest, reply: FastifyReply): Promise<Account> => {
const [getAccountRequestParams, error] = validateEmptyFields(request.params ?? {}, ["userId"]);
if (error) {
void reply.status(400);
throw error;
}
if (!Types.ObjectId.isValid(getAccountRequestParams.userId)) {
void reply.status(400);
throw new Error("invalid user id");
}
const account = await AccountModel.findOne({ userId: getAccountRequestParams.userId }).lean();
if (!account) {
void reply.status(404);
throw new Error("account not found");
}
return account;
};
export const getAccount = async (request: GetAccountRequest, reply: FastifyReply): Promise<Account> => {
if (!Types.ObjectId.isValid(request.user.id)) {
void reply.status(400);
throw new Error("invalid user id");
}
const account = await AccountModel.findOne({ userId: request.user.id }).lean();
if (!account) {
void reply.status(404);
throw new Error("account not found");
}
return account;
};
export const setAccountRole = async (request: SetAccountRoleRequest, reply: FastifyReply) => {
const [setAccountRoleBody, error] = validateEmptyFields(request.body || {}, ["userId", "role"]);
if (error) {
void reply.status(400);
return error;
}
const role = await RoleModel.findOne({ name: setAccountRoleBody.role }).lean();
if (!role) {
void reply.status(404);
return new Error("role not found");
}
const account = await AccountModel.findOneAndUpdate(
{ userId: setAccountRoleBody.userId },
{ $set: { role: role.name } }
);
if (!account) {
void reply.status(404);
return new Error("account not found");
}
return account;
};
export const removeAccount = async (request: FastifyRequest, reply: FastifyReply): Promise<Account> => {
if (!Types.ObjectId.isValid(request.user.id)) {
void reply.status(400);
throw new Error("invalid user id");
}
const account = await AccountModel.findOneAndUpdate(
{ userId: request.user.id },
{ $set: { isDeleted: true, deletedAt: new Date() } }
).lean();
if (!account) {
void reply.status(404);
throw new Error("account not found");
}
return account;
};
export const removeAccountById = async (request: GetAccountRequest, reply: FastifyReply): Promise<Account> => {
const [{ userId }, error] = validateEmptyFields(request.params ?? {}, ["userId"]);
if (error) {
void reply.status(400);
throw error;
}
if (!Types.ObjectId.isValid(userId)) {
void reply.status(400);
throw new Error("invalid user id");
}
const account = await AccountModel.findOne({ userId: request.user.id });
if (!account) {
void 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)) {
void reply.status(400);
throw new Error("invalid user id");
}
const account = await AccountModel.findByIdAndDelete({ userId: request.user.id }).lean();
if (!account) {
void reply.status(404);
throw new Error("account not found");
}
return account;
};
export const deleteAccountById = async (request: GetAccountRequest, reply: FastifyReply): Promise<Account> => {
const [{ userId }, error] = validateEmptyFields(request.params ?? {}, ["userId"]);
if (error) {
void reply.status(400);
throw error;
}
if (!Types.ObjectId.isValid(userId)) {
void reply.status(400);
throw new Error("invalid user id");
}
const account = await AccountModel.findByIdAndDelete({ userId: request.user.id }).lean();
if (!account) {
void 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)) {
void reply.status(400);
throw new Error("invalid user id");
}
const account = await AccountModel.findOneAndUpdate(
{ userId: request.user.id },
{ $set: { isDeleted: false } }
).lean();
if (!account) {
void reply.status(404);
throw new Error("account not found");
}
return account;
};

@ -1,25 +0,0 @@
import type { FastifyRequest } from "fastify";
import type { Account } from "@/types/models/account.type";
import type { PaginationParams } from "@/types/messages/pagination.type";
export type GetAccountRequest = FastifyRequest<{
Params?: {
userId?: string;
};
}>;
export type SetAccountRoleRequest = FastifyRequest<{
Body: {
userId?: string;
role?: string;
};
}>;
export type GetAccountsRequest = FastifyRequest<{
Querystring?: PaginationParams;
}>;
export type GetAccountsResponse = {
accounts: Account[];
totalPages: number;
};

@ -1,17 +0,0 @@
import type { FastifyRequest, FastifyReply } from "fastify";
export const verifyUser = async (request: FastifyRequest, reply: FastifyReply) => {
try {
const { id } = await request.jwtVerify<{ id?: string }>();
if (!id) {
void reply.status(401);
return reply.send("user id is empty");
}
request.user = { id };
} catch (nativeError) {
void reply.status(401);
return reply.send(nativeError);
}
};

@ -1,166 +0,0 @@
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) {
void reply.status(400);
throw validateParamsError;
}
if (!Types.ObjectId.isValid(permissionId)) {
void reply.status(400);
throw new Error("invalid permission id");
}
const permission = await PermissionModel.findOne({
_id: new Types.ObjectId(permissionId),
isDeleted: false,
});
if (!permission) {
void 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) {
void 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) {
void reply.status(400);
throw validateBodyError;
}
if (validateParamsError) {
void reply.status(400);
throw validateParamsError;
}
if (!Types.ObjectId.isValid(permissionId)) {
void reply.status(400);
throw new Error("invalid permission id");
}
const findedPermission = await PermissionModel.findByIdAndUpdate(permissionId, {
$set: {
...permission,
updatedAt: new Date(),
},
});
if (!findedPermission) {
void 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) {
void reply.status(400);
throw validateParamsError;
}
if (!Types.ObjectId.isValid(permissionId)) {
void 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) {
void 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) {
void reply.status(400);
throw validateParamsError;
}
if (!Types.ObjectId.isValid(permissionId)) {
void 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) {
void reply.status(404);
throw new Error("permission not found");
}
return permission;
};

@ -1,20 +0,0 @@
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;
};
}>;

@ -1,28 +0,0 @@
import type { Privilege } from "@/types/models/privilege.type";
import type { RawPrivilege } from "./types";
export const validatePrivilege = (privilege: RawPrivilege): Error | null => {
const typeValues: typeof privilege.type[] = ["count", "day", "full"];
if (!typeValues.includes(privilege.type)) {
return new Error("invalid <type> value");
}
if (isNaN(Number(privilege.price))) {
return new Error("price must be a number");
}
return null;
};
export const convertPrivilegiesToMap = (privilegies: Privilege[]) => {
return privilegies.reduce<Record<string, Privilege[]>>((accamulator, privilege) => {
if (!accamulator[privilege.serviceKey]) {
accamulator[privilege.serviceKey] = [];
}
accamulator[privilege.serviceKey].push(privilege);
return accamulator;
}, {});
};

@ -1,280 +0,0 @@
import { Types } from "mongoose";
import { PrivilegeModel } from "@/models/privilege.model";
import { validateEmptyFields } from "@/utils/validate-empty-fields";
import { convertPrivilegiesToMap, validatePrivilege } from "./helpers";
import type { FastifyReply } from "fastify";
import type {
GetServicePrivilegiesRequest,
RegisterPrivilegeRequest,
GetPrivilegeRequest,
RegisterPrivilegiesRequest,
RemovePrivilegeRequest,
} from "./types";
export const registerPrivilegies = async (request: RegisterPrivilegiesRequest, reply: FastifyReply) => {
const [requestBody, errorEmpty] = validateEmptyFields(request.body ?? {}, ["privilegies"]);
if (errorEmpty) {
void reply.status(400);
return errorEmpty;
}
if (!requestBody.privilegies.length) {
void reply.status(400);
return new Error("empty privilege array");
}
for (const rawPrivilege of requestBody.privilegies) {
const errorInvalid = validatePrivilege(rawPrivilege);
if (errorInvalid) {
void reply.status(400);
return errorInvalid;
}
}
const updatePrivilegeRequests = requestBody.privilegies.map(async (privilege) => {
await PrivilegeModel.updateOne(
{ privilegeId: privilege.privilegeId },
{
$set: {
name: privilege.name,
privilegeId: privilege.privilegeId,
serviceKey: privilege.serviceKey,
description: privilege.description,
type: privilege.type,
value: privilege.value,
price: privilege.price,
isDeleted: false,
createdAt: new Date(),
},
},
{ upsert: true }
).lean();
return privilege;
});
return Promise.all(updatePrivilegeRequests);
};
export const registerPrivilege = async (request: RegisterPrivilegeRequest, reply: FastifyReply) => {
const [requestBody, errorEmpty] = validateEmptyFields(
request.body ?? {},
["name", "privilegeId", "serviceKey", "type", "description", "value", "amount"],
false
);
if (errorEmpty) {
void reply.status(400);
return errorEmpty;
}
const errorInvalid = validatePrivilege(requestBody);
if (errorInvalid) {
void reply.status(400);
return errorInvalid;
}
const privilege = await PrivilegeModel.findOne({ privilegeId: requestBody.privilegeId }).lean();
if (privilege) {
void reply.status(409);
return new Error("privilege already exist");
}
const newPrivilege = new PrivilegeModel({
name: requestBody.name,
privilegeId: requestBody.privilegeId,
serviceKey: requestBody.serviceKey,
description: requestBody.description,
type: requestBody.type,
value: requestBody.value,
price: requestBody.price,
});
return newPrivilege.save();
};
export const getAllPrivilegies = async () => PrivilegeModel.find({ isDeleted: false }).lean();
export const getAllPrivilegiesMap = async () => {
const privilegies = await PrivilegeModel.find({ isDeleted: false }).lean();
if (!privilegies.length) {
return {};
}
return convertPrivilegiesToMap(privilegies);
};
export const getServicePrivilegies = async (request: GetServicePrivilegiesRequest, reply: FastifyReply) => {
const [requestParams, error] = validateEmptyFields(request.params ?? {}, ["serviceKey"]);
if (error) {
void reply.status(400);
return error;
}
return PrivilegeModel.find({ serviceKey: requestParams.serviceKey, isDeleted: false }).lean();
};
export const getPrivilege = async (request: GetPrivilegeRequest, reply: FastifyReply) => {
const [requestParams, error] = validateEmptyFields(request.params ?? {}, ["privilegeId"]);
if (error) {
void reply.status(400);
return error;
}
if (!Types.ObjectId.isValid(requestParams.privilegeId)) {
void reply.status(400);
return new Error("invalid id");
}
const privilege = await PrivilegeModel.findOne({ privilegeId: requestParams.privilegeId, isDeleted: false }).lean();
if (!privilege) {
void reply.status(404);
return new Error("privilege not found");
}
return privilege;
};
export const replacePrivilege = async (request: RegisterPrivilegeRequest, reply: FastifyReply) => {
const [requestBody, errorEmpty] = validateEmptyFields(
request.body ?? {},
["name", "privilegeId", "serviceKey", "type", "description", "value", "amount"],
false
);
if (errorEmpty) {
void reply.status(400);
return errorEmpty;
}
const errorInvalid = validatePrivilege(requestBody);
if (errorInvalid) {
void reply.status(400);
return errorInvalid;
}
const privilege = await PrivilegeModel.findOne({ privilegeId: requestBody.privilegeId });
if (!privilege) {
void reply.status(404);
return new Error("privilege not found");
}
await privilege.replaceOne({
name: requestBody.name,
privilegeId: requestBody.privilegeId,
serviceKey: requestBody.serviceKey,
description: requestBody.description,
type: requestBody.type,
value: requestBody.value,
price: requestBody.price,
updatedAt: new Date(),
});
return privilege;
};
export const removePrivilege = async (request: RemovePrivilegeRequest, reply: FastifyReply) => {
const [{ privilegeId }, error] = validateEmptyFields(request.body ?? {}, ["privilegeId"]);
if (error) {
void reply.status(400);
return error;
}
if (!Types.ObjectId.isValid(privilegeId)) {
void reply.status(400);
return new Error("invalid id");
}
const privilege = await PrivilegeModel.findOneAndUpdate(
{ privilegeId },
{ $set: { isDeleted: true, deletedAt: new Date() } }
);
if (!privilege) {
void reply.status(404);
return new Error("privilege not found");
}
return privilege;
};
export const replacePrivilegies = async (request: RegisterPrivilegiesRequest, reply: FastifyReply) => {
const [requestBody, errorEmpty] = validateEmptyFields(request.body ?? {}, ["privilegies"]);
if (errorEmpty) {
void reply.status(400);
return errorEmpty;
}
if (!requestBody.privilegies.length) {
void reply.status(400);
return new Error("empty privilege array");
}
for (const rawPrivilege of requestBody.privilegies) {
const errorInvalid = validatePrivilege(rawPrivilege);
if (errorInvalid) {
void reply.status(400);
return errorInvalid;
}
}
const replacePrivilegeRequests = requestBody.privilegies.map(async (privilege) => {
await PrivilegeModel.replaceOne(
{ privilegeId: privilege.privilegeId },
{
name: privilege.name,
privilegeId: privilege.privilegeId,
serviceKey: privilege.serviceKey,
description: privilege.description,
type: privilege.type,
value: privilege.value,
price: privilege.price,
updatedAt: new Date(),
isDeleted: false,
}
).lean();
return privilege;
});
return Promise.all(replacePrivilegeRequests);
};
export const restorePrivilege = async (request: RemovePrivilegeRequest, reply: FastifyReply) => {
const [{ privilegeId }, error] = validateEmptyFields(request.body ?? {}, ["privilegeId"]);
if (error) {
void reply.status(400);
return error;
}
if (!Types.ObjectId.isValid(privilegeId)) {
void reply.status(400);
return new Error("invalid id");
}
const privilege = await PrivilegeModel.findOneAndUpdate({ privilegeId }, { $set: { isDeleted: false } });
if (!privilege) {
void reply.status(404);
return new Error("privilege not found");
}
return privilege;
};

@ -1,33 +0,0 @@
import type { FastifyRequest } from "fastify";
import type { Privilege } from "@/types/models/privilege.type";
import type { Eloquent } from "@/types/models/eloquent.type";
export type RawPrivilege = Partial<Omit<Privilege, keyof Eloquent>>;
export type RegisterPrivilegeRequest = FastifyRequest<{
Body?: RawPrivilege;
}>;
export type RegisterPrivilegiesRequest = FastifyRequest<{
Body?: {
privilegies?: RawPrivilege[];
};
}>;
export type GetServicePrivilegiesRequest = FastifyRequest<{
Params?: {
serviceKey?: string;
};
}>;
export type GetPrivilegeRequest = FastifyRequest<{
Params?: {
privilegeId?: string;
};
}>;
export type RemovePrivilegeRequest = FastifyRequest<{
Body?: {
privilegeId?: string;
};
}>;

@ -1,225 +0,0 @@
import { Types } from "mongoose";
import { RoleModel } from "@/models/role.model";
import { convertArrayToRecord } from "@/utils/convert-array-to-record";
import { validateEmptyFields } from "@/utils/validate-empty-fields";
import type { FastifyReply } from "fastify";
import type { CreateRoleRequest, GetRoleRequest, RemoveRoleRequest, UpdateRoleRequest } from "./types";
export const getAllRoles = async () => RoleModel.find({ isDeleted: false }).lean();
export const createRole = async (request: CreateRoleRequest, reply: FastifyReply) => {
const [createRoleRequest, error] = validateEmptyFields(request.body, ["name", "permissions"]);
if (error || !createRoleRequest) {
void reply.status(400);
return error;
}
const isRoleExist = await RoleModel.exists({ name: createRoleRequest.name }).lean();
if (isRoleExist) {
void reply.status(409);
return new Error("role already exist");
}
const permissionsRecord = convertArrayToRecord(createRoleRequest.permissions, true);
return RoleModel.create({ name: createRoleRequest.name, permissions: permissionsRecord });
};
export const deleteRole = async (request: RemoveRoleRequest, reply: FastifyReply) => {
const { id } = request.body || {};
if (!id || !Types.ObjectId.isValid(id)) {
void reply.status(400);
return new Error("wrong id");
}
const deletedRole = await RoleModel.findByIdAndDelete(id);
if (!deletedRole) {
void reply.status(404);
return new Error("role not found");
}
return deletedRole.toJSON();
};
export const removeRole = async (request: RemoveRoleRequest, reply: FastifyReply) => {
const { id } = request.body || {};
if (!id || !Types.ObjectId.isValid(id)) {
void reply.status(400);
return new Error("wrong id");
}
const role = await RoleModel.findById(id);
if (!role) {
void reply.status(404);
return new Error("role by id not found");
}
if (role.isDeleted) {
void reply.status(409);
return new Error("role already deleted");
}
await role.updateOne({ $set: { isDeleted: true, deletedAt: Date.now() } });
return role;
};
export const getRole = async (request: GetRoleRequest, reply: FastifyReply) => {
const { query } = request.params || {};
if (!query) {
void reply.status(400);
return new Error("query is empty");
}
if (Types.ObjectId.isValid(query)) {
const role = await RoleModel.findOne({ _id: new Types.ObjectId(query), isDeleted: false }).lean();
if (!role) {
void reply.status(404);
return new Error("role not found");
}
return role;
}
const role = await RoleModel.findOne({ name: query, isDeleted: false }).lean();
if (!role) {
void reply.status(404);
return new Error("role not found");
}
return role;
};
export const replaceRole = async (request: UpdateRoleRequest, reply: FastifyReply) => {
const [replaceRoleRequest, error] = validateEmptyFields(request.body ?? {}, ["name", "permissions"]);
const { query } = request.params || {};
if (error || !replaceRoleRequest) {
void reply.status(400);
return error;
}
if (!query) {
void reply.status(400);
return new Error("either name or id must be filled");
}
if (Types.ObjectId.isValid(query)) {
const role = await RoleModel.findById(query);
if (!role) {
void reply.status(404);
return new Error("role by id not found");
}
await role.replaceOne({
name: replaceRoleRequest.name,
permissions: convertArrayToRecord(replaceRoleRequest.permissions, true),
updatedAt: Date.now(),
});
return role;
}
const role = await RoleModel.findOne({ name: query });
if (!role) {
void reply.status(404);
return new Error("role not found");
}
await role.replaceOne({
name: replaceRoleRequest.name,
permissions: convertArrayToRecord(replaceRoleRequest.permissions, true),
updatedAt: Date.now(),
});
return role;
};
export const updateRole = async (request: UpdateRoleRequest, reply: FastifyReply) => {
const { query } = request.params;
if (!query) {
void reply.status(400);
return new Error("query is empty");
}
if (!request.body?.name && !request.body?.permissions) {
void reply.status(400);
return new Error("either name or permissions must be filled");
}
if (Types.ObjectId.isValid(query)) {
const role = await RoleModel.findById(query);
if (!role) {
void reply.status(404);
return new Error("role by id not found");
}
await role.updateOne({
$set: {
name: request.body?.name,
permissions: request.body?.permissions && convertArrayToRecord(request.body.permissions, true),
updatedAt: Date.now(),
},
});
return role;
}
const role = await RoleModel.findOne({ name: query });
if (!role) {
void reply.status(404);
return new Error("role not found");
}
await role.updateOne({
$set: {
name: request.body?.name,
permissions: request.body?.permissions && convertArrayToRecord(request.body.permissions, true),
updatedAt: Date.now(),
},
});
return role;
};
export const restoreRole = async (request: RemoveRoleRequest, reply: FastifyReply) => {
const { id } = request.body || {};
if (!id || !Types.ObjectId.isValid(id)) {
void reply.status(400);
return new Error("wrong id");
}
const role = await RoleModel.findById(id);
if (!role) {
void reply.status(404);
return new Error("role by id not found");
}
if (!role.isDeleted) {
void reply.status(409);
return new Error("role not removed");
}
await role.updateOne({ $set: { isDeleted: false } });
return role;
};

@ -1,22 +0,0 @@
import type { FastifyRequest } from "fastify";
type RoleRequest = {
name?: string;
permissions?: string[];
};
export type CreateRoleRequest = FastifyRequest<{ Body: RoleRequest }>;
export type RemoveRoleRequest = FastifyRequest<{ Body: { id?: string } }>;
export type GetRoleRequest = FastifyRequest<{
Params: {
query?: string;
};
}>;
export type UpdateRoleRequest = FastifyRequest<{
Body?: RoleRequest;
Params: {
query?: string;
};
}>;

@ -1,18 +0,0 @@
import { validateEmptyFields } from "@/utils/validate-empty-fields";
import type { TariffMessage } from "@/types/messages/tariff-message.type";
import type { ObjectWithRequiredFields } from "@/types/object-with-required-fields";
export const validateTariff = (tariff?: TariffMessage): [ObjectWithRequiredFields<TariffMessage>, Error | null] => {
const [validatedTariff, errorEmpty] = validateEmptyFields(tariff ?? {}, ["isCustom", "name", "privilegies"], false);
if (errorEmpty) {
return [validatedTariff, errorEmpty];
}
if (validatedTariff.price && isNaN(Number(validatedTariff.price))) {
return [validatedTariff, new Error("invalid 'price' value")];
}
return [validatedTariff, null];
};

@ -1,221 +0,0 @@
import { Types } from "mongoose";
import { TariffModel } from "@/models/tariff.model";
import { PrivilegeModel } from "@/models/privilege.model";
import { validateEmptyFields } from "@/utils/validate-empty-fields";
import { determinePaginationParameters } from "@/utils/determine-pagination-parameters";
import { validateTariff } from "./helpers";
import type { FastifyReply } from "fastify";
import type { Privilege } from "@/types/models/privilege.type";
import type { Tariff } from "@/types/models/tariff.type";
import type { Eloquent } from "@/types/models/eloquent.type";
import type {
CreateTariffRequest,
GetTariffRequest,
ReplaceTariffRequest,
RemoveTariffRequest,
GetTariffsResponse,
GetTariffsRequest,
} from "./types";
export const getTariffs = async (request: GetTariffsRequest): Promise<GetTariffsResponse> => {
const { page, limit } = determinePaginationParameters(request?.query ?? {});
const tariffsCount = await TariffModel.countDocuments({
$or: [{ isCustom: false }, { isCustom: true, userId: request.user.id }],
});
const totalPages = Math.ceil(tariffsCount / limit);
const offset = (page - 1) * limit;
const tariffs = await TariffModel.find({ $or: [{ isCustom: false }, { isCustom: true, userId: request.user.id }] })
.sort({ createdAt: "desc" })
.skip(offset)
.limit(limit)
.lean();
return { tariffs, totalPages };
};
export const getTariff = async (request: GetTariffRequest, reply: FastifyReply): Promise<Tariff> => {
const [requestParams, error] = validateEmptyFields(request.params ?? {}, ["id"]);
if (error) {
void reply.status(400);
throw error;
}
if (!Types.ObjectId.isValid(requestParams.id)) {
void reply.status(400);
throw new Error("invalid id");
}
const tariff = await TariffModel.findOne({
$or: [
{ _id: new Types.ObjectId(requestParams.id), isCustom: false },
{ _id: new Types.ObjectId(requestParams.id), isCustom: true, userId: request.user.id },
],
}).lean();
if (!tariff) {
void reply.status(404);
throw new Error("tariff not found");
}
return tariff;
};
export const createTariff = async (request: CreateTariffRequest, reply: FastifyReply): Promise<Tariff> => {
const [requestBody, error] = validateTariff(request.body);
if (error) {
void reply.status(400);
throw error;
}
for (const privilege of requestBody.privilegies) {
if (!Types.ObjectId.isValid(privilege.privilegeId)) {
void reply.status(404);
throw new Error(`privilege id <${privilege.privilegeId}> invalid`);
}
}
const newTariff = new TariffModel({
name: requestBody.name,
price: requestBody.price,
userId: request.user.id,
isCustom: requestBody.isCustom,
privilegies: requestBody.privilegies,
});
return newTariff.save();
};
export const replaceTariff = async (request: ReplaceTariffRequest, reply: FastifyReply): Promise<Tariff> => {
const [requestBody, error] = validateTariff(request.body ?? {});
if (error) {
void reply.status(400);
throw error;
}
if (!Types.ObjectId.isValid(request.params?.id || "")) {
void reply.status(400);
throw new Error("invalid id");
}
const tariff = await TariffModel.findById(request.params?.id);
if (!tariff) {
void reply.status(404);
throw new Error("tariff not found");
}
for (const privilege of requestBody.privilegies) {
if (!Types.ObjectId.isValid(privilege.privilegeId)) {
void reply.status(404);
throw new Error(`privilege id <${privilege.privilegeId}> invalid`);
}
}
const privilegiesMap = new Map(requestBody.privilegies.map((privilege) => [privilege.privilegeId, privilege]));
const privilegeIDs = requestBody.privilegies.map(({ privilegeId }) => privilegeId);
const privilegies = await PrivilegeModel.find({ _id: privilegeIDs }).lean();
const cleanPrivilegies = privilegies.map<Omit<Privilege, keyof Eloquent>>((privilege) => {
const currentPrivilege = privilegiesMap.get(privilege._id.toString());
return {
name: privilege.name,
privilegeId: privilege.privilegeId,
serviceKey: privilege.serviceKey,
description: privilege.description,
amount: currentPrivilege?.amount ?? 0,
type: privilege.type,
value: privilege.value,
price: privilege.price,
};
});
await tariff.replaceOne({
name: requestBody.name,
price: requestBody.price,
isCustom: requestBody.isCustom,
privilegies: cleanPrivilegies,
});
return tariff;
};
export const removeTariff = async (request: RemoveTariffRequest, reply: FastifyReply): Promise<Tariff> => {
const [{ id }, error] = validateEmptyFields(request.body ?? {}, ["id"]);
if (error) {
void reply.status(400);
throw error;
}
if (!Types.ObjectId.isValid(id)) {
void reply.status(400);
throw new Error("invalid id");
}
const tariff = await TariffModel.findByIdAndUpdate(id, {
$set: { isDeleted: true, deletedAt: new Date() },
}).lean();
if (!tariff) {
void reply.status(404);
throw new Error("tariff not found");
}
return tariff;
};
export const deleteTariff = async (request: RemoveTariffRequest, reply: FastifyReply): Promise<Tariff> => {
const [{ id }, error] = validateEmptyFields(request.body ?? {}, ["id"]);
if (error) {
void reply.status(400);
throw error;
}
if (!Types.ObjectId.isValid(id)) {
void reply.status(400);
throw new Error("invalid id");
}
const tariff = await TariffModel.findByIdAndDelete(id).lean();
if (!tariff) {
void reply.status(404);
throw new Error("tariff not found");
}
return tariff;
};
export const restoreTariff = async (request: RemoveTariffRequest, reply: FastifyReply): Promise<Tariff> => {
const [{ id }, error] = validateEmptyFields(request.body ?? {}, ["id"]);
if (error) {
void reply.status(400);
throw error;
}
if (!Types.ObjectId.isValid(id)) {
void reply.status(400);
throw new Error("invalid id");
}
const tariff = await TariffModel.findByIdAndUpdate(id, { $set: { isDeleted: false } }).lean();
if (!tariff) {
void reply.status(404);
throw new Error("tariff not found");
}
return tariff;
};

@ -1,34 +0,0 @@
import type { FastifyRequest } from "fastify";
import type { TariffMessage } from "@/types/messages/tariff-message.type";
import type { Tariff } from "@/types/models/tariff.type";
import type { PaginationParams } from "@/types/messages/pagination.type";
export type GetTariffRequest = FastifyRequest<{
Params?: {
id?: string;
};
}>;
export type RemoveTariffRequest = FastifyRequest<{
Body?: {
id?: string;
};
}>;
export type CreateTariffRequest = FastifyRequest<{
Body?: TariffMessage;
}>;
export type ReplaceTariffRequest = FastifyRequest<{
Body?: TariffMessage;
Params?: { id?: string };
}>;
export type GetTariffsRequest = FastifyRequest<{
Querystring?: PaginationParams;
}>;
export type GetTariffsResponse = {
tariffs: Tariff[];
totalPages: number;
};

@ -1,27 +0,0 @@
import "./configuration/configure-env";
import { Server } from "./server";
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({
serverOptions: CONFIGURATION.http,
databaseOptions: CONFIGURATION.db,
pluginsOptions: {
jwt: {
secret: {
public: CONFIGURATION.service.publicAccessSecretKey,
private: "secret",
},
verify: {
allowedIss: "pena-auth-service",
allowedAud: "pena",
},
},
},
});
server.start();

@ -1,36 +0,0 @@
import { Schema, model, SchemaDefinition } from "mongoose";
import { eloquentSchema } from "./eloquent.schema";
import type { Account } from "@/types/models/account.type";
const schema: SchemaDefinition<Account> = {
userId: {
type: String,
required: true,
index: { unique: true },
},
nickname: {
type: String,
required: true,
default: "nickname",
},
avatar: {
type: String,
default: "/media/img/no-avatar.png",
},
role: {
type: String,
default: "user",
},
...eloquentSchema,
};
const schemaSettings = {
versionKey: false,
collection: "accounts",
};
const AccountSchema = new Schema<Account>(schema, schemaSettings);
export const AccountModel = model("Account", AccountSchema);

@ -1,25 +0,0 @@
import { SchemaDefinition } from "mongoose";
import type { Eloquent } from "@/types/models/eloquent.type";
export const eloquentSchema: SchemaDefinition<Eloquent> = {
createdAt: {
type: Date,
required: true,
default: Date.now,
},
updatedAt: {
type: Date,
required: true,
default: Date.now,
},
deletedAt: {
type: Date,
required: false,
},
isDeleted: {
type: Boolean,
required: true,
default: false,
},
};

@ -1,28 +0,0 @@
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);

@ -1,55 +0,0 @@
import { Schema, model, SchemaDefinition } from "mongoose";
import { eloquentSchema } from "./eloquent.schema";
import type { Privilege } from "@/types/models/privilege.type";
const schema: SchemaDefinition<Privilege> = {
name: {
type: String,
required: true,
},
privilegeId: {
type: String,
required: true,
index: true,
},
amount: {
type: Number,
require: false,
default: 1,
},
serviceKey: {
type: String,
required: true,
index: true,
},
description: {
type: String,
default: "",
},
type: {
type: String,
required: true,
default: "count",
},
value: {
type: String,
required: true,
default: "1",
},
price: {
type: Number,
required: false,
},
...eloquentSchema,
};
const schemaSettings = {
versionKey: false,
collection: "privilegies",
};
export const PrivilegeSchema = new Schema<Privilege>(schema, schemaSettings);
export const PrivilegeModel = model("Privilege", PrivilegeSchema);

@ -1,27 +0,0 @@
import { Schema, model, SchemaDefinition } from "mongoose";
import { eloquentSchema } from "./eloquent.schema";
import type { Role } from "@/types/models/role.type";
const schema: SchemaDefinition<Role> = {
name: {
type: String,
required: true,
},
permissions: {
type: Map,
of: Boolean,
default: {},
},
...eloquentSchema,
};
const schemaSettings = {
versionKey: false,
collection: "roles",
};
const RoleSchema = new Schema<Role>(schema, schemaSettings);
export const RoleModel = model("Role", RoleSchema);

@ -1,39 +0,0 @@
import { Schema, model, SchemaDefinition } from "mongoose";
import { eloquentSchema } from "./eloquent.schema";
import { PrivilegeSchema } from "./privilege.model";
import type { Tariff } from "@/types/models/tariff.type";
const schema: SchemaDefinition<Tariff> = {
name: {
type: String,
required: true,
},
price: {
type: Number,
required: false,
},
userId: {
type: String,
required: true,
},
isCustom: {
type: Boolean,
default: false,
},
privilegies: {
type: [PrivilegeSchema],
default: [],
},
...eloquentSchema,
};
const schemaSettings = {
versionKey: false,
collection: "tariffs",
};
const TariffSchema = new Schema<Tariff>(schema, schemaSettings);
export const TariffModel = model("Tariff", TariffSchema);

@ -1,44 +0,0 @@
import fastifyPlugin from "fastify-plugin";
import type { FastifyError, FastifyInstance, FastifyPluginOptions, RouteOptions } from "fastify";
type PrintRoutesRouteOptions = {
hide?: boolean;
};
const METHODS_ORDER = ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS"];
const printRoutes = (routes: Array<RouteOptions & PrintRoutesRouteOptions> = [], isSwagger = false) => {
if (routes.length === 0) {
return;
}
const filteredRoutes = routes.filter((route) => route.hide !== true);
const sortedRoutes = [...filteredRoutes].sort((routeA, routeB) => routeA.url.localeCompare(routeB.url));
const output = sortedRoutes.reduce<string>((accamulator, route) => {
const methods = Array.isArray(route.method) ? route.method : [route.method];
const methodsValue = [...methods].sort((a, b) => METHODS_ORDER.indexOf(a) - METHODS_ORDER.indexOf(b)).join(" | ");
return accamulator + `${methodsValue}\t${route.url}\n`;
}, "");
const swaggerOutput = isSwagger ? "GET\t/swagger\n" : "";
console.info(`\n\nAvailable routes:\n${output + swaggerOutput}`);
};
export default fastifyPlugin(
(instance: FastifyInstance, _: FastifyPluginOptions, done: (error?: FastifyError) => void) => {
const routes: Array<RouteOptions> = [];
instance.addHook("onRoute", (route) => {
routes.push(route);
});
instance.addHook("onReady", (next) => {
printRoutes(routes, !!instance.swagger);
next();
});
done();
}
);

@ -1,50 +0,0 @@
import { Router } from "@/server/router";
import {
createAccount,
getAccountByID,
getAccount,
setAccountRole,
getAccounts,
deleteAccount,
deleteAccountById,
removeAccount,
removeAccountById,
restoreAccount,
} from "@/handlers/account";
import { verifyUser } from "@/handlers/auth/middleware";
import {
createAccountSchema,
getAccountByIdSchema,
getAccountSchema,
setAccountRoleSchema,
getAccountsSchema,
removeAccountSchema,
removeAccountByIdSchema,
restoreAccountSchema,
deleteAccountSchema,
deleteAccountByIdSchema,
} from "@/swagger/account";
export const setAccountRoutes = (router: Router): void => {
router.get("/pagination", getAccounts, { schema: getAccountsSchema });
router.get("/:userId", getAccountByID, { schema: getAccountByIdSchema });
router.get("/", getAccount, { preHandler: [verifyUser], schema: getAccountSchema });
router.post("/", createAccount, { preHandler: [verifyUser], schema: createAccountSchema });
router.post("/restore", restoreAccount, { preHandler: [verifyUser], schema: restoreAccountSchema });
router.patch("/role", setAccountRole, { preHandler: [verifyUser], schema: setAccountRoleSchema });
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/:userId", deleteAccountById, {
preHandler: [verifyUser],
schema: deleteAccountByIdSchema,
});
};

@ -1,31 +0,0 @@
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 });
};

@ -1,40 +0,0 @@
import { Router } from "@/server/router";
import {
registerPrivilege,
getAllPrivilegies,
getAllPrivilegiesMap,
getPrivilege,
getServicePrivilegies,
replacePrivilege,
registerPrivilegies,
removePrivilege,
replacePrivilegies,
restorePrivilege,
} from "@/handlers/privilege";
import {
getPrivilegiesSchema,
getPrivilegiesMapSchema,
getPrivilegeSchema,
getServicePrivilegiesSchema,
registerPrivilegeSchema,
registerPrivilegiesSchema,
replacePrivilegeSchema,
replacePrivilegiesSchema,
removePrivilegeSchema,
restorePrivilegeSchema,
} from "@/swagger/privilege";
export const setPrivilegeRoutes = (router: Router): void => {
router.get("/", getAllPrivilegies, { schema: getPrivilegiesSchema });
router.get("/service", getAllPrivilegiesMap, { schema: getPrivilegiesMapSchema });
router.get("/:privilegeId", getPrivilege, { schema: getPrivilegeSchema });
router.get("/service/:serviceKey", getServicePrivilegies, { schema: getServicePrivilegiesSchema });
router.post("/", registerPrivilege, { schema: registerPrivilegeSchema });
router.post("/many", registerPrivilegies, { schema: registerPrivilegiesSchema });
router.post("/restore/", restorePrivilege, { schema: restorePrivilegeSchema });
router.put("/", replacePrivilege, { schema: replacePrivilegeSchema });
router.put("/many", replacePrivilegies, { schema: replacePrivilegiesSchema });
router.delete("/", removePrivilege, { schema: removePrivilegeSchema });
};

@ -1,34 +0,0 @@
import { Router } from "@/server/router";
import {
getRole,
getAllRoles,
createRole,
deleteRole,
updateRole,
replaceRole,
removeRole,
restoreRole,
} from "@/handlers/roles";
import {
getRolesSchema,
getRoleSchema,
createRoleSchema,
restoreRoleSchema,
updateRoleSchema,
replaceRoleSchema,
removeRoleSchema,
deleteRoleSchema,
} from "@/swagger/role";
export const setRoleRoutes = (router: Router): void => {
router.get("/", getAllRoles, { schema: getRolesSchema });
router.get("/:query", getRole, { schema: getRoleSchema });
router.post("/", createRole, { schema: createRoleSchema });
router.post("/restore", restoreRole, { schema: restoreRoleSchema });
router.patch("/:query", updateRole, { schema: updateRoleSchema });
router.put("/:query", replaceRole, { schema: replaceRoleSchema });
router.delete("/", removeRole, { schema: removeRoleSchema });
router.delete("/delete", deleteRole, { schema: deleteRoleSchema });
};

@ -1,32 +0,0 @@
import { Router } from "@/server/router";
import {
createTariff,
replaceTariff,
getTariff,
getTariffs,
removeTariff,
restoreTariff,
deleteTariff,
} from "@/handlers/tariff";
import { verifyUser } from "@/handlers/auth/middleware";
import {
getTariffSchema,
getTariffsSchema,
createTariffsSchema,
replaceTariffsSchema,
removeTariffsSchema,
restoreTariffsSchema,
deleteTariffsSchema,
} from "@/swagger/tariff";
export const setTariffRoutes = (router: Router): void => {
router.get("/", getTariffs, { preHandler: [verifyUser], schema: getTariffsSchema });
router.get("/:id", getTariff, { preHandler: [verifyUser], schema: getTariffSchema });
router.post("/", createTariff, { preHandler: [verifyUser], schema: createTariffsSchema });
router.post("/restore", restoreTariff, { preHandler: [verifyUser], schema: restoreTariffsSchema });
router.put("/:id", replaceTariff, { preHandler: [verifyUser], schema: replaceTariffsSchema });
router.delete("/", removeTariff, { preHandler: [verifyUser], schema: removeTariffsSchema });
router.delete("/delete", deleteTariff, { preHandler: [verifyUser], schema: deleteTariffsSchema });
};

@ -1,61 +0,0 @@
import fastify, { FastifyInstance } from "fastify";
import { connect as mongoConnect } from "mongoose";
import { Router } from "./router";
import { registerFastifyPlugins } from "@/configuration/register-fastify-plugins";
import { combineRoutes } from "@/configuration/combine-routes";
import { constituteMongoURI } from "@/configuration/constitute-mongo-uri";
import { DEFAULT } from "@/constants/default";
import type { PluginsOptions } from "@/types/configuration/plugins-options";
import type { DatabaseOptions } from "@/types/configuration/database-options";
type ServerOptions = {
port?: number;
host?: string;
backlog?: number;
};
type ServerArgs = {
serverOptions?: ServerOptions;
pluginsOptions?: PluginsOptions;
databaseOptions?: DatabaseOptions;
};
export class Server {
private fastify: FastifyInstance;
private serverOptions?: ServerOptions;
private databaseOptions?: DatabaseOptions;
/**
* Настройка сервера
* @param {ServerOptions} serverOptions объект настроек сервера.
* @param {DatabaseOptions} databaseOptions объект настроек подключения к базе данных.
* @param {PluginsOptions} pluginsOptions объект настроек плагинов fastify.
*/
constructor({ serverOptions, databaseOptions, pluginsOptions }: ServerArgs) {
this.serverOptions = serverOptions;
this.databaseOptions = databaseOptions;
this.fastify = fastify(DEFAULT.fastifyOptions);
if (pluginsOptions) {
registerFastifyPlugins(this.fastify, pluginsOptions);
}
combineRoutes(new Router(this.fastify));
}
public start = async () => {
const fasticyConnection = this.fastify.listen(this.serverOptions);
const databaseConnection = this.databaseOptions ? mongoConnect(constituteMongoURI(this.databaseOptions)) : null;
await Promise.all([databaseConnection, fasticyConnection])
.then(() => {
this.fastify.swagger();
console.info(`server started on ${this.serverOptions?.host}:${this.serverOptions?.port}`);
})
.catch((reason) => console.error(reason));
};
}

@ -1,77 +0,0 @@
import type {
FastifyInstance,
RouteHandlerMethod,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
RouteShorthandOptions,
FastifyPluginOptions,
RawServerDefault,
RouteGenericInterface,
} from "fastify";
type HandlerMethod<T extends RouteGenericInterface> = RouteHandlerMethod<
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
T
>;
export class Router {
private fastifyInstance: FastifyInstance;
constructor(fastifyInstance: FastifyInstance) {
this.fastifyInstance = fastifyInstance;
}
public group = (path: string, combineRoutes: (router: Router) => void) => {
this.fastifyInstance.register(
(server: FastifyInstance, opts: FastifyPluginOptions, done: () => void) => {
const router = new Router(server);
combineRoutes(router);
done();
},
{ prefix: path }
);
};
public get = <T extends RouteGenericInterface>(
path: string,
handler: HandlerMethod<T>,
options: RouteShorthandOptions = {}
) => {
this.fastifyInstance.get(path, options, handler);
};
public post = <T extends RouteGenericInterface>(
path: string,
handler: HandlerMethod<T>,
options: RouteShorthandOptions = {}
) => {
this.fastifyInstance.post(path, options, handler);
};
public put = <T extends RouteGenericInterface>(
path: string,
handler: HandlerMethod<T>,
options: RouteShorthandOptions = {}
) => {
this.fastifyInstance.put(path, options, handler);
};
public patch = <T extends RouteGenericInterface>(
path: string,
handler: HandlerMethod<T>,
options: RouteShorthandOptions = {}
) => {
this.fastifyInstance.patch(path, options, handler);
};
public delete = <T extends RouteGenericInterface>(
path: string,
handler: HandlerMethod<T>,
options: RouteShorthandOptions = {}
) => {
this.fastifyInstance.delete(path, options, handler);
};
}

@ -1,16 +0,0 @@
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;
}
}

@ -1,6 +0,0 @@
import type { Account } from "@/types/models/account.type";
import type { Role } from "@/types/models/role.type";
export interface AccountService {
determineAccountRole(account: Account): Promise<Role>;
}

@ -1,6 +0,0 @@
import { AccountService } from "@/services/account/account.service";
import { PermissionService } from "./permission.service";
export const PermissionModule = new PermissionService({
accountService: AccountService,
});

@ -1,78 +0,0 @@
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);
});
});
});

@ -1,39 +0,0 @@
import { PermissionModel } from "@/models/permission.model";
import { PERMISSIONS, PERMISSION_LIST } from "@/constants/permissions";
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];
}
}

@ -1,91 +0,0 @@
import { getAccountParams, setAccountRoleBody, getAccountsQuerystring } from "./inputs";
import {
getAccountResponse,
createAccountResponse,
setAccountRoleResponse,
getAccountsResponse,
removeAccountResponse,
} from "./responses";
import type { SwaggerSchema } from "@/types/swagger.type";
export const getAccountsSchema: SwaggerSchema = {
summary: "Получение информации об аккаунтах",
description: "Получение список аккаунтов с пагинацией из БД",
tags: ["account"],
querystring: getAccountsQuerystring,
response: getAccountsResponse,
};
export const getAccountByIdSchema: SwaggerSchema = {
summary: "Получение информации об аккаунте",
description: "Получение аккаунта по ID",
tags: ["account"],
params: getAccountParams,
response: getAccountResponse,
};
export const getAccountSchema: SwaggerSchema = {
summary: "Получение информации об аккаунте",
description: "Получение информации об аккаунте через токен доступа",
tags: ["account"],
security: [{ bearer: [] }],
response: getAccountResponse,
};
export const createAccountSchema: SwaggerSchema = {
summary: "Создание аккаунта",
tags: ["account"],
response: createAccountResponse,
security: [{ bearer: [] }],
};
export const setAccountRoleSchema: SwaggerSchema = {
summary: "Присвоение роли пользователя",
tags: ["account"],
body: setAccountRoleBody,
response: setAccountRoleResponse,
security: [{ bearer: [] }],
};
export const removeAccountSchema: SwaggerSchema = {
summary: "Удаление аккаунта",
description: "Помечает аккаунт удалённым, но не удаляет его из БД",
tags: ["account"],
response: removeAccountResponse,
security: [{ bearer: [] }],
};
export const removeAccountByIdSchema: SwaggerSchema = {
summary: "Удаление аккаунта по ID",
description: "Помечает аккаунт удалённым, но не удаляет его из БД",
tags: ["account"],
params: getAccountParams,
response: removeAccountResponse,
security: [{ bearer: [] }],
};
export const deleteAccountSchema: SwaggerSchema = {
summary: "Удаление аккаунта",
description: "Удаляет аккаунт из БД окончательно",
tags: ["account"],
response: removeAccountResponse,
security: [{ bearer: [] }],
};
export const deleteAccountByIdSchema: SwaggerSchema = {
summary: "Удаление аккаунта по ID",
description: "Удаляет аккаунт из БД окончательно",
tags: ["account"],
params: getAccountParams,
response: removeAccountResponse,
security: [{ bearer: [] }],
};
export const restoreAccountSchema: SwaggerSchema = {
summary: "Восстановление аккаунта",
description: "Восстанавливает аккаунт, который не был удалён окончательно",
tags: ["account"],
response: removeAccountResponse,
security: [{ bearer: [] }],
};

@ -1,41 +0,0 @@
import type { SwaggerMessage } from "@/types/swagger.type";
export const getAccountParams: SwaggerMessage = {
type: "object",
required: ["userId"],
properties: {
userId: {
type: "string",
description: "ID пользователя",
},
},
};
export const getAccountsQuerystring: SwaggerMessage = {
type: "object",
properties: {
page: {
type: "number",
description: "номер страницы",
},
limit: {
type: "number",
description: "Лимит количества аккаунтов (больше 100 не обрабатывается)",
},
},
};
export const setAccountRoleBody: SwaggerMessage = {
type: "object",
required: ["userId", "role"],
properties: {
userId: {
type: "string",
description: "ID пользователя",
},
role: {
type: "string",
description: "название роли",
},
},
};

Some files were not shown because too many files have changed in this diff Show More