Vadim 8 kuukautta sitten
vanhempi
commit
57328f09eb

+ 1 - 1
nodemon.json

@@ -1,5 +1,5 @@
 {
-  "watch": ["src", ".env"],
+  "watch": ["src", ".env", ".dev.env"],
   "ext": ".ts, .js",
   "ignore": [],
   "exec": "npx tsx ./src/main.ts && npx eslint ./src"

+ 1 - 1
package.json

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

+ 4 - 3
src/api/auth-api.ts

@@ -20,6 +20,7 @@ class authApi {
       password: z.string(),
       transactionId: z.string().uuid(),
       confirmPin: z.number().min(1000).max(9999),
+      timezone: z.string(),
     }),
     res: z.object({
       code: z.enum(["registered", "pinIsWrong", "pinIsRotten", "tooManyTries"]),
@@ -29,7 +30,7 @@ class authApi {
           accessToken: z.string(),
           refreshToken: z.string(),
           email: z.string().email(),
-          userId: z.string().uuid(),
+          userId: z.string(),
         })
         .optional(),
     }),
@@ -49,7 +50,7 @@ class authApi {
           accessToken: z.string(),
           refreshToken: z.string(),
           email: z.string().email(),
-          userId: z.string().uuid(),
+          userId: z.string(),
         })
         .optional(),
     }),
@@ -77,7 +78,7 @@ class authApi {
         accessToken: z.string(),
         refreshToken: z.string(),
         email: z.string().email(),
-        userId: z.string().uuid(),
+        userId: z.string(),
       }),
     }),
   };

+ 26 - 0
src/api/company-manager-api.ts

