diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..6385f49 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,14 @@ +FROM node:23-alpine + +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ +COPY tsconfig* ./ + +RUN pnpm install --no-frozen-lockfile + +COPY . . + +CMD ["pnpm", "run", "dev"] \ No newline at end of file diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx deleted file mode 100644 index 5099a8f..0000000 --- a/app/(auth)/login/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { LoginPage as default } from 'pages/login'; diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx deleted file mode 100644 index 7210324..0000000 --- a/app/(auth)/register/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { RegisterPage as default } from 'pages/register'; diff --git a/app/(auth)/signin/page.tsx b/app/(auth)/signin/page.tsx new file mode 100644 index 0000000..8a87638 --- /dev/null +++ b/app/(auth)/signin/page.tsx @@ -0,0 +1 @@ +export { SigninPage as default } from 'pages/signin'; diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..47b581d --- /dev/null +++ b/app/(auth)/signup/page.tsx @@ -0,0 +1 @@ +export { SignupPage as default } from 'pages/signup'; diff --git a/app/layout.tsx b/app/layout.tsx index 25aafad..f9a51b9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,14 @@ import 'app/styles/global.css'; +import { QueryProvider } from 'shared/providers'; +import { Toaster } from 'shared/ui'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} + + ); } diff --git a/app/team/page.tsx b/app/team/page.tsx new file mode 100644 index 0000000..f2d5f3f --- /dev/null +++ b/app/team/page.tsx @@ -0,0 +1 @@ +export { ProfilePage as default } from 'pages/profile'; diff --git a/app/team/profile/page.tsx b/app/team/profile/page.tsx new file mode 100644 index 0000000..f2d5f3f --- /dev/null +++ b/app/team/profile/page.tsx @@ -0,0 +1 @@ +export { ProfilePage as default } from 'pages/profile'; diff --git a/app/team/projects/page.tsx b/app/team/projects/page.tsx new file mode 100644 index 0000000..e50f0df --- /dev/null +++ b/app/team/projects/page.tsx @@ -0,0 +1 @@ +export { ProjectsPage as default } from 'pages/projects'; diff --git a/eslint.config.mjs b/eslint.config.mjs index d849b72..c7c86c8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,6 +2,7 @@ import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; import prettier from 'eslint-config-prettier'; import storybook from 'eslint-plugin-storybook'; +import checkFile from 'eslint-plugin-check-file'; import { defineConfig, globalIgnores } from 'eslint/config'; import nextVitals from 'eslint-config-next/core-web-vitals'; import nextTs from 'eslint-config-next/typescript'; @@ -15,6 +16,47 @@ const eslintConfig = defineConfig([ ...nextTs, ...pluginQuery.configs['flat/recommended'], ...storybook.configs['flat/recommended'], + { + files: ['src/**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}'], + plugins: { + 'check-file': checkFile, + }, + rules: { + 'check-file/filename-naming-convention': [ + 'error', + { + '**/{page,layout,loading,error,not-found,template,default,route}.{jsx,tsx}': + 'NEXT_JS_PAGE_ROUTER_FILENAME_CASE', + '**/!({page,layout,loading,error,not-found,template,default,route}).{jsx,tsx}': + 'PASCAL_CASE', + '**/use*.{ts,tsx}': 'CAMEL_CASE', + '**/*{Error,Type,Types,Interface,Props,Dto,Response,Request,Contract,Contracts}.ts': + 'PASCAL_CASE', + '**/!(*{Error,Type,Types,Interface,Props,Dto,Response,Request,Contract,Contracts}|use*).ts': + 'KEBAB_CASE', + '**/*.{js,mjs,cjs,mts,cts}': 'KEBAB_CASE', + }, + { + ignoreMiddleExtensions: true, + }, + ], + 'check-file/folder-naming-convention': [ + 'error', + { + 'src/**/': 'KEBAB_CASE', + }, + ], + }, + }, + // исключения для автогенерируемых API-файлов + { + files: ['src/shared/api/endpoints/**/*.{ts,js}', 'src/shared/api/schemas/**/*.{ts,js}'], + rules: { + 'check-file/filename-naming-convention': 'off', + 'check-file/folder-naming-convention': 'off', + 'no-useless-escape': 'off', + }, + }, globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']), ]); diff --git a/infra/dev/.env.example b/infra/dev/.env.example new file mode 100644 index 0000000..02cb0df --- /dev/null +++ b/infra/dev/.env.example @@ -0,0 +1,40 @@ +# --- APP --- +PORT=3000 +NODE_ENV=development +COOKIE_SECRET=same-serious-secret +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 + +# --- POSTGRES --- +DB_USERNAME=admin +DB_PASSWORD=p@ssword123 +DB_DATABASE=task_tracker +DB_PORT=6000 +DB_SCHEMA=base + +DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_DATABASE} + +# --- REDIS --- +REDIS_HOST=127.0.0.1 +REDIS_PORT=7000 + +JWT_AUDIENCE="task-tracker-client" + +JWT_ACCESS_SECRET=same-same-same-same-same +JWT_ACCESS_EXPIRES_IN=15m + +JWT_REFRESH_SECRET=same-same-same-same-same +JWT_REFRESH_EXPIRES_IN=15m + +# --- MAIL SETTINGS --- +MAIL_HOST=smtp.gmail.com +MAIL_PORT=465 +MAIL_USER=example@gmail.com +MAIL_PASSWORD=xxxxxxxxyyyyyyyy +MAIL_FROM_NAME="Task Tracker" +MAIL_FROM_EMAIL=example@gmail.com + +S3_BUCKET_NAME='' +S3_ENDPOINT='' +S3_REGION='' +S3_ACCESS_KEY='' +S3_SECRET_KEY='' \ No newline at end of file diff --git a/infra/dev/README.md b/infra/dev/README.md new file mode 100644 index 0000000..7674876 --- /dev/null +++ b/infra/dev/README.md @@ -0,0 +1,42 @@ +# Локальная разработка (Docker Compose) + +## Описание + +Данный конфиг разворачивает полный инстанс бэкенда (API + DB + Redis + Next) +для локальной разработки фронтенда. + +## Требования + +1. Положить актуальный файл .env в директорию с этим файлом + (путь: ./infra/dev/.env). +2. Наличие Docker Desktop / Docker Engine. + +## Запуск + +Выполните команду из корня проекта: + +```sh +docker compose -f ./infra/dev/compose.dev.yaml --profile infra up --pull always --build -d -V +``` + +## Что внутри: + +- API: http://localhost:3000 +- Postgres: localhost:6000 (пароли и база берутся из .env) +- Redis: localhost:7000 +- Next: localhost:4000 + +## Особенности + +- Авто-миграции: Приложение само накатит SQL-схему при старте. +- Healthchecks: Контейнер API не поднимется, пока DB и Redis + не станут доступны (status: healthy). +- Изоляция: Используется выделенная сеть 'task-tracker-gateway'. + +## Reset: + +Если нужно полностью очистить базу и начать с нуля: + +```sh +docker compose -f ./infra/dev/compose.dev.yaml --profile infra down -v +``` diff --git a/infra/dev/compose.dev.yaml b/infra/dev/compose.dev.yaml new file mode 100644 index 0000000..f3f8e69 --- /dev/null +++ b/infra/dev/compose.dev.yaml @@ -0,0 +1,109 @@ +version: '3.9' + +name: task-tracker + +services: + frontend: + hostname: frontend + container_name: frontend + build: + context: ../.. + dockerfile: Dockerfile.dev + environment: + WATCHPACK_POLLING: 'true' + ports: + - '4000:4000' + command: pnpm run dev -p 4000 + volumes: + - ../..:/app + - frontend_node_modules:/app/node_modules + depends_on: + api: + condition: service_started + networks: + - backend + deploy: + resources: + limits: + cpus: '3.0' + memory: 3072M + reservations: + cpus: '0.5' + memory: 512M + + api: + hostname: api + container_name: api + platform: linux/amd64 + image: ghcr.io/task-tracker-lab/task-tracker-backend:dev + env_file: + - .env + ports: + - '3000:3000' + depends_on: + database: + condition: service_healthy + redis: + condition: service_healthy + networks: + - backend + deploy: + resources: + limits: + cpus: '2.0' + memory: 1024M + reservations: + cpus: '0.5' + memory: 256M + + database: + hostname: database + container_name: database + image: postgres:16-alpine + restart: always + env_file: + - .env + environment: + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE} + ports: + - '6000:5432' + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -q || exit 1'] + interval: 5s + timeout: 5s + retries: 5 + profiles: ['infra'] + + redis: + hostname: redis + container_name: redis + image: redis:7-alpine + restart: always + ports: + - '7001:6379' + command: redis-server --save 60 1 --loglevel notice + volumes: + - redis_data:/data + networks: + - backend + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 3s + retries: 5 + profiles: ['infra'] + +volumes: + postgres_data: + redis_data: + frontend_node_modules: + +networks: + backend: + name: task-tracker-gateway diff --git a/next-env.d.ts b/next-env.d.ts index c05d9f7..cdb6b7b 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,7 +1,7 @@ /// /// /// -import './.next/types/routes.d.ts'; +import './.next/dev/types/routes.d.ts'; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts index 16ee426..70a56ee 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { + typedRoutes: true, turbopack: { root: __dirname, }, diff --git a/orval.config.ts b/orval.config.ts new file mode 100644 index 0000000..9866515 --- /dev/null +++ b/orval.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from 'orval'; +import path from 'path'; + +const WORK_SPACE = 'src/shared/api'; + +export default defineConfig({ + api: { + input: { + target: path.resolve(__dirname, WORK_SPACE, 'openapi', 'openapi.json'), + filters: { + mode: 'exclude', + tags: ['Prometheus', 'System'], + }, + }, + output: { + workspace: WORK_SPACE, + mode: 'tags-split', + client: 'react-query', + target: './endpoints', + schemas: { + path: './schemas', + type: 'zod', + }, + tsconfig: 'tsconfig.json', + httpClient: 'axios', + override: { + mutator: { + path: 'instance.ts', + name: 'instance', + }, + }, + clean: true, + }, + hooks: { + afterAllFilesWrite: 'prettier --write .', + }, + }, +}); diff --git a/package.json b/package.json index 4630f97..f840893 100644 --- a/package.json +++ b/package.json @@ -20,21 +20,29 @@ "build-storybook": "storybook build", "test": "vitest", "test:ci": "vitest run", + "openapi:pull": "node src/shared/api/openapi/pull-openapi.mjs", + "openapi:orval": "orval", + "openapi:sync": "pnpm openapi:pull && pnpm openapi:orval", "prepare": "husky" }, "dependencies": { "@hookform/resolvers": "^5.2.2", "@tanstack/react-query": "^5.90.21", "@tanstack/react-query-devtools": "^5.91.3", + "axios": "^1.15.0", + "axios-auth-refresh": "^5.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "input-otp": "^1.4.2", "lucide-react": "^0.574.0", "next": "^16.1.6", + "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", "react-hook-form": "^7.71.2", "socket.io-client": "^4.8.3", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.1", "zod": "^4.3.6", "zustand": "^5.0.11" @@ -53,13 +61,16 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "dotenv": "^17.4.2", "eslint": "^9.39.2", "eslint-config-next": "^16.1.6", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-check-file": "^3.3.1", "eslint-plugin-storybook": "^10.3.3", "husky": "^9.1.7", "jsdom": "^29.0.1", "lint-staged": "^16.3.1", + "orval": "^8.7.0", "postcss": "^8.5.6", "prettier": "^3.0.0", "prettier-plugin-tailwindcss": "^0.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 993a530..db2e442 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,18 +17,30 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.91.3 version: 5.99.0(@tanstack/react-query@5.99.0(react@19.2.5))(react@19.2.5) + axios: + specifier: ^1.15.0 + version: 1.15.0 + axios-auth-refresh: + specifier: ^5.0.2 + version: 5.0.2(axios@1.15.0) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 clsx: specifier: ^2.1.1 version: 2.1.1 + input-otp: + specifier: ^1.4.2 + version: 1.4.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) lucide-react: specifier: ^0.574.0 version: 0.574.0(react@19.2.5) next: specifier: ^16.1.6 version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -44,6 +56,9 @@ importers: socket.io-client: specifier: ^4.8.3 version: 4.8.3 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tailwind-merge: specifier: ^3.4.1 version: 3.5.0 @@ -93,6 +108,9 @@ importers: '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)) + dotenv: + specifier: ^17.4.2 + version: 17.4.2 eslint: specifier: ^9.39.2 version: 9.39.4(jiti@2.6.1) @@ -102,6 +120,9 @@ importers: eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-check-file: + specifier: ^3.3.1 + version: 3.3.1(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-storybook: specifier: ^10.3.3 version: 10.3.5(eslint@9.39.4(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) @@ -114,6 +135,9 @@ importers: lint-staged: specifier: ^16.3.1 version: 16.4.0 + orval: + specifier: ^8.7.0 + version: 8.7.0(prettier@3.8.2)(typescript@5.9.3) postcss: specifier: ^8.5.6 version: 8.5.9 @@ -318,6 +342,11 @@ packages: '@clack/prompts@0.9.1': resolution: {integrity: sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==} + '@commander-js/extra-typings@14.0.0': + resolution: {integrity: sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==} + peerDependencies: + commander: ~14.0.0 + '@csstools/color-helpers@6.0.2': resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} @@ -605,6 +634,9 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@gerrit0/mini-shiki@3.23.0': + resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} + '@hono/node-server@1.19.13': resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} engines: {node: '>=18.14.1'} @@ -966,6 +998,44 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@orval/angular@8.7.0': + resolution: {integrity: sha512-7BbTUezuj9vqYRljtDVINJ5N7IrJQQCFSYCe0HaSrPzM3bRPGxuDVMXq++nEp4Kv1MLxXj4J7gOiReh3QVwIYg==} + + '@orval/axios@8.7.0': + resolution: {integrity: sha512-GIFJQ24uVxRlc1iy/S6wpH8I4vHyDc/a8q0FzyH4X11jsqBNPQQY099CxZBOKY0V/NHj2JPmvjDtnm1inVBJNA==} + + '@orval/core@8.7.0': + resolution: {integrity: sha512-RmlRrVkUKROkq41VnM3ojtofqrvACNxEjM36xl269E81aVMoiTEgrNDr9orwhcm+VnUgrE1C6ahYLa109ITfLg==} + peerDependencies: + '@faker-js/faker': '>=10' + peerDependenciesMeta: + '@faker-js/faker': + optional: true + + '@orval/fetch@8.7.0': + resolution: {integrity: sha512-rCDs4Ea7S5ARrUKWVBsZiKFqvtKFXp8THMdfQlIL57A3tIAru7l5w3mDJp0rQZHvn+rK887wD0h31CnPbx7S7A==} + + '@orval/hono@8.7.0': + resolution: {integrity: sha512-Uxt9L2sUbjBMwkFf+6xjSfUEHvs/4PnqK3ZNd9bVaM1OehhxJd9TPLHcOs+PojfnGWByNMV1xGG9HmOnaR/i+Q==} + + '@orval/mcp@8.7.0': + resolution: {integrity: sha512-jxgmGhFdAFxhgF8R0Uf1EXw8BQhGt9OZGwOeGr8YSiA9eC0nu9w4QLeFlHno/7W7ZD7SQqH+N1odLzKL10SsAw==} + + '@orval/mock@8.7.0': + resolution: {integrity: sha512-MhgiJSLA3kFrOh8hKojncIoHQQoVt1MjUMaKvb9q07LRJ9drc8QXZZkH4W4Fw+ZiufwYGfKBg86fj2Ipkx8zzQ==} + + '@orval/query@8.7.0': + resolution: {integrity: sha512-BWFWujWUHPQSxW0SZn7ZKTP16ev5arctjbpL0OEaEYAA36s72n/dnh/XM6ms1eaqKwMuAtHjidh5vx70JPdPSQ==} + + '@orval/solid-start@8.7.0': + resolution: {integrity: sha512-FcW6lo7k6Ec0D3fCxkNYHAjzc1p7M+UAIckmOnP2X3OQU0jjFnAk8Ny4uRcuCWCMrNuG9XcCb+Br+Wa3VpB11g==} + + '@orval/swr@8.7.0': + resolution: {integrity: sha512-j6xYXCCkisOS8lxUrQGLIJJk2Gg0QcIDlDUJ59xcCXnA/8CG8PGfTYeqv9mAoQQzXBdpir4BXUwe580boxA2IA==} + + '@orval/zod@8.7.0': + resolution: {integrity: sha512-hrqN6fW5+g8B+30QaaEb3Tpdaq1ePq297kowsjvV5QmSGnWu4NZ5YrnQDrO4ST8WavmTspEDPOU7pcTSRELuHQ==} + '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} @@ -1772,9 +1842,48 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@scalar/helpers@0.4.3': + resolution: {integrity: sha512-Gv2V7SFreLx3DltzF2lKXdaJSH5cP1LOyt9PxON1cSWGxkrs3sg93c1taEJsW24E9ckfYXkL5hjCAVLfAN3wQw==} + engines: {node: '>=22'} + + '@scalar/json-magic@0.12.5': + resolution: {integrity: sha512-MkGOjodEeQ7V7M78W6Oq+t3q1LaUR+SRLZLqFbU6s26Gc+12T+v89JXcHvd+3ug0xFVMg/kdczZ3O6miBhyNsA==} + engines: {node: '>=22'} + + '@scalar/openapi-parser@0.25.8': + resolution: {integrity: sha512-09yGXQSMYVlxJkLIn9Nz2q7Du7/olHKhR4oU0/JgkOdcKBiixSeLmhcAm7Hmj2Z82xOYpF+ZJUTCzsh8DQv5Fg==} + engines: {node: '>=22'} + + '@scalar/openapi-types@0.6.1': + resolution: {integrity: sha512-P1RvyTFN0vRSL136OqWjlZfSFjY9JoJfuD6LM1mIjoocfwmqX3WuzsFEFX6hAeeDlTh6gjbiy+OdhSee8GFfSA==} + engines: {node: '>=22'} + + '@scalar/openapi-types@0.7.0': + resolution: {integrity: sha512-kN0PwlJW0de4bwQ4ib+mBHzKJUvBCyR/gwU4zLEq6SCbj+GfgYUh+2a0/yl1WYVUiSkkwFsHjfmQ8KjhR3HK0Q==} + engines: {node: '>=22'} + + '@scalar/openapi-upgrader@0.2.4': + resolution: {integrity: sha512-AcrF7BMxKCTHnT82SHbHun6dJO4XC9tS5gD7EJsr/7YwFkx9JtbtZCryJXtqWJ5c7i1v1KH4PRRjDga/hCULTQ==} + engines: {node: '>=22'} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -2047,6 +2156,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2070,6 +2182,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} @@ -2325,6 +2440,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -2339,6 +2462,10 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -2432,6 +2559,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -2440,6 +2570,14 @@ packages: resolution: {integrity: sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==} engines: {node: '>=4'} + axios-auth-refresh@5.0.2: + resolution: {integrity: sha512-1UvQMxwCR5Y2V7i7862kIQDbuWqw4LgWRjaxtFg9+bwKPIQW3kvh/Ay6uVYhUInAkPgN0rLzH0sU8ECd3qKPBQ==} + peerDependencies: + axios: '>= 1.0.0' + + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2530,6 +2668,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2573,6 +2715,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -2585,6 +2731,9 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2725,6 +2874,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2855,6 +3008,14 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -2982,6 +3143,12 @@ packages: eslint-import-resolver-webpack: optional: true + eslint-plugin-check-file@3.3.1: + resolution: {integrity: sha512-b7fDkp8Y0T9vloTYSrkMnb5Cqpk1bc6+jDQdgKIhzdwb47NmLAvMFYo3SzHOPF8u4SEsOVhmrNSbo6Sp633syg==} + engines: {node: '>=18'} + peerDependencies: + eslint: '>=9.0.0' + eslint-plugin-import@2.32.0: resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} engines: {node: '>=4'} @@ -3170,6 +3337,10 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + find-up@8.0.0: + resolution: {integrity: sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==} + engines: {node: '>=20'} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -3177,10 +3348,23 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -3297,6 +3481,10 @@ packages: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} + globby@16.1.0: + resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==} + engines: {node: '>=20'} + globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} @@ -3416,6 +3604,12 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + input-otp@1.4.2: + resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -3531,6 +3725,10 @@ packages: resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} engines: {node: '>=12'} + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -3680,6 +3878,10 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3702,6 +3904,10 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + leven@4.1.0: + resolution: {integrity: sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3783,6 +3989,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + lint-staged@16.4.0: resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} engines: {node: '>=20.17'} @@ -3796,6 +4005,10 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + locate-path@8.0.0: + resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==} + engines: {node: '>=20'} + lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} @@ -3829,6 +4042,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -3836,6 +4052,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3843,6 +4063,9 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -3862,10 +4085,18 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} @@ -3938,6 +4169,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next@16.2.3: resolution: {integrity: sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==} engines: {node: '>=20.9.0'} @@ -4057,6 +4294,16 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} + orval@8.7.0: + resolution: {integrity: sha512-BFUBXWlwS/+gw2gJk5w67RjH9aULPsjpXvQfxr0wwSOwn8ACPtTBeSIlv8vWYxY2euGXDctdGMKUxBhPcnwOsA==} + engines: {node: '>=22.18.0'} + hasBin: true + peerDependencies: + prettier: '>=3.0.0' + peerDependenciesMeta: + prettier: + optional: true + outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -4068,10 +4315,18 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -4275,6 +4530,14 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4374,6 +4637,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -4390,6 +4657,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remeda@2.33.7: + resolution: {integrity: sha512-cXlyjevWx5AcslOUEETG4o8XYi9UkoCXcJmj7XhPFVbla+ITuOBxv6ijBrmbeg+ZhzmDThkNdO+iXKUfrJep1w==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4564,6 +4834,12 @@ packages: resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} engines: {node: '>=10.0.0'} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4835,6 +5111,25 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typedoc-plugin-coverage@4.0.3: + resolution: {integrity: sha512-baim3wyMkqpX7rBzL/6iZ7wzKJuSr9ffP16RHOsdTUNoHUZeXLIZHSUBtUhXmNHaUNRgfqdmKLBwyggbJjGdeQ==} + engines: {node: '>= 18'} + peerDependencies: + typedoc: 0.28.x + + typedoc-plugin-markdown@4.11.0: + resolution: {integrity: sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw==} + engines: {node: '>= 18'} + peerDependencies: + typedoc: 0.28.x + + typedoc@0.28.19: + resolution: {integrity: sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==} + engines: {node: '>= 18', pnpm: '>= 10'} + hasBin: true + peerDependencies: + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x + typescript-eslint@8.58.1: resolution: {integrity: sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4847,6 +5142,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -4862,6 +5160,10 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -5164,6 +5466,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + yocto-spinner@1.1.0: resolution: {integrity: sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA==} engines: {node: '>=18.19'} @@ -5449,6 +5755,10 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@commander-js/extra-typings@14.0.0(commander@14.0.3)': + dependencies: + commander: 14.0.3 + '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -5674,6 +5984,14 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@gerrit0/mini-shiki@3.23.0': + dependencies: + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@hono/node-server@1.19.13(hono@4.12.12)': dependencies: hono: 4.12.12 @@ -5954,6 +6272,115 @@ snapshots: '@open-draft/until@2.1.0': {} + '@orval/angular@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/axios@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/core@8.7.0(typescript@5.9.3)': + dependencies: + '@scalar/openapi-types': 0.6.1 + acorn: 8.16.0 + compare-versions: 6.1.1 + debug: 4.4.3 + esbuild: 0.27.7 + esutils: 2.0.3 + fs-extra: 11.3.4 + globby: 16.1.0 + jiti: 2.6.1 + remeda: 2.33.7 + typedoc: 0.28.19(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@orval/fetch@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + '@scalar/openapi-types': 0.6.1 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/hono@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + '@orval/zod': 8.7.0(typescript@5.9.3) + fs-extra: 11.3.4 + remeda: 2.33.7 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/mcp@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + '@orval/fetch': 8.7.0(typescript@5.9.3) + '@orval/zod': 8.7.0(typescript@5.9.3) + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/mock@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + remeda: 2.33.7 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/query@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + '@orval/fetch': 8.7.0(typescript@5.9.3) + remeda: 2.33.7 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/solid-start@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + '@scalar/openapi-types': 0.6.1 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/swr@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + '@orval/fetch': 8.7.0(typescript@5.9.3) + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/zod@8.7.0(typescript@5.9.3)': + dependencies: + '@orval/core': 8.7.0(typescript@5.9.3) + remeda: 2.33.7 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + '@oxc-project/types@0.124.0': {} '@radix-ui/number@1.1.1': {} @@ -6764,8 +7191,59 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@scalar/helpers@0.4.3': {} + + '@scalar/json-magic@0.12.5': + dependencies: + '@scalar/helpers': 0.4.3 + pathe: 2.0.3 + yaml: 2.8.3 + + '@scalar/openapi-parser@0.25.8': + dependencies: + '@scalar/helpers': 0.4.3 + '@scalar/json-magic': 0.12.5 + '@scalar/openapi-types': 0.7.0 + '@scalar/openapi-upgrader': 0.2.4 + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-formats: 3.0.1(ajv@8.18.0) + jsonpointer: 5.0.1 + leven: 4.1.0 + yaml: 2.8.3 + + '@scalar/openapi-types@0.6.1': + dependencies: + zod: 4.3.6 + + '@scalar/openapi-types@0.7.0': {} + + '@scalar/openapi-upgrader@0.2.4': + dependencies: + '@scalar/openapi-types': 0.7.0 + '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/merge-streams@2.3.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -7042,6 +7520,10 @@ snapshots: '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -7062,6 +7544,8 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/unist@3.0.3': {} + '@types/validate-npm-package-name@4.0.2': {} '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': @@ -7330,6 +7814,10 @@ snapshots: agent-base@7.1.4: {} + ajv-draft-04@1.0.0(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -7348,6 +7836,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-colors@4.1.3: {} + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -7457,12 +7947,26 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.11.2: {} + axios-auth-refresh@5.0.2(axios@1.15.0): + dependencies: + axios: 1.15.0 + + axios@1.15.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -7560,6 +8064,10 @@ snapshots: dependencies: readdirp: 4.1.2 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -7597,12 +8105,18 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@11.1.0: {} commander@12.1.0: {} commander@14.0.3: {} + compare-versions@6.1.1: {} + concat-map@0.0.1: {} content-disposition@1.1.0: {} @@ -7716,6 +8230,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -7846,6 +8362,13 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + entities@4.5.0: {} + entities@6.0.1: {} entities@7.0.1: {} @@ -8062,6 +8585,12 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-check-file@3.3.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + is-glob: 4.0.3 + micromatch: 4.0.8 + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 @@ -8374,6 +8903,11 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + find-up@8.0.0: + dependencies: + locate-path: 8.0.0 + unicorn-magic: 0.3.0 + flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -8381,10 +8915,20 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.16.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -8503,6 +9047,15 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.3.0 + globby@16.1.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + is-path-inside: 4.0.0 + slash: 5.1.0 + unicorn-magic: 0.4.0 + globrex@0.1.2: {} gonzales-pe@4.3.0: @@ -8597,6 +9150,11 @@ snapshots: inherits@2.0.4: {} + input-otp@1.4.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -8702,6 +9260,8 @@ snapshots: is-obj@3.0.0: {} + is-path-inside@4.0.0: {} + is-plain-obj@4.1.0: {} is-potential-custom-element-name@1.0.1: {} @@ -8842,6 +9402,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpointer@5.0.1: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -8863,6 +9425,8 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + leven@4.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -8919,6 +9483,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + lint-staged@16.4.0: dependencies: commander: 14.0.3 @@ -8941,6 +9509,10 @@ snapshots: dependencies: p-locate: 5.0.0 + locate-path@8.0.0: + dependencies: + p-locate: 6.0.0 + lodash-es@4.18.1: {} lodash.merge@4.6.2: {} @@ -8974,16 +9546,29 @@ snapshots: dependencies: react: 19.2.5 + lunr@2.3.9: {} + lz-string@1.5.0: {} magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + math-intrinsics@1.1.0: {} mdn-data@2.27.1: {} + mdurl@2.0.0: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -8997,8 +9582,14 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -9065,6 +9656,11 @@ snapshots: negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 16.2.3 @@ -9218,6 +9814,44 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 + orval@8.7.0(prettier@3.8.2)(typescript@5.9.3): + dependencies: + '@commander-js/extra-typings': 14.0.0(commander@14.0.3) + '@orval/angular': 8.7.0(typescript@5.9.3) + '@orval/axios': 8.7.0(typescript@5.9.3) + '@orval/core': 8.7.0(typescript@5.9.3) + '@orval/fetch': 8.7.0(typescript@5.9.3) + '@orval/hono': 8.7.0(typescript@5.9.3) + '@orval/mcp': 8.7.0(typescript@5.9.3) + '@orval/mock': 8.7.0(typescript@5.9.3) + '@orval/query': 8.7.0(typescript@5.9.3) + '@orval/solid-start': 8.7.0(typescript@5.9.3) + '@orval/swr': 8.7.0(typescript@5.9.3) + '@orval/zod': 8.7.0(typescript@5.9.3) + '@scalar/json-magic': 0.12.5 + '@scalar/openapi-parser': 0.25.8 + '@scalar/openapi-types': 0.6.1 + chokidar: 5.0.0 + commander: 14.0.3 + enquirer: 2.4.1 + execa: 9.6.1 + find-up: 8.0.0 + fs-extra: 11.3.4 + jiti: 2.6.1 + js-yaml: 4.1.1 + remeda: 2.33.7 + string-argv: 0.3.2 + tsconfck: 3.1.6(typescript@5.9.3) + typedoc: 0.28.19(typescript@5.9.3) + typedoc-plugin-coverage: 4.0.3(typedoc@0.28.19(typescript@5.9.3)) + typedoc-plugin-markdown: 4.11.0(typedoc@0.28.19(typescript@5.9.3)) + optionalDependencies: + prettier: 3.8.2 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + outvariant@1.4.3: {} own-keys@1.0.1: @@ -9230,10 +9864,18 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + package-manager-detector@1.6.0: {} parent-module@1.0.1: @@ -9378,6 +10020,10 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@2.1.0: {} + + punycode.js@2.3.1: {} + punycode@2.3.1: {} qs@6.15.1: @@ -9523,6 +10169,8 @@ snapshots: readdirp@4.1.2: {} + readdirp@5.0.0: {} + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -9556,6 +10204,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remeda@2.33.7: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -9856,6 +10506,11 @@ snapshots: transitivePeerDependencies: - supports-color + sonner@2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + source-map-js@1.2.1: {} source-map@0.6.1: {} @@ -10160,6 +10815,23 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typedoc-plugin-coverage@4.0.3(typedoc@0.28.19(typescript@5.9.3)): + dependencies: + typedoc: 0.28.19(typescript@5.9.3) + + typedoc-plugin-markdown@4.11.0(typedoc@0.28.19(typescript@5.9.3)): + dependencies: + typedoc: 0.28.19(typescript@5.9.3) + + typedoc@0.28.19(typescript@5.9.3): + dependencies: + '@gerrit0/mini-shiki': 3.23.0 + lunr: 2.3.9 + markdown-it: 14.1.1 + minimatch: 10.2.5 + typescript: 5.9.3 + yaml: 2.8.3 + typescript-eslint@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -10173,6 +10845,8 @@ snapshots: typescript@5.9.3: {} + uc.micro@2.1.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -10186,6 +10860,8 @@ snapshots: unicorn-magic@0.3.0: {} + unicorn-magic@0.4.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -10461,6 +11137,8 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + yocto-spinner@1.1.0: dependencies: yoctocolors: 2.1.2 diff --git a/src/pages/login/index.ts b/src/pages/login/index.ts deleted file mode 100644 index 270ae4d..0000000 --- a/src/pages/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as LoginPage } from './ui/LoginPage'; diff --git a/src/pages/login/model/loginSchema.ts b/src/pages/login/model/loginSchema.ts deleted file mode 100644 index e8fc086..0000000 --- a/src/pages/login/model/loginSchema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from 'zod'; - -export const formSchema = z.object({ - email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')), - password: z - .string() - .min(1, 'Обязательное поле') - .min(6, 'Минимум 6 символов') - .max(32, 'Слишком длинный пароль'), -}); - -export type FormState = z.infer; diff --git a/src/pages/login/ui/LoginForm.tsx b/src/pages/login/ui/LoginForm.tsx deleted file mode 100644 index f03c4dd..0000000 --- a/src/pages/login/ui/LoginForm.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client'; - -import { Controller, useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { formSchema, FormState } from '../model/loginSchema'; -import { - Field, - FieldDescription, - FieldLabel, - Button, - FieldGroup, - FieldError, - Link, - InputPassword, - InputEmail, -} from 'shared/ui'; -import { cn } from 'shared/lib/utils'; -import * as z from 'zod'; - -export function LoginForm({ className, ...props }: Omit, 'children'>) { - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - email: '', - password: '', - }, - }); - - const onSubmit = (data: z.infer) => { - alert(JSON.stringify(data)); - }; - - return ( -
- - ( - - Email - - {fieldState.invalid && } - - )} - /> - ( - -
- Пароль - - Забыли пароль? - -
- - {fieldState.invalid && } -
- )} - /> - - - - - - Нет аккаунта? Зарегистрироваться - - -
-
- ); -} diff --git a/src/pages/login/ui/LoginPage.tsx b/src/pages/login/ui/LoginPage.tsx deleted file mode 100644 index 353b7bd..0000000 --- a/src/pages/login/ui/LoginPage.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { LoginForm } from './LoginForm'; -import { LoginImageLight, LoginImageDark } from 'shared/assests'; -import { AppCopyright, Logo, ThemedImage } from 'shared/ui'; -import * as React from 'react'; - -export default function LoginPage() { - return ( -
-
-
-
-

Вход в систему

-

- Пожалуйста, введите ваши данные для входа. -

-
- -
-
- -
- ); -} diff --git a/src/pages/main/index.ts b/src/pages/main/index.ts index 05d6ef6..513ae61 100644 --- a/src/pages/main/index.ts +++ b/src/pages/main/index.ts @@ -1 +1 @@ -export { default as MainPage } from './ui/MainPage'; +export { MainPage } from './ui/MainPage'; diff --git a/src/pages/main/ui/MainPage.tsx b/src/pages/main/ui/MainPage.tsx index 9d718e4..cafd342 100644 --- a/src/pages/main/ui/MainPage.tsx +++ b/src/pages/main/ui/MainPage.tsx @@ -1,44 +1,19 @@ -import { FC } from 'react'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, - Button, -} from 'shared/ui'; +import { Link } from 'shared/ui'; +import { routes } from 'shared/config'; interface MainPageProps { className?: string; } -const MainPage: FC = ({ className }) => { +function MainPage({ className }: MainPageProps) { return (
- - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete your account from our - servers. - - - - Cancel - Continue - - - +

main page

+ Signup +
+ Signin
); -}; +} -export default MainPage; +export { MainPage }; diff --git a/src/pages/profile/index.ts b/src/pages/profile/index.ts new file mode 100644 index 0000000..7d9c888 --- /dev/null +++ b/src/pages/profile/index.ts @@ -0,0 +1 @@ +export { ProfilePage } from './ui/ProfilePage'; diff --git a/src/pages/profile/ui/ProfilePage.tsx b/src/pages/profile/ui/ProfilePage.tsx new file mode 100644 index 0000000..ce81e8f --- /dev/null +++ b/src/pages/profile/ui/ProfilePage.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { api } from 'shared/api'; +import { useQuery } from '@tanstack/react-query'; + +interface ProfilePageProps { + className?: string; +} + +const getUser = () => + api({ + url: '/users/me', + method: 'GET', + }); + +function ProfilePage({ className }: ProfilePageProps) { + const query = useQuery({ queryKey: ['user'], queryFn: getUser }); + + return ( +
+
{JSON.stringify(query.data, null, 2)}
+
+ ); +} + +export { ProfilePage }; diff --git a/src/pages/projects/index.ts b/src/pages/projects/index.ts new file mode 100644 index 0000000..463973c --- /dev/null +++ b/src/pages/projects/index.ts @@ -0,0 +1 @@ +export { ProjectsPage } from './ui/ProjectsPage'; diff --git a/src/pages/projects/ui/ProjectsPage.tsx b/src/pages/projects/ui/ProjectsPage.tsx new file mode 100644 index 0000000..2ac26ea --- /dev/null +++ b/src/pages/projects/ui/ProjectsPage.tsx @@ -0,0 +1,9 @@ +interface ProjectsPageProps { + className?: string; +} + +function ProjectsPage({ className }: ProjectsPageProps) { + return
Проекты
; +} + +export { ProjectsPage }; diff --git a/src/pages/register/index.ts b/src/pages/register/index.ts deleted file mode 100644 index 4498c04..0000000 --- a/src/pages/register/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as RegisterPage } from './ui/RegisterPage'; diff --git a/src/pages/register/model/registerSchema.ts b/src/pages/register/model/registerSchema.ts deleted file mode 100644 index c2324f4..0000000 --- a/src/pages/register/model/registerSchema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from 'zod'; - -export const formSchema = z - .object({ - name: z - .string() - .trim() - .min(1, 'Обязательное поле') - .min(2, 'Слишком короткое имя') - .max(15, 'Слишком длинное имя'), - email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')), - password: z - .string() - .min(1, 'Обязательное поле') - .min(6, 'Минимум 6 символов') - .max(32, 'Слишком длинный пароль'), - confirmPassword: z.string().min(1, 'Обязательное поле'), - }) - .refine((data) => data.password === data.confirmPassword, { - message: 'Пароли не совпадают', - path: ['confirmPassword'], - }); - -export type FormState = z.infer; diff --git a/src/pages/register/ui/RegisterForm.tsx b/src/pages/register/ui/RegisterForm.tsx deleted file mode 100644 index 4ca69f3..0000000 --- a/src/pages/register/ui/RegisterForm.tsx +++ /dev/null @@ -1,121 +0,0 @@ -'use client'; - -import { Controller, useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { InputPassword, Input, InputEmail } from 'shared/ui'; -import { formSchema, FormState } from '../model/registerSchema'; -import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from 'shared/ui'; -import { Button, Link } from 'shared/ui'; -import { cn } from 'shared/lib/utils'; -import * as z from 'zod'; -import { useState } from 'react'; - -export function RegisterForm({ - className, - ...props -}: Omit, 'children'>) { - const [showPassword, setShowPassword] = useState(false); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - name: '', - email: '', - password: '', - confirmPassword: '', - }, - }); - - const onSubmit = (data: z.infer) => { - alert(JSON.stringify(data)); - }; - - return ( -
- - ( - - Имя - - {fieldState.invalid && } - - )} - /> - ( - - Email - - {fieldState.invalid && } - - )} - /> - ( - - Пароль - - {fieldState.invalid && } - - )} - /> - ( - - Повторите пароль - - {fieldState.invalid && } - - )} - /> - - - - - - Уже есть аккаунт? Войти - - - -
- ); -} diff --git a/src/pages/register/ui/RegisterPage.tsx b/src/pages/register/ui/RegisterPage.tsx deleted file mode 100644 index ea7db5a..0000000 --- a/src/pages/register/ui/RegisterPage.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { AppCopyright, Logo, ThemedImage } from 'shared/ui'; -import { RegisterImageDark, RegisterImageLight } from 'shared/assests'; -import * as React from 'react'; -import { RegisterForm } from './RegisterForm'; - -export default function RegisterPage() { - return ( -
-
-
-
-

Создать аккаунт

-

- Заполните форму ниже, чтобы начать работу. -

-
- -
-
- -
- ); -} diff --git a/src/pages/signin/index.ts b/src/pages/signin/index.ts new file mode 100644 index 0000000..074c74a --- /dev/null +++ b/src/pages/signin/index.ts @@ -0,0 +1 @@ +export { SigninPage } from './ui/SigninPage'; diff --git a/src/pages/signin/model/schemas/login-schema.ts b/src/pages/signin/model/schemas/login-schema.ts new file mode 100644 index 0000000..bc750e9 --- /dev/null +++ b/src/pages/signin/model/schemas/login-schema.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +const MIN_PASS_LENGTH = 8; +const MAX_PASS_LENGTH = 32; + +export const SigninBody = z + .object({ + email: z.email().describe('Email пользователя'), + password: z.string().describe('Пароль пользователя'), + }) + .describe('Схема входа в систему'); + +export const SigninResponse = z.object({ + success: z.boolean().describe('Успешное обновление токенов'), + token: z.string().describe('Новый access token (JWT)'), + message: z.string().optional().describe('Дополнительное сообщение (опционально)'), +}); + +export const SigninFormSchema = z.object({ + email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')), + password: z + .string() + .min(1, 'Обязательное поле') + .min(MIN_PASS_LENGTH, `Минимум ${MIN_PASS_LENGTH} символов`) + .max(MAX_PASS_LENGTH, 'Слишком длинный пароль'), +}); diff --git a/src/pages/signin/model/services/signin.ts b/src/pages/signin/model/services/signin.ts new file mode 100644 index 0000000..e01fd5f --- /dev/null +++ b/src/pages/signin/model/services/signin.ts @@ -0,0 +1,20 @@ +import { api } from 'shared/api'; +import { SigninBody, SigninResponse } from '../schemas/login-schema'; +import { z } from 'zod'; + +export function signin(data: z.infer): Promise> { + return api( + { + url: '/auth/sign-in', + method: 'POST', + data: data, + skipAuthRefresh: true, + }, + { + contracts: { + body: SigninBody, + response: SigninResponse, + }, + } + ); +} diff --git a/src/pages/signin/ui/SigninForm.tsx b/src/pages/signin/ui/SigninForm.tsx new file mode 100644 index 0000000..878c2ca --- /dev/null +++ b/src/pages/signin/ui/SigninForm.tsx @@ -0,0 +1,138 @@ +'use client'; + +import { Controller, type FieldPath, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { SigninBody, SigninFormSchema, SigninResponse } from '../model/schemas/login-schema'; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Field, + FieldDescription, + FieldError, + FieldGroup, + FieldLabel, + InputEmail, + InputPassword, + Link, +} from 'shared/ui'; +import { cn } from 'shared/lib/utils'; +import { routes } from 'shared/config'; +import * as z from 'zod'; +import { useMutation } from '@tanstack/react-query'; +import { signin } from '../model/services/signin'; +import { extractValidationIssues, ValidationIssue } from 'shared/api'; + +type FSchema = z.infer; +type BSchema = z.infer; +type RSchema = z.infer; + +interface SigninFormProps extends Omit, 'children' | 'onSubmit'> { + onSuccess?: (body: BSchema, res: RSchema) => void; +} + +export function SigninForm({ className, onSuccess, ...props }: SigninFormProps) { + const sendUserData = useMutation({ + mutationFn: (data: BSchema) => { + return signin(data); + }, + meta: { + skipGlobalValidationToast: true, + }, + }); + + const form = useForm({ + resolver: zodResolver(SigninFormSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + function setFormErrors(errors: ValidationIssue[]) { + if (Array.isArray(errors)) { + errors.forEach(({ message, path: [path] }) => { + const filedName = path as FieldPath; + form.setError(filedName, { message }); + }); + } + } + + const onSubmit = (data: FSchema) => { + const body: BSchema = { + email: data.email, + password: data.password, + }; + + sendUserData.mutate(body, { + onSuccess: (res) => { + onSuccess?.(body, res); + }, + onError: (err) => { + setFormErrors(extractValidationIssues(err)); + }, + }); + }; + + return ( + + + Вход в систему + Пожалуйста, введите ваши данные для входа. + + +
+ + ( + + Email + + {fieldState.invalid && } + + )} + /> + ( + +
+ Пароль + + Забыли пароль? + +
+ + {fieldState.invalid && } +
+ )} + /> + + + + + + Нет аккаунта? Зарегистрироваться + + +
+
+
+
+ ); +} diff --git a/src/pages/signin/ui/SigninPage.tsx b/src/pages/signin/ui/SigninPage.tsx new file mode 100644 index 0000000..ac8420d --- /dev/null +++ b/src/pages/signin/ui/SigninPage.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { SigninForm } from './SigninForm'; +import { Link, Logo } from 'shared/ui'; +import * as React from 'react'; +import { routes } from 'shared/config'; +import { accessToken } from 'shared/api'; +import { toast } from 'sonner'; +import { useRouter } from 'next/navigation'; + +function SigninPage() { + const router = useRouter(); + + return ( +
+
+ + + + { + if (res.success) { + accessToken.token = res.token; + router.replace(routes.team.profile()); + if (res.message) { + toast.success(res.message); + } + } + }} + /> +
+
+ ); +} + +export { SigninPage }; diff --git a/src/pages/signup/index.ts b/src/pages/signup/index.ts new file mode 100644 index 0000000..524c288 --- /dev/null +++ b/src/pages/signup/index.ts @@ -0,0 +1 @@ +export { SignupPage } from './ui/SignupPage'; diff --git a/src/pages/signup/model/schemas/confirm-schema.ts b/src/pages/signup/model/schemas/confirm-schema.ts new file mode 100644 index 0000000..7fa1ac0 --- /dev/null +++ b/src/pages/signup/model/schemas/confirm-schema.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +export const ConfirmBody = z + .object({ + email: z.email().describe('Email пользователя, на который был отправлен код'), + code: z.string().min(6).max(6).describe('6-значный OTP код подтверждения'), + }) + .describe('Схема верификации OTP кода'); + +export const ConfirmResponse = z.object({ + success: z.boolean().describe('Успешное подтверждение'), + token: z.string().describe('Token (JWT)'), + message: z.string().optional().describe('Дополнительное сообщение (опционально)'), +}); + +export const ConfirmFormSchema = z.object({ + code: z.string().min(6, 'Обязательное поле'), +}); diff --git a/src/pages/signup/model/schemas/signup-schema.ts b/src/pages/signup/model/schemas/signup-schema.ts new file mode 100644 index 0000000..27e5457 --- /dev/null +++ b/src/pages/signup/model/schemas/signup-schema.ts @@ -0,0 +1,60 @@ +import { z } from 'zod'; + +const MIN_PASS_LENGTH = 8; +const MAX_PASS_LENGTH = 32; + +export const SignupBody = z + .object({ + email: z.email('Некорректный формат email').describe('Email пользователя'), + password: z + .string() + .min(MIN_PASS_LENGTH, `Пароль должен содержать минимум ${MIN_PASS_LENGTH} символов`) + .max(MAX_PASS_LENGTH, `Пароль должен содержать максимум ${MAX_PASS_LENGTH} символа`) + .describe('Пароль (минимум 8 символов)'), + firstName: z + .string() + .min(2, 'Имя должно содержать минимум 2 символа') + .max(50) + .trim() + .describe('Имя'), + lastName: z + .string() + .min(2, 'Фамилия должна содержать минимум 2 символа') + .max(50) + .trim() + .describe('Фамилия'), + middleName: z + .string() + .max(50) + .trim() + .optional() + .or(z.literal('')) + .describe('Отчество (опционально)'), + }) + .describe('Схема регистрации пользователя'); + +export const SignupResponse = z.object({ + success: z.boolean().describe('Статус операции'), + message: z.string().optional().describe('Сообщение для пользователя'), +}); + +export const SignupFormSchema = z + .object({ + name: z + .string() + .trim() + .min(1, 'Обязательное поле') + .min(2, 'Слишком короткое имя') + .max(100, 'Слишком длинное имя'), + email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')), + password: z + .string() + .min(1, 'Обязательное поле') + .min(MIN_PASS_LENGTH, `Минимум ${MIN_PASS_LENGTH} символов`) + .max(MAX_PASS_LENGTH, 'Слишком длинный пароль'), + confirmPassword: z.string().min(1, 'Обязательное поле'), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Пароли не совпадают', + path: ['confirmPassword'], + }); diff --git a/src/pages/signup/model/services/confirm.ts b/src/pages/signup/model/services/confirm.ts new file mode 100644 index 0000000..d20ee6c --- /dev/null +++ b/src/pages/signup/model/services/confirm.ts @@ -0,0 +1,21 @@ +import { api } from 'shared/api'; +import { ConfirmBody, ConfirmResponse } from '../schemas/confirm-schema'; +import { z } from 'zod'; + +export function confirm( + data: z.infer +): Promise> { + return api( + { + url: '/auth/sign-up/confirm', + method: 'POST', + data: data, + }, + { + contracts: { + body: ConfirmBody, + response: ConfirmResponse, + }, + } + ); +} diff --git a/src/pages/signup/model/services/signup.ts b/src/pages/signup/model/services/signup.ts new file mode 100644 index 0000000..306634e --- /dev/null +++ b/src/pages/signup/model/services/signup.ts @@ -0,0 +1,19 @@ +import { api } from 'shared/api'; +import { SignupBody, SignupResponse } from '../schemas/signup-schema'; +import { z } from 'zod'; + +export function signup(data: z.infer): Promise> { + return api( + { + url: '/auth/sign-up', + method: 'POST', + data: data, + }, + { + contracts: { + body: SignupBody, + response: SignupResponse, + }, + } + ); +} diff --git a/src/pages/signup/model/utils/field-name-mapper.ts b/src/pages/signup/model/utils/field-name-mapper.ts new file mode 100644 index 0000000..8905e36 --- /dev/null +++ b/src/pages/signup/model/utils/field-name-mapper.ts @@ -0,0 +1,16 @@ +import type { FieldPath } from 'react-hook-form'; +import { SignupBody, SignupFormSchema } from '../schemas/signup-schema'; +import { z } from 'zod'; + +export const fieldNameMapper = ( + fieldName: FieldPath> +): FieldPath> => { + switch (fieldName) { + case 'firstName': + case 'lastName': + case 'middleName': + return 'name'; + default: + return fieldName; + } +}; diff --git a/src/pages/signup/model/utils/prepare-fullname.ts b/src/pages/signup/model/utils/prepare-fullname.ts new file mode 100644 index 0000000..14be09d --- /dev/null +++ b/src/pages/signup/model/utils/prepare-fullname.ts @@ -0,0 +1,13 @@ +import { capitalize } from 'shared/lib/utils'; + +const STRING_SEPARATOR = ' '; + +export const prepareFullName = (name: string): { firstName: string; lastName: string } => { + const [firstName = '', lastName = ''] = name + .trim() + .replace(/\s+/gm, STRING_SEPARATOR) + .split(STRING_SEPARATOR) + .map(capitalize); + + return { firstName, lastName }; +}; diff --git a/src/pages/signup/ui/OTPForm.tsx b/src/pages/signup/ui/OTPForm.tsx new file mode 100644 index 0000000..ffcf9fb --- /dev/null +++ b/src/pages/signup/ui/OTPForm.tsx @@ -0,0 +1,151 @@ +'use client'; + +import type { FieldPath } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Field, + FieldError, + FieldGroup, + InputOtp, + InputOTPGroup, + InputOTPSlot, + Spinner, +} from 'shared/ui'; +import { ConfirmBody, ConfirmFormSchema, ConfirmResponse } from '../model/schemas/confirm-schema'; +import { cn } from 'shared/lib/utils'; +import { z } from 'zod'; +import { useMutation } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { GlobalErrorResponseType, isAxiosValidationError } from 'shared/api'; +import { confirm } from '../model/services/confirm'; +import { REGEXP_ONLY_DIGITS } from 'input-otp'; + +type FSchema = z.infer; +type BSchema = z.infer; +type RSchema = z.infer; + +interface OTPFormProps extends Omit, 'children'> { + email: string; + onSuccess?: (body: BSchema, res: RSchema) => void; + autoFocusCode?: boolean; +} + +export function OTPForm({ + className, + email, + onSuccess, + autoFocusCode = false, + ...props +}: OTPFormProps) { + const sendConfirm = useMutation({ + mutationFn: (data: BSchema) => { + return confirm(data); + }, + meta: { + skipGlobalValidationToast: true, + }, + }); + + const form = useForm({ + resolver: zodResolver(ConfirmFormSchema), + defaultValues: { + code: '', + }, + }); + + function setFormErrors

(errors: { message: string; path: P[] }[]) { + errors.forEach(({ message, path: [path] }) => { + const typedPath = path as FieldPath; + + form.setError(typedPath, { message }); + }); + } + + const onSubmit = (data: FSchema) => { + const body: BSchema = { + code: data.code, + email: email, + }; + + sendConfirm.mutate(body, { + onSuccess: (res) => { + onSuccess?.(body, res); + }, + onError: (err) => { + //ошибка валидации локальная + if (isAxiosValidationError(err)) { + setFormErrors(err?.issues ?? []); + } + //ошибка валидации серверная + if (isAxiosError(err)) { + setFormErrors(err?.response?.data?.details ?? []); + } + }, + }); + }; + + const disabled = sendConfirm.isPending || sendConfirm.isSuccess; + + return ( + + + Введите код + Код подтверждения отправлен на вашу почту. + + +

+ + ( + + + + + + + + + + + + {fieldState.invalid && ( + + )} + + )} + /> + + + + +
+ + + ); +} diff --git a/src/pages/signup/ui/SignupForm.tsx b/src/pages/signup/ui/SignupForm.tsx new file mode 100644 index 0000000..7d1d9b7 --- /dev/null +++ b/src/pages/signup/ui/SignupForm.tsx @@ -0,0 +1,199 @@ +'use client'; + +import type { FieldPath } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Field, + FieldDescription, + FieldError, + FieldGroup, + FieldLabel, + Input, + InputEmail, + InputPassword, + Link, + Spinner, +} from 'shared/ui'; +import { SignupBody, SignupFormSchema, SignupResponse } from '../model/schemas/signup-schema'; +import { cn } from 'shared/lib/utils'; +import { routes } from 'shared/config'; +import { z } from 'zod'; +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { signup } from '../model/services/signup'; +import { fieldNameMapper } from '../model/utils/field-name-mapper'; +import { prepareFullName } from '../model/utils/prepare-fullname'; +import { extractValidationIssues, ValidationIssue } from 'shared/api'; + +type FSchema = z.infer; +type BSchema = z.infer; +type RSchema = z.infer; + +interface SignupFormProps extends Omit, 'children' | 'onSubmit'> { + onSuccess?: (body: BSchema, res: RSchema) => void; +} + +export function SignupForm({ className, onSuccess, ...props }: SignupFormProps) { + const [showPassword, setShowPassword] = useState(false); + const sendUserData = useMutation({ + mutationFn: (data: BSchema) => { + return signup(data); + }, + meta: { + skipGlobalValidationToast: true, + }, + }); + + const form = useForm({ + resolver: zodResolver(SignupFormSchema), + defaultValues: { + name: '', + email: '', + password: '', + confirmPassword: '', + }, + }); + + function setFormErrors(errors: ValidationIssue[]) { + if (Array.isArray(errors)) { + errors.forEach(({ message, path: [path] }) => { + const typedPath = path as FieldPath; + const filedName = fieldNameMapper(typedPath); + + form.setError(filedName, { message }); + }); + } + } + + const onSubmit = (data: FSchema) => { + const body: BSchema = { + email: data.email, + password: data.password, + ...prepareFullName(data.name), + }; + + sendUserData.mutate(body, { + onSuccess: (res) => { + onSuccess?.(body, res); + }, + onError: (err) => { + setFormErrors(extractValidationIssues(err)); + }, + }); + }; + + return ( + + + Создать аккаунт + Заполните форму ниже, чтобы начать работу. + + +
+ + ( + + Имя + + {fieldState.invalid && } + + )} + /> + ( + + Email + + {fieldState.invalid && } + + )} + /> + ( + + Пароль + + {fieldState.invalid && } + + )} + /> + ( + + Повторите пароль + + {fieldState.invalid && } + + )} + /> + + + + + + Уже есть аккаунт? Войти + + + +
+
+
+ ); +} diff --git a/src/pages/signup/ui/SignupPage.tsx b/src/pages/signup/ui/SignupPage.tsx new file mode 100644 index 0000000..ed43aa9 --- /dev/null +++ b/src/pages/signup/ui/SignupPage.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { Link, Logo } from 'shared/ui'; +import { SignupForm } from './SignupForm'; +import { OTPForm } from './OTPForm'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { accessToken } from 'shared/api'; +import { routes } from 'shared/config'; +import { toast } from 'sonner'; + +function SignupPage() { + const [email, setEmail] = useState(''); + const router = useRouter(); + + return ( +
+
+ + + + + {!email ? ( + setEmail(email)} /> + ) : ( + { + if (res.success) { + accessToken.token = res.token; + router.replace(routes.team.profile()); + if (res.message) { + toast.success(res.message); + } + } + }} + /> + )} +
+
+ ); +} + +export { SignupPage }; diff --git a/src/shared/api/endpoints/auth/auth.ts b/src/shared/api/endpoints/auth/auth.ts new file mode 100644 index 0000000..dc2a5ee --- /dev/null +++ b/src/shared/api/endpoints/auth/auth.ts @@ -0,0 +1,709 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { useMutation } from '@tanstack/react-query'; +import type { + MutationFunction, + QueryClient, + UseMutationOptions, + UseMutationResult, +} from '@tanstack/react-query'; + +import type { + ActionResponseOutput, + GlobalErrorResponseOutput, + PasswordResetConfirmDtoOutput, + RefreshTokenResponseOutput, + ResetPasswordDtoOutput, + SignInDtoOutput, + SignUpDtoOutput, + VerifyDtoOutput, + VerifyResetCodeDtoOutput, +} from '../../schemas'; + +import { instance } from '../../instance'; +import type { ErrorType, BodyType } from '../../instance'; + +type SecondParameter unknown> = Parameters[1]; + +/** + * Создает пользователя, базовые настройки безопасности и уведомлений. + * @summary Регистрация нового пользователя + */ +export const authControllerSignUp = ( + signUpDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/auth/sign-up`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: signUpDtoOutput, + signal, + }, + options + ); +}; + +export const getAuthControllerSignUpMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['authControllerSignUp']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return authControllerSignUp(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AuthControllerSignUpMutationResult = NonNullable< + Awaited> +>; +export type AuthControllerSignUpMutationBody = BodyType; +export type AuthControllerSignUpMutationError = ErrorType; + +/** + * @summary Регистрация нового пользователя + */ +export const useAuthControllerSignUp = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getAuthControllerSignUpMutationOptions(options), queryClient); +}; +/** + * Проверяет OTP из письма, создаёт аккаунт, выдаёт access-токен в теле ответа и устанавливает refresh в httpOnly cookie. + * @summary Подтверждение регистрации по коду + */ +export const authControllerVerify = ( + verifyDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/auth/sign-up/confirm`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: verifyDtoOutput, + signal, + }, + options + ); +}; + +export const getAuthControllerVerifyMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['authControllerVerify']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return authControllerVerify(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AuthControllerVerifyMutationResult = NonNullable< + Awaited> +>; +export type AuthControllerVerifyMutationBody = BodyType; +export type AuthControllerVerifyMutationError = ErrorType; + +/** + * @summary Подтверждение регистрации по коду + */ +export const useAuthControllerVerify = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getAuthControllerVerifyMutationOptions(options), queryClient); +}; +/** + * Возвращает Access/Refresh токены. Если у пользователя включена 2FA, вернет временный токен. + * @summary Вход в систему + */ +export const authControllerSignIn = ( + signInDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/auth/sign-in`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: signInDtoOutput, + signal, + }, + options + ); +}; + +export const getAuthControllerSignInMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['authControllerSignIn']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return authControllerSignIn(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AuthControllerSignInMutationResult = NonNullable< + Awaited> +>; +export type AuthControllerSignInMutationBody = BodyType; +export type AuthControllerSignInMutationError = ErrorType; + +/** + * @summary Вход в систему + */ +export const useAuthControllerSignIn = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getAuthControllerSignInMutationOptions(options), queryClient); +}; +/** + * Удаляет текущую сессию пользователя из Redis. + * @summary Выход из системы + */ +export const authControllerLogout = ( + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/auth/sign-out`, method: 'POST', signal }, + options + ); +}; + +export const getAuthControllerLogoutMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + void, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + void, + TContext +> => { + const mutationKey = ['authControllerLogout']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + void + > = () => { + return authControllerLogout(requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AuthControllerLogoutMutationResult = NonNullable< + Awaited> +>; + +export type AuthControllerLogoutMutationError = ErrorType; + +/** + * @summary Выход из системы + */ +export const useAuthControllerLogout = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + void, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult>, TError, void, TContext> => { + return useMutation(getAuthControllerLogoutMutationOptions(options), queryClient); +}; +/** + * Выдает новую пару Access и Refresh токенов. + * @summary Обновление токенов + */ +export const authControllerRefresh = ( + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/auth/refresh`, method: 'POST', signal }, + options + ); +}; + +export const getAuthControllerRefreshMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + void, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + void, + TContext +> => { + const mutationKey = ['authControllerRefresh']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + void + > = () => { + return authControllerRefresh(requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AuthControllerRefreshMutationResult = NonNullable< + Awaited> +>; + +export type AuthControllerRefreshMutationError = ErrorType; + +/** + * @summary Обновление токенов + */ +export const useAuthControllerRefresh = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + void, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult>, TError, void, TContext> => { + return useMutation(getAuthControllerRefreshMutationOptions(options), queryClient); +}; +/** + * Отправляет одноразовый код на email, если пользователь существует. + * @summary Запрос кода восстановления пароля + */ +export const authControllerResetPasswordRequest = ( + resetPasswordDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/auth/password/reset`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: resetPasswordDtoOutput, + signal, + }, + options + ); +}; + +export const getAuthControllerResetPasswordRequestMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['authControllerResetPasswordRequest']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return authControllerResetPasswordRequest(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AuthControllerResetPasswordRequestMutationResult = NonNullable< + Awaited> +>; +export type AuthControllerResetPasswordRequestMutationBody = BodyType; +export type AuthControllerResetPasswordRequestMutationError = ErrorType; + +/** + * @summary Запрос кода восстановления пароля + */ +export const useAuthControllerResetPasswordRequest = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getAuthControllerResetPasswordRequestMutationOptions(options), queryClient); +}; +/** + * Проверяет код из письма и помечает сессию сброса как подтверждённую. + * @summary Проверка кода восстановления пароля + */ +export const authControllerVerifyResetCode = ( + verifyResetCodeDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/auth/password/reset/verify`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: verifyResetCodeDtoOutput, + signal, + }, + options + ); +}; + +export const getAuthControllerVerifyResetCodeMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['authControllerVerifyResetCode']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return authControllerVerifyResetCode(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AuthControllerVerifyResetCodeMutationResult = NonNullable< + Awaited> +>; +export type AuthControllerVerifyResetCodeMutationBody = BodyType; +export type AuthControllerVerifyResetCodeMutationError = ErrorType; + +/** + * @summary Проверка кода восстановления пароля + */ +export const useAuthControllerVerifyResetCode = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getAuthControllerVerifyResetCodeMutationOptions(options), queryClient); +}; +/** + * Доступно только после успешной проверки кода на шаге verify. + * @summary Установка нового пароля после сброса + */ +export const authControllerConfirmPasswordReset = ( + passwordResetConfirmDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/auth/password/reset/confirm`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: passwordResetConfirmDtoOutput, + signal, + }, + options + ); +}; + +export const getAuthControllerConfirmPasswordResetMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['authControllerConfirmPasswordReset']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return authControllerConfirmPasswordReset(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AuthControllerConfirmPasswordResetMutationResult = NonNullable< + Awaited> +>; +export type AuthControllerConfirmPasswordResetMutationBody = + BodyType; +export type AuthControllerConfirmPasswordResetMutationError = ErrorType; + +/** + * @summary Установка нового пароля после сброса + */ +export const useAuthControllerConfirmPasswordReset = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getAuthControllerConfirmPasswordResetMutationOptions(options), queryClient); +}; diff --git a/src/shared/api/endpoints/teams/teams.ts b/src/shared/api/endpoints/teams/teams.ts new file mode 100644 index 0000000..e677733 --- /dev/null +++ b/src/shared/api/endpoints/teams/teams.ts @@ -0,0 +1,1552 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { useMutation, useQuery } from '@tanstack/react-query'; +import type { + DataTag, + DefinedInitialDataOptions, + DefinedUseQueryResult, + MutationFunction, + QueryClient, + QueryFunction, + QueryKey, + UndefinedInitialDataOptions, + UseMutationOptions, + UseMutationResult, + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query'; + +import type { + ActionResponseOutput, + CheckSlugResponseOutput, + CreateTeamDtoOutput, + FileUploadResponseOutput, + GlobalErrorResponseOutput, + InviteMemberDtoOutput, + SyncTagsDtoOutput, + TeamMemberResponseOutput, + TeamsControllerFindOne200, + TeamsControllerUpdateTeamAvatarBody, + TeamsControllerUpdateTeamBannerBody, + UpdateMemberDtoOutput, + UpdateTeamDtoOutput, + UserInviteResponseOutput, + UserTeamResponseOutput, +} from '../../schemas'; + +import { instance } from '../../instance'; +import type { ErrorType, BodyType } from '../../instance'; + +type SecondParameter unknown> = Parameters[1]; + +/** + * @summary Создать новую команду + */ +export const teamsControllerCreate = ( + createTeamDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/teams`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: createTeamDtoOutput, + signal, + }, + options + ); +}; + +export const getTeamsControllerCreateMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['teamsControllerCreate']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return teamsControllerCreate(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type TeamsControllerCreateMutationResult = NonNullable< + Awaited> +>; +export type TeamsControllerCreateMutationBody = BodyType; +export type TeamsControllerCreateMutationError = ErrorType; + +/** + * @summary Создать новую команду + */ +export const useTeamsControllerCreate = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getTeamsControllerCreateMutationOptions(options), queryClient); +}; +/** + * Проверяет, свободен ли уникальный адрес команды для использования. + * @summary Проверить доступность слага + */ +export const teamsControllerCheckSlug = ( + slug: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/teams/check-slug/${slug}`, method: 'GET', signal }, + options + ); +}; + +export const getTeamsControllerCheckSlugQueryKey = (slug: string) => { + return [`/api/v1/teams/check-slug/${slug}`] as const; +}; + +export const getTeamsControllerCheckSlugQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getTeamsControllerCheckSlugQueryKey(slug); + + const queryFn: QueryFunction>> = ({ + signal, + }) => teamsControllerCheckSlug(slug, requestOptions, signal); + + return { queryKey, queryFn, enabled: !!slug, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type TeamsControllerCheckSlugQueryResult = NonNullable< + Awaited> +>; +export type TeamsControllerCheckSlugQueryError = ErrorType; + +export function useTeamsControllerCheckSlug< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useTeamsControllerCheckSlug< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useTeamsControllerCheckSlug< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Проверить доступность слага + */ + +export function useTeamsControllerCheckSlug< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getTeamsControllerCheckSlugQueryOptions(slug, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Возвращает все команды, в которых текущий пользователь является участником или владельцем. + * @summary Получить список команд пользователя + */ +export const teamsControllerFindAll = ( + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/teams/my`, method: 'GET', signal }, + options + ); +}; + +export const getTeamsControllerFindAllQueryKey = () => { + return [`/api/v1/teams/my`] as const; +}; + +export const getTeamsControllerFindAllQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getTeamsControllerFindAllQueryKey(); + + const queryFn: QueryFunction>> = ({ signal }) => + teamsControllerFindAll(requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type TeamsControllerFindAllQueryResult = NonNullable< + Awaited> +>; +export type TeamsControllerFindAllQueryError = ErrorType; + +export function useTeamsControllerFindAll< + TData = Awaited>, + TError = ErrorType, +>( + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useTeamsControllerFindAll< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useTeamsControllerFindAll< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Получить список команд пользователя + */ + +export function useTeamsControllerFindAll< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getTeamsControllerFindAllQueryOptions(options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Возвращает все активные приглашения в команды, отправленные на email текущего пользователя. + * @summary Получить список входящих приглашений + */ +export const teamsControllerFindAllInvites = ( + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/teams/my/invites`, method: 'GET', signal }, + options + ); +}; + +export const getTeamsControllerFindAllInvitesQueryKey = () => { + return [`/api/v1/teams/my/invites`] as const; +}; + +export const getTeamsControllerFindAllInvitesQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getTeamsControllerFindAllInvitesQueryKey(); + + const queryFn: QueryFunction>> = ({ + signal, + }) => teamsControllerFindAllInvites(requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type TeamsControllerFindAllInvitesQueryResult = NonNullable< + Awaited> +>; +export type TeamsControllerFindAllInvitesQueryError = ErrorType; + +export function useTeamsControllerFindAllInvites< + TData = Awaited>, + TError = ErrorType, +>( + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useTeamsControllerFindAllInvites< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useTeamsControllerFindAllInvites< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Получить список входящих приглашений + */ + +export function useTeamsControllerFindAllInvites< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getTeamsControllerFindAllInvitesQueryOptions(options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary Получить детальную информацию о команде по slug + */ +export const teamsControllerFindOne = ( + slug: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/teams/${slug}`, method: 'GET', signal }, + options + ); +}; + +export const getTeamsControllerFindOneQueryKey = (slug: string) => { + return [`/api/v1/teams/${slug}`] as const; +}; + +export const getTeamsControllerFindOneQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getTeamsControllerFindOneQueryKey(slug); + + const queryFn: QueryFunction>> = ({ signal }) => + teamsControllerFindOne(slug, requestOptions, signal); + + return { queryKey, queryFn, enabled: !!slug, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type TeamsControllerFindOneQueryResult = NonNullable< + Awaited> +>; +export type TeamsControllerFindOneQueryError = ErrorType; + +export function useTeamsControllerFindOne< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useTeamsControllerFindOne< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useTeamsControllerFindOne< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Получить детальную информацию о команде по slug + */ + +export function useTeamsControllerFindOne< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getTeamsControllerFindOneQueryOptions(slug, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary Обновить данные команды + */ +export const teamsControllerUpdate = ( + slug: string, + updateTeamDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/teams/${slug}`, + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + data: updateTeamDtoOutput, + signal, + }, + options + ); +}; + +export const getTeamsControllerUpdateMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + const mutationKey = ['teamsControllerUpdate']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string; data: BodyType } + > = (props) => { + const { slug, data } = props ?? {}; + + return teamsControllerUpdate(slug, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type TeamsControllerUpdateMutationResult = NonNullable< + Awaited> +>; +export type TeamsControllerUpdateMutationBody = BodyType; +export type TeamsControllerUpdateMutationError = ErrorType; + +/** + * @summary Обновить данные команды + */ +export const useTeamsControllerUpdate = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + return useMutation(getTeamsControllerUpdateMutationOptions(options), queryClient); +}; +/** + * @summary Удалить команду + */ +export const teamsControllerRemove = ( + slug: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/teams/${slug}`, method: 'DELETE', signal }, + options + ); +}; + +export const getTeamsControllerRemoveMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string }, + TContext +> => { + const mutationKey = ['teamsControllerRemove']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string } + > = (props) => { + const { slug } = props ?? {}; + + return teamsControllerRemove(slug, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type TeamsControllerRemoveMutationResult = NonNullable< + Awaited> +>; + +export type TeamsControllerRemoveMutationError = ErrorType; + +/** + * @summary Удалить команду + */ +export const useTeamsControllerRemove = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string }, + TContext +> => { + return useMutation(getTeamsControllerRemoveMutationOptions(options), queryClient); +}; +/** + * @summary Синхронизировать теги команды + */ +export const teamsControllerSyncTags = ( + slug: string, + syncTagsDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/teams/${slug}/tags`, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + data: syncTagsDtoOutput, + signal, + }, + options + ); +}; + +export const getTeamsControllerSyncTagsMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + const mutationKey = ['teamsControllerSyncTags']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string; data: BodyType } + > = (props) => { + const { slug, data } = props ?? {}; + + return teamsControllerSyncTags(slug, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type TeamsControllerSyncTagsMutationResult = NonNullable< + Awaited> +>; +export type TeamsControllerSyncTagsMutationBody = BodyType; +export type TeamsControllerSyncTagsMutationError = ErrorType; + +/** + * @summary Синхронизировать теги команды + */ +export const useTeamsControllerSyncTags = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + return useMutation(getTeamsControllerSyncTagsMutationOptions(options), queryClient); +}; +/** + * Загрузка файла изображения для профиля команды. + * @summary Обновить аватар команды + */ +export const teamsControllerUpdateTeamAvatar = ( + slug: string, + teamsControllerUpdateTeamAvatarBody: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + const formData = new FormData(); + if (teamsControllerUpdateTeamAvatarBody.file !== undefined) { + formData.append(`file`, teamsControllerUpdateTeamAvatarBody.file); + } + + return instance( + { url: `/api/v1/teams/${slug}/avatar`, method: 'PATCH', data: formData, signal }, + options + ); +}; + +export const getTeamsControllerUpdateTeamAvatarMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + const mutationKey = ['teamsControllerUpdateTeamAvatar']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string; data: BodyType } + > = (props) => { + const { slug, data } = props ?? {}; + + return teamsControllerUpdateTeamAvatar(slug, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type TeamsControllerUpdateTeamAvatarMutationResult = NonNullable< + Awaited> +>; +export type TeamsControllerUpdateTeamAvatarMutationBody = + BodyType; +export type TeamsControllerUpdateTeamAvatarMutationError = ErrorType; + +/** + * @summary Обновить аватар команды + */ +export const useTeamsControllerUpdateTeamAvatar = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + return useMutation(getTeamsControllerUpdateTeamAvatarMutationOptions(options), queryClient); +}; +/** + * Загрузка файла изображения для обложки (баннера) команды. + * @summary Обновить баннер команды + */ +export const teamsControllerUpdateTeamBanner = ( + slug: string, + teamsControllerUpdateTeamBannerBody: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + const formData = new FormData(); + if (teamsControllerUpdateTeamBannerBody.file !== undefined) { + formData.append(`file`, teamsControllerUpdateTeamBannerBody.file); + } + + return instance( + { url: `/api/v1/teams/${slug}/banner`, method: 'PATCH', data: formData, signal }, + options + ); +}; + +export const getTeamsControllerUpdateTeamBannerMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + const mutationKey = ['teamsControllerUpdateTeamBanner']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string; data: BodyType } + > = (props) => { + const { slug, data } = props ?? {}; + + return teamsControllerUpdateTeamBanner(slug, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type TeamsControllerUpdateTeamBannerMutationResult = NonNullable< + Awaited> +>; +export type TeamsControllerUpdateTeamBannerMutationBody = + BodyType; +export type TeamsControllerUpdateTeamBannerMutationError = ErrorType; + +/** + * @summary Обновить баннер команды + */ +export const useTeamsControllerUpdateTeamBanner = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + return useMutation(getTeamsControllerUpdateTeamBannerMutationOptions(options), queryClient); +}; +/** + * @summary Получить список всех участников команды + */ +export const membersControllerGetMembers = ( + slug: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/teams/${slug}/members`, method: 'GET', signal }, + options + ); +}; + +export const getMembersControllerGetMembersQueryKey = (slug: string) => { + return [`/api/v1/teams/${slug}/members`] as const; +}; + +export const getMembersControllerGetMembersQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getMembersControllerGetMembersQueryKey(slug); + + const queryFn: QueryFunction>> = ({ + signal, + }) => membersControllerGetMembers(slug, requestOptions, signal); + + return { queryKey, queryFn, enabled: !!slug, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type MembersControllerGetMembersQueryResult = NonNullable< + Awaited> +>; +export type MembersControllerGetMembersQueryError = ErrorType; + +export function useMembersControllerGetMembers< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useMembersControllerGetMembers< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useMembersControllerGetMembers< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Получить список всех участников команды + */ + +export function useMembersControllerGetMembers< + TData = Awaited>, + TError = ErrorType, +>( + slug: string, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getMembersControllerGetMembersQueryOptions(slug, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Создает запись об участнике со статусом "pending". Если пользователь уже зарегистрирован — он увидит приглашение в разделе "my/invites". Если нет — ему уйдет письмо на указанный Email. + * @summary Пригласить пользователя в команду по Email + */ +export const membersControllerInvite = ( + slug: string, + inviteMemberDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/teams/${slug}/invitations`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: inviteMemberDtoOutput, + signal, + }, + options + ); +}; + +export const getMembersControllerInviteMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + const mutationKey = ['membersControllerInvite']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string; data: BodyType } + > = (props) => { + const { slug, data } = props ?? {}; + + return membersControllerInvite(slug, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type MembersControllerInviteMutationResult = NonNullable< + Awaited> +>; +export type MembersControllerInviteMutationBody = BodyType; +export type MembersControllerInviteMutationError = ErrorType; + +/** + * @summary Пригласить пользователя в команду по Email + */ +export const useMembersControllerInvite = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string; data: BodyType }, + TContext +> => { + return useMutation(getMembersControllerInviteMutationOptions(options), queryClient); +}; +/** + * Активирует участие пользователя в команде по уникальному коду приглашения. После успешного принятия статус участника меняется с "pending" на "active". Система автоматически связывает текущего авторизованного пользователя с инвайтом через Email. + * @summary Принять приглашение в команду + */ +export const membersControllerAccept = ( + slug: string, + code: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/teams/${slug}/invitations/${code}/accept`, method: 'POST', signal }, + options + ); +}; + +export const getMembersControllerAcceptMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; code: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string; code: string }, + TContext +> => { + const mutationKey = ['membersControllerAccept']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string; code: string } + > = (props) => { + const { slug, code } = props ?? {}; + + return membersControllerAccept(slug, code, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type MembersControllerAcceptMutationResult = NonNullable< + Awaited> +>; + +export type MembersControllerAcceptMutationError = ErrorType; + +/** + * @summary Принять приглашение в команду + */ +export const useMembersControllerAccept = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; code: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string; code: string }, + TContext +> => { + return useMutation(getMembersControllerAcceptMutationOptions(options), queryClient); +}; +/** + * Позволяет изменить роль участника (member -> admin) или вручную изменить его статус. Владелец команды (Owner) не может понизить свою роль через этот эндпоинт. + * @summary Изменить роль или статус участника + */ +export const membersControllerUpdateMember = ( + slug: string, + userId: string, + updateMemberDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/teams/${slug}/members/${userId}`, + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + data: updateMemberDtoOutput, + signal, + }, + options + ); +}; + +export const getMembersControllerUpdateMemberMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; userId: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string; userId: string; data: BodyType }, + TContext +> => { + const mutationKey = ['membersControllerUpdateMember']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string; userId: string; data: BodyType } + > = (props) => { + const { slug, userId, data } = props ?? {}; + + return membersControllerUpdateMember(slug, userId, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type MembersControllerUpdateMemberMutationResult = NonNullable< + Awaited> +>; +export type MembersControllerUpdateMemberMutationBody = BodyType; +export type MembersControllerUpdateMemberMutationError = ErrorType; + +/** + * @summary Изменить роль или статус участника + */ +export const useMembersControllerUpdateMember = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; userId: string; data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string; userId: string; data: BodyType }, + TContext +> => { + return useMutation(getMembersControllerUpdateMemberMutationOptions(options), queryClient); +}; +/** + * @summary Удалить участника из команды + */ +export const membersControllerRemoveMember = ( + slug: string, + userId: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/teams/${slug}/members/${userId}`, method: 'DELETE', signal }, + options + ); +}; + +export const getMembersControllerRemoveMemberMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; userId: string }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { slug: string; userId: string }, + TContext +> => { + const mutationKey = ['membersControllerRemoveMember']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { slug: string; userId: string } + > = (props) => { + const { slug, userId } = props ?? {}; + + return membersControllerRemoveMember(slug, userId, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type MembersControllerRemoveMemberMutationResult = NonNullable< + Awaited> +>; + +export type MembersControllerRemoveMemberMutationError = ErrorType; + +/** + * @summary Удалить участника из команды + */ +export const useMembersControllerRemoveMember = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { slug: string; userId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { slug: string; userId: string }, + TContext +> => { + return useMutation(getMembersControllerRemoveMemberMutationOptions(options), queryClient); +}; diff --git a/src/shared/api/endpoints/users/users.ts b/src/shared/api/endpoints/users/users.ts new file mode 100644 index 0000000..7eaaa33 --- /dev/null +++ b/src/shared/api/endpoints/users/users.ts @@ -0,0 +1,554 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { useMutation, useQuery } from '@tanstack/react-query'; +import type { + DataTag, + DefinedInitialDataOptions, + DefinedUseQueryResult, + MutationFunction, + QueryClient, + QueryFunction, + QueryKey, + UndefinedInitialDataOptions, + UseMutationOptions, + UseMutationResult, + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query'; + +import type { + ActionResponseOutput, + GlobalErrorResponseOutput, + UpdateNotificationsDtoOutput, + UpdateProfileDtoOutput, + UserControllerGetActivityParams, + UserControllerUploadAvatarBody, + UserResponseOutput, +} from '../../schemas'; + +import { instance } from '../../instance'; +import type { ErrorType, BodyType } from '../../instance'; + +type SecondParameter unknown> = Parameters[1]; + +/** + * Возвращает полную структуру профиля, включая вложенные объекты безопасности и настроек. + * @summary Получить профиль текущего пользователя + */ +export const userControllerGetProfile = ( + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance({ url: `/api/v1/users/me`, method: 'GET', signal }, options); +}; + +export const getUserControllerGetProfileQueryKey = () => { + return [`/api/v1/users/me`] as const; +}; + +export const getUserControllerGetProfileQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getUserControllerGetProfileQueryKey(); + + const queryFn: QueryFunction>> = ({ + signal, + }) => userControllerGetProfile(requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type UserControllerGetProfileQueryResult = NonNullable< + Awaited> +>; +export type UserControllerGetProfileQueryError = ErrorType; + +export function useUserControllerGetProfile< + TData = Awaited>, + TError = ErrorType, +>( + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useUserControllerGetProfile< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useUserControllerGetProfile< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Получить профиль текущего пользователя + */ + +export function useUserControllerGetProfile< + TData = Awaited>, + TError = ErrorType, +>( + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getUserControllerGetProfileQueryOptions(options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Позволяет точечно обновить имя, bio, часовой пояс и язык интерфейса. + * @summary Обновить данные профиля + */ +export const userControllerUpdateProfile = ( + updateProfileDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/users/me`, + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + data: updateProfileDtoOutput, + signal, + }, + options + ); +}; + +export const getUserControllerUpdateProfileMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['userControllerUpdateProfile']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return userControllerUpdateProfile(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type UserControllerUpdateProfileMutationResult = NonNullable< + Awaited> +>; +export type UserControllerUpdateProfileMutationBody = BodyType; +export type UserControllerUpdateProfileMutationError = ErrorType; + +/** + * @summary Обновить данные профиля + */ +export const useUserControllerUpdateProfile = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getUserControllerUpdateProfileMutationOptions(options), queryClient); +}; +/** + * Частичное обновление настроек email и push уведомлений. + * @summary Обновить настройки уведомлений + */ +export const userControllerUpdateNotifications = ( + updateNotificationsDtoOutput: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { + url: `/api/v1/users/me/notifications`, + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + data: updateNotificationsDtoOutput, + signal, + }, + options + ); +}; + +export const getUserControllerUpdateNotificationsMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['userControllerUpdateNotifications']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return userControllerUpdateNotifications(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type UserControllerUpdateNotificationsMutationResult = NonNullable< + Awaited> +>; +export type UserControllerUpdateNotificationsMutationBody = BodyType; +export type UserControllerUpdateNotificationsMutationError = ErrorType; + +/** + * @summary Обновить настройки уведомлений + */ +export const useUserControllerUpdateNotifications = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getUserControllerUpdateNotificationsMutationOptions(options), queryClient); +}; +/** + * Возвращает список последних действий пользователя (логи). + * @summary Получить ленту активности пользователя + */ +export const userControllerGetActivity = ( + params?: UserControllerGetActivityParams, + options?: SecondParameter, + signal?: AbortSignal +) => { + return instance( + { url: `/api/v1/users/me/activity`, method: 'GET', params, signal }, + options + ); +}; + +export const getUserControllerGetActivityQueryKey = (params?: UserControllerGetActivityParams) => { + return [`/api/v1/users/me/activity`, ...(params ? [params] : [])] as const; +}; + +export const getUserControllerGetActivityQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + params?: UserControllerGetActivityParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getUserControllerGetActivityQueryKey(params); + + const queryFn: QueryFunction>> = ({ + signal, + }) => userControllerGetActivity(params, requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type UserControllerGetActivityQueryResult = NonNullable< + Awaited> +>; +export type UserControllerGetActivityQueryError = ErrorType; + +export function useUserControllerGetActivity< + TData = Awaited>, + TError = ErrorType, +>( + params: undefined | UserControllerGetActivityParams, + options: { + query: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useUserControllerGetActivity< + TData = Awaited>, + TError = ErrorType, +>( + params?: UserControllerGetActivityParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + 'initialData' + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useUserControllerGetActivity< + TData = Awaited>, + TError = ErrorType, +>( + params?: UserControllerGetActivityParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Получить ленту активности пользователя + */ + +export function useUserControllerGetActivity< + TData = Awaited>, + TError = ErrorType, +>( + params?: UserControllerGetActivityParams, + options?: { + query?: Partial< + UseQueryOptions>, TError, TData> + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getUserControllerGetActivityQueryOptions(params, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Загрузка файла изображения для профиля пользователя. + * @summary Загрузить новую аватарку + */ +export const userControllerUploadAvatar = ( + userControllerUploadAvatarBody: BodyType, + options?: SecondParameter, + signal?: AbortSignal +) => { + const formData = new FormData(); + if (userControllerUploadAvatarBody.file !== undefined) { + formData.append(`file`, userControllerUploadAvatarBody.file); + } + + return instance( + { url: `/api/v1/users/me/avatar`, method: 'POST', data: formData, signal }, + options + ); +}; + +export const getUserControllerUploadAvatarMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['userControllerUploadAvatar']; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return userControllerUploadAvatar(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type UserControllerUploadAvatarMutationResult = NonNullable< + Awaited> +>; +export type UserControllerUploadAvatarMutationBody = BodyType; +export type UserControllerUploadAvatarMutationError = ErrorType; + +/** + * @summary Загрузить новую аватарку + */ +export const useUserControllerUploadAvatar = < + TError = ErrorType, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getUserControllerUploadAvatarMutationOptions(options), queryClient); +}; diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts new file mode 100644 index 0000000..4b6c5e9 --- /dev/null +++ b/src/shared/api/index.ts @@ -0,0 +1,14 @@ +export * from './endpoints/auth/auth'; +export * from './endpoints/users/users'; +export * from './endpoints/teams/teams'; +export * from './schemas'; +export { instance as api } from './instance'; +export { + AxiosValidationError, + type GlobalErrorResponseType, + isAxiosValidationError, + extractValidationIssues, + type ValidationIssue, +} from './validation'; +export { accessToken } from './token'; +export { queryClient } from './query-client'; diff --git a/src/shared/api/instance.ts b/src/shared/api/instance.ts new file mode 100644 index 0000000..739d745 --- /dev/null +++ b/src/shared/api/instance.ts @@ -0,0 +1,19 @@ +import Axios, { AxiosError } from 'axios'; +import type { AxiosAuthRefreshRequestConfig } from 'axios-auth-refresh'; +import { applyInterceptors } from './interceptors'; + +const AXIOS_INSTANCE = Axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, +}); + +applyInterceptors(AXIOS_INSTANCE); + +export const instance = ( + config: AxiosAuthRefreshRequestConfig, + options?: AxiosAuthRefreshRequestConfig +): Promise => { + return AXIOS_INSTANCE({ ...config, ...options }).then(({ data }) => data); +}; + +export type ErrorType = AxiosError; +export type BodyType = BodyData; diff --git a/src/shared/api/interceptors.ts b/src/shared/api/interceptors.ts new file mode 100644 index 0000000..cab3ee6 --- /dev/null +++ b/src/shared/api/interceptors.ts @@ -0,0 +1,22 @@ +import { AxiosInstance } from 'axios'; +import { createAuthRefresh } from 'axios-auth-refresh'; +import { accessToken, refreshAuth } from './token'; +import { AxiosContracts } from 'shared/api/validation'; + +export function applyInterceptors(instance: AxiosInstance) { + //установка актуального токена доступа + instance.interceptors.request.use((config) => { + if (accessToken.header) { + config.headers.set('Authorization', accessToken.header); + } + return config; + }); + //валидация запросов + instance.interceptors.request.use(AxiosContracts.requestContractInterceptor); + + //обновление токена доступа + createAuthRefresh(instance, refreshAuth(instance), { maxRetries: 1 }); + + //валидация ответов + instance.interceptors.response.use(AxiosContracts.responseContractInterceptor); +} diff --git a/src/shared/api/openapi/openapi.json b/src/shared/api/openapi/openapi.json new file mode 100644 index 0000000..5443b6c --- /dev/null +++ b/src/shared/api/openapi/openapi.json @@ -0,0 +1,4373 @@ +{ + "openapi": "3.0.0", + "paths": { + "/api/v1/dump": { + "get": { + "operationId": "PrometheusController_index", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": ["Prometheus"] + } + }, + "/api/v1/auth/sign-up": { + "post": { + "description": "Создает пользователя, базовые настройки безопасности и уведомлений.", + "operationId": "AuthController_signUp", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignUpDto_Output" + } + } + } + }, + "responses": { + "201": { + "description": "Пользователь успешно зарегистрирован.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Ошибка валидации данных (например, неверный формат email)", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "VALIDATION_FAILED", + "message": "Ошибка валидации данных (например, неверный формат email)", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.197Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "409": { + "description": "Пользователь с таким email уже существует", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "CONFLICT", + "message": "Пользователь с таким email уже существует", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.197Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Регистрация нового пользователя", + "tags": ["Auth"] + } + }, + "/api/v1/auth/sign-up/confirm": { + "post": { + "description": "Проверяет OTP из письма, создаёт аккаунт, выдаёт access-токен в теле ответа и устанавливает refresh в httpOnly cookie.", + "operationId": "AuthController_verify", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyDto_Output" + } + } + } + }, + "responses": { + "201": { + "description": "Аккаунт подтверждён, сессия создана.", + "content": { + "application/json": { + "schema": { + "example": { + "success": true, + "message": "Аккаунт успешно подтвержден", + "token": "eyJhbGciOiJIUzI1NiIsInR5c..." + } + } + } + } + }, + "400": { + "description": "Неверный или истёкший код подтверждения", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Неверный или истёкший код подтверждения", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.198Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Подтверждение регистрации по коду", + "tags": ["Auth"] + } + }, + "/api/v1/auth/sign-in": { + "post": { + "description": "Возвращает Access/Refresh токены. Если у пользователя включена 2FA, вернет временный токен.", + "operationId": "AuthController_signIn", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignInDto_Output" + } + } + } + }, + "responses": { + "200": { + "description": "Успешный вход.", + "content": { + "application/json": { + "schema": { + "example": { + "success": true, + "message": false, + "token": "eyJhbGciOiJIUzI1NiIsInR5c..." + } + } + } + } + }, + "400": { + "description": "Неверный формат email", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Неверный формат email", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.198Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Неверный email или пароль", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Неверный email или пароль", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.198Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Вход в систему", + "tags": ["Auth"] + } + }, + "/api/v1/auth/sign-out": { + "post": { + "description": "Удаляет текущую сессию пользователя из Redis.", + "operationId": "AuthController_logout", + "parameters": [], + "responses": { + "200": { + "description": "Успешный выход.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.198Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Выход из системы", + "tags": ["Auth"] + } + }, + "/api/v1/auth/refresh": { + "post": { + "description": "Выдает новую пару Access и Refresh токенов.", + "operationId": "AuthController_refresh", + "parameters": [], + "responses": { + "200": { + "description": "Токены успешно обновлены.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenResponse_Output" + }, + "example": { + "success": true, + "token": "eyJhbGciOiJIUzI1NiIsInR5c...", + "message": "def50200508a1768c7e..." + } + } + } + }, + "400": { + "description": "Ошибка валидации (не передан refresh токен)", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Ошибка валидации (не передан refresh токен)", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.198Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Refresh токен недействителен, истек или отозван", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Refresh токен недействителен, истек или отозван", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Обновление токенов", + "tags": ["Auth"] + } + }, + "/api/v1/auth/password/reset": { + "post": { + "description": "Отправляет одноразовый код на email, если пользователь существует.", + "operationId": "AuthController_resetPasswordRequest", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResetPasswordDto_Output" + } + } + } + }, + "responses": { + "201": { + "description": "Код отправлен на почту (при успешной обработке запроса).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Некорректный формат email", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "VALIDATION_FAILED", + "message": "Некорректный формат email", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Пользователь с таким email не найден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Пользователь с таким email не найден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "422": { + "description": "Указанный email адрес имеет некорректный формат", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INVALID_EMAIL_FORMAT", + "message": "Указанный email адрес имеет некорректный формат", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Запрос кода восстановления пароля", + "tags": ["Auth"] + } + }, + "/api/v1/auth/password/reset/verify": { + "post": { + "description": "Проверяет код из письма и помечает сессию сброса как подтверждённую.", + "operationId": "AuthController_verifyResetCode", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyResetCodeDto_Output" + } + } + } + }, + "responses": { + "201": { + "description": "Код подтверждён, можно задать новый пароль.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Неверный или истёкший код подтверждения", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Неверный или истёкший код подтверждения", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Проверка кода восстановления пароля", + "tags": ["Auth"] + } + }, + "/api/v1/auth/password/reset/confirm": { + "post": { + "description": "Доступно только после успешной проверки кода на шаге verify.", + "operationId": "AuthController_confirmPasswordReset", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordResetConfirmDto_Output" + } + } + } + }, + "responses": { + "201": { + "description": "Пароль успешно изменён.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Сессия восстановления не найдена или истекла", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Сессия восстановления не найдена или истекла", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Не удалось обновить пароль. Попробуйте позже.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "PASSWORD_UPDATE_FAILED", + "message": "Не удалось обновить пароль. Попробуйте позже.", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.199Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Установка нового пароля после сброса", + "tags": ["Auth"] + } + }, + "/api/v1/users/me": { + "get": { + "description": "Возвращает полную структуру профиля, включая вложенные объекты безопасности и настроек.", + "operationId": "UserController_getProfile", + "parameters": [], + "responses": { + "200": { + "description": "Данные профиля успешно получены.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse_Output" + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.751Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.758Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Получить профиль текущего пользователя", + "tags": ["Users"] + }, + "patch": { + "description": "Позволяет точечно обновить имя, bio, часовой пояс и язык интерфейса.", + "operationId": "UserController_updateProfile", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProfileDto_Output" + } + } + } + }, + "responses": { + "200": { + "description": "Профиль успешно обновлен.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Ошибка валидации (например, слишком короткое имя)", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "VALIDATION_FAILED", + "message": "Ошибка валидации (например, слишком короткое имя)", + "retryable": false, + "details": [ + { + "field": "fullName", + "message": "Строка должна содержать минимум 2 символа", + "code": "too_small" + } + ], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.755Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.755Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.758Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Обновить данные профиля", + "tags": ["Users"] + } + }, + "/api/v1/users/me/notifications": { + "patch": { + "description": "Частичное обновление настроек email и push уведомлений.", + "operationId": "UserController_updateNotifications", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNotificationsDto_Output" + } + } + } + }, + "responses": { + "200": { + "description": "Настройки успешно сохранены.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Некорректный формат настроек", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "VALIDATION_FAILED", + "message": "Некорректный формат настроек", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.756Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.757Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.758Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Обновить настройки уведомлений", + "tags": ["Users"] + } + }, + "/api/v1/users/me/activity": { + "get": { + "description": "Возвращает список последних действий пользователя (логи).", + "operationId": "UserController_getActivity", + "parameters": [ + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 9007199254740991, + "default": 1, + "type": "integer" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Количество записей для вывода (по умолчанию 20)", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "example": 15, + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Список активностей успешно получен.", + "content": { + "application/json": { + "schema": { + "example": { + "data": [ + { + "id": "clj1abc230000jk78", + "eventType": "TASK_COMPLETED", + "description": "Завершена задача \"Обновить текст лендинга\"", + "createdAt": "2026-04-10T20:00:00.000Z", + "metadata": { + "taskId": "clj1xyz990000abc1" + } + } + ], + "meta": { + "total": 45, + "page": 1, + "limit": 20, + "totalPages": 3 + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.758Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.758Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Получить ленту активности пользователя", + "tags": ["Users"] + } + }, + "/api/v1/users/me/avatar": { + "post": { + "description": "Загрузка файла изображения для профиля пользователя.", + "operationId": "UserController_uploadAvatar", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UserController_UploadAvatarBody" + } + } + } + }, + "responses": { + "201": { + "description": "Аватар успешно загружен.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Файл не передан или имеет неверный формат", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Файл не передан или имеет неверный формат", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.758Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.758Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:23.758Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Загрузить новую аватарку", + "tags": ["Users"] + } + }, + "/api/v1/teams": { + "post": { + "operationId": "TeamsController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamDto_Output" + } + } + } + }, + "responses": { + "201": { + "description": "Команда успешно создана", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Ошибка валидации входных данных", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "VALIDATION_FAILED", + "message": "Ошибка валидации входных данных", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.293Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.293Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "409": { + "description": "Команда с таким slug уже существует", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "CONFLICT", + "message": "Команда с таким slug уже существует", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.293Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Создать новую команду", + "tags": ["Teams"] + } + }, + "/api/v1/teams/check-slug/{slug}": { + "get": { + "description": "Проверяет, свободен ли уникальный адрес команды для использования.", + "operationId": "TeamsController_checkSlug", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Желаемый слаг команды", + "schema": { + "example": "my-super-team", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Результат проверки доступности", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckSlugResponse_Output" + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.294Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Проверить доступность слага", + "tags": ["Teams"] + } + }, + "/api/v1/teams/my": { + "get": { + "description": "Возвращает все команды, в которых текущий пользователь является участником или владельцем.", + "operationId": "TeamsController_findAll", + "parameters": [], + "responses": { + "200": { + "description": "Список команд получен", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserTeamResponse_Output" + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.294Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Получить список команд пользователя", + "tags": ["Teams"] + } + }, + "/api/v1/teams/my/invites": { + "get": { + "description": "Возвращает все активные приглашения в команды, отправленные на email текущего пользователя.", + "operationId": "TeamsController_findAllInvites", + "parameters": [], + "responses": { + "200": { + "description": "Список приглашений успешно получен", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserInviteResponse_Output" + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.294Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Получить список входящих приглашений", + "tags": ["Teams"] + } + }, + "/api/v1/teams/{slug}": { + "get": { + "operationId": "TeamsController_findOne", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Уникальный идентификатор (слаг) команды", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Данные команды получены", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.294Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Команда не найдена", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Команда не найдена", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.294Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Получить детальную информацию о команде по slug", + "tags": ["Teams"] + }, + "patch": { + "operationId": "TeamsController_update", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Слаг команды для редактирования", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamDto_Output" + } + } + } + }, + "responses": { + "200": { + "description": "Команда успешно обновлена", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Ошибка валидации входных данных", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "VALIDATION_FAILED", + "message": "Ошибка валидации входных данных", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.295Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.295Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.295Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Ресурс не найден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Ресурс не найден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.295Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Обновить данные команды", + "tags": ["Teams"] + }, + "delete": { + "operationId": "TeamsController_remove", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Слаг команды для удаления", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Команда успешно удалена", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.295Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.295Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Ресурс не найден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Ресурс не найден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.295Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Удалить команду", + "tags": ["Teams"] + } + }, + "/api/v1/teams/{slug}/tags": { + "put": { + "operationId": "TeamsController_syncTags", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncTagsDto_Output" + } + } + } + }, + "responses": { + "200": { + "description": "Теги обновлены", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.296Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.296Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Ресурс не найден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Ресурс не найден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.296Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Синхронизировать теги команды", + "tags": ["Teams"] + } + }, + "/api/v1/teams/{slug}/avatar": { + "patch": { + "description": "Загрузка файла изображения для профиля команды.", + "operationId": "TeamsController_updateTeamAvatar", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/TeamsController_UpdateTeamAvatarBody" + } + } + } + }, + "responses": { + "200": { + "description": "Аватар команды успешно обновлен.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadResponse_Output" + } + } + } + }, + "400": { + "description": "Файл не передан или имеет неверный формат", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Файл не передан или имеет неверный формат", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.296Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.296Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.296Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Команда не найдена", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Команда не найдена", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.296Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Обновить аватар команды", + "tags": ["Teams"] + } + }, + "/api/v1/teams/{slug}/banner": { + "patch": { + "description": "Загрузка файла изображения для обложки (баннера) команды.", + "operationId": "TeamsController_updateTeamBanner", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/TeamsController_UpdateTeamBannerBody" + } + } + } + }, + "responses": { + "200": { + "description": "Баннер команды успешно обновлен.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadResponse_Output" + } + } + } + }, + "400": { + "description": "Файл не передан или имеет неверный форм��т", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Файл не передан или имеет неверный форм��т", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Команда не найдена", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Команда не найдена", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Обновить баннер команды", + "tags": ["Teams"] + } + }, + "/api/v1/teams/{slug}/members": { + "get": { + "operationId": "MembersController_getMembers", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Слаг команды", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Список участников получен", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamMemberResponse_Output" + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.297Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.299Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Получить список всех участников команды", + "tags": ["Teams"] + } + }, + "/api/v1/teams/{slug}/invitations": { + "post": { + "description": "Создает запись об участнике со статусом \"pending\". Если пользователь уже зарегистрирован — он увидит приглашение в разделе \"my/invites\". Если нет — ему уйдет письмо на указанный Email.", + "operationId": "MembersController_invite", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Слаг команды, в которую приглашаем", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InviteMemberDto_Output" + } + } + } + }, + "responses": { + "201": { + "description": "Инвайт создан и отправлен", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Некорректный формат Email или роль не поддерживается", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "VALIDATION_FAILED", + "message": "Некорректный формат Email или роль не поддерживается", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.299Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Пригласить пользователя в команду по Email", + "tags": ["Teams"] + } + }, + "/api/v1/teams/{slug}/invitations/{code}/accept": { + "post": { + "description": "Активирует участие пользователя в команде по уникальному коду приглашения. После успешного принятия статус участника меняется с \"pending\" на \"active\". Система автоматически связывает текущего авторизованного пользователя с инвайтом через Email.", + "operationId": "MembersController_accept", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Слаг команды", + "schema": { + "type": "string" + } + }, + { + "name": "code", + "required": true, + "in": "path", + "description": "Уникальный код/токен приглашения (из ссылки или письма)", + "schema": { + "example": "7df1-4a2b-9e8c", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Приглашение успешно принято. Пользователь теперь участник команды.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "400": { + "description": "Невалидный код, срок действия приглашения истек или оно уже использовано", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "BAD_REQUEST", + "message": "Невалидный код, срок действия приглашения истек или оно уже использовано", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Приглашение с таким кодом не найдено", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Приглашение с таким кодом не найдено", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "409": { + "description": "Пользователь уже является участником этой команды", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "CONFLICT", + "message": "Пользователь уже является участником этой команды", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.299Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Принять приглашение в команду", + "tags": ["Teams"] + } + }, + "/api/v1/teams/{slug}/members/{userId}": { + "patch": { + "description": "Позволяет изменить роль участника (member -> admin) или вручную изменить его статус. Владелец команды (Owner) не может понизить свою роль через этот эндпоинт.", + "operationId": "MembersController_updateMember", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Слаг команды", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "description": "ID пользователя, чьи права редактируются", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMemberDto_Output" + } + } + } + }, + "responses": { + "200": { + "description": "Данные участника обновлены", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Участник или команда не найдены", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Участник или команда не найдены", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.298Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.299Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Изменить роль или статус участника", + "tags": ["Teams"] + }, + "delete": { + "operationId": "MembersController_removeMember", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Слаг команды", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "description": "ID пользователя", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Участник успешно удален", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActionResponse_Output" + } + } + } + }, + "401": { + "description": "Сессия истекла или токен не валиден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "AUTH_REQUIRED", + "message": "Сессия истекла или токен не валиден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.299Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "403": { + "description": "У вас недостаточно прав для этого действия", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "ACCESS_DENIED", + "message": "У вас недостаточно прав для этого действия", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.299Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "404": { + "description": "Ресурс не найден", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "NOT_FOUND", + "message": "Ресурс не найден", + "retryable": false, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.299Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + }, + "500": { + "description": "Произошла критическая ошибка на стороне сервера", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/GlobalErrorResponse_Output" + } + ], + "example": { + "code": "INTERNAL_SERVER_ERROR", + "message": "Произошла критическая ошибка на стороне сервера", + "retryable": true, + "details": [], + "meta": { + "requestId": "req-clj1abc230000jk78", + "timestamp": "2026-04-15T17:35:24.299Z", + "path": "/api/v1/...", + "method": "POST", + "service": "main-backend" + } + } + } + } + } + } + }, + "summary": "Удалить участника из команды", + "tags": ["Teams"] + } + }, + "/api/v1/health": { + "get": { + "description": "Используется внешними системами для проверки доступности сервиса.", + "operationId": "HealthController_checkHealth", + "parameters": [], + "responses": { + "200": { + "description": "Сервис работает нормально", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "503": { + "description": "Сервис недоступен или критическая ошибка" + } + }, + "summary": "Краткий статус (Health Check)", + "tags": ["System"] + } + }, + "/api/v1/ping": { + "get": { + "description": "Возвращает аптайм, время старта и метрики памяти.", + "operationId": "HealthController_ping", + "parameters": [], + "responses": { + "200": { + "description": "Полная статистика сервиса", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse_Output" + } + } + } + } + }, + "summary": "Детальный дамп состояния", + "tags": ["System"] + } + } + }, + "info": { + "title": "Task Tracker API", + "description": "API бэкенда таск-трекера", + "version": "0.1.0", + "contact": {} + }, + "tags": [], + "servers": [ + { + "url": "http://localhost:3000", + "description": "Local" + } + ], + "components": { + "securitySchemes": { + "bearer": { + "scheme": "bearer", + "bearerFormat": "JWT", + "type": "http" + } + }, + "schemas": { + "SignUpDto_Output": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email пользователя" + }, + "password": { + "type": "string", + "minLength": 8, + "maxLength": 32, + "description": "Пароль (минимум 8 символов)" + }, + "firstName": { + "type": "string", + "minLength": 2, + "maxLength": 50, + "description": "Имя" + }, + "lastName": { + "type": "string", + "minLength": 2, + "maxLength": 50, + "description": "Фамилия" + }, + "middleName": { + "anyOf": [ + { + "type": "string", + "maxLength": 50 + }, + { + "type": "string", + "enum": [""] + } + ], + "description": "Отчество (опционально)" + } + }, + "required": ["email", "password", "firstName", "lastName"], + "description": "Схема регистрации пользователя", + "additionalProperties": false + }, + "ActionResponse_Output": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Статус операции" + }, + "message": { + "type": "string", + "description": "Сообщение для пользователя" + } + }, + "required": ["success"], + "additionalProperties": false + }, + "RefreshTokenResponse_Output": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Успешное обновление токенов" + }, + "token": { + "type": "string", + "description": "Новый access token (JWT)" + }, + "message": { + "type": "string", + "description": "Дополнительное сообщение (опционально)" + } + }, + "required": ["success", "token"], + "additionalProperties": false, + "description": "Ответ при обновлении пары access/refresh токенов" + }, + "UserController_UploadAvatarBody": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "Файл изображения аватара" + } + }, + "description": "Тело запроса загрузки аватара пользователя (multipart/form-data)" + }, + "TeamsController_UpdateTeamAvatarBody": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "Файл изображения аватара команды" + } + }, + "description": "Тело запроса обновления аватара команды (multipart/form-data)" + }, + "TeamsController_UpdateTeamBannerBody": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "Файл изображения баннера команды" + } + }, + "description": "Тело запроса обновления баннера команды (multipart/form-data)" + }, + "VerifyDto_Output": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email пользователя, на который был отправлен код" + }, + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6, + "description": "6-значный OTP код подтверждения" + } + }, + "required": ["email", "code"], + "description": "Схема верификации OTP кода", + "additionalProperties": false + }, + "SignInDto_Output": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email пользователя" + }, + "password": { + "type": "string", + "description": "Пароль пользователя" + } + }, + "required": ["email", "password"], + "description": "Схема входа в систему", + "additionalProperties": false + }, + "ResetPasswordDto_Output": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email для восстановления" + } + }, + "required": ["email"], + "additionalProperties": false + }, + "VerifyResetCodeDto_Output": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + }, + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6, + "description": "Код из письма" + } + }, + "required": ["email", "code"], + "additionalProperties": false + }, + "PasswordResetConfirmDto_Output": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + }, + "password": { + "type": "string", + "minLength": 8, + "maxLength": 32, + "description": "Новый пароль" + }, + "confirmPassword": { + "type": "string", + "description": "Повторите новый пароль" + } + }, + "required": ["email", "password", "confirmPassword"], + "additionalProperties": false + }, + "UserResponse_Output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Уникальный идентификатор (CUID/UUID)" + }, + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Электронная почта" + }, + "profile": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "Имя пользователя" + }, + "lastName": { + "type": "string", + "description": "Фамилия" + }, + "middleName": { + "type": "string", + "description": "Отчество", + "nullable": true + }, + "bio": { + "type": "string", + "description": "О себе", + "nullable": true + }, + "avatarUrl": { + "type": "string", + "format": "uri", + "description": "Ссылка на аватар в S3", + "nullable": true + }, + "timezone": { + "type": "string", + "description": "Временная зона" + }, + "language": { + "type": "string", + "description": "Язык интерфейса" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Дата регистрации" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Дата последнего обновления профиля" + } + }, + "required": [ + "firstName", + "lastName", + "middleName", + "bio", + "avatarUrl", + "timezone", + "language", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + }, + "security": { + "type": "object", + "properties": { + "is2faEnabled": { + "type": "boolean", + "description": "Статус двухфакторной аутентификации" + }, + "lastPasswordChange": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Дата последнего изменения пароля" + } + }, + "required": ["is2faEnabled", "lastPasswordChange"], + "additionalProperties": false, + "description": "Данные безопасности аккаунта" + }, + "notifications": { + "type": "object", + "properties": { + "email": { + "type": "object", + "properties": { + "task_assigned": { + "type": "boolean", + "description": "Уведомление на п��чту при назначении задачи" + }, + "mentions": { + "type": "boolean", + "description": "Уведомление на почту при упоминании в комментариях" + }, + "daily_summary": { + "type": "boolean", + "description": "Ежедневная сводка задач на почту" + } + }, + "required": ["task_assigned", "mentions", "daily_summary"], + "additionalProperties": false + }, + "push": { + "type": "object", + "properties": { + "task_assigned": { + "type": "boolean", + "description": "Push-уведомление при назначении задачи" + }, + "reminders": { + "type": "boolean", + "description": "Push-уведомления о дедлайнах" + } + }, + "required": ["task_assigned", "reminders"], + "additionalProperties": false + } + }, + "required": ["email", "push"], + "additionalProperties": false, + "description": "Настройки уведомлений пользователя" + } + }, + "required": ["id", "email", "profile", "security", "notifications"], + "additionalProperties": false + }, + "UpdateProfileDto_Output": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "lastName": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "middleName": { + "type": "string", + "maxLength": 50, + "nullable": true + }, + "bio": { + "type": "string", + "maxLength": 1000, + "nullable": true + }, + "timezone": { + "type": "string", + "maxLength": 50 + }, + "language": { + "type": "string", + "minLength": 2, + "maxLength": 2 + } + }, + "description": "Схема для частичного обновления данных профиля", + "additionalProperties": false + }, + "UpdateNotificationsDto_Output": { + "type": "object", + "properties": { + "email": { + "type": "object", + "properties": { + "task_assigned": { + "type": "boolean", + "description": "Уведомление на п��чту при назначении задачи" + }, + "mentions": { + "type": "boolean", + "description": "Уведомление на почту при упоминании в комментариях" + }, + "daily_summary": { + "type": "boolean", + "description": "Ежедневная сводка задач на почту" + } + }, + "required": ["task_assigned", "mentions", "daily_summary"], + "additionalProperties": false + }, + "push": { + "type": "object", + "properties": { + "task_assigned": { + "type": "boolean", + "description": "Push-уведомление при назначении задачи" + }, + "reminders": { + "type": "boolean", + "description": "Push-уведомления о дедлайнах" + } + }, + "required": ["task_assigned", "reminders"], + "additionalProperties": false + } + }, + "description": "Схема для частичного обновления настроек уведомлений", + "additionalProperties": false + }, + "CreateTeamDto_Output": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100, + "description": "Название команды, отображаемое в интерфейсе" + }, + "description": { + "type": "string", + "description": "Краткое описание деятельнос��и или целей команды", + "maxLength": 500 + }, + "slug": { + "type": "string", + "description": "Уникальная ссылка на изображение команду" + }, + "tags": { + "type": "array", + "description": "Список строковых названий тегов для классификации", + "items": { + "type": "string" + } + } + }, + "required": ["name"], + "additionalProperties": false + }, + "CheckSlugResponse_Output": { + "type": "object", + "properties": { + "available": { + "type": "boolean", + "description": "Флаг доступности: true — адрес свободен, false — уже занят или некорректен" + } + }, + "required": ["available"], + "additionalProperties": false + }, + "UserTeamResponse_Output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "description": "Уникальный ID команды" + }, + "name": { + "type": "string", + "description": "Название команды" + }, + "slug": { + "type": "string", + "description": "Уникальный URL-путь команды" + }, + "description": { + "type": "string", + "description": "Краткое описание команды", + "nullable": true + }, + "avatarUrl": { + "type": "string", + "description": "URL изображения профиля команды", + "nullable": true + }, + "role": { + "type": "string", + "description": "Системное название роли пользователя" + }, + "joinedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Дата, когда пользователь вступил в команду" + }, + "permissions": { + "type": "object", + "properties": { + "canEdit": { + "type": "boolean", + "description": "Разрешено ли редактировать настройки и профиль команды" + }, + "canDelete": { + "type": "boolean", + "description": "Разрешено ли полностью удалить команду (только для владельца)" + }, + "canManageMembers": { + "type": "boolean", + "description": "Разрешено ли менять роли и исключать участников" + }, + "canInvite": { + "type": "boolean", + "description": "Разрешено ли приглашать новых участников" + }, + "isOwner": { + "type": "boolean", + "description": "Является ли текущий пользователь владельцем (Owner)" + } + }, + "required": ["canEdit", "canDelete", "canManageMembers", "canInvite", "isOwner"], + "additionalProperties": false, + "description": "Объект прав доступа текущего пользователя" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "avatarUrl", + "role", + "joinedAt", + "permissions" + ], + "additionalProperties": false + }, + "UserInviteResponse_Output": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Код инвайта" + }, + "teamName": { + "type": "string", + "description": "Название команды" + }, + "teamAvatar": { + "type": "string", + "description": "Аватар команды", + "nullable": true + }, + "role": { + "type": "string", + "description": "Роль" + }, + "inviterName": { + "type": "string", + "description": "Имя пригласившего" + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Дата истечения" + } + }, + "required": ["code", "teamName", "teamAvatar", "role", "inviterName", "expiresAt"], + "additionalProperties": false + }, + "UpdateTeamDto_Output": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100, + "description": "Название команды, отображаемое в интерфейсе" + }, + "description": { + "type": "string", + "description": "Краткое описание деятельнос��и или целей команды", + "maxLength": 500 + }, + "slug": { + "type": "string", + "description": "Уникальная ссылка на изображение команду" + }, + "tags": { + "type": "array", + "description": "Список строковых названий тегов для классификации", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "SyncTagsDto_Output": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "minItems": 1, + "maxItems": 15, + "items": { + "type": "string" + }, + "description": "Массив названий тегов для привязки к команде. Если тега нет в базе, он будет создан." + } + }, + "required": ["tags"], + "additionalProperties": false + }, + "FileUploadResponse_Output": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Статус операции" + }, + "url": { + "type": "string", + "description": "URL загруженного файла" + }, + "message": { + "type": "string", + "description": "Сообщение для пользователя" + } + }, + "required": ["success", "url"], + "additionalProperties": false + }, + "TeamMemberResponse_Output": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Уникальный ID пользователя (UUID или ULID)" + }, + "role": { + "type": "string", + "enum": ["owner", "admin", "member"], + "description": "Роль участника в рамках конкретной команды" + }, + "status": { + "type": "string", + "enum": ["active", "pending", "blocked"], + "description": "Текущий статус членства (активен, ожидает приглашения, заблокирован)" + }, + "fullName": { + "type": "string", + "description": "Полное имя для отображения (Фамилия Имя Отчество)" + }, + "firstName": { + "type": "string", + "description": "Имя пользователя" + }, + "lastName": { + "type": "string", + "description": "Фамилия пользователя" + }, + "avatarUrl": { + "type": "string", + "format": "uri", + "description": "Прямая ссылка на изображение профиля или null, если не задано", + "nullable": true + }, + "initials": { + "type": "string", + "maxLength": 2, + "description": "Две буквы для аватара-заглушки (например, \"ИИ\")" + }, + "joinedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Дата и время вступления в команду в формате ISO 8601" + } + }, + "required": [ + "id", + "role", + "status", + "fullName", + "firstName", + "lastName", + "avatarUrl", + "initials", + "joinedAt" + ], + "additionalProperties": false + }, + "InviteMemberDto_Output": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "Email пользователя, которого нужно пригласить" + }, + "role": { + "type": "string", + "default": "member", + "description": "Роль, которая будет назначена пользователю после принятия инвайта" + } + }, + "required": ["email", "role"], + "additionalProperties": false + }, + "UpdateMemberDto_Output": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "Новая роль участника" + }, + "status": { + "type": "string", + "description": "Новый статус (active, blocked и т.д.)" + } + }, + "additionalProperties": false + }, + "HealthResponse_Output": { + "type": "object", + "properties": { + "service": { + "type": "string", + "description": "Название сервиса" + }, + "status": { + "type": "string", + "enum": ["up", "down"], + "description": "Текущий статус" + }, + "info": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "Версия приложения" + }, + "node": { + "type": "string", + "description": "Версия Node.js" + }, + "pid": { + "type": "number", + "description": "ID процесса" + } + }, + "required": ["version", "node", "pid"], + "additionalProperties": false + }, + "time": { + "type": "object", + "properties": { + "now": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Текущее время сервера" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Время старта сервера" + }, + "uptime": { + "type": "string", + "description": "Аптайм в формате ч/м/с" + }, + "uptimeSeconds": { + "type": "number", + "description": "Аптайм в секундах" + } + }, + "required": ["now", "startedAt", "uptime", "uptimeSeconds"], + "additionalProperties": false + }, + "metrics": { + "type": "object", + "properties": { + "rss": { + "type": "string", + "description": "Resident Set Size (общая память)" + }, + "heapUsed": { + "type": "string", + "description": "Использованная память в куче" + }, + "loadAverage": { + "type": "string", + "description": "Средняя нагрузка на CPU" + } + }, + "required": ["rss", "heapUsed", "loadAverage"], + "additionalProperties": false + } + }, + "required": ["service", "status", "info", "time", "metrics"], + "additionalProperties": false + }, + "GlobalErrorResponse_Output": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Уникальный бизнес-код ошибки (например, \"INSUFFICIENT_FUNDS\", \"TEAM_NOT_FOUND\")" + }, + "message": { + "type": "string", + "description": "Краткое описание ошибки для пользователя или разработчика" + }, + "retryable": { + "type": "boolean", + "description": "Флаг, указывающий клиенту, есть ли смысл повторять запрос без изменений (например, при 503 или Lock Timeout)" + }, + "details": { + "type": "array", + "description": "Список ошибок валидации (заполняется только для 400 ошибок)", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Путь к полю в формате dot-notation (например, \"user.email\")" + }, + "message": { + "type": "string", + "description": "Человекочитаемое сообщение о конкретной ошибке в этом поле" + }, + "code": { + "type": "string", + "description": "Машиночитаемый код ошибки валидации (например, \"invalid_email\", \"too_short\")" + } + }, + "required": ["field", "message", "code"], + "additionalProperties": false, + "description": "Детальная информация о конкретном нарушении в запросе" + } + }, + "meta": { + "type": "object", + "properties": { + "requestId": { + "type": "string", + "description": "Уникальный ID запроса (Trace ID). Используется для поиска логов в Sentry/ELK/Kibana" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Точное время возникновения ошибки в формате ISO 8601" + }, + "path": { + "type": "string", + "description": "URL-путь эндпоинта, который вернул ошибку" + }, + "method": { + "type": "string", + "description": "HTTP метод запроса (GET, POST, etc.)" + }, + "service": { + "description": "Имя микросервиса, в котором произошел сбой (полезно для будущего масштабирования)", + "type": "string" + } + }, + "required": ["requestId", "timestamp", "path", "method"], + "additionalProperties": false, + "description": "Техническая мета-информация для мониторинга и отладки" + } + }, + "required": ["code", "message", "retryable", "meta"], + "additionalProperties": false + } + } + } +} diff --git a/src/shared/api/openapi/pull-openapi.mjs b/src/shared/api/openapi/pull-openapi.mjs new file mode 100644 index 0000000..bd999df --- /dev/null +++ b/src/shared/api/openapi/pull-openapi.mjs @@ -0,0 +1,52 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import dotenv from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +//.env должен быть в директории из которой запускается процесс +dotenv.config(); + +if (!process.env.OPENAPI_URL) { + throw new Error('OPENAPI_URL is not set'); +} + +const OPENAPI_URL = process.env.OPENAPI_URL; + +const OUT = path.resolve(__dirname, 'openapi.json'); + +/** + * OpenAPI 3.0 не принимают `const` в части схем — заменяем на `enum: [value]`. + */ +function replaceConstWithEnum(node) { + if (node === null || typeof node !== 'object') return; + if (Array.isArray(node)) { + for (const item of node) replaceConstWithEnum(item); + return; + } + for (const key of Object.keys(node)) { + if (key === 'const') continue; + replaceConstWithEnum(node[key]); + } + if (Object.prototype.hasOwnProperty.call(node, 'const')) { + if (!Object.prototype.hasOwnProperty.call(node, 'enum')) { + node.enum = [node.const]; + } + delete node.const; + } +} + +const res = await fetch(OPENAPI_URL); + +if (!res.ok) { + throw new Error(`OpenAPI fetch failed: ${res.status} ${res.statusText} (${OPENAPI_URL})`); +} + +const spec = await res.json(); + +replaceConstWithEnum(spec); +fs.writeFileSync(OUT, `${JSON.stringify(spec, null, 2)}\n`, 'utf8'); +console.info(`Wrote ${path.relative(process.cwd(), OUT)}`); diff --git a/src/shared/api/query-client.ts b/src/shared/api/query-client.ts new file mode 100644 index 0000000..f3c724a --- /dev/null +++ b/src/shared/api/query-client.ts @@ -0,0 +1,67 @@ +import { isAxiosError } from 'axios'; +import { AxiosValidationError, GlobalErrorResponseType } from 'shared/api/validation'; +import { toast } from 'sonner'; +import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query'; + +interface AppQueryMeta extends Record { + skipGlobalErrorToast?: boolean; + skipGlobalValidationToast?: boolean; +} + +declare module '@tanstack/react-query' { + interface Register { + queryMeta: AppQueryMeta; + mutationMeta: AppQueryMeta; + } +} + +const SERVER_BAD_VALIDATION_CODE = 'VALIDATION_FAILED'; + +function handleValidationError(error: unknown, meta?: AppQueryMeta): boolean { + if (!(error instanceof AxiosValidationError)) return false; + if (error.code !== AxiosValidationError.ERR_BAD_VALIDATION) return false; + if (meta?.skipGlobalValidationToast) return true; + + toast.error(error.message, { description: error.issues?.[0]?.message }); + return true; +} + +function handleServerError(error: unknown, meta?: AppQueryMeta): boolean { + if (!isAxiosError(error)) return false; + + const data = error.response?.data; + + if (!data) return false; + + if (data.error.code === SERVER_BAD_VALIDATION_CODE && meta?.skipGlobalValidationToast) + return true; + + if (meta?.skipGlobalErrorToast) return true; + + toast.error(data.error.message, { description: data.details?.[0]?.message }); + return true; +} + +function handleGlobalError(error: unknown, meta: AppQueryMeta | undefined, title: string): void { + if (meta?.skipGlobalErrorToast) return; + + const message = error instanceof Error ? error.message : 'Неизвестная ошибка'; + toast.error(title, { description: message }); +} + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error, query) => { + if (handleValidationError(error, query.meta)) return; + if (handleServerError(error, query.meta)) return; + handleGlobalError(error, query.meta, 'Ошибка загрузки данных'); + }, + }), + mutationCache: new MutationCache({ + onError: (error, _variables, _context, mutation) => { + if (handleValidationError(error, mutation.meta)) return; + if (handleServerError(error, mutation.meta)) return; + handleGlobalError(error, mutation.meta, 'Ошибка выполнения операции'); + }, + }), +}); diff --git a/src/shared/api/schemas/actionResponseOutput.zod.ts b/src/shared/api/schemas/actionResponseOutput.zod.ts new file mode 100644 index 0000000..80492f0 --- /dev/null +++ b/src/shared/api/schemas/actionResponseOutput.zod.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const ActionResponseOutput = zod.object({ + success: zod.boolean().describe('Статус операции'), + message: zod.string().optional().describe('Сообщение для пользователя'), +}); + +export type ActionResponseOutput = zod.input; +export type ActionResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/authControllerConfirmPasswordResetBody.zod.ts b/src/shared/api/schemas/authControllerConfirmPasswordResetBody.zod.ts new file mode 100644 index 0000000..6d77c32 --- /dev/null +++ b/src/shared/api/schemas/authControllerConfirmPasswordResetBody.zod.ts @@ -0,0 +1,31 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const authControllerConfirmPasswordResetBodyEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const authControllerConfirmPasswordResetBodyPasswordMin = 8; +export const authControllerConfirmPasswordResetBodyPasswordMax = 32; + +export const AuthControllerConfirmPasswordResetBody = zod.object({ + email: zod.email().regex(authControllerConfirmPasswordResetBodyEmailRegExp), + password: zod + .string() + .min(authControllerConfirmPasswordResetBodyPasswordMin) + .max(authControllerConfirmPasswordResetBodyPasswordMax) + .describe('Новый пароль'), + confirmPassword: zod.string().describe('Повторите новый пароль'), +}); + +export type AuthControllerConfirmPasswordResetBody = zod.input< + typeof AuthControllerConfirmPasswordResetBody +>; +export type AuthControllerConfirmPasswordResetBodyOutput = zod.output< + typeof AuthControllerConfirmPasswordResetBody +>; diff --git a/src/shared/api/schemas/authControllerResetPasswordRequestBody.zod.ts b/src/shared/api/schemas/authControllerResetPasswordRequestBody.zod.ts new file mode 100644 index 0000000..c5e1560 --- /dev/null +++ b/src/shared/api/schemas/authControllerResetPasswordRequestBody.zod.ts @@ -0,0 +1,26 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const authControllerResetPasswordRequestBodyEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); + +export const AuthControllerResetPasswordRequestBody = zod.object({ + email: zod + .email() + .regex(authControllerResetPasswordRequestBodyEmailRegExp) + .describe('Email для восстановления'), +}); + +export type AuthControllerResetPasswordRequestBody = zod.input< + typeof AuthControllerResetPasswordRequestBody +>; +export type AuthControllerResetPasswordRequestBodyOutput = zod.output< + typeof AuthControllerResetPasswordRequestBody +>; diff --git a/src/shared/api/schemas/authControllerSignInBody.zod.ts b/src/shared/api/schemas/authControllerSignInBody.zod.ts new file mode 100644 index 0000000..8c5fa61 --- /dev/null +++ b/src/shared/api/schemas/authControllerSignInBody.zod.ts @@ -0,0 +1,22 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const authControllerSignInBodyEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); + +export const AuthControllerSignInBody = zod + .object({ + email: zod.email().regex(authControllerSignInBodyEmailRegExp).describe('Email пользователя'), + password: zod.string().describe('Пароль пользователя'), + }) + .describe('Схема входа в систему'); + +export type AuthControllerSignInBody = zod.input; +export type AuthControllerSignInBodyOutput = zod.output; diff --git a/src/shared/api/schemas/authControllerSignUpBody.zod.ts b/src/shared/api/schemas/authControllerSignUpBody.zod.ts new file mode 100644 index 0000000..0d6c900 --- /dev/null +++ b/src/shared/api/schemas/authControllerSignUpBody.zod.ts @@ -0,0 +1,50 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const authControllerSignUpBodyEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const authControllerSignUpBodyPasswordMin = 8; +export const authControllerSignUpBodyPasswordMax = 32; + +export const authControllerSignUpBodyFirstNameMin = 2; +export const authControllerSignUpBodyFirstNameMax = 50; + +export const authControllerSignUpBodyLastNameMin = 2; +export const authControllerSignUpBodyLastNameMax = 50; + +export const authControllerSignUpBodyMiddleNameOneMax = 50; + +export const AuthControllerSignUpBody = zod + .object({ + email: zod.email().regex(authControllerSignUpBodyEmailRegExp).describe('Email пользователя'), + password: zod + .string() + .min(authControllerSignUpBodyPasswordMin) + .max(authControllerSignUpBodyPasswordMax) + .describe('Пароль (минимум 8 символов)'), + firstName: zod + .string() + .min(authControllerSignUpBodyFirstNameMin) + .max(authControllerSignUpBodyFirstNameMax) + .describe('Имя'), + lastName: zod + .string() + .min(authControllerSignUpBodyLastNameMin) + .max(authControllerSignUpBodyLastNameMax) + .describe('Фамилия'), + middleName: zod + .union([zod.string().max(authControllerSignUpBodyMiddleNameOneMax), zod.enum([''])]) + .optional() + .describe('Отчество (опционально)'), + }) + .describe('Схема регистрации пользователя'); + +export type AuthControllerSignUpBody = zod.input; +export type AuthControllerSignUpBodyOutput = zod.output; diff --git a/src/shared/api/schemas/authControllerVerifyBody.zod.ts b/src/shared/api/schemas/authControllerVerifyBody.zod.ts new file mode 100644 index 0000000..c1fb4a6 --- /dev/null +++ b/src/shared/api/schemas/authControllerVerifyBody.zod.ts @@ -0,0 +1,31 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const authControllerVerifyBodyEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const authControllerVerifyBodyCodeMin = 6; +export const authControllerVerifyBodyCodeMax = 6; + +export const AuthControllerVerifyBody = zod + .object({ + email: zod + .email() + .regex(authControllerVerifyBodyEmailRegExp) + .describe('Email пользователя, на который был отправлен код'), + code: zod + .string() + .min(authControllerVerifyBodyCodeMin) + .max(authControllerVerifyBodyCodeMax) + .describe('6-значный OTP код подтверждения'), + }) + .describe('Схема верификации OTP кода'); + +export type AuthControllerVerifyBody = zod.input; +export type AuthControllerVerifyBodyOutput = zod.output; diff --git a/src/shared/api/schemas/authControllerVerifyResetCodeBody.zod.ts b/src/shared/api/schemas/authControllerVerifyResetCodeBody.zod.ts new file mode 100644 index 0000000..595d76d --- /dev/null +++ b/src/shared/api/schemas/authControllerVerifyResetCodeBody.zod.ts @@ -0,0 +1,28 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const authControllerVerifyResetCodeBodyEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const authControllerVerifyResetCodeBodyCodeMin = 6; +export const authControllerVerifyResetCodeBodyCodeMax = 6; + +export const AuthControllerVerifyResetCodeBody = zod.object({ + email: zod.email().regex(authControllerVerifyResetCodeBodyEmailRegExp), + code: zod + .string() + .min(authControllerVerifyResetCodeBodyCodeMin) + .max(authControllerVerifyResetCodeBodyCodeMax) + .describe('Код из письма'), +}); + +export type AuthControllerVerifyResetCodeBody = zod.input; +export type AuthControllerVerifyResetCodeBodyOutput = zod.output< + typeof AuthControllerVerifyResetCodeBody +>; diff --git a/src/shared/api/schemas/checkSlugResponseOutput.zod.ts b/src/shared/api/schemas/checkSlugResponseOutput.zod.ts new file mode 100644 index 0000000..33aebfa --- /dev/null +++ b/src/shared/api/schemas/checkSlugResponseOutput.zod.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const CheckSlugResponseOutput = zod.object({ + available: zod + .boolean() + .describe('Флаг доступности: true — адрес свободен, false — уже занят или некорректен'), +}); + +export type CheckSlugResponseOutput = zod.input; +export type CheckSlugResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/createTeamDtoOutput.zod.ts b/src/shared/api/schemas/createTeamDtoOutput.zod.ts new file mode 100644 index 0000000..09f06cc --- /dev/null +++ b/src/shared/api/schemas/createTeamDtoOutput.zod.ts @@ -0,0 +1,34 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const createTeamDtoOutputNameMin = 2; +export const createTeamDtoOutputNameMax = 100; + +export const createTeamDtoOutputDescriptionMax = 500; + +export const CreateTeamDtoOutput = zod.object({ + name: zod + .string() + .min(createTeamDtoOutputNameMin) + .max(createTeamDtoOutputNameMax) + .describe('Название команды, отображаемое в интерфейсе'), + description: zod + .string() + .max(createTeamDtoOutputDescriptionMax) + .optional() + .describe('Краткое описание деятельнос��и или целей команды'), + slug: zod.string().optional().describe('Уникальная ссылка на изображение команду'), + tags: zod + .array(zod.string()) + .optional() + .describe('Список строковых названий тегов для классификации'), +}); + +export type CreateTeamDtoOutput = zod.input; +export type CreateTeamDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/fileUploadResponseOutput.zod.ts b/src/shared/api/schemas/fileUploadResponseOutput.zod.ts new file mode 100644 index 0000000..0287f72 --- /dev/null +++ b/src/shared/api/schemas/fileUploadResponseOutput.zod.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const FileUploadResponseOutput = zod.object({ + success: zod.boolean().describe('Статус операции'), + url: zod.string().describe('URL загруженного файла'), + message: zod.string().optional().describe('Сообщение для пользователя'), +}); + +export type FileUploadResponseOutput = zod.input; +export type FileUploadResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/globalErrorResponseOutput.zod.ts b/src/shared/api/schemas/globalErrorResponseOutput.zod.ts new file mode 100644 index 0000000..d5e845e --- /dev/null +++ b/src/shared/api/schemas/globalErrorResponseOutput.zod.ts @@ -0,0 +1,70 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const globalErrorResponseOutputOneMetaTimestampRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); + +export const GlobalErrorResponseOutput = zod.object({ + code: zod + .string() + .describe( + 'Уникальный бизнес-код ошибки (например, \"INSUFFICIENT_FUNDS\", \"TEAM_NOT_FOUND\")' + ), + message: zod.string().describe('Краткое описание ошибки для пользователя или разработчика'), + retryable: zod + .boolean() + .describe( + 'Флаг, указывающий клиенту, есть ли смысл повторять запрос без изменений (например, при 503 или Lock Timeout)' + ), + details: zod + .array( + zod + .object({ + field: zod + .string() + .describe('Путь к полю в формате dot-notation (например, \"user.email\")'), + message: zod + .string() + .describe('Человекочитаемое сообщение о конкретной ошибке в этом поле'), + code: zod + .string() + .describe( + 'Машиночитаемый код ошибки валидации (например, \"invalid_email\", \"too_short\")' + ), + }) + .describe('Детальная информация о конкретном нарушении в запросе') + ) + .optional() + .describe('Список ошибок валидации (заполняется только для 400 ошибок)'), + meta: zod + .object({ + requestId: zod + .string() + .describe( + 'Уникальный ID запроса (Trace ID). Используется для поиска логов в Sentry\/ELK\/Kibana' + ), + timestamp: zod.iso + .datetime({}) + .regex(globalErrorResponseOutputOneMetaTimestampRegExp) + .describe('Точное время возникновения ошибки в формате ISO 8601'), + path: zod.string().describe('URL-путь эндпоинта, который вернул ошибку'), + method: zod.string().describe('HTTP метод запроса (GET, POST, etc.)'), + service: zod + .string() + .optional() + .describe( + 'Имя микросервиса, в котором произошел сбой (полезно для будущего масштабирования)' + ), + }) + .describe('Техническая мета-информация для мониторинга и отладки'), +}); + +export type GlobalErrorResponseOutput = zod.input; +export type GlobalErrorResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/healthResponseOutput.zod.ts b/src/shared/api/schemas/healthResponseOutput.zod.ts new file mode 100644 index 0000000..66f9856 --- /dev/null +++ b/src/shared/api/schemas/healthResponseOutput.zod.ts @@ -0,0 +1,45 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const healthResponseOutputTimeNowRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); +export const healthResponseOutputTimeStartedAtRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); + +export const HealthResponseOutput = zod.object({ + service: zod.string().describe('Название сервиса'), + status: zod.enum(['up', 'down']).describe('Текущий статус'), + info: zod.object({ + version: zod.string().describe('Версия приложения'), + node: zod.string().describe('Версия Node.js'), + pid: zod.number().describe('ID процесса'), + }), + time: zod.object({ + now: zod.iso + .datetime({}) + .regex(healthResponseOutputTimeNowRegExp) + .describe('Текущее время сервера'), + startedAt: zod.iso + .datetime({}) + .regex(healthResponseOutputTimeStartedAtRegExp) + .describe('Время старта сервера'), + uptime: zod.string().describe('Аптайм в формате ч\/м\/с'), + uptimeSeconds: zod.number().describe('Аптайм в секундах'), + }), + metrics: zod.object({ + rss: zod.string().describe('Resident Set Size (общая память)'), + heapUsed: zod.string().describe('Использованная память в куче'), + loadAverage: zod.string().describe('Средняя нагрузка на CPU'), + }), +}); + +export type HealthResponseOutput = zod.input; +export type HealthResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/index.ts b/src/shared/api/schemas/index.ts new file mode 100644 index 0000000..96a3183 --- /dev/null +++ b/src/shared/api/schemas/index.ts @@ -0,0 +1,49 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ + +export * from './actionResponseOutput.zod'; +export * from './authControllerConfirmPasswordResetBody.zod'; +export * from './authControllerResetPasswordRequestBody.zod'; +export * from './authControllerSignInBody.zod'; +export * from './authControllerSignUpBody.zod'; +export * from './authControllerVerifyBody.zod'; +export * from './authControllerVerifyResetCodeBody.zod'; +export * from './checkSlugResponseOutput.zod'; +export * from './createTeamDtoOutput.zod'; +export * from './fileUploadResponseOutput.zod'; +export * from './globalErrorResponseOutput.zod'; +export * from './healthResponseOutput.zod'; +export * from './inviteMemberDtoOutput.zod'; +export * from './membersControllerInviteBody.zod'; +export * from './membersControllerUpdateMemberBody.zod'; +export * from './passwordResetConfirmDtoOutput.zod'; +export * from './refreshTokenResponseOutput.zod'; +export * from './resetPasswordDtoOutput.zod'; +export * from './signInDtoOutput.zod'; +export * from './signUpDtoOutput.zod'; +export * from './syncTagsDtoOutput.zod'; +export * from './teamMemberResponseOutput.zod'; +export * from './teamsControllerCreateBody.zod'; +export * from './teamsControllerFindOne200.zod'; +export * from './teamsControllerSyncTagsBody.zod'; +export * from './teamsControllerUpdateBody.zod'; +export * from './teamsControllerUpdateTeamAvatarBody.zod'; +export * from './teamsControllerUpdateTeamBannerBody.zod'; +export * from './updateMemberDtoOutput.zod'; +export * from './updateNotificationsDtoOutput.zod'; +export * from './updateProfileDtoOutput.zod'; +export * from './updateTeamDtoOutput.zod'; +export * from './userControllerGetActivityParams.zod'; +export * from './userControllerUpdateNotificationsBody.zod'; +export * from './userControllerUpdateProfileBody.zod'; +export * from './userControllerUploadAvatarBody.zod'; +export * from './userInviteResponseOutput.zod'; +export * from './userResponseOutput.zod'; +export * from './userTeamResponseOutput.zod'; +export * from './verifyDtoOutput.zod'; +export * from './verifyResetCodeDtoOutput.zod'; diff --git a/src/shared/api/schemas/inviteMemberDtoOutput.zod.ts b/src/shared/api/schemas/inviteMemberDtoOutput.zod.ts new file mode 100644 index 0000000..d3217fc --- /dev/null +++ b/src/shared/api/schemas/inviteMemberDtoOutput.zod.ts @@ -0,0 +1,26 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const inviteMemberDtoOutputEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const inviteMemberDtoOutputRoleDefault = `member`; +export const InviteMemberDtoOutput = zod.object({ + email: zod + .email() + .regex(inviteMemberDtoOutputEmailRegExp) + .describe('Email пользователя, которого нужно пригласить'), + role: zod + .string() + .default(inviteMemberDtoOutputRoleDefault) + .describe('Роль, которая будет назначена пользователю после принятия инвайта'), +}); + +export type InviteMemberDtoOutput = zod.input; +export type InviteMemberDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/membersControllerInviteBody.zod.ts b/src/shared/api/schemas/membersControllerInviteBody.zod.ts new file mode 100644 index 0000000..dc53e96 --- /dev/null +++ b/src/shared/api/schemas/membersControllerInviteBody.zod.ts @@ -0,0 +1,26 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const membersControllerInviteBodyEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const membersControllerInviteBodyRoleDefault = `member`; +export const MembersControllerInviteBody = zod.object({ + email: zod + .email() + .regex(membersControllerInviteBodyEmailRegExp) + .describe('Email пользователя, которого нужно пригласить'), + role: zod + .string() + .default(membersControllerInviteBodyRoleDefault) + .describe('Роль, которая будет назначена пользователю после принятия инвайта'), +}); + +export type MembersControllerInviteBody = zod.input; +export type MembersControllerInviteBodyOutput = zod.output; diff --git a/src/shared/api/schemas/membersControllerUpdateMemberBody.zod.ts b/src/shared/api/schemas/membersControllerUpdateMemberBody.zod.ts new file mode 100644 index 0000000..1630e95 --- /dev/null +++ b/src/shared/api/schemas/membersControllerUpdateMemberBody.zod.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const MembersControllerUpdateMemberBody = zod.object({ + role: zod.string().optional().describe('Новая роль участника'), + status: zod.string().optional().describe('Новый статус (active, blocked и т.д.)'), +}); + +export type MembersControllerUpdateMemberBody = zod.input; +export type MembersControllerUpdateMemberBodyOutput = zod.output< + typeof MembersControllerUpdateMemberBody +>; diff --git a/src/shared/api/schemas/passwordResetConfirmDtoOutput.zod.ts b/src/shared/api/schemas/passwordResetConfirmDtoOutput.zod.ts new file mode 100644 index 0000000..a3a76b9 --- /dev/null +++ b/src/shared/api/schemas/passwordResetConfirmDtoOutput.zod.ts @@ -0,0 +1,27 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const passwordResetConfirmDtoOutputEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const passwordResetConfirmDtoOutputPasswordMin = 8; +export const passwordResetConfirmDtoOutputPasswordMax = 32; + +export const PasswordResetConfirmDtoOutput = zod.object({ + email: zod.email().regex(passwordResetConfirmDtoOutputEmailRegExp), + password: zod + .string() + .min(passwordResetConfirmDtoOutputPasswordMin) + .max(passwordResetConfirmDtoOutputPasswordMax) + .describe('Новый пароль'), + confirmPassword: zod.string().describe('Повторите новый пароль'), +}); + +export type PasswordResetConfirmDtoOutput = zod.input; +export type PasswordResetConfirmDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/refreshTokenResponseOutput.zod.ts b/src/shared/api/schemas/refreshTokenResponseOutput.zod.ts new file mode 100644 index 0000000..148b769 --- /dev/null +++ b/src/shared/api/schemas/refreshTokenResponseOutput.zod.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const RefreshTokenResponseOutput = zod + .object({ + success: zod.boolean().describe('Успешное обновление токенов'), + token: zod.string().describe('Новый access token (JWT)'), + message: zod.string().optional().describe('Дополнительное сообщение (опционально)'), + }) + .describe('Ответ при обновлении пары access\/refresh токенов'); + +export type RefreshTokenResponseOutput = zod.input; +export type RefreshTokenResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/resetPasswordDtoOutput.zod.ts b/src/shared/api/schemas/resetPasswordDtoOutput.zod.ts new file mode 100644 index 0000000..7fd4745 --- /dev/null +++ b/src/shared/api/schemas/resetPasswordDtoOutput.zod.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const resetPasswordDtoOutputEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); + +export const ResetPasswordDtoOutput = zod.object({ + email: zod.email().regex(resetPasswordDtoOutputEmailRegExp).describe('Email для восстановления'), +}); + +export type ResetPasswordDtoOutput = zod.input; +export type ResetPasswordDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/signInDtoOutput.zod.ts b/src/shared/api/schemas/signInDtoOutput.zod.ts new file mode 100644 index 0000000..9a44eb0 --- /dev/null +++ b/src/shared/api/schemas/signInDtoOutput.zod.ts @@ -0,0 +1,22 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const signInDtoOutputEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); + +export const SignInDtoOutput = zod + .object({ + email: zod.email().regex(signInDtoOutputEmailRegExp).describe('Email пользователя'), + password: zod.string().describe('Пароль пользователя'), + }) + .describe('Схема входа в систему'); + +export type SignInDtoOutput = zod.input; +export type SignInDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/signUpDtoOutput.zod.ts b/src/shared/api/schemas/signUpDtoOutput.zod.ts new file mode 100644 index 0000000..646a4b0 --- /dev/null +++ b/src/shared/api/schemas/signUpDtoOutput.zod.ts @@ -0,0 +1,50 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const signUpDtoOutputEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const signUpDtoOutputPasswordMin = 8; +export const signUpDtoOutputPasswordMax = 32; + +export const signUpDtoOutputFirstNameMin = 2; +export const signUpDtoOutputFirstNameMax = 50; + +export const signUpDtoOutputLastNameMin = 2; +export const signUpDtoOutputLastNameMax = 50; + +export const signUpDtoOutputMiddleNameOneMax = 50; + +export const SignUpDtoOutput = zod + .object({ + email: zod.email().regex(signUpDtoOutputEmailRegExp).describe('Email пользователя'), + password: zod + .string() + .min(signUpDtoOutputPasswordMin) + .max(signUpDtoOutputPasswordMax) + .describe('Пароль (минимум 8 символов)'), + firstName: zod + .string() + .min(signUpDtoOutputFirstNameMin) + .max(signUpDtoOutputFirstNameMax) + .describe('Имя'), + lastName: zod + .string() + .min(signUpDtoOutputLastNameMin) + .max(signUpDtoOutputLastNameMax) + .describe('Фамилия'), + middleName: zod + .union([zod.string().max(signUpDtoOutputMiddleNameOneMax), zod.enum([''])]) + .optional() + .describe('Отчество (опционально)'), + }) + .describe('Схема регистрации пользователя'); + +export type SignUpDtoOutput = zod.input; +export type SignUpDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/syncTagsDtoOutput.zod.ts b/src/shared/api/schemas/syncTagsDtoOutput.zod.ts new file mode 100644 index 0000000..49ffc12 --- /dev/null +++ b/src/shared/api/schemas/syncTagsDtoOutput.zod.ts @@ -0,0 +1,23 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const syncTagsDtoOutputTagsMax = 15; + +export const SyncTagsDtoOutput = zod.object({ + tags: zod + .array(zod.string()) + .min(1) + .max(syncTagsDtoOutputTagsMax) + .describe( + 'Массив названий тегов для привязки к команде. Если тега нет в базе, он будет создан.' + ), +}); + +export type SyncTagsDtoOutput = zod.input; +export type SyncTagsDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/teamMemberResponseOutput.zod.ts b/src/shared/api/schemas/teamMemberResponseOutput.zod.ts new file mode 100644 index 0000000..428981f --- /dev/null +++ b/src/shared/api/schemas/teamMemberResponseOutput.zod.ts @@ -0,0 +1,42 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const teamMemberResponseOutputInitialsMax = 2; + +export const teamMemberResponseOutputJoinedAtRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); + +export const TeamMemberResponseOutput = zod.object({ + id: zod.string().describe('Уникальный ID пользователя (UUID или ULID)'), + role: zod + .enum(['owner', 'admin', 'member']) + .describe('Роль участника в рамках конкретной команды'), + status: zod + .enum(['active', 'pending', 'blocked']) + .describe('Текущий статус членства (активен, ожидает приглашения, заблокирован)'), + fullName: zod.string().describe('Полное имя для отображения (Фамилия Имя Отчество)'), + firstName: zod.string().describe('Имя пользователя'), + lastName: zod.string().describe('Фамилия пользователя'), + avatarUrl: zod + .url() + .nullable() + .describe('Прямая ссылка на изображение профиля или null, если не задано'), + initials: zod + .string() + .max(teamMemberResponseOutputInitialsMax) + .describe('Две буквы для аватара-заглушки (например, \"ИИ\")'), + joinedAt: zod.iso + .datetime({}) + .regex(teamMemberResponseOutputJoinedAtRegExp) + .describe('Дата и время вступления в команду в формате ISO 8601'), +}); + +export type TeamMemberResponseOutput = zod.input; +export type TeamMemberResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/teamsControllerCreateBody.zod.ts b/src/shared/api/schemas/teamsControllerCreateBody.zod.ts new file mode 100644 index 0000000..e6928df --- /dev/null +++ b/src/shared/api/schemas/teamsControllerCreateBody.zod.ts @@ -0,0 +1,34 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const teamsControllerCreateBodyNameMin = 2; +export const teamsControllerCreateBodyNameMax = 100; + +export const teamsControllerCreateBodyDescriptionMax = 500; + +export const TeamsControllerCreateBody = zod.object({ + name: zod + .string() + .min(teamsControllerCreateBodyNameMin) + .max(teamsControllerCreateBodyNameMax) + .describe('Название команды, отображаемое в интерфейсе'), + description: zod + .string() + .max(teamsControllerCreateBodyDescriptionMax) + .optional() + .describe('Краткое описание деятельнос��и или целей команды'), + slug: zod.string().optional().describe('Уникальная ссылка на изображение команду'), + tags: zod + .array(zod.string()) + .optional() + .describe('Список строковых названий тегов для классификации'), +}); + +export type TeamsControllerCreateBody = zod.input; +export type TeamsControllerCreateBodyOutput = zod.output; diff --git a/src/shared/api/schemas/teamsControllerFindOne200.zod.ts b/src/shared/api/schemas/teamsControllerFindOne200.zod.ts new file mode 100644 index 0000000..dd245af --- /dev/null +++ b/src/shared/api/schemas/teamsControllerFindOne200.zod.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const TeamsControllerFindOne200 = zod.looseObject({}); + +export type TeamsControllerFindOne200 = zod.input; +export type TeamsControllerFindOne200Output = zod.output; diff --git a/src/shared/api/schemas/teamsControllerSyncTagsBody.zod.ts b/src/shared/api/schemas/teamsControllerSyncTagsBody.zod.ts new file mode 100644 index 0000000..09c36b8 --- /dev/null +++ b/src/shared/api/schemas/teamsControllerSyncTagsBody.zod.ts @@ -0,0 +1,23 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const teamsControllerSyncTagsBodyTagsMax = 15; + +export const TeamsControllerSyncTagsBody = zod.object({ + tags: zod + .array(zod.string()) + .min(1) + .max(teamsControllerSyncTagsBodyTagsMax) + .describe( + 'Массив названий тегов для привязки к команде. Если тега нет в базе, он будет создан.' + ), +}); + +export type TeamsControllerSyncTagsBody = zod.input; +export type TeamsControllerSyncTagsBodyOutput = zod.output; diff --git a/src/shared/api/schemas/teamsControllerUpdateBody.zod.ts b/src/shared/api/schemas/teamsControllerUpdateBody.zod.ts new file mode 100644 index 0000000..48a9eee --- /dev/null +++ b/src/shared/api/schemas/teamsControllerUpdateBody.zod.ts @@ -0,0 +1,35 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const teamsControllerUpdateBodyNameMin = 2; +export const teamsControllerUpdateBodyNameMax = 100; + +export const teamsControllerUpdateBodyDescriptionMax = 500; + +export const TeamsControllerUpdateBody = zod.object({ + name: zod + .string() + .min(teamsControllerUpdateBodyNameMin) + .max(teamsControllerUpdateBodyNameMax) + .optional() + .describe('Название команды, отображаемое в интерфейсе'), + description: zod + .string() + .max(teamsControllerUpdateBodyDescriptionMax) + .optional() + .describe('Краткое описание деятельнос��и или целей команды'), + slug: zod.string().optional().describe('Уникальная ссылка на изображение команду'), + tags: zod + .array(zod.string()) + .optional() + .describe('Список строковых названий тегов для классификации'), +}); + +export type TeamsControllerUpdateBody = zod.input; +export type TeamsControllerUpdateBodyOutput = zod.output; diff --git a/src/shared/api/schemas/teamsControllerUpdateTeamAvatarBody.zod.ts b/src/shared/api/schemas/teamsControllerUpdateTeamAvatarBody.zod.ts new file mode 100644 index 0000000..71e8065 --- /dev/null +++ b/src/shared/api/schemas/teamsControllerUpdateTeamAvatarBody.zod.ts @@ -0,0 +1,21 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const TeamsControllerUpdateTeamAvatarBody = zod + .object({ + file: zod.instanceof(File).optional().describe('Файл изображения аватара команды'), + }) + .describe('Тело запроса обновления аватара команды (multipart\/form-data)'); + +export type TeamsControllerUpdateTeamAvatarBody = zod.input< + typeof TeamsControllerUpdateTeamAvatarBody +>; +export type TeamsControllerUpdateTeamAvatarBodyOutput = zod.output< + typeof TeamsControllerUpdateTeamAvatarBody +>; diff --git a/src/shared/api/schemas/teamsControllerUpdateTeamBannerBody.zod.ts b/src/shared/api/schemas/teamsControllerUpdateTeamBannerBody.zod.ts new file mode 100644 index 0000000..0a6fd86 --- /dev/null +++ b/src/shared/api/schemas/teamsControllerUpdateTeamBannerBody.zod.ts @@ -0,0 +1,21 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const TeamsControllerUpdateTeamBannerBody = zod + .object({ + file: zod.instanceof(File).optional().describe('Файл изображения баннера команды'), + }) + .describe('Тело запроса обновления баннера команды (multipart\/form-data)'); + +export type TeamsControllerUpdateTeamBannerBody = zod.input< + typeof TeamsControllerUpdateTeamBannerBody +>; +export type TeamsControllerUpdateTeamBannerBodyOutput = zod.output< + typeof TeamsControllerUpdateTeamBannerBody +>; diff --git a/src/shared/api/schemas/updateMemberDtoOutput.zod.ts b/src/shared/api/schemas/updateMemberDtoOutput.zod.ts new file mode 100644 index 0000000..ea55ce5 --- /dev/null +++ b/src/shared/api/schemas/updateMemberDtoOutput.zod.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const UpdateMemberDtoOutput = zod.object({ + role: zod.string().optional().describe('Новая роль участника'), + status: zod.string().optional().describe('Новый статус (active, blocked и т.д.)'), +}); + +export type UpdateMemberDtoOutput = zod.input; +export type UpdateMemberDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/updateNotificationsDtoOutput.zod.ts b/src/shared/api/schemas/updateNotificationsDtoOutput.zod.ts new file mode 100644 index 0000000..6d940a4 --- /dev/null +++ b/src/shared/api/schemas/updateNotificationsDtoOutput.zod.ts @@ -0,0 +1,29 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const UpdateNotificationsDtoOutput = zod + .object({ + email: zod + .object({ + task_assigned: zod.boolean().describe('Уведомление на п��чту при назначении задачи'), + mentions: zod.boolean().describe('Уведомление на почту при упоминании в комментариях'), + daily_summary: zod.boolean().describe('Ежедневная сводка задач на почту'), + }) + .optional(), + push: zod + .object({ + task_assigned: zod.boolean().describe('Push-уведомление при назначении задачи'), + reminders: zod.boolean().describe('Push-уведомления о дедлайнах'), + }) + .optional(), + }) + .describe('Схема для частичного обновления настроек уведомлений'); + +export type UpdateNotificationsDtoOutput = zod.input; +export type UpdateNotificationsDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/updateProfileDtoOutput.zod.ts b/src/shared/api/schemas/updateProfileDtoOutput.zod.ts new file mode 100644 index 0000000..9fedf66 --- /dev/null +++ b/src/shared/api/schemas/updateProfileDtoOutput.zod.ts @@ -0,0 +1,39 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const updateProfileDtoOutputFirstNameMax = 50; + +export const updateProfileDtoOutputLastNameMax = 50; + +export const updateProfileDtoOutputMiddleNameMax = 50; + +export const updateProfileDtoOutputBioMax = 1000; + +export const updateProfileDtoOutputTimezoneMax = 50; + +export const updateProfileDtoOutputLanguageMin = 2; +export const updateProfileDtoOutputLanguageMax = 2; + +export const UpdateProfileDtoOutput = zod + .object({ + firstName: zod.string().min(1).max(updateProfileDtoOutputFirstNameMax).optional(), + lastName: zod.string().min(1).max(updateProfileDtoOutputLastNameMax).optional(), + middleName: zod.string().max(updateProfileDtoOutputMiddleNameMax).nullish(), + bio: zod.string().max(updateProfileDtoOutputBioMax).nullish(), + timezone: zod.string().max(updateProfileDtoOutputTimezoneMax).optional(), + language: zod + .string() + .min(updateProfileDtoOutputLanguageMin) + .max(updateProfileDtoOutputLanguageMax) + .optional(), + }) + .describe('Схема для частичного обновления данных профиля'); + +export type UpdateProfileDtoOutput = zod.input; +export type UpdateProfileDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/updateTeamDtoOutput.zod.ts b/src/shared/api/schemas/updateTeamDtoOutput.zod.ts new file mode 100644 index 0000000..24526cc --- /dev/null +++ b/src/shared/api/schemas/updateTeamDtoOutput.zod.ts @@ -0,0 +1,35 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const updateTeamDtoOutputNameMin = 2; +export const updateTeamDtoOutputNameMax = 100; + +export const updateTeamDtoOutputDescriptionMax = 500; + +export const UpdateTeamDtoOutput = zod.object({ + name: zod + .string() + .min(updateTeamDtoOutputNameMin) + .max(updateTeamDtoOutputNameMax) + .optional() + .describe('Название команды, отображаемое в интерфейсе'), + description: zod + .string() + .max(updateTeamDtoOutputDescriptionMax) + .optional() + .describe('Краткое описание деятельнос��и или целей команды'), + slug: zod.string().optional().describe('Уникальная ссылка на изображение команду'), + tags: zod + .array(zod.string()) + .optional() + .describe('Список строковых названий тегов для классификации'), +}); + +export type UpdateTeamDtoOutput = zod.input; +export type UpdateTeamDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/userControllerGetActivityParams.zod.ts b/src/shared/api/schemas/userControllerGetActivityParams.zod.ts new file mode 100644 index 0000000..e842ace --- /dev/null +++ b/src/shared/api/schemas/userControllerGetActivityParams.zod.ts @@ -0,0 +1,32 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const userControllerGetActivityParamsPageDefault = 1; +export const userControllerGetActivityParamsPageMax = 9007199254740991; + +export const userControllerGetActivityParamsLimitDefault = 20; +export const userControllerGetActivityParamsLimitMax = 100; + +export const UserControllerGetActivityParams = zod.object({ + page: zod + .number() + .min(1) + .max(userControllerGetActivityParamsPageMax) + .default(userControllerGetActivityParamsPageDefault), + limit: zod + .number() + .min(1) + .max(userControllerGetActivityParamsLimitMax) + .default(userControllerGetActivityParamsLimitDefault), +}); + +export type UserControllerGetActivityParams = zod.input; +export type UserControllerGetActivityParamsOutput = zod.output< + typeof UserControllerGetActivityParams +>; diff --git a/src/shared/api/schemas/userControllerUpdateNotificationsBody.zod.ts b/src/shared/api/schemas/userControllerUpdateNotificationsBody.zod.ts new file mode 100644 index 0000000..4490614 --- /dev/null +++ b/src/shared/api/schemas/userControllerUpdateNotificationsBody.zod.ts @@ -0,0 +1,33 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const UserControllerUpdateNotificationsBody = zod + .object({ + email: zod + .object({ + task_assigned: zod.boolean().describe('Уведомление на п��чту при назначении задачи'), + mentions: zod.boolean().describe('Уведомление на почту при упоминании в комментариях'), + daily_summary: zod.boolean().describe('Ежедневная сводка задач на почту'), + }) + .optional(), + push: zod + .object({ + task_assigned: zod.boolean().describe('Push-уведомление при назначении задачи'), + reminders: zod.boolean().describe('Push-уведомления о дедлайнах'), + }) + .optional(), + }) + .describe('Схема для частичного обновления настроек уведомлений'); + +export type UserControllerUpdateNotificationsBody = zod.input< + typeof UserControllerUpdateNotificationsBody +>; +export type UserControllerUpdateNotificationsBodyOutput = zod.output< + typeof UserControllerUpdateNotificationsBody +>; diff --git a/src/shared/api/schemas/userControllerUpdateProfileBody.zod.ts b/src/shared/api/schemas/userControllerUpdateProfileBody.zod.ts new file mode 100644 index 0000000..af9e913 --- /dev/null +++ b/src/shared/api/schemas/userControllerUpdateProfileBody.zod.ts @@ -0,0 +1,41 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const userControllerUpdateProfileBodyFirstNameMax = 50; + +export const userControllerUpdateProfileBodyLastNameMax = 50; + +export const userControllerUpdateProfileBodyMiddleNameMax = 50; + +export const userControllerUpdateProfileBodyBioMax = 1000; + +export const userControllerUpdateProfileBodyTimezoneMax = 50; + +export const userControllerUpdateProfileBodyLanguageMin = 2; +export const userControllerUpdateProfileBodyLanguageMax = 2; + +export const UserControllerUpdateProfileBody = zod + .object({ + firstName: zod.string().min(1).max(userControllerUpdateProfileBodyFirstNameMax).optional(), + lastName: zod.string().min(1).max(userControllerUpdateProfileBodyLastNameMax).optional(), + middleName: zod.string().max(userControllerUpdateProfileBodyMiddleNameMax).nullish(), + bio: zod.string().max(userControllerUpdateProfileBodyBioMax).nullish(), + timezone: zod.string().max(userControllerUpdateProfileBodyTimezoneMax).optional(), + language: zod + .string() + .min(userControllerUpdateProfileBodyLanguageMin) + .max(userControllerUpdateProfileBodyLanguageMax) + .optional(), + }) + .describe('Схема для частичного обновления данных профиля'); + +export type UserControllerUpdateProfileBody = zod.input; +export type UserControllerUpdateProfileBodyOutput = zod.output< + typeof UserControllerUpdateProfileBody +>; diff --git a/src/shared/api/schemas/userControllerUploadAvatarBody.zod.ts b/src/shared/api/schemas/userControllerUploadAvatarBody.zod.ts new file mode 100644 index 0000000..e98cb11 --- /dev/null +++ b/src/shared/api/schemas/userControllerUploadAvatarBody.zod.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const UserControllerUploadAvatarBody = zod + .object({ + file: zod.instanceof(File).optional().describe('Файл изображения аватара'), + }) + .describe('Тело запроса загрузки аватара пользователя (multipart\/form-data)'); + +export type UserControllerUploadAvatarBody = zod.input; +export type UserControllerUploadAvatarBodyOutput = zod.output< + typeof UserControllerUploadAvatarBody +>; diff --git a/src/shared/api/schemas/userInviteResponseOutput.zod.ts b/src/shared/api/schemas/userInviteResponseOutput.zod.ts new file mode 100644 index 0000000..94142ff --- /dev/null +++ b/src/shared/api/schemas/userInviteResponseOutput.zod.ts @@ -0,0 +1,27 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const userInviteResponseOutputExpiresAtRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); + +export const UserInviteResponseOutput = zod.object({ + code: zod.string().describe('Код инвайта'), + teamName: zod.string().describe('Название команды'), + teamAvatar: zod.string().nullable().describe('Аватар команды'), + role: zod.string().describe('Роль'), + inviterName: zod.string().describe('Имя пригласившего'), + expiresAt: zod.iso + .datetime({}) + .regex(userInviteResponseOutputExpiresAtRegExp) + .describe('Дата истечения'), +}); + +export type UserInviteResponseOutput = zod.input; +export type UserInviteResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/userResponseOutput.zod.ts b/src/shared/api/schemas/userResponseOutput.zod.ts new file mode 100644 index 0000000..dd5ed1b --- /dev/null +++ b/src/shared/api/schemas/userResponseOutput.zod.ts @@ -0,0 +1,68 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const userResponseOutputEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const userResponseOutputProfileCreatedAtRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); +export const userResponseOutputProfileUpdatedAtRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); +export const userResponseOutputSecurityLastPasswordChangeRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); + +export const UserResponseOutput = zod.object({ + id: zod.string().describe('Уникальный идентификатор (CUID\/UUID)'), + email: zod.email().regex(userResponseOutputEmailRegExp).describe('Электронная почта'), + profile: zod.object({ + firstName: zod.string().describe('Имя пользователя'), + lastName: zod.string().describe('Фамилия'), + middleName: zod.string().nullable().describe('Отчество'), + bio: zod.string().nullable().describe('О себе'), + avatarUrl: zod.url().nullable().describe('Ссылка на аватар в S3'), + timezone: zod.string().describe('Временная зона'), + language: zod.string().describe('Язык интерфейса'), + createdAt: zod.iso + .datetime({}) + .regex(userResponseOutputProfileCreatedAtRegExp) + .describe('Дата регистрации'), + updatedAt: zod.iso + .datetime({}) + .regex(userResponseOutputProfileUpdatedAtRegExp) + .describe('Дата последнего обновления профиля'), + }), + security: zod + .object({ + is2faEnabled: zod.boolean().describe('Статус двухфакторной аутентификации'), + lastPasswordChange: zod.iso + .datetime({}) + .regex(userResponseOutputSecurityLastPasswordChangeRegExp) + .describe('Дата последнего изменения пароля'), + }) + .describe('Данные безопасности аккаунта'), + notifications: zod + .object({ + email: zod.object({ + task_assigned: zod.boolean().describe('Уведомление на п��чту при назначении задачи'), + mentions: zod.boolean().describe('Уведомление на почту при упоминании в комментариях'), + daily_summary: zod.boolean().describe('Ежедневная сводка задач на почту'), + }), + push: zod.object({ + task_assigned: zod.boolean().describe('Push-уведомление при назначении задачи'), + reminders: zod.boolean().describe('Push-уведомления о дедлайнах'), + }), + }) + .describe('Настройки уведомлений пользователя'), +}); + +export type UserResponseOutput = zod.input; +export type UserResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/userTeamResponseOutput.zod.ts b/src/shared/api/schemas/userTeamResponseOutput.zod.ts new file mode 100644 index 0000000..6455176 --- /dev/null +++ b/src/shared/api/schemas/userTeamResponseOutput.zod.ts @@ -0,0 +1,42 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const userTeamResponseOutputIdRegExp = new RegExp( + '^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$' +); +export const userTeamResponseOutputJoinedAtRegExp = new RegExp( + '^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$' +); + +export const UserTeamResponseOutput = zod.object({ + id: zod.uuid().regex(userTeamResponseOutputIdRegExp).describe('Уникальный ID команды'), + name: zod.string().describe('Название команды'), + slug: zod.string().describe('Уникальный URL-путь команды'), + description: zod.string().nullable().describe('Краткое описание команды'), + avatarUrl: zod.string().nullable().describe('URL изображения профиля команды'), + role: zod.string().describe('Системное название роли пользователя'), + joinedAt: zod.iso + .datetime({}) + .regex(userTeamResponseOutputJoinedAtRegExp) + .describe('Дата, когда пользователь вступил в команду'), + permissions: zod + .object({ + canEdit: zod.boolean().describe('Разрешено ли редактировать настройки и профиль команды'), + canDelete: zod + .boolean() + .describe('Разрешено ли полностью удалить команду (только для владельца)'), + canManageMembers: zod.boolean().describe('Разрешено ли менять роли и исключать участников'), + canInvite: zod.boolean().describe('Разрешено ли приглашать новых участников'), + isOwner: zod.boolean().describe('Является ли текущий пользователь владельцем (Owner)'), + }) + .describe('Объект прав доступа текущего пользователя'), +}); + +export type UserTeamResponseOutput = zod.input; +export type UserTeamResponseOutputOutput = zod.output; diff --git a/src/shared/api/schemas/verifyDtoOutput.zod.ts b/src/shared/api/schemas/verifyDtoOutput.zod.ts new file mode 100644 index 0000000..8003a5d --- /dev/null +++ b/src/shared/api/schemas/verifyDtoOutput.zod.ts @@ -0,0 +1,31 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const verifyDtoOutputEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const verifyDtoOutputCodeMin = 6; +export const verifyDtoOutputCodeMax = 6; + +export const VerifyDtoOutput = zod + .object({ + email: zod + .email() + .regex(verifyDtoOutputEmailRegExp) + .describe('Email пользователя, на который был отправлен код'), + code: zod + .string() + .min(verifyDtoOutputCodeMin) + .max(verifyDtoOutputCodeMax) + .describe('6-значный OTP код подтверждения'), + }) + .describe('Схема верификации OTP кода'); + +export type VerifyDtoOutput = zod.input; +export type VerifyDtoOutputOutput = zod.output; diff --git a/src/shared/api/schemas/verifyResetCodeDtoOutput.zod.ts b/src/shared/api/schemas/verifyResetCodeDtoOutput.zod.ts new file mode 100644 index 0000000..cfac74e --- /dev/null +++ b/src/shared/api/schemas/verifyResetCodeDtoOutput.zod.ts @@ -0,0 +1,26 @@ +/** + * Generated by orval v8.7.0 🍺 + * Do not edit manually. + * Task Tracker API + * API бэкенда таск-трекера + * OpenAPI spec version: 0.1.0 + */ +import { z as zod } from 'zod'; + +export const verifyResetCodeDtoOutputEmailRegExp = new RegExp( + "^(?!\\.)(?!.\*\\.\\.)([A-Za-z0-9_'+\\-\\.]\*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]\*\\.)+[A-Za-z]{2,}$" +); +export const verifyResetCodeDtoOutputCodeMin = 6; +export const verifyResetCodeDtoOutputCodeMax = 6; + +export const VerifyResetCodeDtoOutput = zod.object({ + email: zod.email().regex(verifyResetCodeDtoOutputEmailRegExp), + code: zod + .string() + .min(verifyResetCodeDtoOutputCodeMin) + .max(verifyResetCodeDtoOutputCodeMax) + .describe('Код из письма'), +}); + +export type VerifyResetCodeDtoOutput = zod.input; +export type VerifyResetCodeDtoOutputOutput = zod.output; diff --git a/src/shared/api/token/access-token.ts b/src/shared/api/token/access-token.ts new file mode 100644 index 0000000..9d6bffe --- /dev/null +++ b/src/shared/api/token/access-token.ts @@ -0,0 +1,23 @@ +'use client'; + +class AccessToken { + #tokenKey: string = 'token'; + + set token(token: string) { + localStorage.setItem(this.#tokenKey, token); + } + + get token(): string | null { + return localStorage.getItem(this.#tokenKey); + } + + get header() { + const token = this.token; + + return token && `Bearer ${token}`; + } +} + +const accessToken = new AccessToken(); + +export { accessToken }; diff --git a/src/shared/api/token/index.ts b/src/shared/api/token/index.ts new file mode 100644 index 0000000..a0cd6c6 --- /dev/null +++ b/src/shared/api/token/index.ts @@ -0,0 +1,2 @@ +export { refreshAuth } from './refresh-auth'; +export { accessToken } from './access-token'; diff --git a/src/shared/api/token/refresh-auth.ts b/src/shared/api/token/refresh-auth.ts new file mode 100644 index 0000000..739cb92 --- /dev/null +++ b/src/shared/api/token/refresh-auth.ts @@ -0,0 +1,22 @@ +import { AxiosError, AxiosInstance } from 'axios'; +import type { AxiosAuthRefreshRequestConfig } from 'axios-auth-refresh'; +import { z } from 'zod'; +import { RefreshTokenResponse } from './response-schema'; +import { accessToken } from './access-token'; + +export const refreshAuth = + (instance: AxiosInstance) => + (failedRequest: AxiosError): Promise => + instance>({ + url: `/auth/refresh`, + method: 'POST', + skipAuthRefresh: true, + contracts: { + response: RefreshTokenResponse, + }, + } as AxiosAuthRefreshRequestConfig).then(({ data }) => { + if (data.success) { + accessToken.token = data.token; + failedRequest.response?.config.headers.set('Authorization', accessToken.header); + } + }); diff --git a/src/shared/api/token/response-schema.ts b/src/shared/api/token/response-schema.ts new file mode 100644 index 0000000..2845105 --- /dev/null +++ b/src/shared/api/token/response-schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const RefreshTokenResponse = z + .object({ + success: z.boolean().describe('Успешное обновление токенов'), + token: z.string().describe('Новый access token (JWT)'), + message: z.string().optional().describe('Дополнительное сообщение (опционально)'), + }) + .describe('Ответ при обновлении пары access/refresh токенов'); diff --git a/src/shared/api/validation/AxiosContracts.ts b/src/shared/api/validation/AxiosContracts.ts new file mode 100644 index 0000000..65131c2 --- /dev/null +++ b/src/shared/api/validation/AxiosContracts.ts @@ -0,0 +1,101 @@ +import { AxiosHeaders, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'; +import type { ZodType } from 'zod'; +import { AxiosValidationError } from './AxiosValidationError'; + +declare module 'axios' { + export interface AxiosRequestConfig { + contracts?: { + response?: ZodType; + body?: ZodType; + params?: ZodType; + }; + } +} + +/** + * A class containing static methods for validating and transforming data using Zod schemas. + */ +export class AxiosContracts { + /** + * Validates and normalizes request body and query params according to configured contracts. + * + * @param data Axios request config to validate before sending. + * @returns Updated request config with parsed payload. + * @throws {AxiosValidationError} When request data does not satisfy contract schema. + */ + static requestContractInterceptor(data: InternalAxiosRequestConfig) { + const { contracts } = data; + + if (contracts?.body !== undefined && data.data !== undefined) { + data.data = AxiosContracts.parseRequest(contracts.body, data.data); + } + if (contracts?.params !== undefined && data.params !== undefined) { + data.params = AxiosContracts.parseRequest(contracts.params, data.params); + } + + return data; + } + + /** + * Parses request data with the provided Zod schema. + * + * @template Data Expected request data type. + * @param schema Zod schema used to validate request data. + * @param data Request data to validate. + * @returns Parsed and type-safe request data. + * @throws {AxiosValidationError} When validation fails. + */ + static parseRequest(schema: ZodType, data: Data): Data { + const validation = schema.safeParse(data); + + if (validation.error) { + throw new AxiosValidationError( + { headers: new AxiosHeaders() }, + undefined, + undefined, + validation.error.issues + ); + } + + return validation.data; + } + + /** + * Validates response payload using response contract from request config. + * + * @param response Axios response to validate. + * @returns Original response with validated and parsed data. + * @throws {AxiosValidationError} When response data does not satisfy contract schema. + */ + static responseContractInterceptor(response: AxiosResponse): AxiosResponse { + const schema = response.config.contracts?.response; + + if (schema !== undefined) { + response.data = AxiosContracts.parseResponse(schema, response); + } + return response; + } + + /** + * Parses response data with the provided Zod schema. + * + * @template Data Expected response data type. + * @param schema Zod schema used to validate response data. + * @param response Axios response containing raw data. + * @returns Parsed and type-safe response data. + * @throws {AxiosValidationError} When validation fails. + */ + static parseResponse(schema: ZodType, response?: AxiosResponse): Data { + const validation = schema.safeParse(response?.data); + + if (validation.error) { + throw new AxiosValidationError( + response?.config, + response?.request, + response, + validation.error.issues + ); + } + return validation.data; + } +} diff --git a/src/shared/api/validation/AxiosValidationError.ts b/src/shared/api/validation/AxiosValidationError.ts new file mode 100644 index 0000000..63444a0 --- /dev/null +++ b/src/shared/api/validation/AxiosValidationError.ts @@ -0,0 +1,56 @@ +import { + AxiosError, + isAxiosError, + type AxiosResponse, + type InternalAxiosRequestConfig, +} from 'axios'; +import type { ZodIssue } from 'zod'; + +/** + * Custom error class for handling validation errors in axios requests. + * Extends the AxiosError class and includes an optional array of ZodIssue objects. + */ +export class AxiosValidationError extends AxiosError { + /** + * Static property representing the error code for bad validation. + */ + static readonly ERR_BAD_VALIDATION = 'ERR_BAD_VALIDATION'; + readonly isAxiosValidationError = true; + + static isAxiosValidationError(error: unknown): error is AxiosValidationError { + return ( + error instanceof AxiosValidationError || + (isAxiosError(error) && + (error as { isAxiosValidationError?: boolean }).isAxiosValidationError === true) + ); + } + + /** + * Constructor for the AxiosValidationError class. + * + * @param config - The configuration object for the axios request. + * @param request - The request object. + * @param response - The response object. + * @param issues - An optional array of ZodIssue objects representing the validation errors. + */ + constructor( + config?: InternalAxiosRequestConfig, + request?: unknown, + response?: AxiosResponse, + public readonly issues?: ZodIssue[] + ) { + super( + 'The provided data does not meet the required criteria.', + AxiosValidationError.ERR_BAD_VALIDATION, + config, + request, + response + ); + } +} + +export function isAxiosValidationError( + error: unknown +): error is AxiosValidationError { + return AxiosValidationError.isAxiosValidationError(error); +} diff --git a/src/shared/api/validation/GlobalErrorResponse.ts b/src/shared/api/validation/GlobalErrorResponse.ts new file mode 100644 index 0000000..9c731a7 --- /dev/null +++ b/src/shared/api/validation/GlobalErrorResponse.ts @@ -0,0 +1,51 @@ +import { z } from 'zod'; + +const ValidationIssueSchema = z + .object({ + origin: z.string().optional(), + code: z.string().describe('Машиночитаемый код ошибки валидации'), + message: z.string().describe('Человекочитаемое сообщение об ошибке поля'), + path: z.array(z.string()).describe('Путь к полю, где произошла ошибка'), + }) + .catchall(z.unknown()); + +export const GlobalErrorResponse = z.object({ + success: z.boolean().describe('Признак успешного выполнения запроса'), + error: z.object({ + code: z.string().describe('Уникальный бизнес-код ошибки'), + message: z.string().describe('Краткое описание ошибки для пользователя или разработчика'), + retryable: z + .boolean() + .describe( + 'Флаг, указывающий клиенту, есть ли смысл повторять запрос без изменений (например, при 503)' + ), + }), + details: z + .array(ValidationIssueSchema.describe('Детальная информация о конкретном нарушении в запросе')) + .optional() + .describe('Список ошибок валидации (заполняется только для 400 ошибок)'), + meta: z + .object({ + service: z.string().optional().describe('Имя микросервиса, в котором произошел сбой'), + request: z + .object({ + requestId: z.string().describe('Уникальный ID запроса (Trace ID)'), + path: z.string().describe('URL-путь эндпоинта, который вернул ошибку'), + method: z.string().describe('HTTP метод запроса (GET, POST, etc.)'), + ip: z.string().optional().describe('IP-адрес клиента'), + }) + .optional(), + timestamp: z.iso + .datetime({}) + .describe('Точное время возникновения ошибки в формате ISO 8601'), + debug: z + .object({ + stack: z.string().optional().describe('Стек вызовов для отладки'), + }) + .optional(), + }) + .catchall(z.unknown()) + .describe('Техническая мета-информация для мониторинга и отладки'), +}); + +export type GlobalErrorResponseType = z.infer; diff --git a/src/shared/api/validation/extract-validation-issues.ts b/src/shared/api/validation/extract-validation-issues.ts new file mode 100644 index 0000000..27ae35d --- /dev/null +++ b/src/shared/api/validation/extract-validation-issues.ts @@ -0,0 +1,30 @@ +import { isAxiosError } from 'axios'; +import { isAxiosValidationError } from './AxiosValidationError'; +import { GlobalErrorResponseType } from './GlobalErrorResponse'; + +export interface ValidationIssue { + message: string; + path: string[]; +} + +export function extractValidationIssues(err: unknown): ValidationIssue[] { + // Локальная ошибка валидации (например, от контрактов на клиенте). + if (isAxiosValidationError(err)) { + return (err.issues ?? []).map((issue) => ({ + message: issue.message, + path: issue.path.map(String), + })); + } + + // Ошибка валидации с бэкенда. + if (isAxiosError(err)) { + return ( + err.response?.data?.details?.map(({ message, path }) => ({ + message, + path, + })) ?? [] + ); + } + + return []; +} diff --git a/src/shared/api/validation/index.ts b/src/shared/api/validation/index.ts new file mode 100644 index 0000000..caed67d --- /dev/null +++ b/src/shared/api/validation/index.ts @@ -0,0 +1,6 @@ +export { type GlobalErrorResponseType } from './GlobalErrorResponse'; +export { GlobalErrorResponse } from './GlobalErrorResponse'; +export { AxiosContracts } from './AxiosContracts'; +export { AxiosValidationError, isAxiosValidationError } from './AxiosValidationError'; +export { extractValidationIssues } from './extract-validation-issues'; +export type { ValidationIssue } from './extract-validation-issues'; diff --git a/src/shared/assests/images/loginPreviewDark.png b/src/shared/assests/images/loginPreviewDark.png deleted file mode 100644 index 3ca976e..0000000 Binary files a/src/shared/assests/images/loginPreviewDark.png and /dev/null differ diff --git a/src/shared/assests/images/loginPreviewLight.png b/src/shared/assests/images/loginPreviewLight.png deleted file mode 100644 index 3ca976e..0000000 Binary files a/src/shared/assests/images/loginPreviewLight.png and /dev/null differ diff --git a/src/shared/assests/images/registerPreviewDark.png b/src/shared/assests/images/registerPreviewDark.png deleted file mode 100644 index 3ca976e..0000000 Binary files a/src/shared/assests/images/registerPreviewDark.png and /dev/null differ diff --git a/src/shared/assests/images/registerPreviewLight.png b/src/shared/assests/images/registerPreviewLight.png deleted file mode 100644 index 3ca976e..0000000 Binary files a/src/shared/assests/images/registerPreviewLight.png and /dev/null differ diff --git a/src/shared/assests/index.ts b/src/shared/assests/index.ts index a348604..c793b1f 100644 --- a/src/shared/assests/index.ts +++ b/src/shared/assests/index.ts @@ -1,5 +1 @@ export { default as LogoImage } from './images/logo.svg'; -export { default as LoginImageLight } from './images/loginPreviewDark.png'; -export { default as LoginImageDark } from './images/loginPreviewLight.png'; -export { default as RegisterImageLight } from './images/registerPreviewDark.png'; -export { default as RegisterImageDark } from './images/registerPreviewLight.png'; diff --git a/src/shared/config/index.ts b/src/shared/config/index.ts new file mode 100644 index 0000000..57f46d7 --- /dev/null +++ b/src/shared/config/index.ts @@ -0,0 +1 @@ +export { routes } from './routes'; diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts new file mode 100644 index 0000000..16809f0 --- /dev/null +++ b/src/shared/config/routes.ts @@ -0,0 +1,14 @@ +import type { Route } from 'next'; + +export const routes = { + home: (): Route => '/', + auth: { + signin: (): Route => '/signin', + signup: (): Route => '/signup', + }, + team: { + root: (): Route => '/team', + projects: (): Route => '/team/projects', + profile: (): Route => '/team/profile', + }, +} as const; diff --git a/src/shared/lib/utils/capitalize/capitalize.test.ts b/src/shared/lib/utils/capitalize/capitalize.test.ts new file mode 100644 index 0000000..4d495a8 --- /dev/null +++ b/src/shared/lib/utils/capitalize/capitalize.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from 'vitest'; +import { capitalize } from './capitalize'; + +test('capitalize "word"', () => { + expect(capitalize('word')).toBe('Word'); +}); + +test('capitalize "two words"', () => { + expect(capitalize('two words')).toBe('Two words'); +}); diff --git a/src/shared/lib/utils/capitalize/capitalize.ts b/src/shared/lib/utils/capitalize/capitalize.ts new file mode 100644 index 0000000..0745d9a --- /dev/null +++ b/src/shared/lib/utils/capitalize/capitalize.ts @@ -0,0 +1,2 @@ +export const capitalize = (value: string) => + value ? value[0].toUpperCase() + value.slice(1).toLowerCase() : value; diff --git a/src/shared/lib/utils/index.ts b/src/shared/lib/utils/index.ts index 414c24d..9fdf1a3 100644 --- a/src/shared/lib/utils/index.ts +++ b/src/shared/lib/utils/index.ts @@ -1 +1,2 @@ export { cn } from './cn'; +export { capitalize } from './capitalize/capitalize'; diff --git a/src/shared/providers/QueryProvider.tsx b/src/shared/providers/QueryProvider.tsx new file mode 100644 index 0000000..885e52c --- /dev/null +++ b/src/shared/providers/QueryProvider.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { QueryClientProvider, QueryClientProviderProps } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { queryClient } from 'shared/api'; + +export function QueryProvider({ children, ...props }: Omit) { + return ( + + {children} + {process.env.NODE_ENV === 'development' ? ( + + ) : null} + + ); +} diff --git a/src/shared/providers/index.ts b/src/shared/providers/index.ts new file mode 100644 index 0000000..9403f80 --- /dev/null +++ b/src/shared/providers/index.ts @@ -0,0 +1 @@ +export { QueryProvider } from './QueryProvider'; diff --git a/src/shared/ui/alert-dialog.tsx b/src/shared/ui/AlertDialog.tsx similarity index 100% rename from src/shared/ui/alert-dialog.tsx rename to src/shared/ui/AlertDialog.tsx diff --git a/src/shared/ui/card.tsx b/src/shared/ui/Card.tsx similarity index 100% rename from src/shared/ui/card.tsx rename to src/shared/ui/Card.tsx diff --git a/src/shared/ui/field.tsx b/src/shared/ui/Field.tsx similarity index 98% rename from src/shared/ui/field.tsx rename to src/shared/ui/Field.tsx index a764d02..4d8b9ba 100644 --- a/src/shared/ui/field.tsx +++ b/src/shared/ui/Field.tsx @@ -2,8 +2,8 @@ import { useMemo } from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from 'shared/lib/utils'; -import { Label } from 'shared/ui/label'; -import { Separator } from 'shared/ui/separator'; +import { Label } from 'shared/ui/Label'; +import { Separator } from 'shared/ui/Separator'; function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) { return ( diff --git a/src/shared/ui/input.tsx b/src/shared/ui/Input.tsx similarity index 100% rename from src/shared/ui/input.tsx rename to src/shared/ui/Input.tsx diff --git a/src/shared/ui/input-group.tsx b/src/shared/ui/InputGroup.tsx similarity index 98% rename from src/shared/ui/input-group.tsx rename to src/shared/ui/InputGroup.tsx index a332cdf..a6e314b 100644 --- a/src/shared/ui/input-group.tsx +++ b/src/shared/ui/InputGroup.tsx @@ -3,8 +3,8 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from 'shared/lib/utils'; import { Button } from 'shared/ui/index'; -import { Input } from 'shared/ui/input'; -import { Textarea } from 'shared/ui/textarea'; +import { Input } from 'shared/ui/Input'; +import { Textarea } from 'shared/ui/Textarea'; function InputGroup({ className, ...props }: React.ComponentProps<'div'>) { return ( diff --git a/src/shared/ui/InputOtp.tsx b/src/shared/ui/InputOtp.tsx new file mode 100644 index 0000000..0514983 --- /dev/null +++ b/src/shared/ui/InputOtp.tsx @@ -0,0 +1,86 @@ +'use client'; + +import * as React from 'react'; +import { OTPInput, OTPInputContext } from 'input-otp'; + +import { cn } from 'shared/lib/utils'; +import { MinusIcon } from 'lucide-react'; + +function InputOtp({ + className, + containerClassName, + ...props +}: React.ComponentProps & { + containerClassName?: string; +}) { + return ( + + ); +} + +function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function InputOTPSlot({ + index, + className, + ...props +}: React.ComponentProps<'div'> & { + index: number; +}) { + const inputOTPContext = React.useContext(OTPInputContext); + const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}; + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ); +} + +function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) { + return ( +
+ +
+ ); +} + +export { InputOtp, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; diff --git a/src/shared/ui/label.tsx b/src/shared/ui/Label.tsx similarity index 100% rename from src/shared/ui/label.tsx rename to src/shared/ui/Label.tsx diff --git a/src/shared/ui/separator.tsx b/src/shared/ui/Separator.tsx similarity index 100% rename from src/shared/ui/separator.tsx rename to src/shared/ui/Separator.tsx diff --git a/src/shared/ui/Sonner.tsx b/src/shared/ui/Sonner.tsx new file mode 100644 index 0000000..12ca2d0 --- /dev/null +++ b/src/shared/ui/Sonner.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Toaster as Sonner, type ToasterProps } from 'sonner'; +import { + CircleCheckIcon, + InfoIcon, + TriangleAlertIcon, + OctagonXIcon, + Loader2Icon, +} from 'lucide-react'; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme(); + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + '--normal-bg': 'var(--popover)', + '--normal-text': 'var(--popover-foreground)', + '--normal-border': 'var(--border)', + '--border-radius': 'var(--radius)', + } as React.CSSProperties + } + toastOptions={{ + classNames: { + toast: 'cn-toast', + }, + }} + {...props} + /> + ); +}; + +export { Toaster }; diff --git a/src/shared/ui/Spinner.tsx b/src/shared/ui/Spinner.tsx new file mode 100644 index 0000000..dd816ca --- /dev/null +++ b/src/shared/ui/Spinner.tsx @@ -0,0 +1,15 @@ +import { cn } from 'shared/lib/utils'; +import { Loader2Icon } from 'lucide-react'; + +function Spinner({ className, ...props }: React.ComponentProps<'svg'>) { + return ( + + ); +} + +export { Spinner }; diff --git a/src/shared/ui/textarea.tsx b/src/shared/ui/Textarea.tsx similarity index 100% rename from src/shared/ui/textarea.tsx rename to src/shared/ui/Textarea.tsx diff --git a/src/shared/ui/AppCopyright/app-copyright.tsx b/src/shared/ui/app-copyright/AppCopyright.tsx similarity index 100% rename from src/shared/ui/AppCopyright/app-copyright.tsx rename to src/shared/ui/app-copyright/AppCopyright.tsx diff --git a/src/shared/ui/AppCopyright/app-copyright.stories.ts b/src/shared/ui/app-copyright/app-copyright.stories.ts similarity index 87% rename from src/shared/ui/AppCopyright/app-copyright.stories.ts rename to src/shared/ui/app-copyright/app-copyright.stories.ts index 8494822..e48a85b 100644 --- a/src/shared/ui/AppCopyright/app-copyright.stories.ts +++ b/src/shared/ui/app-copyright/app-copyright.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; -import { AppCopyright } from './app-copyright'; +import { AppCopyright } from './AppCopyright'; const meta = { title: 'Shared/AppCopyright', diff --git a/src/shared/ui/Button/button.test.tsx b/src/shared/ui/button/Button.test.tsx similarity index 97% rename from src/shared/ui/Button/button.test.tsx rename to src/shared/ui/button/Button.test.tsx index bf17674..2cd2b35 100644 --- a/src/shared/ui/Button/button.test.tsx +++ b/src/shared/ui/button/Button.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Button } from './button'; +import { Button } from './Button'; test('рендерит кнопку с текстом', () => { render(); diff --git a/src/shared/ui/Button/button.tsx b/src/shared/ui/button/Button.tsx similarity index 100% rename from src/shared/ui/Button/button.tsx rename to src/shared/ui/button/Button.tsx diff --git a/src/shared/ui/Button/button.stories.ts b/src/shared/ui/button/button.stories.ts similarity index 94% rename from src/shared/ui/Button/button.stories.ts rename to src/shared/ui/button/button.stories.ts index ddb97bd..38b1fb0 100644 --- a/src/shared/ui/Button/button.stories.ts +++ b/src/shared/ui/button/button.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; -import { Button } from './button'; +import { Button } from './Button'; const meta = { title: 'Shared/Button', diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 48b37fa..5273c7e 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,15 +1,18 @@ -export * from './alert-dialog'; -export * from './Button/button'; -export * from './card'; -export * from './field'; -export * from './input'; -export * from './label'; -export * from './separator'; -export * from './Logo/logo'; -export * from './AppCopyright/app-copyright'; -export * from './ThemedImage/themed-image'; -export * from './Link/link'; -export * from './input-group'; -export * from './textarea'; -export * from './InputPassword/input-password'; -export * from './InputEmail/input-email'; +export * from './AlertDialog'; +export * from './button/Button'; +export * from './Card'; +export * from './Field'; +export * from './Input'; +export * from './Label'; +export * from './Separator'; +export * from './logo/Logo'; +export * from './app-copyright/AppCopyright'; +export * from './themed-image/ThemedImage'; +export * from './link/Link'; +export * from './InputGroup'; +export * from './Textarea'; +export * from './input-password/InputPassword'; +export * from './input-email/InputEmail'; +export * from './Sonner'; +export * from './InputOtp'; +export * from './Spinner'; diff --git a/src/shared/ui/InputEmail/input-email.tsx b/src/shared/ui/input-email/InputEmail.tsx similarity index 100% rename from src/shared/ui/InputEmail/input-email.tsx rename to src/shared/ui/input-email/InputEmail.tsx diff --git a/src/shared/ui/InputEmail/input-email.stories.ts b/src/shared/ui/input-email/input-email.stories.ts similarity index 88% rename from src/shared/ui/InputEmail/input-email.stories.ts rename to src/shared/ui/input-email/input-email.stories.ts index 626a8cb..62c62f0 100644 --- a/src/shared/ui/InputEmail/input-email.stories.ts +++ b/src/shared/ui/input-email/input-email.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; -import { InputEmail } from './input-email'; +import { InputEmail } from './InputEmail'; const meta = { title: 'Shared/InputEmail', diff --git a/src/shared/ui/InputPassword/input-password.tsx b/src/shared/ui/input-password/InputPassword.tsx similarity index 100% rename from src/shared/ui/InputPassword/input-password.tsx rename to src/shared/ui/input-password/InputPassword.tsx diff --git a/src/shared/ui/InputPassword/input-password.stories.ts b/src/shared/ui/input-password/input-password.stories.ts similarity index 91% rename from src/shared/ui/InputPassword/input-password.stories.ts rename to src/shared/ui/input-password/input-password.stories.ts index cdf9b24..3e5ca41 100644 --- a/src/shared/ui/InputPassword/input-password.stories.ts +++ b/src/shared/ui/input-password/input-password.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; -import { InputPassword } from './input-password'; +import { InputPassword } from './InputPassword'; const meta = { title: 'Shared/InputPassword', diff --git a/src/shared/ui/Link/link.tsx b/src/shared/ui/link/Link.tsx similarity index 100% rename from src/shared/ui/Link/link.tsx rename to src/shared/ui/link/Link.tsx diff --git a/src/shared/ui/Link/link.stories.ts b/src/shared/ui/link/link.stories.ts similarity index 95% rename from src/shared/ui/Link/link.stories.ts rename to src/shared/ui/link/link.stories.ts index ac65cb7..9765985 100644 --- a/src/shared/ui/Link/link.stories.ts +++ b/src/shared/ui/link/link.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; -import { Link } from './link'; +import { Link } from './Link'; const meta = { title: 'Shared/Link', diff --git a/src/shared/ui/Logo/logo.tsx b/src/shared/ui/logo/Logo.tsx similarity index 96% rename from src/shared/ui/Logo/logo.tsx rename to src/shared/ui/logo/Logo.tsx index 9af6bc2..114da23 100644 --- a/src/shared/ui/Logo/logo.tsx +++ b/src/shared/ui/logo/Logo.tsx @@ -35,7 +35,7 @@ function Logo({ {...props} > Logo - TaskTracker Lab + Task Tracker
); } diff --git a/src/shared/ui/Logo/logo.stories.ts b/src/shared/ui/logo/logo.stories.ts similarity index 95% rename from src/shared/ui/Logo/logo.stories.ts rename to src/shared/ui/logo/logo.stories.ts index 2aff911..4d72942 100644 --- a/src/shared/ui/Logo/logo.stories.ts +++ b/src/shared/ui/logo/logo.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; -import { Logo } from './logo'; +import { Logo } from './Logo'; const meta = { title: 'Shared/Logo', diff --git a/src/shared/ui/ThemedImage/themed-image.tsx b/src/shared/ui/themed-image/ThemedImage.tsx similarity index 76% rename from src/shared/ui/ThemedImage/themed-image.tsx rename to src/shared/ui/themed-image/ThemedImage.tsx index 67487ee..8a1739c 100644 --- a/src/shared/ui/ThemedImage/themed-image.tsx +++ b/src/shared/ui/themed-image/ThemedImage.tsx @@ -10,11 +10,11 @@ interface ThemedImageProps extends Omit< srcDark: React.ComponentProps['src']; } -function ThemedImage({ className, srcDark, srcLight, ...props }: ThemedImageProps) { +function ThemedImage({ className, srcDark, srcLight, alt, ...props }: ThemedImageProps) { return ( <> - - + {alt} + {alt} ); }