Sfoglia il codice sorgente

добавлена валидация .env

Vadim 7 mesi fa
parent
commit
f6198952e5

+ 68 - 0
config/config.ts

@@ -0,0 +1,68 @@
+import "../src/plugins/dotenv.js";
+import { z } from "zod";
+
+// Создаем схему конфигурации с проверками и преобразованиями
+const ZConfigShema = z.object({
+  PORT: z
+    .string()
+    .min(1, "PORT must be defined")
+    .transform((val) => parseInt(val, 10)),
+  LOGS_LEVEL: z.enum(["silly", "verbose", "info", "warn", "error", "fatal"]),
+  API_URL: z.string().min(1, "API_URL must be defined"),
+
+  // DB
+  DB_USER: z.string().min(1, "DB_USER must be defined"),
+  DB_HOST: z.string().min(1, "DB_HOST must be defined"),
+  DB_PORT: z
+    .string()
+    .min(1, "DB_PORT must be defined")
+    .transform((val) => parseInt(val, 10)),
+  DB_NAME: z.string().min(1, "DB_NAME must be defined"),
+  DB_PASS: z.string().min(1, "DB_PASS must be defined"),
+
+  // MAIL
+  SMTP_HOST: z.string().min(1, "SMTP_HOST must be defined"),
+  SMTP_PORT: z
+    .string()
+    .min(1, "SMTP_PORT must be defined")
+    .transform((val) => parseInt(val, 10)),
+  SMTP_USER: z.string().min(1, "SMTP_USER must be defined"),
+  SMTP_PASS: z.string().min(1, "SMTP_PASS must be defined"),
+
+  // AUTH
+  JWT_ACCESS_SECRET: z.string().min(1, "JWT_ACCESS_SECRET must be defined"),
+  JWT_REFRESH_SECRET: z.string().min(1, "JWT_REFRESH_SECRET must be defined"),
+  PASSWORD_MAX_TRIES: z
+    .string()
+    .min(1, "PASSWORD_MAX_TRIES must be defined")
+    .transform((val) => parseInt(val, 10)),
+  ACCESS_TOKEN_LIFETIME_MINS: z
+    .string()
+    .min(1, "ACCESS_TOKEN_LIFETIME_MINS must be defined")
+    .transform((val) => parseInt(val, 10)),
+  REFRESH_TOKEN_LIFETIME_DAYS: z
+    .string()
+    .min(1, "REFRESH_TOKEN_LIFETIME_DAYS must be defined")
+    .transform((val) => parseInt(val, 10)),
+
+  // CONFIRM PINS
+  CONFIRM_PIN_LIFETIME_MINS: z
+    .string()
+    .min(1, "CONFIRM_PIN_LIFETIME_MINS must be defined")
+    .transform((val) => parseInt(val, 10)),
+  CONFIRM_PIN_MAX_TRIES: z
+    .string()
+    .min(1, "CONFIRM_PIN_MAX_TRIES must be defined")
+    .transform((val) => parseInt(val, 10)),
+
+  // UIDS
+  COMPANY_DEFAULT_ROLE_MANAGER_ID: z
+    .string()
+    .uuid("COMPANY_DEFAULT_ROLE_MANAGER_ID must be defined"),
+  COMPANY_DEFAULT_ROLE_EMPLOYEE_ID: z
+    .string()
+    .uuid("COMPANY_DEFAULT_ROLE_EMPLOYEE_ID must be defined"),
+});
+
+// Парсим и валидируем переменные окружения
+export const config = ZConfigShema.parse(process.env);

+ 8 - 8
package-lock.json

@@ -32,7 +32,7 @@
         "@types/express": "^4.17.21",
         "@types/jsonwebtoken": "^9.0.5",
         "@types/multer": "^1.4.11",
-        "@types/node": "^20.11.3",
+        "@types/node": "^20.17.16",
         "@types/nodemailer": "^6.4.16",
         "@types/pg": "^8.10.9",
         "@types/uuid": "^10.0.0",
