diff --git a/env.example b/env.example index a09cbce..772ff72 100644 --- a/env.example +++ b/env.example @@ -14,6 +14,10 @@ JIRA_BASE_URL="" # https://votre-domaine.atlassian.net JIRA_EMAIL="" # votre.email@domaine.com 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) VERBOSE_LOGGING="false" # Logs détaillés en développement NODE_ENV="development" # development | production diff --git a/package-lock.json b/package-lock.json index 3ee4f23..64feaef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,11 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@prisma/client": "^6.16.1", + "bcryptjs": "^3.0.2", "clsx": "^2.1.1", "date-fns": "^4.1.0", "next": "15.5.3", + "next-auth": "^4.24.11", "prisma": "^6.16.1", "react": "19.1.0", "react-dom": "19.1.0", @@ -24,6 +26,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -47,6 +50,15 @@ "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": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -1746,6 +1758,15 @@ "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": { "version": "6.16.1", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz", @@ -2173,6 +2194,13 @@ "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": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -3143,6 +3171,15 @@ "dev": true, "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": { "version": "1.1.12", "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_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": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5486,6 +5532,15 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5974,6 +6029,18 @@ "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": { "version": "0.30.19", "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": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6194,6 +6293,12 @@ "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6204,6 +6309,15 @@ "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": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6323,6 +6437,30 @@ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "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": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6547,6 +6685,28 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6557,6 +6717,12 @@ "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": { "version": "6.16.1", "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" } }, + "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": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -7950,6 +8125,12 @@ "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": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 63e699f..5ee8f15 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,11 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@prisma/client": "^6.16.1", + "bcryptjs": "^3.0.2", "clsx": "^2.1.1", "date-fns": "^4.1.0", "next": "15.5.3", + "next-auth": "^4.24.11", "prisma": "^6.16.1", "react": "19.1.0", "react-dom": "19.1.0", @@ -38,6 +40,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ab1c360..679d34a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,6 +7,23 @@ datasource db { 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 { id String @id @default(cuid()) title String diff --git a/src/actions/profile.ts b/src/actions/profile.ts new file mode 100644 index 0000000..e4df1ba --- /dev/null +++ b/src/actions/profile.ts @@ -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' } + } +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..b6149fb --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -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 } diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..06cf94e --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -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 } + ) + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index df21886..b9d14e5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext"; import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext"; import { userPreferencesService } from "@/services/core/user-preferences"; import { KeyboardShortcuts } from "@/components/KeyboardShortcuts"; +import { AuthProvider } from "../components/AuthProvider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -36,19 +37,21 @@ export default async function RootLayout({
-