@@ -0,0 +1,26 @@
+import { z } from "zod";
+
+class companyManagerApi {
+  public ZAddCompany = {
+    req: z.object({
+      name: z.string(),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  public ZGetUserCompanies = {
+    res: z.object({
+      code: z.enum(["success"]),
+      companies: z.array(
+        z.object({
+          company_id: z.string(),
+          name: z.string(),
+        }),
+      ),
+    }),
+  };
+}
+
+export const CompanyManagerApi = new companyManagerApi();

+ 241 - 0
src/api/event-manager-api.ts

@@ -0,0 +1,241 @@
+import { z } from "zod";
+
+class eventManagerApi {
+  // /event/create-event
+  ZCreateEvent = {
+    req: z.object({
+      name: z.string(),
+      timezone: z.string().optional(),
+      dates: z.array(z.string()),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+      event_id: z.string(),
+    }),
+  };
+
+  ZGetEventList = {
+    req: z.object({}),
+    res: z.object({
+      code: z.enum(["success"]),
+      events: z.array(
+        z.object({
+          event_id: z.string(),
+          local_name: z.string(),
+          timezone: z.string(),
+          dates: z.array(z.string()),
+        }),
+      ),
+    }),
+  };
+
+  ZGetEvent = {
+    req: z.object({ eventId: z.string() }),
+    res: z.object({
+      code: z.enum(["success"]),
+      event: z.object({
+        event_id: z.string(),
+        local_name: z.string(),
+        timezone: z.string(),
+        dates: z.array(z.string()),
+        programPoints: z.array(
+          z.object({
+            point_id: z.string(),
+            name: z.string(),
+            date_start: z.date(),
+            date_end: z.date(),
+            room_id: z.string().nullable(),
+            is_internal: z.boolean(),
+          }),
+        ),
+
+        rooms: z.array(
+          z.object({
+            room_id: z.string(),
+            name: z.string(),
+          }),
+        ),
+      }),
+    }),
+  };
+
+  // /event/get-month-events
+  // ZGetMonthEvents = {
+  //   req: z.object({
+  //     month: z.number().min(0).max(11),
+  //     year: z.number(),
+  //     timezone: z.string(),
+  //   }),
+  //   res: z.object({
+  //     code: z.enum(["success"]),
+  //     events: z.array(
+  //       z.object({
+  //         event_id: z.string(),
+  //         local_name: z.string(),
+  //         date_start: z.date(),
+  //         date_end: z.date(),
+  //         timezone: z.string(),
+  //       }),
+  //     ),
+  //   }),
+  // };
+
+  // /event/add-program-point
+  ZAddProgramPoint = {
+    req: z.object({
+      eventId: z.string(),
+      name: z.string(),
+      dateStart: z.string(),
+      dateEnd: z.string(),
+      roomId: z.string().optional(),
+      isInternal: z.boolean(),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  // /event/add-user-room
+  ZAddUserRoom = {
+    req: z.object({
+      name: z.string(),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  // /event/add-user-room
+  ZGetUserRooms = {
+    // req: z.object({
+    //   permission: z.enum(["creator", "editor", "viewer"]),
+    // }),
+
+    res: z.object({
+      code: z.enum(["success"]),
+      rooms: z.array(
+        z.object({
+          room_id: z.string(),
+          name: z.string(),
+          permission: z.enum(["editor", "viewer"]),
+        }),
+      ),
+    }),
+  };
+
+  ZUpdateEvent = {
+    req: z.object({
+      eventId: z.string(),
+      name: z.string(),
+      timezone: z.string(),
+      dates: z.array(z.string()),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  ZUpdateProgramPoint = {
+    req: z.object({
+      pointId: z.string(),
+      name: z.string(),
+      dateStart: z.string(),
+      dateEnd: z.string(),
+      roomId: z.string().optional(),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  ZAddTaskBlock = {
+    req: z.object({
+      eventId: z.string(),
+      name: z.string(),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  ZUpdateTaskBlock = {
+    req: z.object({
+      taskBlockId: z.string(),
+      name: z.string(),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  ZGetTaskBlocks = {
+    req: z.object({ eventId: z.string() }),
+    res: z.object({
+      code: z.enum(["success"]),
+      taskBlocks: z.array(
+        z.object({
+          task_block_id: z.string(),
+          name: z.string(),
+        }),
+      ),
+    }),
+  };
+
+  ZAddTask = {
+    req: z.object({
+      name: z.string(),
+      dateStart: z.string().nullable(),
+      dateEnd: z.string().nullable(),
+      ownerId: z.string(),
+      isTodo: z.boolean(),
+      taskBlockId: z.string(),
+      pointId: z.string().nullable(),
+      roomId: z.string().nullable(),
+      executors: z.array(z.string()),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  ZUpdateTask = {
+    req: z.object({
+      taskId: z.string(),
+      name: z.string(),
+      dateStart: z.string().nullable(),
+      dateEnd: z.string().nullable(),
+      ownerId: z.string(),
+      isTodo: z.boolean(),
+      taskBlockId: z.string(),
+      pointId: z.string().nullable(),
+      roomId: z.string().nullable(),
+      executors: z.array(z.string()),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+    }),
+  };
+
+  ZGetBlockTasks = {
+    req: z.object({ taskBlockId: z.string() }),
+    res: z.object({
+      code: z.enum(["success"]),
+      tasks: z.array(
+        z.object({
+          task_id: z.string(),
+          name: z.string(),
+          date_start: z.date().nullable(),
+          date_end: z.date().nullable(),
+          owner_id: z.string(),
+          is_todo: z.boolean(),
+          task_block_id: z.string(),
+          point_id: z.string().nullable(),
+          room_id: z.string().nullable(),
+          executors: z.array(z.string()),
+        }),
+      ),
+    }),
+  };
+}
+
+export const EventManagerApi = new eventManagerApi();

+ 0 - 1
src/db/db-interrceptor.ts

@@ -14,7 +14,6 @@ export const createResultParserInterceptor = (): Interceptor => {
     // transform results as needed in `transformRow`.
     transformRow: (executionContext, actualQuery, row) => {
       const { resultParser } = executionContext;
-
       logger.trace("Запрос: ", { actualQuery, row });
 
       if (!resultParser) {

+ 82 - 2
src/db/db-shema.ts

@@ -3,20 +3,100 @@ import { z } from "zod";
 const ZDbShema = {
   users: {
     user_profiles: {
-      user_id: z.string().uuid(),
+      user_id: z.string(),
       email: z.string().email(),
       password: z.string(),
       wrong_pass_tries: z.number(),
+      timezone: z.string(),
     },
 
     user_confirm_pins: {
-      transaction_id: z.string().uuid(),
+      transaction_id: z.string(),
       email: z.string().email(),
       confirm_pin: z.number().min(1000).max(9999),
       create_time: z.date(),
       wrong_pin_tries: z.number(),
     },
   },
+
+  event_manager: {
+    events: {
+      event_id: z.string(),
+      local_name: z.string(),
+      timezone: z.string(),
+      dates: z.array(z.string()),
+    },
+
+    event_permissions: {
+      permission_id: z.number(),
+      event_id: z.string(),
+      user_id: z.string(),
+      permission: z.enum(["editor", "viewer"]),
+    },
+
+    program_points: {
+      point_id: z.string(),
+      name: z.string().min(1),
+      date_start: z.date(),
+      date_end: z.date(),
+      room_id: z.string().nullable(),
+      event_id: z.string(),
+      is_internal: z.boolean(),
+    },
+
+    rooms: {
+      room_id: z.string(),
+      name: z.string(),
+    },
+  },
+
+  task_manager: {
+    tasks: {
+      task_id: z.string(),
+      name: z.string(),
+      date_start: z.date().nullable(),
+      date_end: z.date().nullable(),
+      owner_id: z.string(),
+      is_todo: z.boolean(),
+      task_block_id: z.string(),
+      point_id: z.string().nullable(),
+      room_id: z.string().nullable(),
+    },
+
+    task_blocks: {
+      task_block_id: z.string(),
+      name: z.string(),
+      event_id: z.string(),
+    },
+
+    task_block_permissions: {
+      task_block_permission_id: z.number(),
+      task_block_id: z.string(),
+      user_id: z.string(),
+      permission: z.enum(["editor", "viewer"]),
+    },
+
+    task_executors: {
+      task_executor_id: z.string(),
+      task_id: z.string(),
+      user_id: z.string(),
+    },
+  },
+
+  company_manager: {
+    companies: {
+      company_id: z.string(),
+      name: z.string(),
+      owner_id: z.string(),
+    },
+
+    company_permissions: {
+      company_permission_id: z.string(),
+      company_id: z.string(),
+      user_id: z.string(),
+      permission: z.enum(["editor", "viewer"]),
+    },
+  },
 };
 
 export { ZDbShema };

+ 4 - 0
src/exceptions/api-error.ts

@@ -23,6 +23,10 @@ export const ApiError = class ApiError extends Error {
     );
   }
 
+  static ForbiddenError() {
+    return new ApiError(403, "forbiddenError", "Недостаточно прав");
+  }
+
   static BadRequest(code: string, message: string, errors?) {
     return new ApiError(400, code, message, errors || []);
   }

+ 4 - 0
src/main.ts

@@ -35,10 +35,14 @@ app.use((req, res, next) => {
 });
 
 // роутеры
+import authMiddleware from "./middlewares/auth-middleware.js";
 
 import authRouter from "./modules/user/auth/routers/auth-router.js";
 app.use("/api/auth/", authRouter);
 
+import EventManagerRouter from "./modules/event-manager/event-manager-router.js";
+app.use("/api/event-manager/", authMiddleware(), EventManagerRouter);
+
 // import sessionRouter from "./modules/user/routers/session-router.js";
 // app.use("/api/session/", sessionRouter);
 

+ 2 - 1
src/middlewares/auth-middleware.ts

@@ -1,8 +1,9 @@
 import { ApiError } from "../exceptions/api-error.js";
 import tokenService from "#modules/user/auth/services/token-service.js";
+import type { NextFunction, Request, Response } from "express";
 
 export default function () {
-  return function (req, res, next) {
+  return function (req: Request, res: Response, next: NextFunction) {
     try {
       // авторизация
       const authorizationHeader = req.headers.authorization;

+ 11 - 6
src/middlewares/error-middleware.ts

@@ -13,23 +13,28 @@ export default function (err, req, res, next) {
   }
 
   if (err instanceof ZodError) {
-    logger.error({ message: "Ошибка валидации", err: err });
-    return res.status(400).json({ message: "Ошибка валидации", err: err });
+    logger.error({ message: "Ошибка валидации", err: err.toString() });
+    return res
+      .status(400)
+      .json({ message: "Ошибка валидации", err: err.toString() });
   }
 
   if (err instanceof SchemaValidationError) {
-    logger.error({ message: "Ошибка несоотвествия схемы БД", err: err });
+    logger.error({
+      message: "Ошибка несоотвествия схемы БД",
+      err: err.toString(),
+    });
     return res.status(500).json({ message: "Ошибка несоотвествия схемы БД" });
   }
 
   if (err instanceof SlonikError) {
-    logger.error({ message: "Ошибка запроса БД", err: err });
+    logger.error({ message: "Ошибка запроса БД", err: err.toString() });
     return res.status(500).json({ message: "Ошибка запроса БД" });
   }
 
-  logger.error({ message: "Непредвиденная ошибка", err: err });
+  logger.error({ message: "Непредвиденная ошибка", err: err.toString() });
 
   return res
     .status(500)
-    .json({ message: "Непредвиденная ошибка", err: String(err) });
+    .json({ message: "Непредвиденная ошибка", err: err.toString() });
 }

+ 66 - 0
src/modules/company/company-router.ts

@@ -0,0 +1,66 @@
+// router
+import express from "express";
+const router = express.Router();
+export default router;
+
+// db
+import { db } from "#db";
+import { ZDbShema } from "#db-shema";
+import { sql } from "slonik";
+
+// api
+import { CompanyManagerApi } from "#api/company-manager-api.ts";
+
+// error
+import { ApiError } from "#exceptions/api-error.ts";
+
+// dayjs
+import dayjs from "dayjs";
+import utc from "dayjs/plugin/utc.js";
+dayjs.extend(utc);
+import timezone from "dayjs/plugin/timezone.js";
+dayjs.extend(timezone);
+
+// other
+import { z } from "zod";
+
+import { v7 as uuidv7 } from "uuid";
+// import { logger } from "#logger";
+import { UserUtils } from "#utils/user-utils.js";
+
+dayjs.extend(utc);
+
+router.post("/add-company", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = CompanyManagerApi.ZAddCompany.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { name } = CompanyManagerApi.ZAddCompany.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    const companyId = uuidv7();
+
+    // company
+    await db.query(
+      sql.unsafe`
+      insert into users.companies
+        (company_id, name, owner_id)
+      values
+        (${companyId}, ${name}, ${userId})`,
+    );
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});

+ 859 - 0
src/modules/event-manager/event-manager-router.ts

@@ -0,0 +1,859 @@
+// router
+import express from "express";
+const router = express.Router();
+export default router;
+
+// db
+import { db } from "#db";
+import { ZDbShema } from "#db-shema";
+import { sql } from "slonik";
+
+// api
+import { EventManagerApi } from "#api/event-manager-api.ts";
+
+// error
+import { ApiError } from "#exceptions/api-error.ts";
+
+// dayjs
+import dayjs from "dayjs";
+import utc from "dayjs/plugin/utc.js";
+dayjs.extend(utc);
+import timezone from "dayjs/plugin/timezone.js";
+dayjs.extend(timezone);
+
+// other
+import { z } from "zod";
+
+import { v7 as uuidv7 } from "uuid";
+// import { logger } from "#logger";
+import { UserUtils } from "#utils/user-utils.js";
+
+dayjs.extend(utc);
+
+router.post("/create-event", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZCreateEvent.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { name, dates, timezone } = EventManagerApi.ZCreateEvent.req.parse(
+      req.body,
+    );
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // если тз не указана, то ставим тз пользователя
+    let tz = timezone;
+    if (!tz) {
+      const userTz = (
+        await db.one(
+          sql.type(
+            z.object({ timezone: ZDbShema.users.user_profiles.timezone }),
+          )`select timezone from users.user_profiles where user_id = ${userId}`,
+        )
+      ).timezone;
+
+      tz = userTz;
+    }
+    //
+
+    const eventId = uuidv7();
+
+    // event
+    await db.query(
+      sql.unsafe`
+      insert into event_manager.events 
+        (event_id, local_name, timezone, dates) 
+      values 
+        (${eventId}, ${name}, ${tz}, ${JSON.stringify(dates)})`,
+    );
+
+    // permission
+    await db.query(
+      sql.unsafe`
+      insert into event_manager.event_permissions 
+        (event_id, user_id, permission) 
+      values 
+        (${eventId}, ${userId}, 'creator')`,
+    );
+
+    sendRes(200, { code: "success", event_id: eventId });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/get-event-list", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZGetEventList.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+    // TODO сделать проверку на юзера
+    // const userId = UserUtils.getUserFromReq(req).userId;
+    // const userTz = (
+    //   await db.one(
+    //     sql.type(
+    //       z.object({ timezone: ZDbShema.users.user_profiles.timezone }),
+    //     )`select timezone from users.user_profiles where user_id = ${userId}`,
+    //   )
+    // ).timezone;
+
+    const nowWithUzerTz = dayjs.utc().startOf("day");
+
+    const DbEventsTypes = ZDbShema.event_manager.events;
+    const events = await db.any(
+      sql.type(
+        z.object({
+          event_id: DbEventsTypes.event_id,
+          local_name: DbEventsTypes.event_id,
+          timezone: DbEventsTypes.timezone,
+          dates: DbEventsTypes.dates,
+        }),
+      )`
+      select
+        event_id,
+        local_name,
+        timezone,
+        dates
+      from
+        event_manager.events
+      where
+        exists (
+          select
+          from
+            jsonb_array_elements_text(dates)
+          where
+            value::date >= ${nowWithUzerTz.toISOString()}
+        )
+        `,
+    );
+
+    sendRes(200, { code: "success", events: [...events] });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/get-event", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZGetEvent.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { eventId } = EventManagerApi.ZGetEvent.req.parse(req.body);
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // проверка прав
+    await UserUtils.checkEventPermissions(eventId, userId, "viewer");
+
+    // event
+    const DbEventsTypes = ZDbShema.event_manager.events;
+    const event = await db.maybeOne(
+      sql.type(
+        z.object({
+          event_id: DbEventsTypes.event_id,
+          local_name: DbEventsTypes.local_name,
+          timezone: DbEventsTypes.timezone,
+          dates: DbEventsTypes.dates,
+        }),
+      )`
+      select
+        event_id,
+        local_name,
+        timezone,
+        dates
+      from
+        event_manager.events
+      where 
+        event_id = ${eventId}
+        `,
+    );
+
+    if (!event) throw ApiError.BadRequest("EventNotFound", "Ивент не найден");
+
+    // points
+    const DbPointsType = ZDbShema.event_manager.program_points;
+    const programPoints = await db.any(
+      sql.type(
+        z.object({
+          point_id: DbPointsType.point_id,
+          name: DbPointsType.name,
+          date_start: DbPointsType.date_start,
+          date_end: DbPointsType.date_end,
+          room_id: DbPointsType.room_id,
+          is_internal: DbPointsType.is_internal,
+        }),
+      )`
+        select
+          point_id,
+          name,
+          date_start,
+          date_end,
+          room_id,
+          is_internal
+        from
+          event_manager.program_points pp
+        where
+          pp.event_id = ${eventId}
+        `,
+    );
+
+    //
+    const eventRoomsIds = new Set<string>();
+
+    for (const point of programPoints) {
+      if (point.room_id) eventRoomsIds.add(point.room_id);
+    }
+    //
+    // rooms
+    const eventRoomsIdsArr = Array.from(eventRoomsIds);
+    let rooms: readonly {
+      name: string;
+      room_id: string;
+    }[] = [];
+
+    // TODO ошибка в in?
+    if (eventRoomsIdsArr.length > 0) {
+      console.log(1);
+
+      rooms = await db.any(
+        sql.type(
+          z.object({
+            room_id: ZDbShema.event_manager.rooms.room_id,
+            name: ZDbShema.event_manager.rooms.name,
+          }),
+        )`
+          select
+            room_id, name
+          from
+            event_manager.rooms
+          where
+            room_id in (${sql.join(eventRoomsIdsArr, sql.fragment`, `)})
+          `,
+      );
+    }
+
+    // res
+    sendRes(200, {
+      code: "success",
+      event: {
+        ...event,
+        programPoints: [...programPoints],
+        rooms: [...rooms],
+      },
+    });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/add-program-point", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZAddProgramPoint.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { name, dateStart, dateEnd, eventId, roomId, isInternal } =
+      EventManagerApi.ZAddProgramPoint.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // проверка прав
+    await UserUtils.checkEventPermissions(eventId, userId, "editor");
+
+    const pointId = uuidv7();
+
+    // event
+    await db.query(
+      sql.unsafe`
+        insert into event_manager.program_points
+          (point_id, name, date_start, date_end, room_id, event_id, is_internal)
+        values
+          (${pointId}, ${name}, ${dateStart}, ${dateEnd}, ${roomId || null}, ${eventId}, ${isInternal})
+      `,
+    );
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/add-user-room", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZAddUserRoom.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { name } = EventManagerApi.ZAddUserRoom.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    const roomId = uuidv7();
+
+    await db.query(
+      sql.unsafe`
+      insert into event_manager.rooms 
+        (room_id, name) 
+      values 
+        (${roomId}, ${name})`,
+    );
+
+    await db.query(
+      sql.unsafe`
+      insert into event_manager.room_user_permissions 
+        (user_id, room_id, permission) 
+      values 
+        (${userId}, ${roomId}, 'creator')`,
+    );
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/get-user-rooms", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZGetUserRooms.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    // const { permission } = EventManagerApi.ZGetUserRooms.req.parse(req.body);
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    const permissions = ["viewer", "editor"];
+
+    const rooms = await db.any(
+      sql.type(
+        z.object({
+          room_id: ZDbShema.event_manager.rooms.room_id,
+          name: ZDbShema.event_manager.rooms.name,
+          permission: ZDbShema.event_manager.event_permissions.permission,
+        }),
+      )`
+      select
+        r.room_id,
+        r."name",
+        rup."permission"
+      from
+        event_manager.rooms r
+      join event_manager.room_user_permissions rup on
+        r.room_id = rup.room_id
+      where
+        rup.user_id = ${userId}
+        and rup.permission in (${sql.join(permissions, sql.fragment`, `)})
+        `,
+    );
+
+    // res
+    sendRes(200, {
+      code: "success",
+      rooms: [...rooms],
+    });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/update-event", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZUpdateEvent.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { eventId, name, dates, timezone } =
+      EventManagerApi.ZUpdateEvent.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // проверка прав
+    await UserUtils.checkEventPermissions(eventId, userId, "editor");
+
+    // event
+    await db.query(
+      sql.unsafe`
+      update event_manager.events
+      set
+        local_name = ${name},
+        timezone = ${timezone},
+        dates = ${JSON.stringify(dates)}
+      where
+        event_id = ${eventId}`,
+    );
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/update-program-point", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZUpdateProgramPoint.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { pointId, name, dateStart, dateEnd, roomId } =
+      EventManagerApi.ZUpdateProgramPoint.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    const eventId = await db.maybeOneFirst(
+      sql.type(
+        z.object({
+          event_id: ZDbShema.event_manager.events.event_id,
+        }),
+      )`
+      select
+        event_id
+      from
+        event_manager.program_points
+      where 
+        point_id = ${pointId}
+      `,
+    );
+
+    if (!eventId) {
+      throw ApiError.BadRequest("Point not found", "Point not found");
+    }
+
+    // проверка прав
+    await UserUtils.checkEventPermissions(eventId, userId, "editor");
+
+    // point
+    await db.query(
+      sql.unsafe`
+      update event_manager.program_points
+      set
+        name = ${name},
+        date_start = ${dateStart},
+        date_end = ${dateEnd},
+        room_id = ${roomId || null}
+      where
+        point_id = ${pointId}`,
+    );
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/add-task-block", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZAddTaskBlock.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { name, eventId } = EventManagerApi.ZAddTaskBlock.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // проверка прав
+    await UserUtils.checkEventPermissions(eventId, userId, "editor");
+
+    const blockId = uuidv7();
+
+    // block
+    await db.query(
+      sql.unsafe`
+      insert into task_manager.task_blocks
+        (task_block_id, name, event_id)
+      values
+        (${blockId}, ${name}, ${eventId})`,
+    );
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/update-task-block", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZUpdateTaskBlock.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { taskBlockId, name } = EventManagerApi.ZUpdateTaskBlock.req.parse(
+      req.body,
+    );
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    const eventId = await db.maybeOneFirst(
+      sql.type(
+        z.object({
+          event_id: ZDbShema.event_manager.events.event_id,
+        }),
+      )`
+      select
+        event_id
+      from
+        task_manager.task_blocks
+      where 
+        task_block_id = ${taskBlockId}
+      `,
+    );
+
+    if (!eventId) {
+      throw ApiError.BadRequest("Block not found", "Block not found");
+    }
+
+    // проверка прав
+    await UserUtils.checkEventPermissions(eventId, userId, "editor");
+
+    // block
+    await db.query(
+      sql.unsafe`
+      update task_manager.task_blocks
+      set
+        name = ${name}
+      where
+        task_block_id = ${taskBlockId}`,
+    );
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/get-task-blocks", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZGetTaskBlocks.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { eventId } = EventManagerApi.ZGetTaskBlocks.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // проверка прав
+    await UserUtils.checkEventPermissions(eventId, userId, "viewer");
+
+    const taskBlocks = await db.any(
+      sql.type(
+        z.object({
+          task_block_id: ZDbShema.task_manager.task_blocks.task_block_id,
+          name: ZDbShema.task_manager.task_blocks.name,
+        }),
+      )`
+        select
+          task_block_id,
+          name
+        from
+          task_manager.task_blocks
+        where
+          event_id = ${eventId}
+        `,
+    );
+
+    // res
+    sendRes(200, {
+      code: "success",
+      taskBlocks: [...taskBlocks],
+    });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/add-task", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZAddTask.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const {
+      dateEnd,
+      dateStart,
+      isTodo,
+      name,
+      ownerId,
+      taskBlockId,
+      pointId,
+      executors,
+      roomId,
+    } = EventManagerApi.ZAddTask.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // проверка прав
+
+    await UserUtils.checkTaskBlockPermissions(taskBlockId, userId, "editor");
+
+    const taskId = uuidv7();
+
+    // task
+    await db.query(
+      sql.unsafe`
+      insert into task_manager.tasks
+        (task_id, name, date_start, date_end, owner_id, is_todo, task_block_id, point_id, room_id)
+      values
+        (${taskId}, ${name}, ${dateStart}, ${dateEnd}, ${ownerId}, ${isTodo}, ${taskBlockId}, ${pointId}, ${roomId})`,
+    );
+
+    // executors
+    const executorsWithId = executors.map((e) => ({
+      task_executor_id: uuidv7(),
+      task_id: taskId,
+      user_id: e,
+    }));
+
+    for (const executor of executorsWithId) {
+      await db.query(
+        sql.unsafe`
+        insert into task_manager.task_executors
+          (task_executor_id, task_id, user_id)
+        values
+          (${executor.task_executor_id}, ${executor.task_id}, ${executor.user_id})`,
+      );
+    }
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/update-task", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZUpdateTask.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const {
+      dateEnd,
+      dateStart,
+      isTodo,
+      name,
+      pointId,
+      taskBlockId,
+      executors,
+      taskId,
+      roomId,
+    } = EventManagerApi.ZUpdateTask.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // проверка прав
+
+    await UserUtils.checkTaskBlockPermissions(taskBlockId, userId, "editor");
+
+    // task
+    await db.query(
+      sql.unsafe`
+      update task_manager.tasks
+      set
+        name = ${name},
+        date_start = ${dateStart},
+        date_end = ${dateEnd},
+        is_todo = ${isTodo},
+        point_id = ${pointId},
+        room_id = ${roomId}
+      where
+        task_id = ${taskId}`,
+    );
+
+    // executors
+    await db.query(
+      sql.unsafe`
+      delete from task_manager.task_executors
+      where
+        task_id = ${taskId}`,
+    );
+
+    const executorsWithId = executors.map((e) => ({
+      task_executor_id: uuidv7(),
+      task_id: taskId,
+      user_id: e,
+    }));
+
+    for (const executor of executorsWithId) {
+      await db.query(
+        sql.unsafe`
+        insert into task_manager.task_executors
+          (task_executor_id, task_id, user_id)
+        values
+          (${executor.task_executor_id}, ${executor.task_id}, ${executor.user_id})`,
+      );
+    }
+
+    sendRes(200, { code: "success" });
+  } catch (e) {
+    next(e);
+  }
+});
+
+router.post("/get-block-tasks", async (req, res, next) => {
+  try {
+    // функция валидации ответа
+    const ZResType = EventManagerApi.ZGetBlockTasks.res; //change
+    const sendRes = (
+      status: 200 | 400 | 500,
+      resData: z.infer<typeof ZResType>,
+    ) => {
+      const response = ZResType.parse(resData);
+      res.status(status).json(response);
+    };
+    //
+
+    // валидация запроса
+    const { taskBlockId } = EventManagerApi.ZGetBlockTasks.req.parse(req.body);
+
+    const userId = UserUtils.getUserFromReq(req).userId;
+
+    // проверка прав
+
+    await UserUtils.checkTaskBlockPermissions(taskBlockId, userId, "viewer");
+
+    const tasks = await db.any(
+      sql.type(
+        z.object({
+          task_id: ZDbShema.task_manager.tasks.task_id,
+          name: ZDbShema.task_manager.tasks.name,
+          date_start: ZDbShema.task_manager.tasks.date_start,
+          date_end: ZDbShema.task_manager.tasks.date_end,
+          is_todo: ZDbShema.task_manager.tasks.is_todo,
+          owner_id: ZDbShema.task_manager.tasks.owner_id,
+          task_block_id: ZDbShema.task_manager.tasks.task_block_id,
+          point_id: ZDbShema.task_manager.tasks.point_id,
+          room_id: ZDbShema.task_manager.tasks.room_id,
+          executors: z.array(ZDbShema.task_manager.task_executors.user_id),
+        }),
+      )`
+        select
+          t.task_id,
+          t.name,
+          t.date_start,
+          t.date_end,
+          t.is_todo,
+          t.owner_id,
+          t.task_block_id,
+          t.point_id,
+          t.room_id,
+          array(
+          select
+            user_id
+          from
+            task_manager.task_executors e
+          where
+            e.task_id = t.task_id) as executors
+        from
+          task_manager.tasks t`,
+    );
+
+    // res
+    sendRes(200, {
+      code: "success",
+      tasks: [...tasks],
+    });
+  } catch (e) {
+    next(e);
+  }
+});

+ 9 - 4
src/modules/user/auth/routers/auth-router.ts

@@ -23,7 +23,7 @@ dayjs.extend(utc);
 import { ZodTypeAny, z } from "zod";
 import bcript from "bcrypt";
 import { v7 as uuidv7 } from "uuid";
-import { DayjsUtils } from "#dayjsUtils";
+
 import { logger } from "#logger";
 import tokenService from "../services/token-service.js";
 import { UserAuthService } from "../services/user-auth-service.js";
@@ -86,7 +86,7 @@ router.post("/confirm-registration", async (req, res, next) => {
     };
     //
     // валидация запроса
-    const { email, password, transactionId, confirmPin } =
+    const { email, password, transactionId, confirmPin, timezone } =
       AuthApi.ZConfirmRegistration.req.parse(req.body);
 
     // проверка пина
@@ -125,7 +125,12 @@ router.post("/confirm-registration", async (req, res, next) => {
         z.object({
           user_id: ZDbShema.users.user_profiles.user_id,
         }),
-      )`insert into users.user_profiles (user_id, email, password) values (${uuidv7()}, ${email}, ${hashPassword}) returning user_id`,
+      )`
+      insert into users.user_profiles 
+        (user_id, email, password, timezone) 
+      values 
+        (${uuidv7()}, ${email}, ${hashPassword}, ${timezone}) 
+      returning user_id`,
     );
 
     // токены
@@ -276,7 +281,7 @@ router.post("/logoutAllDevices", async (req, res, next) => {
   }
 });
 
-router.post("/refresh", async (req, res, next) => {
+router.get("/refresh", async (req, res, next) => {
   try {
     // функция валидации ответа
     const ZResType = AuthApi.ZRefresh.res; //change

+ 1 - 1
src/modules/user/auth/types/token-playload-type.ts

@@ -1,6 +1,6 @@
 import { z } from "zod";
 
 export const ZTokenPayload = z.object({
-  userId: z.string().uuid(),
+  userId: z.string(),
   email: z.string().email(),
 });

+ 3 - 6
src/modules/user/confirm-pins/confirm-pins-service.ts

@@ -7,10 +7,7 @@ import { ZDbShema } from "#db-shema";
 import { z } from "zod";
 
 // dayjs
-import dayjs from "dayjs";
-import utc from "dayjs/plugin/utc.js";
-dayjs.extend(utc);
-import { DayjsUtils } from "#dayjsUtils";
+import { dayjs, DayjsUtils } from "#dayjs";
 
 class confirmPinsService {
   // privalte
@@ -59,7 +56,7 @@ class confirmPinsService {
         ${transactionId}, 
         ${email}, 
         ${confirmPin}, 
-        ${DayjsUtils.createLocalNowDayjs().toISOString()})`,
+        ${DayjsUtils.createDayjsUtcWithoutOffset().toISOString()})`,
     );
 
     logger.info("Отправлен временный код: ", {
@@ -106,7 +103,7 @@ class confirmPinsService {
 
     // просрочка
     if (
-      DayjsUtils.createLocalNowDayjs().isAfter(
+      DayjsUtils.createDayjsUtcWithoutOffset().isAfter(
         dayjs
           .utc(pinInfo.create_time)
           .add(Number(process.env.CONFIRM_PIN_LIFETIME_MINS), "minutes"),

+ 44 - 0
src/plugins/dayjs.ts

@@ -0,0 +1,44 @@
+type dayjsInput = string | number | dayjs.Dayjs | Date | null | undefined;
+type Dayjs = dayjs.Dayjs;
+
+//  plugins
+import dayjs from "dayjs";
+
+import utc from "dayjs/plugin/utc.js";
+dayjs.extend(utc);
+
+import timezone from "dayjs/plugin/timezone.js";
+dayjs.extend(timezone);
+
+// import weekOfYear from "dayjs/plugin/weekOfYear.js";
+// dayjs.extend(weekOfYear);
+
+// import weekday from "dayjs/plugin/weekday.js";
+// dayjs.extend(weekday);
+
+import ru from "dayjs/locale/ru";
+dayjs.locale(ru);
+
+import customParseFormat from "dayjs/plugin/customParseFormat.js";
+dayjs.extend(customParseFormat);
+
+// import localeData from 'dayjs/plugin/localeData';
+// dayjs.extend(localeData);
+
+// import localizedFormat from "dayjs/plugin/localizedFormat.js";
+// dayjs.extend(localizedFormat);
+
+const DayjsUtils = new (class dayjsUtils {
+  public createDayjsUtcWithoutOffset() {
+    return dayjs.utc(
+      dayjs().format("YYYY-MM-DDTHH:mm:ss"),
+      "YYYY-MM-DDTHH:mm:ss",
+    );
+  }
+})();
+
+//
+
+export { DayjsUtils, dayjs };
+
+export type { Dayjs };

+ 1 - 0
src/plugins/logger.ts

@@ -28,4 +28,5 @@ log4js.configure({
     },
   },
 });
+
 export const logger = log4js.getLogger();

+ 8 - 0
src/types/custom.d.ts

@@ -0,0 +1,8 @@
+declare namespace Express {
+  export interface Request {
+    user?: {
+      email: string;
+      userId: string;
+    };
+  }
+}

+ 0 - 16
src/utils/dayjs-utils.ts

@@ -1,16 +0,0 @@
-import dayjs from "dayjs";
-import utc from "dayjs/plugin/utc.js";
-dayjs.extend(utc);
-
-// база данных
-
-class dayjsUtils {
-  public createLocalNowDayjs() {
-    return dayjs.utc(
-      dayjs().format("YYYY-MM-DDTHH:mm:ss"),
-      "YYYY-MM-DDTHH:mm:ss",
-    );
-  }
-}
-
-export const DayjsUtils = new dayjsUtils();

+ 114 - 0
src/utils/user-utils.ts

@@ -0,0 +1,114 @@
+import type { Request } from "express";
+// types
+import { ZTokenPayload } from "#modules/user/auth/types/token-playload-type.js";
+import { z } from "zod";
+import { ApiError } from "#exceptions/api-error.js";
+import { db } from "#db";
+import { sql } from "slonik";
+import { ZDbShema } from "../db/db-shema.js";
+
+class userUtils {
+  public getUserFromReq(req: Request) {
+    const userData = ZTokenPayload.safeParse(req.user);
+
+    if (!userData.success) {
+      throw ApiError.UnauthorizedError();
+    }
+    return userData.data;
+  }
+
+  async checkEventPermissions(
+    eventId: string,
+    userId: string,
+    requiredPermission: "editor" | "viewer",
+  ) {
+    // проверка прав
+    const userEventPermission = await db.maybeOneFirst(
+      sql.type(
+        z.object({
+          permission: ZDbShema.event_manager.event_permissions.permission,
+        }),
+      )`
+          select 
+            permission 
+          from 
+            event_manager.event_permissions 
+          where 
+            event_id = ${eventId} and user_id = ${userId}`,
+    );
+    if (!userEventPermission) throw ApiError.ForbiddenError();
+
+    const permissionList = {
+      creator: ["editor", "viewer"],
+      editor: ["editor", "viewer"],
+      viewer: ["viewer"],
+    };
+
+    if (!permissionList[userEventPermission].includes(requiredPermission))
+      throw ApiError.ForbiddenError();
+
+    return userEventPermission;
+  }
+
+  async checkTaskBlockPermissions(
+    taskBlockId: string,
+    userId: string,
+    requiredPermission: "editor" | "viewer",
+  ) {
+    // проверка прав
+    const userTaskBlockPermission = await db.maybeOneFirst(
+      sql.type(
+        z.object({
+          permission: ZDbShema.task_manager.task_block_permissions.permission,
+        }),
+      )`
+          select 
+            permission 
+          from 
+            task_manager.task_block_permissions 
+          where 
+            task_block_id = ${taskBlockId} and user_id = ${userId}`,
+    );
+    if (userTaskBlockPermission) {
+      // проверка прав на блок задач
+      const permissionList = {
+        editor: ["editor", "viewer"],
+        viewer: ["viewer"],
+      };
+
+      if (
+        requiredPermission &&
+        !permissionList[userTaskBlockPermission].includes(requiredPermission)
+      )
+        throw ApiError.ForbiddenError();
+
+      return userTaskBlockPermission;
+    } else {
+      // проверка прав на ивент, так как владелец ивента владеет и всеми блоками задач
+      const eventId = await db.oneFirst(
+        sql.type(
+          z.object({
+            event_id: ZDbShema.event_manager.events.event_id,
+          }),
+        )`
+              select
+                event_id
+              from
+              event_manager.task_blocks
+              where 
+                task_block_id = ${taskBlockId}
+              `,
+      );
+
+      const eventPermission = await this.checkEventPermissions(
+        eventId,
+        userId,
+        requiredPermission,
+      );
+
+      return eventPermission;
+    }
+  }
+}
+
+export const UserUtils = new userUtils();