@@ -815,11 +815,11 @@
       }
     },
     "node_modules/@types/node": {
-      "version": "20.11.3",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.3.tgz",
-      "integrity": "sha512-nrlmbvGPNGaj84IJZXMPhQuCMEVTT/hXZMJJG/aIqVL9fKxqk814sGGtJA4GI6hpJSLQjpi6cn0Qx9eOf9SDVg==",
+      "version": "20.17.16",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.16.tgz",
+      "integrity": "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==",
       "dependencies": {
-        "undici-types": "~5.26.4"
+        "undici-types": "~6.19.2"
       }
     },
     "node_modules/@types/nodemailer": {
@@ -4909,9 +4909,9 @@
       "dev": true
     },
     "node_modules/undici-types": {
-      "version": "5.26.5",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
-      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+      "version": "6.19.8",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
     },
     "node_modules/universalify": {
       "version": "0.1.2",

+ 2 - 1
package.json

@@ -10,7 +10,8 @@
     "#db-shema": "./src/db/db-shema.ts",
     "#exceptions": "./src/exceptions/",
     "#logger": "./src/plugins/logger.ts",
-    "#dayjs": "./src/plugins/dayjs.ts"
+    "#dayjs": "./src/plugins/dayjs.ts",
+    "#config": "./config/config.ts"
   },
   "husky": {
     "hooks": {

+ 1 - 1
src/db/db-shema.ts

@@ -111,6 +111,7 @@ const ZDbShema = {
     user_roles: {
       user_id: z.string(),
       role_id: z.string(),
+      entity_id: z.string(),
     },
 
     non_default_roles_overrides: {
@@ -125,7 +126,6 @@ const ZDbShema = {
       name: z.string(),
       description: z.string(),
       entity_id: z.string().nullable(),
-      entity_type_id: z.string(),
       is_system: z.boolean(),
     },
 

+ 6 - 5
src/db/db.ts

@@ -3,12 +3,13 @@ import {
   createErrorLoggingInterceptor,
   createResultParserInterceptor,
 } from "./db-interrceptors.js";
+import { config } from "#config";
 
-const host = process.env.DB_HOST;
-const port = process.env.DB_PORT;
-const user = process.env.DB_USER;
-const password = process.env.DB_PASS;
-const databaseName = process.env.DB_NAME;
+const host = config.DB_HOST;
+const port = config.DB_PORT;
+const user = config.DB_USER;
+const password = config.DB_PASS;
+const databaseName = config.DB_NAME;
 
 const pool = await createPool(
   `postgres://${user}:${password}@${host}:${port}/${databaseName}`,

+ 2 - 2
src/main.ts

@@ -1,12 +1,12 @@
 logger.info("Start server");
 
-import "./plugins/dotenv.js";
+import { config } from "#config";
 
 import express from "express";
 import { logger } from "#logger";
 import DbService from "./db/db-service.js";
 
-const PORT = process.env.PORT || 3001;
+const PORT = config.PORT;
 
 const app = express();
 

+ 2 - 2
src/modules/companies-management/companies-router.ts

@@ -32,6 +32,7 @@ import { RouterUtils } from "#utils/router-utils.js";
 import { ApiError } from "#exceptions/api-error.js";
 import { CompaniesService } from "./companies-service.js";
 import { CheckPermissionsService } from "#modules/permissions-management/check-permissions-service.js";
+import { config } from "#config";
 
 dayjs.extend(utc);
 
@@ -69,12 +70,11 @@ router.post("/create-company", async (req, res, next) => {
     );
 
     // permissions
-    const MANAGER_ROLE_ID = process.env.COMPANY_DEFAULT_ROLE_MANAGER_ID;
+    const MANAGER_ROLE_ID = config.COMPANY_DEFAULT_ROLE_MANAGER_ID;
     if (!MANAGER_ROLE_ID) {
       throw Error("COMPANY_DEFAULT_ROLE_MANAGER_ID is not defined");
     }
 
-    // TODO сделать триггер для проверка entity_id и is_defaul в БД при создании роли.
     await db.query(
       sql.unsafe`
       insert into permissions_management.user_roles

+ 2 - 1
src/modules/events-management/events-router.ts

@@ -30,6 +30,7 @@ import { UserUtils } from "#utils/user-utils.js";
 import { CheckPermissionsService } from "#modules/permissions-management/check-permissions-service.js";
 import { EntityesService } from "#modules/entities-management/entityes-service.js";
 import { RouterUtils } from "#utils/router-utils.js";
+import { config } from "#config";
 
 dayjs.extend(utc);
 
@@ -98,7 +99,7 @@ router.post("/create-event", async (req, res, next) => {
     }
 
     // add role to user
-    const MANAGER_ROLE_ID = process.env.EVENT_DEFAULT_ROLE_MANAGER_ID;
+    const MANAGER_ROLE_ID = config.EVENT_DEFAULT_ROLE_MANAGER_ID;
     if (!MANAGER_ROLE_ID) {
       throw Error("EVENT_DEFAULT_ROLE_MANAGER_ID is not defined");
     }

+ 3 - 2
src/modules/users-management/auth/routers/auth-router.ts

@@ -28,6 +28,7 @@ import tokenService from "../services/token-service.js";
 import { UserAuthService } from "../services/user-auth-service.js";
 import { ConfirmPinsService } from "#modules/users-management/confirm-pins/confirm-pins-service.js";
 import { RouterUtils } from "#utils/router-utils.js";
+import { config } from "#config";
 
 dayjs.extend(utc);
 
@@ -199,7 +200,7 @@ router.post("/login", async (req, res, next) => {
     }
 
     // если количество попыток превышено
-    if (user.wrong_pass_tries > Number(process.env.PASSWORD_MAX_TRIES) - 1) {
+    if (user.wrong_pass_tries > config.PASSWORD_MAX_TRIES - 1) {
       RouterUtils.validAndSendResponse(
         AuthApi.ZLogin.res,
         res,
@@ -216,7 +217,7 @@ router.post("/login", async (req, res, next) => {
     if (!isPassEquals) {
       await UserAuthService.authTriesIncrement(user.user_id);
       const triesRemained =
-        Number(process.env.PASSWORD_MAX_TRIES) - 1 - user.wrong_pass_tries;
+        config.PASSWORD_MAX_TRIES - 1 - user.wrong_pass_tries;
 
       RouterUtils.validAndSendResponse(
         AuthApi.ZLogin.res,

+ 6 - 6
src/modules/users-management/auth/services/token-service.ts

@@ -1,3 +1,5 @@
+import { config } from "#config";
+
 import jwt from "jsonwebtoken";
 
 // база данных
@@ -8,13 +10,11 @@ import { sql } from "slonik";
 import { ZTokenPayload } from "../types/token-playload-type.js";
 import { z } from "zod";
 
-const JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || "";
-const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || "";
+const JWT_ACCESS_SECRET = config.JWT_ACCESS_SECRET || "";
+const JWT_REFRESH_SECRET = config.JWT_REFRESH_SECRET || "";
 
-const ACCESS_TOKEN_LIFETIME_MINS =
-  process.env.ACCESS_TOKEN_LIFETIME_MINS || "15";
-const REFRESH_TOKEN_LIFETIME_DAYS =
-  process.env.REFRESH_TOKEN_LIFETIME_DAYS || "30";
+const ACCESS_TOKEN_LIFETIME_MINS = config.ACCESS_TOKEN_LIFETIME_MINS || "15";
+const REFRESH_TOKEN_LIFETIME_DAYS = config.REFRESH_TOKEN_LIFETIME_DAYS || "30";
 
 class TokenService {
   generateTokens(payload: z.infer<typeof ZTokenPayload>): {

+ 4 - 6
src/modules/users-management/confirm-pins/confirm-pins-service.ts

@@ -8,6 +8,7 @@ import { z } from "zod";
 
 // dayjs
 import { dayjs, DayjsUtils } from "#dayjs";
+import { config } from "#config";
 
 class confirmPinsService {
   // privalte
@@ -111,10 +112,7 @@ class confirmPinsService {
     }
 
     // много попыток
-    if (
-      pinInfo.wrong_pin_tries >
-      Number(process.env.CONFIRM_PIN_MAX_TRIES) - 1
-    ) {
+    if (pinInfo.wrong_pin_tries > config.CONFIRM_PIN_MAX_TRIES - 1) {
       return { status: "tooManyTries" };
     }
 
@@ -123,7 +121,7 @@ class confirmPinsService {
       DayjsUtils.createDayjsUtcWithoutOffset().isAfter(
         dayjs
           .utc(pinInfo.create_time)
-          .add(Number(process.env.CONFIRM_PIN_LIFETIME_MINS), "minutes"),
+          .add(config.CONFIRM_PIN_LIFETIME_MINS, "minutes"),
       )
     ) {
       await this.deleteConfirmPin(transactionId);
@@ -135,7 +133,7 @@ class confirmPinsService {
       await this.pinTriesIncrement(transactionId);
 
       const triesRemained =
-        Number(process.env.CONFIRM_PIN_MAX_TRIES) - 1 - pinInfo.wrong_pin_tries;
+        config.CONFIRM_PIN_MAX_TRIES - 1 - pinInfo.wrong_pin_tries;
 
       return {
         status: "wrong",

+ 2 - 1
src/plugins/logger.ts

@@ -1,5 +1,6 @@
 import { createLogger, format, transports } from "winston";
 import path from "path";
+import { config } from "#config";
 
 const __dirname = path.resolve();
 
@@ -37,7 +38,7 @@ const customFormat = format.combine(
 
 // Логгер Winston
 export const logger = createLogger({
-  level: process.env.LOGS_LEVEL || "error", // Уровень логирования
+  level: config.LOGS_LEVEL, // Уровень логирования
   format: customFormat, // Формат логов
   transports: [
     new transports.Console({

+ 14 - 13
src/services/mail-service.ts

@@ -1,44 +1,45 @@
 import nodemailer, { type Transporter } from "nodemailer";
 import { logger } from "#logger";
+import { config } from "#config";
 
 class mailService {
   private transporter: Transporter;
 
   constructor() {
     this.transporter = nodemailer.createTransport({
-      host: process.env.SMTP_HOST,
-      port: Number(process.env.SMTP_PORT),
+      host: config.SMTP_HOST,
+      port: config.SMTP_PORT,
       secure: true,
       auth: {
-        user: process.env.SMTP_USER,
-        pass: process.env.SMTP_PASS,
+        user: config.SMTP_USER,
+        pass: config.SMTP_PASS,
       },
     });
   }
 
   public async sendMail(to: string, title: string, html: string) {
     logger.silly("Попытка отправки письма: ", {
-      host: process.env.SMTP_HOST,
-      port: Number(process.env.SMTP_PORT),
-      user: process.env.SMTP_USER,
-      from: process.env.SMTP_USER,
+      host: config.SMTP_HOST,
+      port: config.SMTP_PORT,
+      user: config.SMTP_USER,
+      from: config.SMTP_USER,
       to: to,
       subject: title,
       html: html,
     });
 
     await this.transporter.sendMail({
-      from: process.env.SMTP_USER,
+      from: config.SMTP_USER,
       to: to,
       subject: title,
       html: html,
     });
 
     logger.silly("Письмо отправлено: ", {
-      host: process.env.SMTP_HOST,
-      port: Number(process.env.SMTP_PORT),
-      user: process.env.SMTP_USER,
-      from: process.env.SMTP_USER,
+      host: config.SMTP_HOST,
+      port: config.SMTP_PORT,
+      user: config.SMTP_USER,
+      from: config.SMTP_USER,
       to: to,
       subject: title,
       html: html,

+ 3 - 1
tsconfig.json

@@ -35,7 +35,9 @@
     // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
     // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
     // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
-    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+    "types": [
+      "node"
+    ] /* Specify type package names to be included without being referenced in a source file. */,
     // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
     // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
     // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */