From 48446b483c1e778901b1df2e6bed5bac852dc79b Mon Sep 17 00:00:00 2001 From: Blackstars64 Date: Thu, 26 Mar 2026 03:41:39 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20phase=201=20=E2=80=94=20auth=20+=20user?= =?UTF-8?q?=20endpoints,=20Prisma=20v7=20adapter,=20DB=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package-lock.json | 274 +++++++++++++++++- backend/package.json | 3 + .../20260326033603_init/migration.sql | 206 +++++++++++++ backend/prisma/migrations/migration_lock.toml | 3 + backend/prisma/schema.prisma | 1 - backend/src/controllers/auth.controller.ts | 93 ++++++ backend/src/controllers/user.controller.ts | 41 +++ backend/src/index.ts | 22 +- backend/src/middlewares/auth.ts | 2 +- backend/src/routes/auth.routes.ts | 12 + backend/src/routes/user.routes.ts | 10 + backend/src/validators/auth.validators.ts | 15 + backend/src/validators/user.validators.ts | 9 + 13 files changed, 675 insertions(+), 16 deletions(-) create mode 100644 backend/prisma/migrations/20260326033603_init/migration.sql create mode 100644 backend/prisma/migrations/migration_lock.toml create mode 100644 backend/src/controllers/auth.controller.ts create mode 100644 backend/src/controllers/user.controller.ts create mode 100644 backend/src/routes/auth.routes.ts create mode 100644 backend/src/routes/user.routes.ts create mode 100644 backend/src/validators/auth.validators.ts create mode 100644 backend/src/validators/user.validators.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 36e4024..222ff13 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@prisma/adapter-pg": "^7.5.0", "@prisma/client": "^7.5.0", "argon2": "^0.44.0", "cookie-parser": "^1.4.7", @@ -16,6 +17,7 @@ "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "multer": "^2.1.1", + "pg": "^8.20.0", "zod": "^4.3.6" }, "devDependencies": { @@ -24,6 +26,7 @@ "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.1.0", + "@types/pg": "^8.20.0", "nodemon": "^3.1.14", "prisma": "^7.5.0", "ts-node": "^10.9.2", @@ -180,6 +183,77 @@ "node": ">=10" } }, + "node_modules/@prisma/adapter-pg": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.5.0.tgz", + "integrity": "sha512-EJx7OLULahcC3IjJgdx2qRDNCT+ToY2v66UkeETMCLhNOTgqVzRzYvOEphY7Zp0eHyzfkC33Edd/qqeadf9R4A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.5.0", + "@types/pg": "8.11.11", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/@types/pg": { + "version": "8.11.11", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", + "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/pg-types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.1.0.tgz", + "integrity": "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "license": "MIT", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/@prisma/client": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.5.0.tgz", @@ -227,7 +301,6 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.5.0.tgz", "integrity": "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/dev": { @@ -256,6 +329,15 @@ "zeptomatch": "2.1.0" } }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.5.0.tgz", + "integrity": "sha512-B79N/amgV677mFesFDBAdrW0OIaqawap9E0sjgLBtzIz2R3hIMS1QB8mLZuUEiS4q5Y8Oh3I25Kw4SLxMypk9Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0" + } + }, "node_modules/@prisma/engines": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.5.0.tgz", @@ -489,12 +571,23 @@ "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" } }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", @@ -2111,6 +2204,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -2181,6 +2280,113 @@ "devOptional": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", @@ -2220,6 +2426,51 @@ "url": "https://github.com/sponsors/porsager" } }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "license": "MIT" + }, "node_modules/prisma": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.5.0.tgz", @@ -2677,6 +2928,15 @@ "node": ">=10" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -2864,7 +3124,6 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -2934,6 +3193,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index 2255884..89da7a2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,7 @@ "license": "ISC", "description": "", "dependencies": { + "@prisma/adapter-pg": "^7.5.0", "@prisma/client": "^7.5.0", "argon2": "^0.44.0", "cookie-parser": "^1.4.7", @@ -22,6 +23,7 @@ "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "multer": "^2.1.1", + "pg": "^8.20.0", "zod": "^4.3.6" }, "devDependencies": { @@ -30,6 +32,7 @@ "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.1.0", + "@types/pg": "^8.20.0", "nodemon": "^3.1.14", "prisma": "^7.5.0", "ts-node": "^10.9.2", diff --git a/backend/prisma/migrations/20260326033603_init/migration.sql b/backend/prisma/migrations/20260326033603_init/migration.sql new file mode 100644 index 0000000..377deea --- /dev/null +++ b/backend/prisma/migrations/20260326033603_init/migration.sql @@ -0,0 +1,206 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('USER', 'COACH', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "Difficulty" AS ENUM ('BEGINNER', 'INTERMEDIATE', 'ADVANCED'); + +-- CreateEnum +CREATE TYPE "GroupRole" AS ENUM ('MEMBER', 'COACH', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "FriendStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED'); + +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT 'USER', + "avatar" TEXT, + "bio" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "exercises" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "difficulty" "Difficulty" NOT NULL DEFAULT 'BEGINNER', + "modelPath" TEXT, + "muscleGroups" TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "exercises_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "programs" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "authorId" TEXT NOT NULL, + + CONSTRAINT "programs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "program_exercises" ( + "id" TEXT NOT NULL, + "sets" INTEGER NOT NULL, + "reps" INTEGER, + "durationSec" INTEGER, + "order" INTEGER NOT NULL DEFAULT 0, + "programId" TEXT NOT NULL, + "exerciseId" TEXT NOT NULL, + + CONSTRAINT "program_exercises_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "histories" ( + "id" TEXT NOT NULL, + "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "notes" TEXT, + "userId" TEXT NOT NULL, + + CONSTRAINT "histories_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "history_entries" ( + "id" TEXT NOT NULL, + "sets" INTEGER NOT NULL, + "reps" INTEGER, + "weightKg" DOUBLE PRECISION, + "durationSec" INTEGER, + "historyId" TEXT NOT NULL, + "exerciseId" TEXT NOT NULL, + + CONSTRAINT "history_entries_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "groups" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "groups_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "group_members" ( + "role" "GroupRole" NOT NULL DEFAULT 'MEMBER', + "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "groupId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "group_members_pkey" PRIMARY KEY ("groupId","userId") +); + +-- CreateTable +CREATE TABLE "group_programs" ( + "addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "groupId" TEXT NOT NULL, + "programId" TEXT NOT NULL, + + CONSTRAINT "group_programs_pkey" PRIMARY KEY ("groupId","programId") +); + +-- CreateTable +CREATE TABLE "friend_requests" ( + "id" TEXT NOT NULL, + "status" "FriendStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "senderId" TEXT NOT NULL, + "receiverId" TEXT NOT NULL, + + CONSTRAINT "friend_requests_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rewards" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "iconPath" TEXT, + "condition" TEXT NOT NULL, + + CONSTRAINT "rewards_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_rewards" ( + "earnedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "rewardId" TEXT NOT NULL, + + CONSTRAINT "user_rewards_pkey" PRIMARY KEY ("userId","rewardId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "program_exercises_programId_exerciseId_key" ON "program_exercises"("programId", "exerciseId"); + +-- CreateIndex +CREATE UNIQUE INDEX "friend_requests_senderId_receiverId_key" ON "friend_requests"("senderId", "receiverId"); + +-- AddForeignKey +ALTER TABLE "programs" ADD CONSTRAINT "programs_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "program_exercises" ADD CONSTRAINT "program_exercises_programId_fkey" FOREIGN KEY ("programId") REFERENCES "programs"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "program_exercises" ADD CONSTRAINT "program_exercises_exerciseId_fkey" FOREIGN KEY ("exerciseId") REFERENCES "exercises"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "histories" ADD CONSTRAINT "histories_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "history_entries" ADD CONSTRAINT "history_entries_historyId_fkey" FOREIGN KEY ("historyId") REFERENCES "histories"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "history_entries" ADD CONSTRAINT "history_entries_exerciseId_fkey" FOREIGN KEY ("exerciseId") REFERENCES "exercises"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_members" ADD CONSTRAINT "group_members_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_members" ADD CONSTRAINT "group_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_programs" ADD CONSTRAINT "group_programs_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "groups"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_programs" ADD CONSTRAINT "group_programs_programId_fkey" FOREIGN KEY ("programId") REFERENCES "programs"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "friend_requests" ADD CONSTRAINT "friend_requests_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "friend_requests" ADD CONSTRAINT "friend_requests_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_rewards" ADD CONSTRAINT "user_rewards_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_rewards" ADD CONSTRAINT "user_rewards_rewardId_fkey" FOREIGN KEY ("rewardId") REFERENCES "rewards"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 94f291e..bbbf2ce 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -4,7 +4,6 @@ generator client { datasource db { provider = "postgresql" - url = env("DATABASE_URL") } // ─── Enums ─────────────────────────────────────────────────────────────────── diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts new file mode 100644 index 0000000..71cf94a --- /dev/null +++ b/backend/src/controllers/auth.controller.ts @@ -0,0 +1,93 @@ +import type { Response } from "express"; +import argon2 from "argon2"; +import jwt from "jsonwebtoken"; +import { prisma } from "../index"; +import type { AppRequest } from "../types/context"; +import { loginSchema, registerSchema } from "../validators/auth.validators"; + +const COOKIE_OPTIONS = { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax" as const, + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours +}; + +export async function register(req: AppRequest, res: Response): Promise { + const parsed = registerSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ message: parsed.error.issues[0].message }); + return; + } + + const { username, email, password } = parsed.data; + + const existing = await prisma.user.findFirst({ + where: { OR: [{ email }, { username }] }, + }); + if (existing) { + res.status(409).json({ message: "Email ou username déjà utilisé." }); + return; + } + + const hashed = await argon2.hash(password); + const user = await prisma.user.create({ + data: { username, email, password: hashed }, + }); + + const token = jwt.sign( + { id: user.id, email: user.email, role: user.role }, + process.env.JWT_SECRET as string, + { expiresIn: "7d" } + ); + + res.cookie("token", token, COOKIE_OPTIONS); + res.status(201).json({ id: user.id, username: user.username, email: user.email, role: user.role }); +} + +export async function login(req: AppRequest, res: Response): Promise { + const parsed = loginSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ message: parsed.error.issues[0].message }); + return; + } + + const { email, password } = parsed.data; + + const user = await prisma.user.findUnique({ where: { email } }); + if (!user) { + res.status(401).json({ message: "Identifiants invalides." }); + return; + } + + const valid = await argon2.verify(user.password, password); + if (!valid) { + res.status(401).json({ message: "Identifiants invalides." }); + return; + } + + const token = jwt.sign( + { id: user.id, email: user.email, role: user.role }, + process.env.JWT_SECRET as string, + { expiresIn: "7d" } + ); + + res.cookie("token", token, COOKIE_OPTIONS); + res.json({ id: user.id, username: user.username, email: user.email, role: user.role }); +} + +export function logout(_req: AppRequest, res: Response): void { + res.clearCookie("token"); + res.json({ message: "Déconnecté." }); +} + +export async function me(req: AppRequest, res: Response): Promise { + const user = await prisma.user.findUnique({ + where: { id: req.user!.id }, + select: { id: true, username: true, email: true, role: true, avatar: true, bio: true, createdAt: true }, + }); + if (!user) { + res.status(404).json({ message: "Utilisateur introuvable." }); + return; + } + res.json(user); +} diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts new file mode 100644 index 0000000..24cf13b --- /dev/null +++ b/backend/src/controllers/user.controller.ts @@ -0,0 +1,41 @@ +import type { Response } from "express"; +import { prisma } from "../index"; +import type { AppRequest } from "../types/context"; +import { updateUserSchema } from "../validators/user.validators"; + +export async function getMe(req: AppRequest, res: Response): Promise { + const user = await prisma.user.findUnique({ + where: { id: req.user!.id }, + select: { id: true, username: true, email: true, role: true, avatar: true, bio: true, createdAt: true }, + }); + if (!user) { + res.status(404).json({ message: "Utilisateur introuvable." }); + return; + } + res.json(user); +} + +export async function updateMe(req: AppRequest, res: Response): Promise { + const parsed = updateUserSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ message: parsed.error.issues[0].message }); + return; + } + + if (parsed.data.username) { + const taken = await prisma.user.findFirst({ + where: { username: parsed.data.username, NOT: { id: req.user!.id } }, + }); + if (taken) { + res.status(409).json({ message: "Username déjà utilisé." }); + return; + } + } + + const user = await prisma.user.update({ + where: { id: req.user!.id }, + data: parsed.data, + select: { id: true, username: true, email: true, role: true, avatar: true, bio: true }, + }); + res.json(user); +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 7346e68..421f91a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,10 +1,15 @@ +import "dotenv/config"; import express from "express"; import cors from "cors"; import cookieParser from "cookie-parser"; import { PrismaClient } from "@prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; +import authRoutes from "./routes/auth.routes"; +import userRoutes from "./routes/user.routes"; +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const app = express(); -const prisma = new PrismaClient(); +const prisma = new PrismaClient({ adapter }); const PORT = process.env.PORT ?? 4000; const CLIENT_URL = process.env.CLIENT_URL ?? "http://localhost:5173"; @@ -15,23 +20,18 @@ if (!process.env.JWT_SECRET) { // ─── Middlewares ────────────────────────────────────────────────────────────── -app.use( - cors({ - origin: CLIENT_URL, - credentials: true, - }) -); +app.use(cors({ origin: CLIENT_URL, credentials: true })); app.use(express.json()); app.use(cookieParser()); -// ─── Static uploads ────────────────────────────────────────────────────────── +// ─── Static uploads ─────────────────────────────────────────────────────────── app.use("/uploads", express.static("uploads")); -// ─── Routes ────────────────────────────────────────────────────────────────── +// ─── Routes ─────────────────────────────────────────────────────────────────── -// app.use("/api/auth", authRoutes); -// app.use("/api/users", userRoutes); +app.use("/api/auth", authRoutes); +app.use("/api/users", userRoutes); // app.use("/api/exercises", exerciseRoutes); // app.use("/api/programs", programRoutes); // app.use("/api/groups", groupRoutes); diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index 6af1daf..bfb0012 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -1,6 +1,6 @@ import type { Response, NextFunction } from "express"; import jwt from "jsonwebtoken"; -import type { AppRequest, AuthPayload } from "../types/context.js"; +import type { AppRequest, AuthPayload } from "../types/context"; export function requireAuth(req: AppRequest, res: Response, next: NextFunction): void { const token = req.cookies?.token as string | undefined; diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts new file mode 100644 index 0000000..a4dbeaa --- /dev/null +++ b/backend/src/routes/auth.routes.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import { login, logout, me, register } from "../controllers/auth.controller"; +import { requireAuth } from "../middlewares/auth"; + +const router = Router(); + +router.post("/register", register); +router.post("/login", login); +router.post("/logout", logout); +router.get("/me", requireAuth, me); + +export default router; diff --git a/backend/src/routes/user.routes.ts b/backend/src/routes/user.routes.ts new file mode 100644 index 0000000..d5869bb --- /dev/null +++ b/backend/src/routes/user.routes.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { getMe, updateMe } from "../controllers/user.controller"; +import { requireAuth } from "../middlewares/auth"; + +const router = Router(); + +router.get("/me", requireAuth, getMe); +router.patch("/me", requireAuth, updateMe); + +export default router; diff --git a/backend/src/validators/auth.validators.ts b/backend/src/validators/auth.validators.ts new file mode 100644 index 0000000..ce6b181 --- /dev/null +++ b/backend/src/validators/auth.validators.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const registerSchema = z.object({ + username: z.string().min(3).max(20), + email: z.string().email(), + password: z.string().min(8), +}); + +export const loginSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), +}); + +export type RegisterInput = z.infer; +export type LoginInput = z.infer; diff --git a/backend/src/validators/user.validators.ts b/backend/src/validators/user.validators.ts new file mode 100644 index 0000000..e339030 --- /dev/null +++ b/backend/src/validators/user.validators.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const updateUserSchema = z.object({ + username: z.string().min(3).max(20).optional(), + bio: z.string().max(300).optional(), + avatar: z.string().url().optional(), +}); + +export type UpdateUserInput = z.infer;