diff --git a/.env.example b/.env.example index 0588c9e..8b13789 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1 @@ -# BackEnd -HOST=localhost -PORT=3005 + diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/CI-workflow.yml b/.github/workflows/CI-workflow.yml index fa1cb13..669a825 100644 --- a/.github/workflows/CI-workflow.yml +++ b/.github/workflows/CI-workflow.yml @@ -1,116 +1,52 @@ name: CI workflow on: - pull_request: - branches: ['dev', 'main'] - paths-ignore: - - '*.md' - - 'docs/**' + pull_request: + branches: ['dev', 'main'] + paths-ignore: + - '*.md' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - frontend: - runs-on: ubuntu-latest + CI: + runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'pnpm' + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'pnpm' - - name: Install deps - run: pnpm install --frozen-lockfile + - name: Install deps + run: pnpm install --frozen-lockfile - - name: Lint - run: pnpm lint:frontend + - name: Lint + run: pnpm lint - - name: Lint FSD - run: pnpm lint:fsd:frontend + - name: Lint FSD + run: pnpm lint:fsd - - name: Format - run: pnpm format:check:frontend + - name: Format + run: pnpm format:check - - name: Typecheck - run: pnpm typecheck:frontend + - name: Typecheck + run: pnpm typecheck - - name: Unit tests - run: pnpm test:frontend:ci + - name: Unit tests + run: pnpm test:ci - - name: Build - run: pnpm build:frontend + - name: Build + run: pnpm build - # - name: Build Storybook - # run: pnpm build-storybook - - backend: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:16 - env: - POSTGRES_DB: test - POSTGRES_USER: test - POSTGRES_PASSWORD: test - ports: - - 5432:5432 - options: >- - --health-cmd="pg_isready" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - - env: - DATABASE_URL: postgresql://test:test@localhost:5432/test - PORT: 3000 - JWT_SECRET: test - JWT_EXPIRES_IN: 7d - NODE_ENV: test - CORS_ALLOWED_ORIGINS: http://localhost:3000 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'pnpm' - - - name: Install deps - run: pnpm install --frozen-lockfile - - - name: Prisma generate - run: pnpm prisma:generate:backend - - - name: Apply migrations (test DB) - run: pnpm prisma:migrate:deploy:backend - - - name: Lint - run: pnpm lint:backend - - - name: Format - run: pnpm format:check:backend - - - name: Typecheck - run: pnpm typecheck:backend - - - name: Unit tests - run: pnpm test:backend:ci - - - name: Build - run: pnpm build:backend + # - name: Build Storybook + # run: pnpm build-storybook diff --git a/.husky/pre-commit b/.husky/pre-commit index ccfd6e5..4d3a7aa 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,3 @@ pnpm exec lint-staged -pnpm lint:fsd:frontend -pnpm typecheck:backend -pnpm typecheck:frontend +pnpm lint:fsd +pnpm typecheck diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index c22122e..48e2a43 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -1,4 +1,5 @@ export default { - '*.{ts,js,mjs}': ['eslint --fix --no-warn-ignored', 'prettier --write'], + '*.{ts,tsx,js,jsx,mjs}': ['eslint --fix --no-warn-ignored', 'prettier --write'], '*.{json,css,md}': ['prettier --write'], + '*.{ts,tsx}': ['vitest related --run'], }; diff --git a/.prettierignore b/.prettierignore index c30a33a..cf0a87c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,6 @@ node_modules -dist -build .next coverage pnpm-lock.yaml -*.tsbuildinfo \ No newline at end of file +*.tsbuildinfo +*.log \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 7986cb9..c1f0782 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "singleQuote": true, "printWidth": 100, "tabWidth": 2, - "endOfLine": "auto" + "endOfLine": "auto", + "plugins": ["prettier-plugin-tailwindcss"] } diff --git a/apps/frontend/.storybook/main.ts b/.storybook/main.ts similarity index 100% rename from apps/frontend/.storybook/main.ts rename to .storybook/main.ts diff --git a/apps/frontend/.storybook/preview.ts b/.storybook/preview.ts similarity index 86% rename from apps/frontend/.storybook/preview.ts rename to .storybook/preview.ts index 118eb24..52a35b3 100644 --- a/apps/frontend/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,5 @@ import type { Preview } from '@storybook/nextjs-vite'; -import '../src/app/styles/global.css'; +import 'app/styles/global.css'; const preview: Preview = { parameters: { diff --git a/README.md b/README.md index fa88b95..636c1c5 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,10 @@ -# Task Tracker +# Task Tracker (frontend) -## Структура проекта - -- `apps/frontend` - Next.js приложение -- `apps/backend` - NestJS API -- `packages/config` - Общие конфиги -- `packages/shared-types` - Общие типы TypeScript -- `docs` - Общая документация +Next.js-приложение. ## Быстрый старт ```bash pnpm install -# Запустить Postgres -docker-compose up -d - -pnpm dev # Запускает и frontend, и backend - +pnpm dev ``` diff --git a/apps/frontend/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx similarity index 100% rename from apps/frontend/app/(auth)/login/page.tsx rename to app/(auth)/login/page.tsx diff --git a/apps/frontend/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx similarity index 100% rename from apps/frontend/app/(auth)/register/page.tsx rename to app/(auth)/register/page.tsx diff --git a/apps/frontend/app/layout.tsx b/app/layout.tsx similarity index 100% rename from apps/frontend/app/layout.tsx rename to app/layout.tsx diff --git a/apps/frontend/app/page.tsx b/app/page.tsx similarity index 100% rename from apps/frontend/app/page.tsx rename to app/page.tsx diff --git a/apps/backend/.env.example b/apps/backend/.env.example deleted file mode 100644 index 651c453..0000000 --- a/apps/backend/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -PORT=3005 -DATABASE_URL="postgresql://postgres:root@localhost:5432/task-tracker" -JWT_SECRET=jwt-secret -JWT_EXPIRES_IN=7d -NODE_ENV=dev -CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,https://my-jira-clone.com \ No newline at end of file diff --git a/apps/backend/.env.test b/apps/backend/.env.test deleted file mode 100644 index 0967e51..0000000 --- a/apps/backend/.env.test +++ /dev/null @@ -1,6 +0,0 @@ -PORT=3001 -DATABASE_URL="postgresql://test:test@localhost:5432/test" -JWT_SECRET=test-secret -JWT_EXPIRES_IN=7d -NODE_ENV=test -CORS_ALLOWED_ORIGINS=http://localhost:3000 \ No newline at end of file diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore deleted file mode 100644 index 9f62ec0..0000000 --- a/apps/backend/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -# Keep environment variables out of version control -.env - -/generated/prisma diff --git a/apps/backend/.lintstagedrc.mjs b/apps/backend/.lintstagedrc.mjs deleted file mode 100644 index 919f5c9..0000000 --- a/apps/backend/.lintstagedrc.mjs +++ /dev/null @@ -1,4 +0,0 @@ -export default { - '*.{ts,js}': ['eslint --fix --no-warn-ignored', 'prettier --write'], - '*.{json,css,md}': ['prettier --write'], -}; diff --git a/apps/backend/eslint.config.mjs b/apps/backend/eslint.config.mjs deleted file mode 100644 index 2610bd9..0000000 --- a/apps/backend/eslint.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import baseConfig from '../../eslint.config.mjs'; -import { defineConfig, globalIgnores } from 'eslint/config'; - -const eslintConfig = defineConfig([...baseConfig, globalIgnores(['dist/**'])]); - -export default eslintConfig; diff --git a/apps/backend/jest.config.ts b/apps/backend/jest.config.ts deleted file mode 100644 index e10d6e8..0000000 --- a/apps/backend/jest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Config } from 'jest'; - -const config: Config = { - moduleFileExtensions: ['js', 'json', 'ts'], - rootDir: 'src', - testRegex: '.*\\.spec\\.ts$', - transform: { - '^.+\\.(t|j)s$': 'ts-jest', - }, - testEnvironment: 'node', - setupFiles: ['/../jest.setup.ts'], -}; - -export default config; diff --git a/apps/backend/jest.setup.ts b/apps/backend/jest.setup.ts deleted file mode 100644 index cbe80bd..0000000 --- a/apps/backend/jest.setup.ts +++ /dev/null @@ -1,5 +0,0 @@ -import dotenv from 'dotenv'; - -dotenv.config({ - path: '.env.test', -}); diff --git a/apps/backend/nest-cli.json b/apps/backend/nest-cli.json deleted file mode 100644 index 60f3c09..0000000 --- a/apps/backend/nest-cli.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true, - "plugins": [ - { - "name": "@nestjs/swagger", - "options": { - "classValidatorShim": true, - "introspectComments": true, - "dtoKeyOfComment": "description" - } - } - ] - } -} diff --git a/apps/backend/package.json b/apps/backend/package.json deleted file mode 100644 index 5a98ecc..0000000 --- a/apps/backend/package.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "@task-tracker/backend", - "version": "1.0.0", - "private": "true", - "description": "", - "main": "index.js", - "scripts": { - "build": "nest build", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "nest dist/main", - "prisma:seed": "prisma db seed", - "prisma:generate": "prisma generate", - "prisma:migrate:dev": "prisma migrate dev", - "prisma:migrate:deploy": "prisma migrate deploy", - "typecheck": "tsc --noEmit", - "test": "jest", - "test:ci": "jest --runInBand", - "lint": "eslint .", - "lint:fix": "eslint --fix", - "format": "prettier --write .", - "format:check": "prettier --check ." - }, - "prisma": { - "seed": "ts-node --transpile-only src/prisma/seed/seed.ts" - }, - "keywords": [], - "author": "", - "license": "ISC", - "packageManager": "pnpm@10.29.3", - "dependencies": { - "@fastify/compress": "^8.3.1", - "@fastify/cookie": "^11.0.2", - "@fastify/cors": "^11.2.0", - "@fastify/static": "^9.0.0", - "@nestjs/bullmq": "^11.0.4", - "@nestjs/common": "^11.1.14", - "@nestjs/config": "^4.0.3", - "@nestjs/core": "^11.1.14", - "@nestjs/jwt": "^11.0.2", - "@nestjs/mapped-types": "^2.1.0", - "@nestjs/passport": "^11.0.5", - "@nestjs/platform-fastify": "^11.1.14", - "@nestjs/platform-socket.io": "^11.1.14", - "@nestjs/swagger": "^11.2.6", - "@nestjs/websockets": "^11.1.14", - "@prisma/client": "6.19.2", - "bcrypt": "^6.0.0", - "bullmq": "^5.70.1", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.3", - "fastify": "^5.7.4", - "ioredis": "^5.9.3", - "passport": "^0.7.0", - "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", - "pg": "^8.18.0", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.2", - "socket.io": "^4.8.3", - "zod": "^4.3.6" - }, - "devDependencies": { - "@nestjs/cli": "^11.0.16", - "@nestjs/schematics": "^11.0.9", - "@nestjs/testing": "^11.1.14", - "@types/bcrypt": "^6.0.0", - "@types/jest": "^30.0.0", - "@types/passport-jwt": "^4.0.1", - "@types/passport-local": "^1.0.38", - "dotenv": "^17.3.1", - "globals": "^17.3.0", - "jest": "^30.2.0", - "prisma": "6.19.2", - "source-map-support": "^0.5.21", - "ts-jest": "^29.4.6", - "ts-loader": "^9.5.4", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0" - } -} diff --git a/apps/backend/prisma/migrations/20260306090912_init/migration.sql b/apps/backend/prisma/migrations/20260306090912_init/migration.sql deleted file mode 100644 index f97a4f3..0000000 --- a/apps/backend/prisma/migrations/20260306090912_init/migration.sql +++ /dev/null @@ -1,258 +0,0 @@ --- CreateEnum -CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN', 'MODERATOR'); - --- CreateEnum -CREATE TYPE "TeamMemberRole" AS ENUM ('MEMBER', 'OWNER', 'ADMIN'); - --- CreateEnum -CREATE TYPE "TaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT'); - --- CreateEnum -CREATE TYPE "TaskType" AS ENUM ('TASK', 'EPIC', 'STORY', 'BUG', 'TECH_DEBT'); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "role" "UserRole" NOT NULL DEFAULT 'USER', - "avatarURL" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TeamMember" ( - "id" TEXT NOT NULL, - "role" "TeamMemberRole" NOT NULL DEFAULT 'MEMBER', - "userId" TEXT NOT NULL, - "teamId" TEXT NOT NULL, - "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Team" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "ownerId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Team_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Project" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "teamId" TEXT NOT NULL, - - CONSTRAINT "Project_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Board" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "projectId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Board_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "BoardColumn" ( - "id" TEXT NOT NULL, - "title" TEXT NOT NULL, - "position" INTEGER NOT NULL, - "boardId" TEXT NOT NULL, - "statusId" TEXT NOT NULL, - - CONSTRAINT "BoardColumn_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Task" ( - "id" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT, - "type" "TaskType" NOT NULL, - "priority" "TaskPriority" NOT NULL, - "statusId" TEXT NOT NULL, - "boardId" TEXT NOT NULL, - "assignedToId" TEXT, - "createdById" TEXT, - "parentId" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Task_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TaskStatus" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "boardId" TEXT NOT NULL, - - CONSTRAINT "TaskStatus_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TasksFilter" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "isPrivate" BOOLEAN NOT NULL DEFAULT true, - "query" JSONB NOT NULL, - "creatorId" TEXT, - "boardId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "TasksFilter_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TaskHistory" ( - "id" TEXT NOT NULL, - "field" TEXT NOT NULL, - "newValue" JSONB, - "oldValue" JSONB, - "teamMemberId" TEXT, - "taskId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "TaskHistory_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TaskComment" ( - "id" TEXT NOT NULL, - "text" TEXT NOT NULL, - "teamMemberId" TEXT, - "taskId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "TaskComment_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE INDEX "TeamMember_teamId_idx" ON "TeamMember"("teamId"); - --- CreateIndex -CREATE UNIQUE INDEX "TeamMember_userId_teamId_key" ON "TeamMember"("userId", "teamId"); - --- CreateIndex -CREATE UNIQUE INDEX "Team_name_key" ON "Team"("name"); - --- CreateIndex -CREATE UNIQUE INDEX "Project_teamId_name_key" ON "Project"("teamId", "name"); - --- CreateIndex -CREATE INDEX "Board_projectId_idx" ON "Board"("projectId"); - --- CreateIndex -CREATE UNIQUE INDEX "Board_projectId_name_key" ON "Board"("projectId", "name"); - --- CreateIndex -CREATE INDEX "BoardColumn_statusId_idx" ON "BoardColumn"("statusId"); - --- CreateIndex -CREATE UNIQUE INDEX "BoardColumn_boardId_position_key" ON "BoardColumn"("boardId", "position"); - --- CreateIndex -CREATE UNIQUE INDEX "BoardColumn_boardId_title_key" ON "BoardColumn"("boardId", "title"); - --- CreateIndex -CREATE INDEX "Task_statusId_idx" ON "Task"("statusId"); - --- CreateIndex -CREATE INDEX "Task_boardId_idx" ON "Task"("boardId"); - --- CreateIndex -CREATE INDEX "Task_assignedToId_idx" ON "Task"("assignedToId"); - --- CreateIndex -CREATE INDEX "Task_parentId_idx" ON "Task"("parentId"); - --- CreateIndex -CREATE UNIQUE INDEX "TaskStatus_boardId_name_key" ON "TaskStatus"("boardId", "name"); - --- CreateIndex -CREATE INDEX "TasksFilter_boardId_idx" ON "TasksFilter"("boardId"); - --- CreateIndex -CREATE INDEX "TasksFilter_creatorId_idx" ON "TasksFilter"("creatorId"); - --- CreateIndex -CREATE INDEX "TaskHistory_taskId_idx" ON "TaskHistory"("taskId"); - --- CreateIndex -CREATE INDEX "TaskHistory_teamMemberId_idx" ON "TaskHistory"("teamMemberId"); - --- CreateIndex -CREATE INDEX "TaskComment_taskId_idx" ON "TaskComment"("taskId"); - --- AddForeignKey -ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Team" ADD CONSTRAINT "Team_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Project" ADD CONSTRAINT "Project_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Board" ADD CONSTRAINT "Board_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "BoardColumn" ADD CONSTRAINT "BoardColumn_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "BoardColumn" ADD CONSTRAINT "BoardColumn_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "TaskStatus"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Task" ADD CONSTRAINT "Task_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "TaskStatus"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Task" ADD CONSTRAINT "Task_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Task" ADD CONSTRAINT "Task_assignedToId_fkey" FOREIGN KEY ("assignedToId") REFERENCES "TeamMember"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Task" ADD CONSTRAINT "Task_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "TeamMember"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Task" ADD CONSTRAINT "Task_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Task"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskStatus" ADD CONSTRAINT "TaskStatus_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TasksFilter" ADD CONSTRAINT "TasksFilter_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "TeamMember"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TasksFilter" ADD CONSTRAINT "TasksFilter_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskHistory" ADD CONSTRAINT "TaskHistory_teamMemberId_fkey" FOREIGN KEY ("teamMemberId") REFERENCES "TeamMember"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskHistory" ADD CONSTRAINT "TaskHistory_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskComment" ADD CONSTRAINT "TaskComment_teamMemberId_fkey" FOREIGN KEY ("teamMemberId") REFERENCES "TeamMember"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskComment" ADD CONSTRAINT "TaskComment_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/backend/prisma/migrations/migration_lock.toml b/apps/backend/prisma/migrations/migration_lock.toml deleted file mode 100644 index 044d57c..0000000 --- a/apps/backend/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma deleted file mode 100644 index 5a064ab..0000000 --- a/apps/backend/prisma/schema.prisma +++ /dev/null @@ -1,232 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -enum UserRole { - USER - ADMIN - MODERATOR -} - -enum TeamMemberRole { - MEMBER - OWNER - ADMIN -} - -enum TaskPriority { - LOW - MEDIUM - HIGH - URGENT -} - -enum TaskType { - TASK - EPIC - STORY - BUG - TECH_DEBT -} - -model User { - id String @id @default(cuid()) - name String - email String @unique - password String - role UserRole @default(USER) - avatarURL String? - - memberships TeamMember[] - ownedTeams Team[] @relation("TeamOwner") - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model TeamMember { - id String @id @default(cuid()) - role TeamMemberRole @default(MEMBER) - - history TaskHistory[] - - tasksAssigned Task[] @relation("AssignedTasks") - tasksComments TaskComment[] - tasksCreated Task[] @relation("CreatedTasks") - tasksFilters TasksFilter[] - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - teamId String - - joinedAt DateTime @default(now()) - - @@unique([userId, teamId]) - @@index([teamId]) -} - -model Team { - id String @id @default(cuid()) - name String @unique - - members TeamMember[] - projects Project[] - - owner User @relation("TeamOwner", fields: [ownerId], references: [id], onDelete: Restrict) - ownerId String - - createdAt DateTime @default(now()) -} - -model Project { - id String @id @default(cuid()) - name String - - boards Board[] - - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - teamId String - - @@unique([teamId, name]) -} - -model Board { - id String @id @default(cuid()) - name String - - columns BoardColumn[] - tasks Task[] - tasksFilters TasksFilter[] - statuses TaskStatus[] - - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) - projectId String - - createdAt DateTime @default(now()) - - @@unique([projectId, name]) - @@index([projectId]) -} - -model BoardColumn { - id String @id @default(cuid()) - title String - position Int - - board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) - boardId String - - status TaskStatus @relation(fields: [statusId], references: [id], onDelete: Restrict) - statusId String - - @@unique([boardId, position]) - @@unique([boardId, title]) - @@index([statusId]) -} - -model Task { - id String @id @default(cuid()) - title String - description String? - type TaskType - priority TaskPriority - - history TaskHistory[] - comments TaskComment[] - children Task[] @relation("TaskHierarchy") - - status TaskStatus @relation(fields: [statusId], references: [id], onDelete: Restrict) - statusId String - - board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) - boardId String - - assignedTo TeamMember? @relation("AssignedTasks", fields: [assignedToId], references: [id], onDelete: SetNull) - assignedToId String? - - createdBy TeamMember? @relation("CreatedTasks", fields: [createdById], references: [id], onDelete: SetNull) - createdById String? - - parentId String? - parent Task? @relation("TaskHierarchy", fields: [parentId], references: [id], onDelete: SetNull) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([statusId]) - @@index([boardId]) - @@index([assignedToId]) - @@index([parentId]) -} - -model TaskStatus { - id String @id @default(cuid()) - name String - - tasks Task[] - boardColumns BoardColumn[] - - board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) - boardId String - - @@unique([boardId, name]) -} - -model TasksFilter { - id String @id @default(cuid()) - name String - isPrivate Boolean @default(true) - query Json - - creator TeamMember? @relation(fields: [creatorId], references: [id], onDelete: SetNull) - creatorId String? - - board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) - boardId String - - createdAt DateTime @default(now()) - - @@index([boardId]) - @@index([creatorId]) -} - -model TaskHistory { - id String @id @default(cuid()) - field String - newValue Json? - oldValue Json? - - teamMember TeamMember? @relation(fields: [teamMemberId], references: [id], onDelete: SetNull) - teamMemberId String? - - task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) - taskId String - - createdAt DateTime @default(now()) - - @@index([taskId]) - @@index([teamMemberId]) -} - -model TaskComment { - id String @id @default(cuid()) - text String - - teamMember TeamMember? @relation(fields: [teamMemberId], references: [id], onDelete: SetNull) - teamMemberId String? - - task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) - taskId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([taskId]) -} diff --git a/apps/backend/src/app.controller.ts b/apps/backend/src/app.controller.ts deleted file mode 100644 index 0ec439e..0000000 --- a/apps/backend/src/app.controller.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Controller, Get, Header } from '@nestjs/common'; -import { AppService } from './app.service'; -import { ApiExcludeEndpoint } from '@nestjs/swagger'; - -@Controller() -export class AppController { - private start: number; - - constructor(private readonly appService: AppService) { - this.start = Date.now(); - } - - @Get() - @ApiExcludeEndpoint() - @Header('Content-Type', 'text/html') - getHello(): Promise { - return this.appService.getHello(); - } - - @Get('healthcheck') - async healthcheck() { - const now = Date.now(); - return { - status: 'API online', - uptime: Number((now - this.start) / 1000).toFixed(0), - }; - } -} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts deleted file mode 100644 index 16e299a..0000000 --- a/apps/backend/src/app.module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { UserModule } from './modules/user/user.module'; -import { PrismaModule } from './prisma/prisma.module'; -import { AuthModule } from './modules/auth/auth.module'; -import { APP_FILTER } from '@nestjs/core'; -import { - AllExceptionsFilter, - DomainExceptionFilter, - PrismaExceptionFilter, -} from './shared/filters'; - -@Module({ - imports: [PrismaModule, UserModule, AuthModule], - controllers: [AppController], - providers: [ - AppService, - { - provide: APP_FILTER, - useClass: AllExceptionsFilter, - }, - { - provide: APP_FILTER, - useClass: DomainExceptionFilter, - }, - { - provide: APP_FILTER, - useClass: PrismaExceptionFilter, - }, - ], -}) -export class AppModule {} diff --git a/apps/backend/src/app.service.ts b/apps/backend/src/app.service.ts deleted file mode 100644 index 9a9b7e5..0000000 --- a/apps/backend/src/app.service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - async getHello(): Promise { - return `
-

