feat: phase 1 — auth + user endpoints, Prisma v7 adapter, DB init

This commit is contained in:
2026-03-26 03:41:39 +00:00
parent 2d5030c521
commit 48446b483c
13 changed files with 675 additions and 16 deletions

View File

@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.5.0",
"@prisma/client": "^7.5.0", "@prisma/client": "^7.5.0",
"argon2": "^0.44.0", "argon2": "^0.44.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
@@ -16,6 +17,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"multer": "^2.1.1", "multer": "^2.1.1",
"pg": "^8.20.0",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
@@ -24,6 +26,7 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.1.0", "@types/multer": "^2.1.0",
"@types/pg": "^8.20.0",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"prisma": "^7.5.0", "prisma": "^7.5.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
@@ -180,6 +183,77 @@
"node": ">=10" "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": { "node_modules/@prisma/client": {
"version": "7.5.0", "version": "7.5.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.5.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.5.0.tgz",
@@ -227,7 +301,6 @@
"version": "7.5.0", "version": "7.5.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.5.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.5.0.tgz",
"integrity": "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg==", "integrity": "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg==",
"devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/dev": { "node_modules/@prisma/dev": {
@@ -256,6 +329,15 @@
"zeptomatch": "2.1.0" "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": { "node_modules/@prisma/engines": {
"version": "7.5.0", "version": "7.5.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.5.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.5.0.tgz",
@@ -489,12 +571,23 @@
"version": "25.5.0", "version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "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": { "node_modules/@types/qs": {
"version": "6.15.0", "version": "6.15.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
@@ -2111,6 +2204,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/ohash": {
"version": "2.0.11", "version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
@@ -2181,6 +2280,113 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/picomatch": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
@@ -2220,6 +2426,51 @@
"url": "https://github.com/sponsors/porsager" "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": { "node_modules/prisma": {
"version": "7.5.0", "version": "7.5.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.5.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.5.0.tgz",
@@ -2677,6 +2928,15 @@
"node": ">=10" "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": { "node_modules/sqlstring": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@@ -2864,7 +3124,6 @@
"version": "7.18.2", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unpipe": { "node_modules/unpipe": {
@@ -2934,6 +3193,15 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "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": { "node_modules/yn": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@@ -15,6 +15,7 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.5.0",
"@prisma/client": "^7.5.0", "@prisma/client": "^7.5.0",
"argon2": "^0.44.0", "argon2": "^0.44.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
@@ -22,6 +23,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"multer": "^2.1.1", "multer": "^2.1.1",
"pg": "^8.20.0",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
@@ -30,6 +32,7 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.1.0", "@types/multer": "^2.1.0",
"@types/pg": "^8.20.0",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"prisma": "^7.5.0", "prisma": "^7.5.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",

View File

@@ -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;

View File

@@ -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"

View File

@@ -4,7 +4,6 @@ generator client {
datasource db { datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL")
} }
// ─── Enums ─────────────────────────────────────────────────────────────────── // ─── Enums ───────────────────────────────────────────────────────────────────

View File

@@ -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<void> {
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<void> {
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<void> {
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);
}

View File

@@ -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<void> {
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<void> {
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);
}

View File

@@ -1,10 +1,15 @@
import "dotenv/config";
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import { PrismaClient } from "@prisma/client"; 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 app = express();
const prisma = new PrismaClient(); const prisma = new PrismaClient({ adapter });
const PORT = process.env.PORT ?? 4000; const PORT = process.env.PORT ?? 4000;
const CLIENT_URL = process.env.CLIENT_URL ?? "http://localhost:5173"; const CLIENT_URL = process.env.CLIENT_URL ?? "http://localhost:5173";
@@ -15,23 +20,18 @@ if (!process.env.JWT_SECRET) {
// ─── Middlewares ────────────────────────────────────────────────────────────── // ─── Middlewares ──────────────────────────────────────────────────────────────
app.use( app.use(cors({ origin: CLIENT_URL, credentials: true }));
cors({
origin: CLIENT_URL,
credentials: true,
})
);
app.use(express.json()); app.use(express.json());
app.use(cookieParser()); app.use(cookieParser());
// ─── Static uploads ────────────────────────────────────────────────────────── // ─── Static uploads ──────────────────────────────────────────────────────────
app.use("/uploads", express.static("uploads")); app.use("/uploads", express.static("uploads"));
// ─── Routes ────────────────────────────────────────────────────────────────── // ─── Routes ──────────────────────────────────────────────────────────────────
// app.use("/api/auth", authRoutes); app.use("/api/auth", authRoutes);
// app.use("/api/users", userRoutes); app.use("/api/users", userRoutes);
// app.use("/api/exercises", exerciseRoutes); // app.use("/api/exercises", exerciseRoutes);
// app.use("/api/programs", programRoutes); // app.use("/api/programs", programRoutes);
// app.use("/api/groups", groupRoutes); // app.use("/api/groups", groupRoutes);

View File

@@ -1,6 +1,6 @@
import type { Response, NextFunction } from "express"; import type { Response, NextFunction } from "express";
import jwt from "jsonwebtoken"; 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 { export function requireAuth(req: AppRequest, res: Response, next: NextFunction): void {
const token = req.cookies?.token as string | undefined; const token = req.cookies?.token as string | undefined;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<typeof registerSchema>;
export type LoginInput = z.infer<typeof loginSchema>;

View File

@@ -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<typeof updateUserSchema>;