feat: add authentication support and user model
- Updated `env.example` to include NextAuth configuration for authentication. - Added `next-auth` dependency to manage user sessions. - Introduced `User` model in Prisma schema with fields for user details and password hashing. - Integrated `AuthProvider` in layout for session management across the app. - Enhanced `Header` component with `AuthButton` for user authentication controls.
This commit is contained in:
@@ -14,6 +14,10 @@ JIRA_BASE_URL="" # https://votre-domaine.atlassian.net
|
|||||||
JIRA_EMAIL="" # votre.email@domaine.com
|
JIRA_EMAIL="" # votre.email@domaine.com
|
||||||
JIRA_API_TOKEN="" # Token API Jira
|
JIRA_API_TOKEN="" # Token API Jira
|
||||||
|
|
||||||
|
# NextAuth (requis)
|
||||||
|
NEXTAUTH_URL="http://localhost:3000" # URL de votre application
|
||||||
|
NEXTAUTH_SECRET="your-secret-key-here" # Clé secrète pour signer les tokens
|
||||||
|
|
||||||
# Debug (optionnel)
|
# Debug (optionnel)
|
||||||
VERBOSE_LOGGING="false" # Logs détaillés en développement
|
VERBOSE_LOGGING="false" # Logs détaillés en développement
|
||||||
NODE_ENV="development" # development | production
|
NODE_ENV="development" # development | production
|
||||||
|
|||||||
181
package-lock.json
generated
181
package-lock.json
generated
@@ -12,9 +12,11 @@
|
|||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@prisma/client": "^6.16.1",
|
"@prisma/client": "^6.16.1",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
|
"next-auth": "^4.24.11",
|
||||||
"prisma": "^6.16.1",
|
"prisma": "^6.16.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -47,6 +50,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dnd-kit/accessibility": {
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
@@ -1746,6 +1758,15 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@panva/hkdf": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "6.16.1",
|
"version": "6.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz",
|
||||||
@@ -2173,6 +2194,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcryptjs": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-array": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
@@ -3143,6 +3171,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@@ -3398,6 +3435,15 @@
|
|||||||
"node": "^14.18.0 || >=16.10.0"
|
"node": "^14.18.0 || >=16.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -5486,6 +5532,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "4.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||||
|
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -5974,6 +6029,18 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.19",
|
"version": "0.30.19",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||||
@@ -6141,6 +6208,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth": {
|
||||||
|
"version": "4.24.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz",
|
||||||
|
"integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@panva/hkdf": "^1.0.2",
|
||||||
|
"cookie": "^0.7.0",
|
||||||
|
"jose": "^4.15.5",
|
||||||
|
"oauth": "^0.9.15",
|
||||||
|
"openid-client": "^5.4.0",
|
||||||
|
"preact": "^10.6.3",
|
||||||
|
"preact-render-to-string": "^5.1.19",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@auth/core": "0.34.2",
|
||||||
|
"next": "^12.2.5 || ^13 || ^14 || ^15",
|
||||||
|
"nodemailer": "^6.6.5",
|
||||||
|
"react": "^17.0.2 || ^18 || ^19",
|
||||||
|
"react-dom": "^17.0.2 || ^18 || ^19"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@auth/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/postcss": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
@@ -6194,6 +6293,12 @@
|
|||||||
"node": "^14.16.0 || >=16.10.0"
|
"node": "^14.16.0 || >=16.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth": {
|
||||||
|
"version": "0.9.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||||
|
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -6204,6 +6309,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-hash": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
@@ -6323,6 +6437,30 @@
|
|||||||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/oidc-token-hash": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || >=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^4.15.9",
|
||||||
|
"lru-cache": "^6.0.0",
|
||||||
|
"object-hash": "^2.2.0",
|
||||||
|
"oidc-token-hash": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -6547,6 +6685,28 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
|
||||||
|
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact-render-to-string": {
|
||||||
|
"version": "5.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
||||||
|
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pretty-format": "^3.8.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -6557,6 +6717,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "3.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||||
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.16.1",
|
"version": "6.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.1.tgz",
|
||||||
@@ -7803,6 +7969,15 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/victory-vendor": {
|
"node_modules/victory-vendor": {
|
||||||
"version": "37.3.6",
|
"version": "37.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
@@ -7950,6 +8125,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -26,9 +26,11 @@
|
|||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@prisma/client": "^6.16.1",
|
"@prisma/client": "^6.16.1",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
|
"next-auth": "^4.24.11",
|
||||||
"prisma": "^6.16.1",
|
"prisma": "^6.16.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
@@ -7,6 +7,23 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String?
|
||||||
|
firstName String?
|
||||||
|
lastName String?
|
||||||
|
avatar String? // URL de l'avatar
|
||||||
|
role String @default("user") // user, admin, etc.
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
lastLoginAt DateTime?
|
||||||
|
password String // Hashé avec bcrypt
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
model Task {
|
model Task {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
title String
|
title String
|
||||||
|
|||||||
103
src/actions/profile.ts
Normal file
103
src/actions/profile.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { getServerSession } from 'next-auth/next'
|
||||||
|
import { authOptions } from '@/lib/auth'
|
||||||
|
import { usersService } from '@/services/users'
|
||||||
|
import { revalidatePath } from 'next/cache'
|
||||||
|
|
||||||
|
export async function updateProfile(formData: {
|
||||||
|
name?: string
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
avatar?: string
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return { success: false, error: 'Non authentifié' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (formData.firstName && formData.firstName.length > 50) {
|
||||||
|
return { success: false, error: 'Le prénom ne peut pas dépasser 50 caractères' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.lastName && formData.lastName.length > 50) {
|
||||||
|
return { success: false, error: 'Le nom ne peut pas dépasser 50 caractères' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.name && formData.name.length > 100) {
|
||||||
|
return { success: false, error: 'Le nom d\'affichage ne peut pas dépasser 100 caractères' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.avatar && formData.avatar.length > 500) {
|
||||||
|
return { success: false, error: 'L\'URL de l\'avatar ne peut pas dépasser 500 caractères' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour l'utilisateur
|
||||||
|
const updatedUser = await usersService.updateUser(session.user.id, {
|
||||||
|
name: formData.name || null,
|
||||||
|
firstName: formData.firstName || null,
|
||||||
|
lastName: formData.lastName || null,
|
||||||
|
avatar: formData.avatar || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Revalider la page de profil
|
||||||
|
revalidatePath('/profile')
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
id: updatedUser.id,
|
||||||
|
email: updatedUser.email,
|
||||||
|
name: updatedUser.name,
|
||||||
|
firstName: updatedUser.firstName,
|
||||||
|
lastName: updatedUser.lastName,
|
||||||
|
avatar: updatedUser.avatar,
|
||||||
|
role: updatedUser.role,
|
||||||
|
createdAt: updatedUser.createdAt.toISOString(),
|
||||||
|
lastLoginAt: updatedUser.lastLoginAt?.toISOString() || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Profile update error:', error)
|
||||||
|
return { success: false, error: 'Erreur lors de la mise à jour du profil' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProfile() {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return { success: false, error: 'Non authentifié' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await usersService.getUserById(session.user.id)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return { success: false, error: 'Utilisateur non trouvé' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
avatar: user.avatar,
|
||||||
|
role: user.role,
|
||||||
|
createdAt: user.createdAt.toISOString(),
|
||||||
|
lastLoginAt: user.lastLoginAt?.toISOString() || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Profile get error:', error)
|
||||||
|
return { success: false, error: 'Erreur lors de la récupération du profil' }
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/app/api/auth/[...nextauth]/route.ts
Normal file
6
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import NextAuth from "next-auth"
|
||||||
|
import { authOptions } from "@/lib/auth"
|
||||||
|
|
||||||
|
const handler = NextAuth(authOptions)
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST }
|
||||||
59
src/app/api/auth/register/route.ts
Normal file
59
src/app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { usersService } from '@/services/users'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { email, name, firstName, lastName, password } = await request.json()
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!email || !password) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Email et mot de passe requis' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Le mot de passe doit contenir au moins 6 caractères' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'email existe déjà
|
||||||
|
const emailExists = await usersService.emailExists(email)
|
||||||
|
if (emailExists) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Un compte avec cet email existe déjà' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer l'utilisateur
|
||||||
|
const user = await usersService.createUser({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Compte créé avec succès',
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erreur lors de la création du compte' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext";
|
|||||||
import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext";
|
import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext";
|
||||||
import { userPreferencesService } from "@/services/core/user-preferences";
|
import { userPreferencesService } from "@/services/core/user-preferences";
|
||||||
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
|
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
|
||||||
|
import { AuthProvider } from "../components/AuthProvider";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -36,19 +37,21 @@ export default async function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<ThemeProvider
|
<AuthProvider>
|
||||||
initialTheme={initialPreferences.viewPreferences.theme}
|
<ThemeProvider
|
||||||
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
|
initialTheme={initialPreferences.viewPreferences.theme}
|
||||||
>
|
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
|
||||||
<KeyboardShortcutsProvider>
|
>
|
||||||
<KeyboardShortcuts />
|
<KeyboardShortcutsProvider>
|
||||||
<JiraConfigProvider config={initialPreferences.jiraConfig}>
|
<KeyboardShortcuts />
|
||||||
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
<JiraConfigProvider config={initialPreferences.jiraConfig}>
|
||||||
{children}
|
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||||||
</UserPreferencesProvider>
|
{children}
|
||||||
</JiraConfigProvider>
|
</UserPreferencesProvider>
|
||||||
</KeyboardShortcutsProvider>
|
</JiraConfigProvider>
|
||||||
</ThemeProvider>
|
</KeyboardShortcutsProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
127
src/app/login/page.tsx
Normal file
127
src/app/login/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { signIn, getSession } from 'next-auth/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { TowerLogo } from '@/components/TowerLogo'
|
||||||
|
import { TowerBackground } from '@/components/TowerBackground'
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsLoading(true)
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await signIn('credentials', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
redirect: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
setError('Email ou mot de passe incorrect')
|
||||||
|
} else {
|
||||||
|
// Vérifier que la session est bien créée
|
||||||
|
const session = await getSession()
|
||||||
|
if (session) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError('Une erreur est survenue')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen relative overflow-hidden">
|
||||||
|
<TowerBackground />
|
||||||
|
|
||||||
|
{/* Contenu principal */}
|
||||||
|
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
{/* Logo et titre */}
|
||||||
|
<TowerLogo size="md" className="mb-8" />
|
||||||
|
|
||||||
|
{/* Formulaire */}
|
||||||
|
<div className="bg-[var(--card)]/80 backdrop-blur-sm rounded-2xl shadow-xl border border-[var(--border)] p-8">
|
||||||
|
<h2 className="text-2xl font-mono font-bold text-[var(--foreground)] text-center mb-6">
|
||||||
|
Connexion
|
||||||
|
</h2>
|
||||||
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder=""
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder=""
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-[var(--destructive)] text-sm text-center bg-[var(--destructive)]/10 p-3 rounded-lg border border-[var(--destructive)]/20">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full py-3 text-lg font-mono"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Connexion...' : 'Se connecter'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center text-sm text-[var(--muted-foreground)]">
|
||||||
|
<p>
|
||||||
|
Pas encore de compte ?{' '}
|
||||||
|
<Link href="/register" className="text-[var(--primary)] hover:underline font-medium">
|
||||||
|
Créer un compte
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
279
src/app/profile/page.tsx
Normal file
279
src/app/profile/page.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect, useTransition } from 'react'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { Header } from '@/components/ui/Header'
|
||||||
|
import { updateProfile, getProfile } from '@/actions/profile'
|
||||||
|
|
||||||
|
interface UserProfile {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
name: string | null
|
||||||
|
firstName: string | null
|
||||||
|
lastName: string | null
|
||||||
|
avatar: string | null
|
||||||
|
role: string
|
||||||
|
createdAt: string
|
||||||
|
lastLoginAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const { data: session, update } = useSession()
|
||||||
|
const router = useRouter()
|
||||||
|
const [isPending, startTransition] = useTransition()
|
||||||
|
const [profile, setProfile] = useState<UserProfile | null>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [success, setSuccess] = useState('')
|
||||||
|
|
||||||
|
// Form data
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
avatar: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session) {
|
||||||
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProfile()
|
||||||
|
}, [session, router])
|
||||||
|
|
||||||
|
const fetchProfile = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getProfile()
|
||||||
|
if (!result.success || !result.user) {
|
||||||
|
throw new Error(result.error || 'Erreur lors du chargement du profil')
|
||||||
|
}
|
||||||
|
setProfile(result.user)
|
||||||
|
setFormData({
|
||||||
|
name: result.user.name || '',
|
||||||
|
firstName: result.user.firstName || '',
|
||||||
|
lastName: result.user.lastName || '',
|
||||||
|
avatar: result.user.avatar || '',
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
setError(error instanceof Error ? error.message : 'Erreur inconnue')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setSuccess('')
|
||||||
|
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
const result = await updateProfile(formData)
|
||||||
|
|
||||||
|
if (!result.success || !result.user) {
|
||||||
|
setError(result.error || 'Erreur lors de la mise à jour')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setProfile(result.user)
|
||||||
|
setSuccess('Profil mis à jour avec succès')
|
||||||
|
|
||||||
|
// Mettre à jour la session NextAuth
|
||||||
|
await update({
|
||||||
|
...session,
|
||||||
|
user: {
|
||||||
|
...session?.user,
|
||||||
|
name: result.user.name || `${result.user.firstName || ''} ${result.user.lastName || ''}`.trim() || result.user.email,
|
||||||
|
firstName: result.user.firstName,
|
||||||
|
lastName: result.user.lastName,
|
||||||
|
avatar: result.user.avatar,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
setError(error instanceof Error ? error.message : 'Erreur inconnue')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
|
<Header title="TowerControl" subtitle="Profil utilisateur" />
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div className="text-center text-[var(--muted-foreground)]">
|
||||||
|
Chargement du profil...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
|
<Header title="TowerControl" subtitle="Profil utilisateur" />
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div className="text-center text-[var(--destructive)]">
|
||||||
|
Erreur lors du chargement du profil
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
|
<Header title="TowerControl" subtitle="Profil utilisateur" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
{/* Informations générales */}
|
||||||
|
<div className="bg-[var(--card)] rounded-lg p-6 mb-6">
|
||||||
|
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-4">
|
||||||
|
Informations générales
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||||
|
{profile.email}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||||
|
L'email ne peut pas être modifié
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||||
|
Rôle
|
||||||
|
</label>
|
||||||
|
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||||
|
{profile.role}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||||
|
Membre depuis
|
||||||
|
</label>
|
||||||
|
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||||
|
{new Date(profile.createdAt).toLocaleDateString('fr-FR')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{profile.lastLoginAt && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||||
|
Dernière connexion
|
||||||
|
</label>
|
||||||
|
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||||
|
{new Date(profile.lastLoginAt).toLocaleString('fr-FR')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Formulaire de modification */}
|
||||||
|
<form onSubmit={handleSubmit} className="bg-[var(--card)] rounded-lg p-6">
|
||||||
|
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-4">
|
||||||
|
Modifier le profil
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Prénom
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="firstName"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={(e) => handleChange('firstName', e.target.value)}
|
||||||
|
placeholder="Votre prénom"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Nom de famille
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="lastName"
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={(e) => handleChange('lastName', e.target.value)}
|
||||||
|
placeholder="Votre nom de famille"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Nom d'affichage (optionnel)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleChange('name', e.target.value)}
|
||||||
|
placeholder="Nom d'affichage personnalisé"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||||
|
Si vide, sera généré automatiquement à partir du prénom et nom
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="avatar" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
URL de l'avatar (optionnel)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="avatar"
|
||||||
|
value={formData.avatar}
|
||||||
|
onChange={(e) => handleChange('avatar', e.target.value)}
|
||||||
|
placeholder="https://example.com/avatar.jpg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 text-[var(--destructive)] text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="mt-4 text-[var(--success)] text-sm">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isPending ? 'Sauvegarde...' : 'Sauvegarder les modifications'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
213
src/app/register/page.tsx
Normal file
213
src/app/register/page.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { TowerLogo } from '@/components/TowerLogo'
|
||||||
|
import { TowerBackground } from '@/components/TowerBackground'
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [firstName, setFirstName] = useState('')
|
||||||
|
const [lastName, setLastName] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsLoading(true)
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
// Validation côté client
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Les mots de passe ne correspondent pas')
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError('Le mot de passe doit contenir au moins 6 caractères')
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Une erreur est survenue')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rediriger vers la page de login avec un message de succès
|
||||||
|
router.push('/login?message=Compte créé avec succès')
|
||||||
|
} catch (error) {
|
||||||
|
setError(error instanceof Error ? error.message : 'Une erreur est survenue')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen relative overflow-hidden">
|
||||||
|
<TowerBackground />
|
||||||
|
|
||||||
|
{/* Contenu principal */}
|
||||||
|
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
{/* Logo et titre */}
|
||||||
|
<TowerLogo size="md" className="mb-8" />
|
||||||
|
|
||||||
|
{/* Formulaire */}
|
||||||
|
<div className="bg-[var(--card)]/80 backdrop-blur-sm rounded-2xl shadow-xl border border-[var(--border)] p-8">
|
||||||
|
<h2 className="text-2xl font-mono font-bold text-[var(--foreground)] text-center mb-6">
|
||||||
|
Créer un compte
|
||||||
|
</h2>
|
||||||
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="votre@email.com"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Prénom
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="firstName"
|
||||||
|
name="firstName"
|
||||||
|
type="text"
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
placeholder="Prénom"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Nom
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="lastName"
|
||||||
|
name="lastName"
|
||||||
|
type="text"
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
placeholder="Nom"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Nom d'affichage (optionnel)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Nom d'affichage personnalisé"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Minimum 6 caractères"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
|
Confirmer le mot de passe
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="Répétez le mot de passe"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-[var(--destructive)] text-sm text-center bg-[var(--destructive)]/10 p-3 rounded-lg border border-[var(--destructive)]/20">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full py-3 text-lg font-mono"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Création du compte...' : 'Créer le compte'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center text-sm text-[var(--muted-foreground)]">
|
||||||
|
<p>
|
||||||
|
Déjà un compte ?{' '}
|
||||||
|
<Link href="/login" className="text-[var(--primary)] hover:underline font-medium">
|
||||||
|
Se connecter
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/components/AuthButton.tsx
Normal file
56
src/components/AuthButton.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useSession, signOut } from 'next-auth/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
|
||||||
|
export function AuthButton() {
|
||||||
|
const { data: session, status } = useSession()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="text-[var(--muted-foreground)] text-sm">
|
||||||
|
Chargement...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push('/login')}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Se connecter
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push('/profile')}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="p-1 h-auto"
|
||||||
|
title={`Profil - ${session.user?.email}`}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="p-1 h-auto"
|
||||||
|
title="Déconnexion"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/components/AuthProvider.tsx
Normal file
7
src/components/AuthProvider.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { SessionProvider } from "next-auth/react"
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
return <SessionProvider>{children}</SessionProvider>
|
||||||
|
}
|
||||||
11
src/components/TowerBackground.tsx
Normal file
11
src/components/TowerBackground.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export function TowerBackground() {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--primary)]/20 via-[var(--background)] to-[var(--accent)]/20">
|
||||||
|
{/* Effet de profondeur */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent"></div>
|
||||||
|
|
||||||
|
{/* Effet de lumière */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-transparent via-[var(--primary)]/5 to-transparent"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
37
src/components/TowerLogo.tsx
Normal file
37
src/components/TowerLogo.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
interface TowerLogoProps {
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
showText?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TowerLogo({ size = 'md', showText = true, className = '' }: TowerLogoProps) {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'w-12 h-12',
|
||||||
|
md: 'w-20 h-20',
|
||||||
|
lg: 'w-32 h-32'
|
||||||
|
}
|
||||||
|
|
||||||
|
const textSizes = {
|
||||||
|
sm: 'text-2xl',
|
||||||
|
md: 'text-4xl',
|
||||||
|
lg: 'text-6xl'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`text-center ${className}`}>
|
||||||
|
<div className={`inline-flex items-center justify-center ${sizeClasses[size]} rounded-2xl mb-4 text-6xl`}>
|
||||||
|
🗼
|
||||||
|
</div>
|
||||||
|
{showText && (
|
||||||
|
<>
|
||||||
|
<h1 className={`${textSizes[size]} font-mono font-bold text-[var(--foreground)] mb-2`}>
|
||||||
|
TowerControl
|
||||||
|
</h1>
|
||||||
|
<p className="text-[var(--muted-foreground)] text-lg">
|
||||||
|
Tour de contrôle de vos projets
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { useState } from 'react';
|
|||||||
import { Theme } from '@/lib/theme-config';
|
import { Theme } from '@/lib/theme-config';
|
||||||
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
||||||
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
|
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
|
||||||
|
import { AuthButton } from '@/components/AuthButton';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -173,6 +174,12 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auth controls à droite mobile */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<AuthButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -264,9 +271,15 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Contrôles à droite */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AuthButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
72
src/lib/auth.ts
Normal file
72
src/lib/auth.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { NextAuthOptions } from "next-auth"
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials"
|
||||||
|
import { usersService } from "@/services/users"
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
providers: [
|
||||||
|
CredentialsProvider({
|
||||||
|
name: "credentials",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "email" },
|
||||||
|
password: { label: "Password", type: "password" }
|
||||||
|
},
|
||||||
|
async authorize(credentials) {
|
||||||
|
if (!credentials?.email || !credentials?.password) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Chercher l'utilisateur dans la base de données
|
||||||
|
const user = await usersService.getUserByEmail(credentials.email)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le mot de passe
|
||||||
|
const isValidPassword = await usersService.verifyPassword(
|
||||||
|
credentials.password,
|
||||||
|
user.password
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.email,
|
||||||
|
firstName: user.firstName || undefined,
|
||||||
|
lastName: user.lastName || undefined,
|
||||||
|
avatar: user.avatar || undefined,
|
||||||
|
role: user.role,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
pages: {
|
||||||
|
signIn: "/login",
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, user }) {
|
||||||
|
if (user) {
|
||||||
|
token.id = user.id
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
if (token && session.user) {
|
||||||
|
session.user.id = token.id as string
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
33
src/middleware.ts
Normal file
33
src/middleware.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { withAuth } from "next-auth/middleware"
|
||||||
|
|
||||||
|
export default withAuth(
|
||||||
|
function middleware() {
|
||||||
|
// Le middleware s'exécute seulement si l'utilisateur est authentifié
|
||||||
|
// grâce à withAuth
|
||||||
|
},
|
||||||
|
{
|
||||||
|
callbacks: {
|
||||||
|
authorized: ({ token }) => {
|
||||||
|
// Vérifier si l'utilisateur a un token valide
|
||||||
|
return !!token
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - api/auth (NextAuth routes)
|
||||||
|
* - login (login page)
|
||||||
|
* - register (registration page)
|
||||||
|
* - profile (profile page)
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
* - public files (images, etc.)
|
||||||
|
*/
|
||||||
|
'/((?!api/auth|login|register|profile|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||||
|
],
|
||||||
|
}
|
||||||
150
src/services/users.ts
Normal file
150
src/services/users.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { prisma } from './core/database'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
export interface CreateUserData {
|
||||||
|
email: string
|
||||||
|
name?: string
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
avatar?: string
|
||||||
|
role?: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
name: string | null
|
||||||
|
firstName: string | null
|
||||||
|
lastName: string | null
|
||||||
|
avatar: string | null
|
||||||
|
role: string
|
||||||
|
isActive: boolean
|
||||||
|
lastLoginAt: Date | null
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usersService = {
|
||||||
|
async createUser(data: CreateUserData): Promise<User> {
|
||||||
|
const hashedPassword = await bcrypt.hash(data.password, 12)
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: data.email,
|
||||||
|
name: data.name,
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
avatar: data.avatar,
|
||||||
|
role: data.role || 'user',
|
||||||
|
password: hashedPassword,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
avatar: true,
|
||||||
|
role: true,
|
||||||
|
isActive: true,
|
||||||
|
lastLoginAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserByEmail(email: string) {
|
||||||
|
return await prisma.user.findUnique({
|
||||||
|
where: { email },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
avatar: true,
|
||||||
|
role: true,
|
||||||
|
isActive: true,
|
||||||
|
lastLoginAt: true,
|
||||||
|
password: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserById(id: string): Promise<User | null> {
|
||||||
|
return await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
avatar: true,
|
||||||
|
role: true,
|
||||||
|
isActive: true,
|
||||||
|
lastLoginAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||||
|
return await bcrypt.compare(password, hashedPassword)
|
||||||
|
},
|
||||||
|
|
||||||
|
async emailExists(email: string): Promise<boolean> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email },
|
||||||
|
select: { id: true }
|
||||||
|
})
|
||||||
|
return !!user
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateLastLogin(userId: string): Promise<void> {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { lastLoginAt: new Date() }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUser(userId: string, data: {
|
||||||
|
name?: string | null
|
||||||
|
firstName?: string | null
|
||||||
|
lastName?: string | null
|
||||||
|
avatar?: string | null
|
||||||
|
}): Promise<User> {
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
avatar: data.avatar,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
avatar: true,
|
||||||
|
role: true,
|
||||||
|
isActive: true,
|
||||||
|
lastLoginAt: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/types/next-auth.d.ts
vendored
Normal file
31
src/types/next-auth.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import NextAuth from "next-auth"
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
name: string
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
avatar?: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
name: string
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
avatar?: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "next-auth/jwt" {
|
||||||
|
interface JWT {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user