Tracker API

-

- Swagger -

-

- HealthCheck -

-
-
`; - } -} diff --git a/apps/backend/src/config/cors.config.ts b/apps/backend/src/config/cors.config.ts deleted file mode 100644 index aca96b2..0000000 --- a/apps/backend/src/config/cors.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FastifyCorsOptions } from '@fastify/cors'; -import { env } from '../env'; - -export const corsConfig: FastifyCorsOptions = { - origin: (origin, callback) => { - if (!origin) { - return callback(null, true); - } - - if (env.CORS_ALLOWED_ORIGINS.includes(origin)) { - return callback(null, true); - } - - callback(new Error(`CORS Policy: Origin ${origin} not allowed`), false); - }, - credentials: true, - methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Accept', 'Authorization'], -}; diff --git a/apps/backend/src/config/swagger.config.ts b/apps/backend/src/config/swagger.config.ts deleted file mode 100644 index e2b6d79..0000000 --- a/apps/backend/src/config/swagger.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { ApiResponse, ApiResponsePaginated } from '../shared/dto'; -import { INestApplication } from '@nestjs/common'; - -export function setupSwagger(app: INestApplication): void { - const config = new DocumentBuilder() - .setTitle('Task Tracker API') - .setDescription('API documentation for Task Tracker') - .setVersion('1.0') - .addCookieAuth('access_token', { - type: 'apiKey', - in: 'cookie', - description: 'JWT token in HttpOnly cookie', - }) - .build(); - - const document = SwaggerModule.createDocument(app, config, { - extraModels: [ApiResponse, ApiResponsePaginated], - }); - - SwaggerModule.setup('doc', app, document); -} diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts deleted file mode 100644 index a61df87..0000000 --- a/apps/backend/src/env.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { z } from 'zod'; -import type { JwtExpires } from './modules/auth/types/jwt-expires.type'; - -enum NodeEnv { - DEV = 'dev', - PRODUCTION = 'production', - TEST = 'test', -} - -const envSchema = z.object({ - PORT: z.string(), - DATABASE_URL: z.url(), - JWT_SECRET: z.string(), - JWT_EXPIRES_IN: z.custom(), - NODE_ENV: z.enum(NodeEnv), - CORS_ALLOWED_ORIGINS: z - .string() - .min(1, "CORS_ALLOWED_ORIGINS can't be empty") - .transform((val) => val.split(',')) - .pipe( - z.array( - z.url({ error: 'Origin must be valid URL' }).refine((val) => { - const url = new URL(val); - return url.origin === val; - }, 'Invalid CORS origin') - ) - ), -}); - -export const env = envSchema.parse(process.env); -export const isDev = env.NODE_ENV === NodeEnv.DEV; -export const isProd = env.NODE_ENV === NodeEnv.PRODUCTION; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts deleted file mode 100644 index 2b5322c..0000000 --- a/apps/backend/src/main.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; -import { ValidationPipe } from '@nestjs/common'; -import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; -import fastifyCompress from '@fastify/compress'; -import fastifyCors from '@fastify/cors'; -import fastifyCookie from '@fastify/cookie'; -import { env } from './env'; -import { TransformResponseInterceptor } from './shared/interceptors'; -import { corsConfig } from './config/cors.config'; -import { setupSwagger } from './config/swagger.config'; - -async function bootstrap() { - const PORT = Number(env.PORT); - if (Number.isNaN(PORT)) { - throw new Error('Не задан порт в .env'); - } - - const adapter = new FastifyAdapter(); - const app = await NestFactory.create(AppModule, adapter, { - rawBody: true, - }); - - await app.register(fastifyCookie); - await app.register(fastifyCompress, { - global: false, - encodings: ['gzip', 'br'], - threshold: 1024, - }); - - await app.getHttpAdapter().getInstance().register(fastifyCors, corsConfig); - - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - }) - ); - app.setGlobalPrefix('api/v1', { - exclude: ['/'], - }); - app.useGlobalInterceptors(new TransformResponseInterceptor()); - - setupSwagger(app); - - await app.listen(PORT, '0.0.0.0', () => - console.log(`\x1b[34mServer started on port = ${PORT}\x1b[0m`) - ); -} - -bootstrap(); diff --git a/apps/backend/src/modules/auth/auth.controller.spec.ts b/apps/backend/src/modules/auth/auth.controller.spec.ts deleted file mode 100644 index ceced60..0000000 --- a/apps/backend/src/modules/auth/auth.controller.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; - -describe('AuthController', () => { - let controller: AuthController; - const authServiceMock = { - login: jest.fn(), - registration: jest.fn(), - me: jest.fn(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AuthController], - providers: [ - AuthService, - { - provide: AuthService, - useValue: authServiceMock, - }, - ], - }).compile(); - - controller = module.get(AuthController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/backend/src/modules/auth/auth.controller.ts b/apps/backend/src/modules/auth/auth.controller.ts deleted file mode 100644 index 316c37e..0000000 --- a/apps/backend/src/modules/auth/auth.controller.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Body, Controller, HttpCode, Post, Res } from '@nestjs/common'; -import { AuthService } from './auth.service'; -import { PrivateUserDto } from '../user/dto/user.dto'; -import { CreateUserDto } from '../user/dto/create-user.dto'; -import { isProd } from '../../env'; -import { type FastifyReply } from 'fastify'; -import { LoginRequestDto } from './dto/login.dto'; -import { ApiOkResponseDto } from '../../shared/decorators'; -import { ApiResponseHttpCodes } from '../../shared/decorators/api-response-http-codes.decorator'; - -@Controller('auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @Post('login') - @HttpCode(200) - @ApiOkResponseDto(PrivateUserDto) - @ApiResponseHttpCodes(401, 400) - async login( - @Body() loginUserDto: LoginRequestDto, - @Res({ passthrough: true }) res: FastifyReply - ): Promise { - const { accessToken, user } = await this.authService.login(loginUserDto); - - res.setCookie('access_token', accessToken, { - httpOnly: true, - secure: isProd, - sameSite: 'strict', - path: '/', - }); - - return user; - } - - @Post('logout') - @HttpCode(204) - @ApiResponseHttpCodes() - async logout(@Res({ passthrough: true }) res: FastifyReply): Promise { - res.clearCookie('access_token', { path: '/' }); - } - - @Post('registration') - @ApiResponseHttpCodes(409, 400) - async registration(@Body() createUserDto: CreateUserDto): Promise { - await this.authService.registration(createUserDto); - } -} diff --git a/apps/backend/src/modules/auth/auth.module.ts b/apps/backend/src/modules/auth/auth.module.ts deleted file mode 100644 index ffaafa0..0000000 --- a/apps/backend/src/modules/auth/auth.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { AuthController } from './auth.controller'; -import { AuthService } from './auth.service'; -import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; -import { env } from '../../env'; -import { JwtStrategy } from './strategies/jwt.strategy'; -import { PassportModule } from '@nestjs/passport'; -import { UserModule } from '../user/user.module'; - -@Module({ - imports: [ - PassportModule, - JwtModule.register({ - secret: env.JWT_SECRET, - signOptions: { expiresIn: env.JWT_EXPIRES_IN }, - }), - UserModule, - ], - controllers: [AuthController], - providers: [AuthService, JwtStrategy], -}) -export class AuthModule {} diff --git a/apps/backend/src/modules/auth/auth.service.spec.ts b/apps/backend/src/modules/auth/auth.service.spec.ts deleted file mode 100644 index 2bbc689..0000000 --- a/apps/backend/src/modules/auth/auth.service.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthService } from './auth.service'; -import { UserService } from '../user/user.service'; -import { JwtService } from '@nestjs/jwt'; - -describe('AuthService', () => { - let service: AuthService; - const userServiceMock = { - findByEmail: jest.fn(), - create: jest.fn(), - findById: jest.fn(), - }; - - const jwtServiceMock = { - sign: jest.fn(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuthService, - { provide: UserService, useValue: userServiceMock }, - { provide: JwtService, useValue: jwtServiceMock }, - ], - }).compile(); - - service = module.get(AuthService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/backend/src/modules/auth/auth.service.ts b/apps/backend/src/modules/auth/auth.service.ts deleted file mode 100644 index bb184e0..0000000 --- a/apps/backend/src/modules/auth/auth.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import bcrypt from 'bcrypt'; -import { CreateUserDto } from '../user/dto/create-user.dto'; -import { hashPassword } from './utils/hashPassword/hashPassword'; -import { JwtService } from '@nestjs/jwt'; -import { UserService } from '../user/user.service'; -import { JwtPayload } from './types/jwt-payload.type'; -import { DomainError } from '../../shared/errors'; -import { LoginRequestDto } from './dto/login.dto'; -import { PrivateUserDto } from '../user/dto/user.dto'; - -@Injectable() -export class AuthService { - constructor( - private readonly userService: UserService, - private readonly jwtService: JwtService - ) {} - - async registration(createUserDto: CreateUserDto): Promise { - const hashedPassword = await hashPassword(createUserDto.password); - await this.userService.create({ - ...createUserDto, - password: hashedPassword, - }); - } - - async login( - loginUserDto: LoginRequestDto - ): Promise<{ user: PrivateUserDto; accessToken: string }> { - const user = await this.userService.findByEmailWithPasswordOrNull(loginUserDto.email); - if (!user) { - throw DomainError.Unauthorized('Invalid email or password'); - } - - const isValidPassword = await bcrypt.compare(loginUserDto.password, user.password); - if (!isValidPassword) { - throw DomainError.Unauthorized('Invalid email or password'); - } - - const { password: _, ...userDto } = user; - - const payload: JwtPayload = { - sub: user.id, - email: user.email, - role: user.role, - }; - - return { - accessToken: this.jwtService.sign(payload), - user: userDto, - }; - } -} diff --git a/apps/backend/src/modules/auth/dto/login.dto.ts b/apps/backend/src/modules/auth/dto/login.dto.ts deleted file mode 100644 index 43c2751..0000000 --- a/apps/backend/src/modules/auth/dto/login.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsEmail, IsString, MinLength } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class LoginRequestDto { - @ApiProperty({ example: 'test@mail.com' }) - @IsEmail() - email: string; - - @ApiProperty({ example: '123456' }) - @IsString() - @MinLength(6) - password: string; -} diff --git a/apps/backend/src/modules/auth/strategies/jwt.strategy.ts b/apps/backend/src/modules/auth/strategies/jwt.strategy.ts deleted file mode 100644 index 91feac5..0000000 --- a/apps/backend/src/modules/auth/strategies/jwt.strategy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { env } from '../../../env'; -import { Injectable } from '@nestjs/common'; -import type { CurrentUserType, JwtPayload } from '../types/jwt-payload.type'; - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor() { - super({ - jwtFromRequest: ExtractJwt.fromExtractors([(req) => req?.cookies?.access_token]), - secretOrKey: env.JWT_SECRET, - }); - } - - async validate(payload: JwtPayload): Promise { - return { - id: payload.sub, - email: payload.email, - role: payload.role, - }; - } -} diff --git a/apps/backend/src/modules/auth/types/jwt-expires.type.ts b/apps/backend/src/modules/auth/types/jwt-expires.type.ts deleted file mode 100644 index c5070a7..0000000 --- a/apps/backend/src/modules/auth/types/jwt-expires.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type JwtExpires = '15m' | '30m' | '1h' | '7d'; diff --git a/apps/backend/src/modules/auth/types/jwt-payload.type.ts b/apps/backend/src/modules/auth/types/jwt-payload.type.ts deleted file mode 100644 index dae2469..0000000 --- a/apps/backend/src/modules/auth/types/jwt-payload.type.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UserRole } from '@prisma/client'; - -export type JwtPayload = { - sub: string; - email: string; - role: UserRole; -}; - -export type CurrentUserType = { - id: string; - email: string; - role: UserRole; -}; diff --git a/apps/backend/src/modules/auth/utils/hashPassword/hashPassword.ts b/apps/backend/src/modules/auth/utils/hashPassword/hashPassword.ts deleted file mode 100644 index dbc8a0f..0000000 --- a/apps/backend/src/modules/auth/utils/hashPassword/hashPassword.ts +++ /dev/null @@ -1,5 +0,0 @@ -import bcrypt from 'bcrypt'; - -export const hashPassword = (password: string, salt: number = 10): Promise => { - return bcrypt.hash(password, salt); -}; diff --git a/apps/backend/src/modules/user/dto/PaginatedListExampleType.dto.ts b/apps/backend/src/modules/user/dto/PaginatedListExampleType.dto.ts deleted file mode 100644 index e6c487f..0000000 --- a/apps/backend/src/modules/user/dto/PaginatedListExampleType.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class PaginatedListExampleType { - @ApiProperty({ example: '1' }) - id: string; - - @ApiProperty({ example: 'Ivan' }) - name: string; -} diff --git a/apps/backend/src/modules/user/dto/create-user.dto.ts b/apps/backend/src/modules/user/dto/create-user.dto.ts deleted file mode 100644 index 8e8418c..0000000 --- a/apps/backend/src/modules/user/dto/create-user.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IsEmail, IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class CreateUserDto { - @ApiProperty({ example: 'Ivan' }) - @IsString() - name: string; - - @ApiProperty({ example: 'test@mail.com' }) - @IsEmail() - email: string; - - @ApiProperty({ example: '123456' }) - @IsString() - password: string; -} diff --git a/apps/backend/src/modules/user/dto/update-auth-user-password.dto.ts b/apps/backend/src/modules/user/dto/update-auth-user-password.dto.ts deleted file mode 100644 index c23e5ae..0000000 --- a/apps/backend/src/modules/user/dto/update-auth-user-password.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsString } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdateAuthUserPasswordDto { - @ApiProperty({ example: '123456' }) - @IsString() - oldPassword: string; - - @ApiProperty({ example: '654321' }) - @IsString() - newPassword: string; -} diff --git a/apps/backend/src/modules/user/dto/update-user.dto.ts b/apps/backend/src/modules/user/dto/update-user.dto.ts deleted file mode 100644 index e61a2b6..0000000 --- a/apps/backend/src/modules/user/dto/update-user.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { OmitType, PartialType } from '@nestjs/swagger'; -import { CreateUserDto } from './create-user.dto'; - -export class UpdateUserDto extends PartialType(OmitType(CreateUserDto, ['password'] as const)) {} diff --git a/apps/backend/src/modules/user/dto/user.dto.ts b/apps/backend/src/modules/user/dto/user.dto.ts deleted file mode 100644 index a2e26d0..0000000 --- a/apps/backend/src/modules/user/dto/user.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { UserRole } from '@prisma/client'; -import { PrivateUserPayload, PublicUserPayload } from '../selectors/user.selectors'; -import { ApiProperty } from '@nestjs/swagger'; - -export class PrivateUserDto implements PrivateUserPayload { - @ApiProperty({ example: 'cmnc4mk5s0000uu54csfyvooh' }) - id: string; - - @ApiProperty({ example: 'Ivan' }) - name: string; - - @ApiProperty({ example: 'test@mail.com' }) - email: string; - - @ApiProperty({ example: 'USER' }) - role: UserRole; -} - -export class PublicUserDto implements PublicUserPayload { - @ApiProperty({ example: 'cmnc4mk5s0000uu54csfyvooh' }) - id: string; - - @ApiProperty({ example: 'Ivan' }) - name: string; -} diff --git a/apps/backend/src/modules/user/selectors/user.selectors.ts b/apps/backend/src/modules/user/selectors/user.selectors.ts deleted file mode 100644 index 0020d2b..0000000 --- a/apps/backend/src/modules/user/selectors/user.selectors.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Prisma } from '@prisma/client'; - -export const publicUserSelect = { - id: true, - name: true, -} satisfies Prisma.UserSelect; - -export const privateUserSelect = { - ...publicUserSelect, - email: true, - role: true, -} satisfies Prisma.UserSelect; - -export type PrivateUserPayload = Prisma.UserGetPayload<{ select: typeof privateUserSelect }>; -export type PublicUserPayload = Prisma.UserGetPayload<{ select: typeof publicUserSelect }>; diff --git a/apps/backend/src/modules/user/user.controller.spec.ts b/apps/backend/src/modules/user/user.controller.spec.ts deleted file mode 100644 index a6269e5..0000000 --- a/apps/backend/src/modules/user/user.controller.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UserController } from './user.controller'; -import { UserService } from './user.service'; - -describe('UserController', () => { - let controller: UserController; - const userServiceMock = { - findAll: jest.fn(), - findById: jest.fn(), - findByEmail: jest.fn(), - create: jest.fn(), - update: jest.fn(), - remove: jest.fn(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [UserController], - providers: [ - { - provide: UserService, - useValue: userServiceMock, - }, - ], - }).compile(); - - controller = module.get(UserController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/backend/src/modules/user/user.controller.ts b/apps/backend/src/modules/user/user.controller.ts deleted file mode 100644 index 377bef3..0000000 --- a/apps/backend/src/modules/user/user.controller.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Controller, Get, Body, Patch, Param, Delete, HttpCode, UseGuards } from '@nestjs/common'; -import { UserService } from './user.service'; -import { UpdateUserDto } from './dto/update-user.dto'; -import type { CurrentUserType } from '../auth/types/jwt-payload.type'; -import { ParseCuidPipe } from '../../shared/pipes'; -import { JwtAuthGuard } from '../../shared/guards'; -import { ApiOkResponseDto, ApiOkResponsePaginatedDto, CurrentUser } from '../../shared/decorators'; -import { PrivateUserDto, PublicUserDto } from './dto/user.dto'; -import { UpdateAuthUserPasswordDto } from './dto/update-auth-user-password.dto'; -import { ApiResponseHttpCodes } from '../../shared/decorators/api-response-http-codes.decorator'; -import { PaginatedListExampleType } from './dto/PaginatedListExampleType.dto'; -import { PaginatedResult } from '../../shared/types'; - -@Controller('users') -export class UserController { - constructor(private readonly userService: UserService) {} - - @UseGuards(JwtAuthGuard) - @Get('me') - @ApiOkResponseDto(PrivateUserDto) - @ApiResponseHttpCodes(401) - async me(@CurrentUser() user: CurrentUserType): Promise { - return this.userService.me(user.id); - } - - @UseGuards(JwtAuthGuard) - @HttpCode(204) - @Patch('me/password') - @ApiResponseHttpCodes(404, 400, 401) - async updatePassword( - @Body() updatePasswordDto: UpdateAuthUserPasswordDto, - @CurrentUser() user: CurrentUserType - ): Promise { - await this.userService.updatePassword(user, updatePasswordDto); - } - - @UseGuards(JwtAuthGuard) - @Get(':id') - @ApiOkResponseDto(PublicUserDto) - @ApiResponseHttpCodes(404, 401, 400) - async findById(@Param('id', ParseCuidPipe) id: string): Promise { - return this.userService.findPublicById(id); - } - - @UseGuards(JwtAuthGuard) - @Patch('/me') - @ApiOkResponseDto(PrivateUserDto) - @ApiResponseHttpCodes(401, 404, 400) - async update( - @Body() updateUserDto: UpdateUserDto, - @CurrentUser() user: CurrentUserType - ): Promise { - return this.userService.update(user.id, updateUserDto); - } - - @UseGuards(JwtAuthGuard) - @Delete('/me') - @HttpCode(204) - @ApiResponseHttpCodes(401, 404) - async remove(@CurrentUser() user: CurrentUserType): Promise { - await this.userService.remove(user.id); - } - - // Example for paginated list response - @Get('/PaginatedListExample') - @ApiOkResponsePaginatedDto(PaginatedListExampleType) - async PaginatedListExample(): Promise> { - const lst = [ - { id: '1', name: 'test1' }, - { id: '2', name: 'test2' }, - ]; - - return { - items: lst, - total: 2, - page: 1, - limit: 10, - }; - } -} diff --git a/apps/backend/src/modules/user/user.module.ts b/apps/backend/src/modules/user/user.module.ts deleted file mode 100644 index c4bfdc2..0000000 --- a/apps/backend/src/modules/user/user.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UserService } from './user.service'; -import { UserController } from './user.controller'; - -@Module({ - controllers: [UserController], - providers: [UserService], - exports: [UserService], -}) -export class UserModule {} diff --git a/apps/backend/src/modules/user/user.service.spec.ts b/apps/backend/src/modules/user/user.service.spec.ts deleted file mode 100644 index 078770a..0000000 --- a/apps/backend/src/modules/user/user.service.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UserService } from './user.service'; -import { PrismaService } from '../../prisma/prisma.service'; - -describe('UserService', () => { - let service: UserService; - const prismaMock = { - user: { - findMany: jest.fn(), - findUnique: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - }, - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - UserService, - { - provide: PrismaService, - useValue: prismaMock, - }, - ], - }).compile(); - - service = module.get(UserService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/backend/src/modules/user/user.service.ts b/apps/backend/src/modules/user/user.service.ts deleted file mode 100644 index b2564d3..0000000 --- a/apps/backend/src/modules/user/user.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { UpdateUserDto } from './dto/update-user.dto'; -import { PrismaService } from '../../prisma/prisma.service'; -import { CreateUserDto } from './dto/create-user.dto'; -import { DomainError } from '../../shared/errors'; -import { privateUserSelect, publicUserSelect } from './selectors/user.selectors'; -import { PrivateUserDto, PublicUserDto } from './dto/user.dto'; -import bcrypt from 'bcrypt'; -import { UpdateAuthUserPasswordDto } from './dto/update-auth-user-password.dto'; -import { CurrentUserType } from '../auth/types/jwt-payload.type'; -import { hashPassword } from '../auth/utils/hashPassword/hashPassword'; - -@Injectable() -export class UserService { - constructor(private readonly prisma: PrismaService) {} - - async me(id: string): Promise { - const user = await this.prisma.user.findUnique({ - where: { id }, - select: privateUserSelect, - }); - if (!user) { - throw DomainError.Unauthorized(); - } - - return user; - } - - async updatePassword( - currentUser: CurrentUserType, - updatePasswordDto: UpdateAuthUserPasswordDto - ): Promise { - const user = await this.findByEmailWithPasswordOrNull(currentUser.email); - if (!user) { - throw DomainError.NotFound('User not found'); - } - - const isValidPassword = await bcrypt.compare(updatePasswordDto.oldPassword, user.password); - if (!isValidPassword) { - throw DomainError.BadRequest('Invalid password'); - } - - const hashedNewPassword = await hashPassword(updatePasswordDto.newPassword); - - await this.prisma.user.update({ - where: { id: currentUser.id }, - data: { - password: hashedNewPassword, - }, - }); - } - - async findPublicById(id: string): Promise { - const user = await this.prisma.user.findUnique({ - where: { id }, - select: publicUserSelect, - }); - if (!user) { - throw DomainError.NotFound('User not found'); - } - - return user; - } - - async findByEmailWithPasswordOrNull( - email: string - ): Promise<(PrivateUserDto & { password: string }) | null> { - return this.prisma.user.findUnique({ - where: { email }, - select: { - ...privateUserSelect, - password: true, - }, - }); - } - - async create(createUserDto: CreateUserDto): Promise { - await this.prisma.user.create({ - data: createUserDto, - }); - } - - async update(id: string, updateUserDto: UpdateUserDto): Promise { - return await this.prisma.user.update({ - where: { id }, - data: updateUserDto, - select: privateUserSelect, - }); - } - - async remove(id: string): Promise { - await this.prisma.user.delete({ - where: { id }, - }); - } -} diff --git a/apps/backend/src/prisma/prisma.module.ts b/apps/backend/src/prisma/prisma.module.ts deleted file mode 100644 index 7207426..0000000 --- a/apps/backend/src/prisma/prisma.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Global, Module } from '@nestjs/common'; -import { PrismaService } from './prisma.service'; - -@Global() -@Module({ - providers: [PrismaService], - exports: [PrismaService], -}) -export class PrismaModule {} diff --git a/apps/backend/src/prisma/prisma.service.ts b/apps/backend/src/prisma/prisma.service.ts deleted file mode 100644 index 359f950..0000000 --- a/apps/backend/src/prisma/prisma.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { PrismaClient } from '@prisma/client'; - -@Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit { - async onModuleInit() { - await this.$connect(); - } -} diff --git a/apps/backend/src/prisma/seed/seed.ts b/apps/backend/src/prisma/seed/seed.ts deleted file mode 100644 index b53b536..0000000 --- a/apps/backend/src/prisma/seed/seed.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { seedTeamMembers } from './seedTeamMembers'; -import { seedTeam } from './seedTeam'; -import { seedUsers } from './seedUsers'; -import { seedProjects } from './seedTeamProjects'; -import { seedBoards } from './seedBoards'; -import { seedBoardStatusesAndColumns } from './seedBoardColumns'; -import { seedTasks } from './seedTasks'; -import { seedTaskComments } from './seedTaskComments'; -import { seedTaskHistory } from './seedTaskHistory'; - -const prisma = new PrismaClient(); - -async function up() { - const users = await seedUsers(prisma); - - const team = await seedTeam(prisma, users.userTest1.id); - - const { teamMemberAdmin, teamMember } = await seedTeamMembers( - prisma, - team.id, - users.userTest2.id, - users.userTest3.id - ); - - const { project1, project2 } = await seedProjects(prisma, team.id); - - const boards = await seedBoards(prisma, project1.id, project2.id); - - for (const board of boards) { - const statuses = await seedBoardStatusesAndColumns(prisma, board.id); - - const tasks = await seedTasks(prisma, board.id, statuses, teamMemberAdmin.id, teamMember.id); - - await seedTaskComments(prisma, tasks, teamMember.id); - - await seedTaskHistory(prisma, tasks, teamMemberAdmin.id); - } -} - -async function down() { - await prisma.$executeRawUnsafe(` - TRUNCATE TABLE - "TaskComment", - "TaskHistory", - "Task", - "BoardColumn", - "TaskStatus", - "Board", - "Project", - "TeamMember", - "Team", - "User" - RESTART IDENTITY CASCADE - `); -} - -async function main() { - try { - await down(); - await up(); - console.log(); - } catch (e) { - console.log(e); - } -} - -main() - .catch(async (e) => { - console.log(e); - process.exit(1); - }) - .finally(async () => { - prisma.$disconnect(); - }); diff --git a/apps/backend/src/prisma/seed/seedBoardColumns.ts b/apps/backend/src/prisma/seed/seedBoardColumns.ts deleted file mode 100644 index f567d6b..0000000 --- a/apps/backend/src/prisma/seed/seedBoardColumns.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { PrismaClient } from '@prisma/client'; - -export async function seedBoardStatusesAndColumns(prisma: PrismaClient, boardId: string) { - return prisma.$transaction(async (tx) => { - const statuses = [ - { name: 'Todo', position: 1 }, - { name: 'In Progress', position: 2 }, - { name: 'Done', position: 3 }, - ]; - - const createdStatuses = []; - - for (const s of statuses) { - const status = await tx.taskStatus.create({ - data: { - name: s.name, - boardId, - }, - }); - - await tx.boardColumn.create({ - data: { - title: s.name, - position: s.position, - boardId, - statusId: status.id, - }, - }); - - createdStatuses.push(status); - } - - return createdStatuses; - }); -} diff --git a/apps/backend/src/prisma/seed/seedBoards.ts b/apps/backend/src/prisma/seed/seedBoards.ts deleted file mode 100644 index 7134508..0000000 --- a/apps/backend/src/prisma/seed/seedBoards.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { PrismaClient } from '@prisma/client'; - -export async function seedBoards(prisma: PrismaClient, project1Id: string, project2Id: string) { - const board1 = await prisma.board.create({ - data: { - name: 'Frontend React', - projectId: project1Id, - }, - }); - - const board2 = await prisma.board.create({ - data: { - name: 'Backend Express', - projectId: project1Id, - }, - }); - - const board3 = await prisma.board.create({ - data: { - name: 'Frontend React Native', - projectId: project2Id, - }, - }); - - const board4 = await prisma.board.create({ - data: { - name: 'Backend NestJS', - projectId: project2Id, - }, - }); - - return [board1, board2, board3, board4]; -} diff --git a/apps/backend/src/prisma/seed/seedTaskComments.ts b/apps/backend/src/prisma/seed/seedTaskComments.ts deleted file mode 100644 index b9aae54..0000000 --- a/apps/backend/src/prisma/seed/seedTaskComments.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type PrismaClient } from '@prisma/client'; - -export async function seedTaskComments( - prisma: PrismaClient, - tasks: { id: string }[], - teamMemberId: string -) { - const comments = []; - - for (const task of tasks) { - const comment = await prisma.taskComment.create({ - data: { - text: `Comment for task ${task.id}`, - taskId: task.id, - teamMemberId, - }, - }); - - comments.push(comment); - } - - return comments; -} diff --git a/apps/backend/src/prisma/seed/seedTaskHistory.ts b/apps/backend/src/prisma/seed/seedTaskHistory.ts deleted file mode 100644 index 9da066a..0000000 --- a/apps/backend/src/prisma/seed/seedTaskHistory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type PrismaClient } from '@prisma/client'; - -export async function seedTaskHistory( - prisma: PrismaClient, - tasks: { id: string }[], - teamMemberId: string -) { - for (const task of tasks) { - await prisma.taskHistory.create({ - data: { - field: 'title', - oldValue: { status: 'todo' }, - newValue: { status: 'To do' }, - taskId: task.id, - teamMemberId, - }, - }); - } -} diff --git a/apps/backend/src/prisma/seed/seedTasks.ts b/apps/backend/src/prisma/seed/seedTasks.ts deleted file mode 100644 index 8ccdf53..0000000 --- a/apps/backend/src/prisma/seed/seedTasks.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { type PrismaClient, TaskPriority, TaskType } from '@prisma/client'; - -export async function seedTasks( - prisma: PrismaClient, - boardId: string, - statuses: { id: string }[], - creatorId: string, - assigneeId: string -) { - const tasks = []; - const priorities = Object.values(TaskPriority); - const types = Object.values(TaskType); - - for (let i = 1; i <= 10; i++) { - const status = statuses[i % statuses.length]; - const randomPriority = priorities[Math.floor(Math.random() * priorities.length)]; - const randomType = types[Math.floor(Math.random() * types.length)]; - - const task = await prisma.task.create({ - data: { - title: `Task ${i}`, - description: `Description for task ${i}`, - type: randomType, - priority: randomPriority, - boardId, - statusId: status.id, - createdById: creatorId, - assignedToId: assigneeId, - }, - }); - - tasks.push(task); - - const shouldCreateSubtasks = Math.random() < 0.3; - - if (shouldCreateSubtasks) { - const subtasksCount = Math.random() < 0.5 ? 1 : 2; - - for (let j = 1; j <= subtasksCount; j++) { - const subPriority = priorities[Math.floor(Math.random() * priorities.length)]; - const subType = types[Math.floor(Math.random() * types.length)]; - const subStatus = statuses[Math.floor(Math.random() * statuses.length)]; - - const subtask = await prisma.task.create({ - data: { - title: `Task ${i}.${j}`, - description: `Subtask ${j} for task ${i}`, - type: subType, - priority: subPriority, - boardId, - statusId: subStatus.id, - createdById: creatorId, - assignedToId: assigneeId, - parentId: task.id, - }, - }); - - tasks.push(subtask); - } - } - } - - return tasks; -} diff --git a/apps/backend/src/prisma/seed/seedTeam.ts b/apps/backend/src/prisma/seed/seedTeam.ts deleted file mode 100644 index 1567bfe..0000000 --- a/apps/backend/src/prisma/seed/seedTeam.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { type PrismaClient, TeamMemberRole } from '@prisma/client'; - -export async function seedTeam(prisma: PrismaClient, ownerId: string) { - return prisma.$transaction(async (tx) => { - const team = await tx.team.create({ - data: { - name: 'Test Team', - ownerId, - }, - }); - - await tx.teamMember.create({ - data: { - userId: ownerId, - teamId: team.id, - role: TeamMemberRole.OWNER, - }, - }); - - return team; - }); -} diff --git a/apps/backend/src/prisma/seed/seedTeamMembers.ts b/apps/backend/src/prisma/seed/seedTeamMembers.ts deleted file mode 100644 index aeb3ceb..0000000 --- a/apps/backend/src/prisma/seed/seedTeamMembers.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { type PrismaClient, TeamMemberRole } from '@prisma/client'; - -export async function seedTeamMembers( - prisma: PrismaClient, - teamId: string, - userAdminId: string, - userMemberId: string -) { - const teamMemberAdmin = await prisma.teamMember.create({ - data: { - userId: userAdminId, - teamId, - role: TeamMemberRole.ADMIN, - }, - }); - - const teamMember = await prisma.teamMember.create({ - data: { - userId: userMemberId, - teamId, - role: TeamMemberRole.MEMBER, - }, - }); - - return { teamMemberAdmin, teamMember }; -} diff --git a/apps/backend/src/prisma/seed/seedTeamProjects.ts b/apps/backend/src/prisma/seed/seedTeamProjects.ts deleted file mode 100644 index ae29d7f..0000000 --- a/apps/backend/src/prisma/seed/seedTeamProjects.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { PrismaClient } from '@prisma/client'; - -export async function seedProjects(prisma: PrismaClient, teamId: string) { - const project1 = await prisma.project.create({ - data: { - name: 'Desktop App', - teamId, - }, - }); - - const project2 = await prisma.project.create({ - data: { - name: 'Mobile App', - teamId, - }, - }); - - return { project1, project2 }; -} diff --git a/apps/backend/src/prisma/seed/seedUsers.ts b/apps/backend/src/prisma/seed/seedUsers.ts deleted file mode 100644 index af4e9a1..0000000 --- a/apps/backend/src/prisma/seed/seedUsers.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { type PrismaClient, UserRole } from '@prisma/client'; -import { hashPassword } from '../../modules/auth/utils/hashPassword/hashPassword'; - -export async function seedUsers(prisma: PrismaClient) { - const userAdmin = await prisma.user.create({ - data: { - name: 'testAdmin', - email: 'testAdmin@gmail.com', - password: await hashPassword('testAdmin'), - role: UserRole.ADMIN, - }, - }); - - const userModerator = await prisma.user.create({ - data: { - name: 'testModerator', - email: 'testModerator@gmail.com', - password: await hashPassword('testModerator'), - role: UserRole.MODERATOR, - }, - }); - - const userTest1 = await prisma.user.create({ - data: { - name: 'userTest1', - email: 'userTest1@gmail.com', - password: await hashPassword('userTest1'), - }, - }); - - const userTest2 = await prisma.user.create({ - data: { - name: 'userTest2', - email: 'userTest2@gmail.com', - password: await hashPassword('userTest2'), - }, - }); - - const userTest3 = await prisma.user.create({ - data: { - name: 'userTest3', - email: 'userTest3@gmail.com', - password: await hashPassword('userTest3'), - }, - }); - - return { userAdmin, userModerator, userTest1, userTest2, userTest3 }; -} diff --git a/apps/backend/src/shared/decorators/api-response-http-codes.decorator.ts b/apps/backend/src/shared/decorators/api-response-http-codes.decorator.ts deleted file mode 100644 index 0e73bf5..0000000 --- a/apps/backend/src/shared/decorators/api-response-http-codes.decorator.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { ApiResponse } from '@nestjs/swagger'; -import { ApiErrorResponseDto } from '../dto'; - -const ErrorDescriptions: Record = { - 400: 'Bad Request', - 401: 'Not authorized', - 403: 'You have no access', - 404: 'Not found', - 409: 'Conflict', - 500: 'Internal server error', -}; - -export function ApiResponseHttpCodes(...codes: number[]) { - const uniqueCodes = Array.from(new Set([...codes, 500])); - - const decorators = uniqueCodes.map((code) => - ApiResponse({ - status: code, - description: ErrorDescriptions[code] || `Ошибка ${code}`, - type: ApiErrorResponseDto, - }) - ); - - return applyDecorators(...decorators); -} diff --git a/apps/backend/src/shared/decorators/api-response.decorator.ts b/apps/backend/src/shared/decorators/api-response.decorator.ts deleted file mode 100644 index 4b0f143..0000000 --- a/apps/backend/src/shared/decorators/api-response.decorator.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { applyDecorators, Type } from '@nestjs/common'; -import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger'; -import { ApiResponse, ApiResponsePaginated } from '../dto'; - -export const ApiOkResponseDto = >(model: TModel) => { - return applyDecorators( - ApiExtraModels(ApiResponse, model), - ApiOkResponse({ - schema: { - allOf: [ - { $ref: getSchemaPath(ApiResponse) }, - { - properties: { - data: { $ref: getSchemaPath(model) }, - }, - }, - ], - }, - }) - ); -}; - -export const ApiOkResponsePaginatedDto = >(model: TModel) => { - return applyDecorators( - ApiExtraModels(ApiResponsePaginated, model), - ApiOkResponse({ - schema: { - allOf: [ - { $ref: getSchemaPath(ApiResponsePaginated) }, - { - properties: { - data: { - type: 'array', - items: { $ref: getSchemaPath(model) }, - }, - }, - }, - ], - }, - }) - ); -}; diff --git a/apps/backend/src/shared/decorators/current-user.decorator.ts b/apps/backend/src/shared/decorators/current-user.decorator.ts deleted file mode 100644 index a9d7963..0000000 --- a/apps/backend/src/shared/decorators/current-user.decorator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; -import { FastifyRequest } from 'fastify'; -import { type CurrentUserType } from '../../modules/auth/types/jwt-payload.type'; - -export const CurrentUser = createParamDecorator( - (data: keyof CurrentUserType | undefined, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const user = request.user; - - if (!user) { - return null; - } - - return data ? user[data] : user; - } -); diff --git a/apps/backend/src/shared/decorators/index.ts b/apps/backend/src/shared/decorators/index.ts deleted file mode 100644 index ca45d49..0000000 --- a/apps/backend/src/shared/decorators/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { CurrentUser } from './current-user.decorator'; -export { Roles } from './roles.decorator'; -export { ApiOkResponseDto, ApiOkResponsePaginatedDto } from './api-response.decorator'; diff --git a/apps/backend/src/shared/decorators/roles.decorator.ts b/apps/backend/src/shared/decorators/roles.decorator.ts deleted file mode 100644 index ec0c377..0000000 --- a/apps/backend/src/shared/decorators/roles.decorator.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; -import { UserRole } from '@prisma/client'; - -export const ROLES_KEY = 'roles'; -export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/backend/src/shared/dto/api-response-error.dto.ts b/apps/backend/src/shared/dto/api-response-error.dto.ts deleted file mode 100644 index 5bfd1f2..0000000 --- a/apps/backend/src/shared/dto/api-response-error.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class ApiErrorResponseDto { - @ApiProperty({ example: 400 }) - statusCode: number; - - @ApiProperty({ example: 'Invalid value' }) - message: string | string[]; - - @ApiProperty({ example: 'BAD_REQUEST' }) - error: string; - - @ApiProperty({ example: '2026-03-31T14:00:00.000Z' }) - timestamp: string; - - @ApiProperty({ example: '/api/v1/users/me' }) - path: string; -} diff --git a/apps/backend/src/shared/dto/api-response.dto.ts b/apps/backend/src/shared/dto/api-response.dto.ts deleted file mode 100644 index b74897c..0000000 --- a/apps/backend/src/shared/dto/api-response.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class ApiResponse { - @ApiProperty() - data: T; - - constructor(data: T) { - this.data = data; - } -} - -export class ApiMeta { - @ApiProperty() - total: number; - - @ApiProperty() - page: number; - - @ApiProperty() - limit: number; -} - -export class ApiResponsePaginated { - @ApiProperty({ isArray: true }) - data: T[]; - - @ApiProperty() - meta: ApiMeta; - - constructor(data: T[], meta: ApiMeta) { - this.data = data; - this.meta = meta; - } -} diff --git a/apps/backend/src/shared/dto/index.ts b/apps/backend/src/shared/dto/index.ts deleted file mode 100644 index 1c6b6bf..0000000 --- a/apps/backend/src/shared/dto/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { ApiResponse, ApiResponsePaginated } from './api-response.dto'; - -export { ApiErrorResponseDto } from './api-response-error.dto'; diff --git a/apps/backend/src/shared/errors/domain.error.ts b/apps/backend/src/shared/errors/domain.error.ts deleted file mode 100644 index bff5b5c..0000000 --- a/apps/backend/src/shared/errors/domain.error.ts +++ /dev/null @@ -1,29 +0,0 @@ -export class DomainError extends Error { - constructor( - public readonly message: string, - public readonly code: 'NOT_FOUND' | 'CONFLICT' | 'BAD_REQUEST' | 'FORBIDDEN' | 'UNAUTHORIZED' - ) { - super(message); - this.name = 'DomainError'; - } - - static NotFound(message: string = 'Not Found'): DomainError { - return new DomainError(message, 'NOT_FOUND'); - } - - static Unauthorized(message: string = 'Not authorized'): DomainError { - return new DomainError(message, 'UNAUTHORIZED'); - } - - static Conflict(message: string = 'Conflict'): DomainError { - return new DomainError(message, 'CONFLICT'); - } - - static BadRequest(message: string = 'Bad Request'): DomainError { - return new DomainError(message, 'BAD_REQUEST'); - } - - static Forbidden(message: string = "You don't have permissions to do this!"): DomainError { - return new DomainError(message, 'FORBIDDEN'); - } -} diff --git a/apps/backend/src/shared/errors/index.ts b/apps/backend/src/shared/errors/index.ts deleted file mode 100644 index 243da4f..0000000 --- a/apps/backend/src/shared/errors/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DomainError } from './domain.error'; diff --git a/apps/backend/src/shared/filters/all-exception.filter.ts b/apps/backend/src/shared/filters/all-exception.filter.ts deleted file mode 100644 index 2193a82..0000000 --- a/apps/backend/src/shared/filters/all-exception.filter.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, - HttpStatus, - Logger, -} from '@nestjs/common'; -import { FastifyRequest, FastifyReply } from 'fastify'; -import { HttpAdapterHost } from '@nestjs/core'; -import { ApiErrorResponseDto } from '../dto'; - -@Catch() -export class AllExceptionsFilter implements ExceptionFilter { - private readonly logger = new Logger(AllExceptionsFilter.name); - - constructor(private readonly httpAdapterHost: HttpAdapterHost) {} - - catch(exception: unknown, host: ArgumentsHost) { - const { httpAdapter } = this.httpAdapterHost; - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - - let status = HttpStatus.INTERNAL_SERVER_ERROR; - let message: string | string[] = 'Internal server error'; - let errorCode = 'INTERNAL_SERVER_ERROR'; - - if (exception instanceof HttpException) { - status = exception.getStatus(); - const exceptionResponse = exception.getResponse(); - - if (typeof exceptionResponse === 'object' && exceptionResponse !== null) { - const responseObj = exceptionResponse as { - message?: string | string[]; - error?: string; - }; - - message = responseObj.message ?? exception.message; - errorCode = responseObj.error ?? exception.name; - } else { - message = exception.message; - errorCode = exception.name; - } - } else if (exception instanceof Error) { - this.logger.error( - `[${request.method}] ${request.url} - ${exception.message}`, - exception.stack - ); - message = 'Internal server error. Please try again later.'; - errorCode = 'INTERNAL_SERVER_ERROR'; - } - - const responseBody: ApiErrorResponseDto = { - statusCode: status, - message: message, - error: errorCode.replace(/\s+/g, '_').toUpperCase(), - timestamp: new Date().toISOString(), - path: request.url, - }; - - httpAdapter.reply(response, responseBody, status); - } -} diff --git a/apps/backend/src/shared/filters/domain-exception.filter.ts b/apps/backend/src/shared/filters/domain-exception.filter.ts deleted file mode 100644 index 0e8fef5..0000000 --- a/apps/backend/src/shared/filters/domain-exception.filter.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; -import { FastifyRequest, FastifyReply } from 'fastify'; -import { DomainError } from '../errors'; -import { HttpAdapterHost } from '@nestjs/core'; -import { ApiErrorResponseDto } from '../dto'; - -@Catch(DomainError) -export class DomainExceptionFilter implements ExceptionFilter { - constructor(private readonly httpAdapterHost: HttpAdapterHost) {} - - catch(exception: DomainError, host: ArgumentsHost) { - const { httpAdapter } = this.httpAdapterHost; - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - - let status = HttpStatus.INTERNAL_SERVER_ERROR; - - switch (exception.code) { - case 'NOT_FOUND': - status = HttpStatus.NOT_FOUND; - break; - case 'CONFLICT': - status = HttpStatus.CONFLICT; - break; - case 'BAD_REQUEST': - status = HttpStatus.BAD_REQUEST; - break; - case 'FORBIDDEN': - status = HttpStatus.FORBIDDEN; - break; - case 'UNAUTHORIZED': - status = HttpStatus.UNAUTHORIZED; - break; - } - - const responseBody: ApiErrorResponseDto = { - statusCode: status, - message: exception.message, - error: exception.code.replace(/\s+/g, '_').toUpperCase(), - timestamp: new Date().toISOString(), - path: request.url, - }; - - httpAdapter.reply(response, responseBody, status); - } -} diff --git a/apps/backend/src/shared/filters/index.ts b/apps/backend/src/shared/filters/index.ts deleted file mode 100644 index 360b7d9..0000000 --- a/apps/backend/src/shared/filters/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { AllExceptionsFilter } from './all-exception.filter'; -export { DomainExceptionFilter } from './domain-exception.filter'; -export { PrismaExceptionFilter } from './prisma-exception.filter'; diff --git a/apps/backend/src/shared/filters/prisma-exception.filter.ts b/apps/backend/src/shared/filters/prisma-exception.filter.ts deleted file mode 100644 index 4212e0b..0000000 --- a/apps/backend/src/shared/filters/prisma-exception.filter.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'; -import { Prisma } from '@prisma/client'; -import { FastifyRequest, FastifyReply } from 'fastify'; -import { HttpAdapterHost } from '@nestjs/core'; -import { ApiErrorResponseDto } from '../dto'; - -@Catch(Prisma.PrismaClientKnownRequestError) -export class PrismaExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(PrismaExceptionFilter.name); - - constructor(private readonly httpAdapterHost: HttpAdapterHost) {} - - catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) { - const { httpAdapter } = this.httpAdapterHost; - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - - let status = HttpStatus.INTERNAL_SERVER_ERROR; - let message = 'Internal database error'; - - switch (exception.code) { - case 'P2002': - // Unique constraint - status = HttpStatus.CONFLICT; - message = 'Record already exist'; - break; - case 'P2025': // Record is not found for create/update/delete - status = HttpStatus.NOT_FOUND; - message = 'Record not found'; - break; - case 'P2003': // Foreign key constraint - status = HttpStatus.BAD_REQUEST; - message = 'Foreign key constraint failed'; - break; - case 'P2000': { - // Value too long - status = HttpStatus.BAD_REQUEST; - const column = exception.meta?.column_name - ? String(exception.meta.column_name) - : 'UNKNOWN_FIELD'; - message = `The provided value is too long for a field "${column}"`; - break; - } - } - - if (status === HttpStatus.INTERNAL_SERVER_ERROR) { - this.logger.error( - `[${request.method}] ${request.url} - Prisma Error ${exception.code}`, - exception.stack - ); - } else { - this.logger.warn( - `[${request.method}] ${request.url} - Client Error (Prisma ${exception.code}): ${message}` - ); - } - - const responseBody: ApiErrorResponseDto = { - statusCode: status, - message, - error: exception.code, - timestamp: new Date().toISOString(), - path: request.url, - }; - - httpAdapter.reply(response, responseBody, status); - } -} diff --git a/apps/backend/src/shared/guards/index.ts b/apps/backend/src/shared/guards/index.ts deleted file mode 100644 index 79af697..0000000 --- a/apps/backend/src/shared/guards/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { JwtAuthGuard } from './jwt-auth.guard'; -export { RolesGuard } from './roles.guard'; diff --git a/apps/backend/src/shared/guards/jwt-auth.guard.ts b/apps/backend/src/shared/guards/jwt-auth.guard.ts deleted file mode 100644 index 47feae7..0000000 --- a/apps/backend/src/shared/guards/jwt-auth.guard.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AuthGuard } from '@nestjs/passport'; - -export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/apps/backend/src/shared/guards/roles.guard.ts b/apps/backend/src/shared/guards/roles.guard.ts deleted file mode 100644 index 6b5fd80..0000000 --- a/apps/backend/src/shared/guards/roles.guard.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { ROLES_KEY } from '../decorators/roles.decorator'; -import { UserRole } from '@prisma/client'; -import { FastifyRequest } from 'fastify'; -import { CurrentUserType } from '../../modules/auth/types/jwt-payload.type'; -import { DomainError } from '../errors'; - -@Injectable() -export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} - - canActivate(context: ExecutionContext): boolean { - const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ - context.getHandler(), - context.getClass(), - ]); - - if (!requiredRoles) { - return true; - } - - const request = context - .switchToHttp() - .getRequest(); - const user = request.user; - - if (!user) { - throw DomainError.Unauthorized(); - } - - const hasRole = requiredRoles.includes(user.role); - - if (!hasRole) { - throw DomainError.Forbidden(); - } - - return true; - } -} diff --git a/apps/backend/src/shared/interceptors/index.ts b/apps/backend/src/shared/interceptors/index.ts deleted file mode 100644 index d38fa8e..0000000 --- a/apps/backend/src/shared/interceptors/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TransformResponseInterceptor } from './transform-response.interceptor'; diff --git a/apps/backend/src/shared/interceptors/transform-response.interceptor.ts b/apps/backend/src/shared/interceptors/transform-response.interceptor.ts deleted file mode 100644 index 2b85881..0000000 --- a/apps/backend/src/shared/interceptors/transform-response.interceptor.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import { map, Observable } from 'rxjs'; -import { ApiResponse } from '../dto'; -import { FastifyReply } from 'fastify'; - -@Injectable() -export class TransformResponseInterceptor implements NestInterceptor> { - intercept(context: ExecutionContext, next: CallHandler): Observable> { - const ctx = context.switchToHttp(); - const response = ctx.getResponse(); - - return next.handle().pipe( - map((result) => { - if (response.statusCode === 204) { - return undefined; - } - if (result == null) { - return {}; - } - if (result.data) { - return result; - } - if (result.items && result.total !== undefined) { - const { items, ...meta } = result; - return { - data: items, - meta: meta, - }; - } - return { data: result }; - }) - ); - } -} diff --git a/apps/backend/src/shared/pipes/index.ts b/apps/backend/src/shared/pipes/index.ts deleted file mode 100644 index 1ce76b3..0000000 --- a/apps/backend/src/shared/pipes/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ParseCuidPipe } from './parse-cuid.pipe'; diff --git a/apps/backend/src/shared/pipes/parse-cuid.pipe.ts b/apps/backend/src/shared/pipes/parse-cuid.pipe.ts deleted file mode 100644 index a7e345e..0000000 --- a/apps/backend/src/shared/pipes/parse-cuid.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PipeTransform, Injectable } from '@nestjs/common'; -import { DomainError } from '../errors'; - -//reg -const CUID_REGEX = /^c[a-z0-9]{24}$/i; - -@Injectable() -export class ParseCuidPipe implements PipeTransform { - transform(value: string): string { - if (!CUID_REGEX.test(value)) { - throw DomainError.BadRequest('Invalid id'); - } - - return value; - } -} diff --git a/apps/backend/src/shared/types/index.ts b/apps/backend/src/shared/types/index.ts deleted file mode 100644 index aaae623..0000000 --- a/apps/backend/src/shared/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { type PaginatedResult } from './paginated-result.type'; diff --git a/apps/backend/src/shared/types/paginated-result.type.ts b/apps/backend/src/shared/types/paginated-result.type.ts deleted file mode 100644 index 9dbdeb9..0000000 --- a/apps/backend/src/shared/types/paginated-result.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type PaginatedResult = { - items: T[]; - total: number; - page: number; - limit: number; -}; diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json deleted file mode 100644 index 4725270..0000000 --- a/apps/backend/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "declaration": true, - "removeComments": true, - "allowSyntheticDefaultImports": true, - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false, - "paths": { - "@shared-types/*": ["../../packages/shared-types/src/*"] - } - }, - "include": ["src/**/*", "jest.setup.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/apps/frontend/.lintstagedrc.mjs b/apps/frontend/.lintstagedrc.mjs deleted file mode 100644 index 48e2a43..0000000 --- a/apps/frontend/.lintstagedrc.mjs +++ /dev/null @@ -1,5 +0,0 @@ -export default { - '*.{ts,tsx,js,jsx,mjs}': ['eslint --fix --no-warn-ignored', 'prettier --write'], - '*.{json,css,md}': ['prettier --write'], - '*.{ts,tsx}': ['vitest related --run'], -}; diff --git a/apps/frontend/eslint.config.mjs b/apps/frontend/eslint.config.mjs deleted file mode 100644 index 19e833e..0000000 --- a/apps/frontend/eslint.config.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import storybook from 'eslint-plugin-storybook'; -import baseConfig from '../../eslint.config.mjs'; -import { defineConfig, globalIgnores } from 'eslint/config'; -import nextVitals from 'eslint-config-next/core-web-vitals'; -import nextTs from 'eslint-config-next/typescript'; -import pluginQuery from '@tanstack/eslint-plugin-query'; - -const eslintConfig = defineConfig([ - ...baseConfig, - ...nextVitals, - ...nextTs, - ...pluginQuery.configs['flat/recommended'], - ...storybook.configs['flat/recommended'], - globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']), -]); - -export default eslintConfig; diff --git a/apps/frontend/package.json b/apps/frontend/package.json deleted file mode 100644 index e964fe5..0000000 --- a/apps/frontend/package.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "@task-tracker/frontend", - "version": "1.0.0", - "private": "true", - "description": "", - "main": "index.js", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint", - "lint:fix": "eslint --fix", - "lint:fsd": "steiger ./src", - "lint:fsd:watch": "steiger ./src --watch", - "typecheck": "tsc --noEmit", - "format": "prettier --write .", - "format:check": "prettier --check .", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", - "test": "vitest", - "test:ci": "vitest run" - }, - "keywords": [], - "author": "", - "license": "ISC", - "packageManager": "pnpm@10.29.3", - "dependencies": { - "@hookform/resolvers": "^5.2.2", - "@tanstack/react-query": "^5.90.21", - "@tanstack/react-query-devtools": "^5.91.3", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.574.0", - "next": "^16.1.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", - "tailwind-merge": "^3.4.1", - "zod": "^4.3.6", - "zustand": "^5.0.11" - }, - "devDependencies": { - "@feature-sliced/steiger-plugin": "^0.5.7", - "@storybook/nextjs-vite": "^10.3.3", - "@tailwindcss/postcss": "^4.2.0", - "@tanstack/eslint-plugin-query": "^5.91.4", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "eslint-config-next": "^16.1.6", - "eslint-plugin-storybook": "^10.3.3", - "jsdom": "^29.0.1", - "postcss": "^8.5.6", - "shadcn": "^3.8.5", - "steiger": "^0.5.11", - "storybook": "^10.3.3", - "tailwindcss": "^4.2.0", - "tw-animate-css": "^1.4.0", - "vite": "^8.0.2", - "vitest": "^4.1.2" - } -} diff --git a/apps/frontend/src/shared/ui/input.tsx b/apps/frontend/src/shared/ui/input.tsx deleted file mode 100644 index c182d7e..0000000 --- a/apps/frontend/src/shared/ui/input.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react'; - -import { cn } from 'shared/lib/utils'; - -function Input({ className, type, ...props }: React.ComponentProps<'input'>) { - return ( - - ); -} - -export { Input }; diff --git a/apps/frontend/src/shared/ui/textarea.tsx b/apps/frontend/src/shared/ui/textarea.tsx deleted file mode 100644 index 4ddb529..0000000 --- a/apps/frontend/src/shared/ui/textarea.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; - -import { cn } from 'shared/lib/utils'; - -function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { - return ( -