diff --git a/.manus/db/db-query-1763546134103.json b/.manus/db/db-query-1763546134103.json new file mode 100644 index 0000000..6e1ab9a --- /dev/null +++ b/.manus/db/db-query-1763546134103.json @@ -0,0 +1,9 @@ +{ + "query": "UPDATE users \nSET password = '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK',\n role = 'admin',\n emailVerified = TRUE\nWHERE email = 'o.pareige@itinova.org';", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute UPDATE users \nSET password = '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK',\n role = 'admin',\n emailVerified = TRUE\nWHERE email = 'o.pareige@itinova.org';", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 414 +} \ No newline at end of file diff --git a/.manus/db/db-query-1763546215617.json b/.manus/db/db-query-1763546215617.json new file mode 100644 index 0000000..373e4a2 --- /dev/null +++ b/.manus/db/db-query-1763546215617.json @@ -0,0 +1,19 @@ +{ + "query": "SELECT id, email, name, role, password, emailVerified, isActive FROM users WHERE email = 'o.pareige@itinova.org';", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT id, email, name, role, password, emailVerified, isActive FROM users WHERE email = 'o.pareige@itinova.org';", + "rows": [ + { + "id": "1", + "email": "o.pareige@itinova.org", + "name": "olivier pareige", + "role": "admin", + "password": "$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK", + "emailVerified": "1", + "isActive": "1" + } + ], + "messages": [], + "stdout": "id\temail\tname\trole\tpassword\temailVerified\tisActive\n1\to.pareige@itinova.org\tolivier pareige\tadmin\t$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK\t1\t1\n", + "stderr": "", + "execution_time_ms": 49 +} \ No newline at end of file diff --git a/.manus/db/db-query-1763546239968.json b/.manus/db/db-query-1763546239968.json new file mode 100644 index 0000000..6250a6f --- /dev/null +++ b/.manus/db/db-query-1763546239968.json @@ -0,0 +1,9 @@ +{ + "query": "DELETE FROM users WHERE email = 'o.pareige@itinova.org';\n\nINSERT INTO users (email, password, name, role, emailVerified, isActive, createdAt, updatedAt, lastSignedIn)\nVALUES (\n 'o.pareige@itinova.org',\n '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK',\n 'Olivier Pareige',\n 'admin',\n 1,\n 1,\n NOW(),\n NOW(),\n NOW()\n);", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute DELETE FROM users WHERE email = 'o.pareige@itinova.org';\n\nINSERT INTO users (email, password, name, role, emailVerified, isActive, createdAt, updatedAt, lastSignedIn)\nVALUES (\n 'o.pareige@itinova.org',\n '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK',\n 'Olivier Pareige',\n 'admin',\n 1,\n 1,\n NOW(),\n NOW(),\n NOW()\n);", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 71 +} \ No newline at end of file diff --git a/.manus/db/db-query-1763546569851.json b/.manus/db/db-query-1763546569851.json new file mode 100644 index 0000000..b328c16 --- /dev/null +++ b/.manus/db/db-query-1763546569851.json @@ -0,0 +1,16 @@ +{ + "query": "SELECT id, email, password, LENGTH(password) as password_length FROM users WHERE email = 'o.pareige@itinova.org';", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT id, email, password, LENGTH(password) as password_length FROM users WHERE email = 'o.pareige@itinova.org';", + "rows": [ + { + "id": "2010001", + "email": "o.pareige@itinova.org", + "password": "$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK", + "password_length": "60" + } + ], + "messages": [], + "stdout": "id\temail\tpassword\tpassword_length\n2010001\to.pareige@itinova.org\t$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK\t60\n", + "stderr": "", + "execution_time_ms": 141 +} \ No newline at end of file diff --git a/.manus/db/db-query-1763546726252.json b/.manus/db/db-query-1763546726252.json new file mode 100644 index 0000000..f5aeda1 --- /dev/null +++ b/.manus/db/db-query-1763546726252.json @@ -0,0 +1,9 @@ +{ + "query": "UPDATE users SET password = '$2b$10$wtfrg13y5qw/VZKdQfcs4epTfZT2Hje5eomHggJEfSLp9m9ZGlk2O' WHERE email = 'o.pareige@itinova.org';", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute UPDATE users SET password = '$2b$10$wtfrg13y5qw/VZKdQfcs4epTfZT2Hje5eomHggJEfSLp9m9ZGlk2O' WHERE email = 'o.pareige@itinova.org';", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 58 +} \ No newline at end of file diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index c88a2a6..93c4b67 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -97,6 +97,33 @@ export default function DashboardLayout({ ); } + // Vérifier que l'utilisateur est administrateur + if (user.role !== 'admin') { + return ( +
+
+
+
+

Accès refusé

+

+ Vous n'avez pas les permissions nécessaires pour accéder à cette page. +

+
+
+ +
+
+ ); + } + return ( (null); const [createForm, setCreateForm] = useState({ - openId: "", name: "", email: "", + password: "", role: "user" as "user" | "admin", }); const [editForm, setEditForm] = useState({ @@ -60,9 +60,9 @@ export default function AdminUsers() { refetch(); setCreateDialogOpen(false); setCreateForm({ - openId: "", name: "", email: "", + password: "", role: "user", }); }, @@ -118,15 +118,19 @@ export default function AdminUsers() { }; const handleSaveCreate = () => { - if (!createForm.openId) { - toast.error("L'identifiant OpenID est obligatoire"); + if (!createForm.email) { + toast.error("L'email est obligatoire"); + return; + } + if (!createForm.password) { + toast.error("Le mot de passe est obligatoire"); return; } createMutation.mutate({ - openId: createForm.openId, + email: createForm.email, + password: createForm.password, name: createForm.name || undefined, - email: createForm.email || undefined, role: createForm.role, isActive: true, }); @@ -373,12 +377,13 @@ export default function AdminUsers() {
- + setCreateForm({ ...createForm, openId: e.target.value })} - placeholder="Identifiant unique de l'utilisateur" + id="createPassword" + type="password" + value={createForm.password} + onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })} + placeholder="Mot de passe de l'utilisateur" />
diff --git a/generate-hash.mjs b/generate-hash.mjs new file mode 100644 index 0000000..5a9d0da --- /dev/null +++ b/generate-hash.mjs @@ -0,0 +1,12 @@ +import bcrypt from 'bcrypt'; + +const password = 'admin123'; + +bcrypt.hash(password, 10).then(hash => { + console.log('Password:', password); + console.log('Hash:', hash); + console.log('\nSQL to update:'); + console.log(`UPDATE users SET password = '${hash}' WHERE email = 'o.pareige@itinova.org';`); +}).catch(err => { + console.error('Error:', err); +}); diff --git a/package.json b/package.json index ac8cd79..6747d2b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "cookie": "^1.0.2", + "cookie-parser": "^1.4.7", "date-fns": "^4.1.0", "dotenv": "^17.2.2", "drizzle-orm": "^0.44.5", @@ -88,6 +89,7 @@ "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.1.3", "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.10", "@types/express": "4.17.21", "@types/google.maps": "^3.58.1", "@types/jsonwebtoken": "^9.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4c2bbf..8542bba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,9 @@ importers: cookie: specifier: ^1.0.2 version: 1.0.2 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -236,6 +239,9 @@ importers: '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 + '@types/cookie-parser': + specifier: ^1.4.10 + version: 1.4.10(@types/express@4.17.21) '@types/express': specifier: 4.17.21 version: 4.17.21 @@ -2178,6 +2184,11 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} + peerDependencies: + '@types/express': '*' + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -2594,6 +2605,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -2601,6 +2616,10 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -6726,6 +6745,10 @@ snapshots: dependencies: '@types/node': 24.7.0 + '@types/cookie-parser@1.4.10(@types/express@4.17.21)': + dependencies: + '@types/express': 4.17.21 + '@types/d3-array@3.2.2': {} '@types/d3-axis@3.0.6': @@ -7201,10 +7224,17 @@ snapshots: convert-source-map@2.0.0: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + cookie-signature@1.0.6: {} cookie@0.7.1: {} + cookie@0.7.2: {} + cookie@1.0.2: {} copy-anything@3.0.5: diff --git a/server/_core/cookies.ts b/server/_core/cookies.ts index 424872c..664b6de 100644 --- a/server/_core/cookies.ts +++ b/server/_core/cookies.ts @@ -42,7 +42,7 @@ export function getSessionCookieOptions( return { httpOnly: true, path: "/", - sameSite: "none", + sameSite: "lax", secure: isSecureRequest(req), }; } diff --git a/server/_core/env.ts b/server/_core/env.ts index 2792b99..305082c 100644 --- a/server/_core/env.ts +++ b/server/_core/env.ts @@ -1,6 +1,7 @@ export const ENV = { appId: process.env.VITE_APP_ID ?? "", cookieSecret: process.env.JWT_SECRET ?? "", + jwtSecret: process.env.JWT_SECRET ?? "", databaseUrl: process.env.DATABASE_URL ?? "", oAuthServerUrl: process.env.OAUTH_SERVER_URL ?? "", ownerOpenId: process.env.OWNER_OPEN_ID ?? "", diff --git a/server/_core/index.ts b/server/_core/index.ts index f472331..01c7d86 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -2,6 +2,7 @@ import "dotenv/config"; import express from "express"; import { createServer } from "http"; import net from "net"; +import cookieParser from "cookie-parser"; import { createExpressMiddleware } from "@trpc/server/adapters/express"; import { registerOAuthRoutes } from "./oauth"; import { appRouter } from "../routers"; @@ -30,6 +31,8 @@ async function findAvailablePort(startPort: number = 3000): Promise { async function startServer() { const app = express(); const server = createServer(app); + // Configure cookie parser + app.use(cookieParser()); // Configure body parser with larger size limit for file uploads app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: true })); diff --git a/server/routers.ts b/server/routers.ts index 774f4a6..6c7302e 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -55,7 +55,7 @@ export const appRouter = router({ // Définir le cookie de session const cookieOptions = getSessionCookieOptions(ctx.req); - ctx.res.cookie(COOKIE_NAME, token, cookieOptions); + ctx.res.cookie(COOKIE_NAME, token, { ...cookieOptions, maxAge: 30 * 24 * 60 * 60 * 1000 }); // 30 jours return { success: true, userId }; }), @@ -79,8 +79,16 @@ export const appRouter = router({ throw new TRPCError({ code: 'FORBIDDEN', message: 'Votre compte a été désactivé' }); } + // Vérifier que le mot de passe existe + if (!user.password) { + console.error('[LOGIN] User has no password:', user.email); + throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' }); + } + // Vérifier le mot de passe + console.log('[LOGIN] Verifying password for:', user.email); const isValid = await verifyPassword(input.password, user.password); + console.log('[LOGIN] Password valid:', isValid); if (!isValid) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' }); } @@ -93,7 +101,7 @@ export const appRouter = router({ // Définir le cookie de session const cookieOptions = getSessionCookieOptions(ctx.req); - ctx.res.cookie(COOKIE_NAME, token, cookieOptions); + ctx.res.cookie(COOKIE_NAME, token, { ...cookieOptions, maxAge: 30 * 24 * 60 * 60 * 1000 }); // 30 jours return { success: true, user: { id: user.id, email: user.email, name: user.name, role: user.role } }; }), @@ -626,13 +634,32 @@ export const appRouter = router({ }), create: adminProcedure.input(z.object({ - openId: z.string().min(1), + email: z.string().email(), + password: z.string().min(6), name: z.string().optional(), - email: z.string().email().optional(), role: z.enum(["user", "admin"]).default("user"), isActive: z.boolean().default(true), })).mutation(async ({ input }) => { - await db.createUser(input); + const { hashPassword } = await import("./_core/auth"); + + // Vérifier si l'utilisateur existe déjà + const existingUser = await db.getUserByEmail(input.email); + if (existingUser) { + throw new TRPCError({ code: 'CONFLICT', message: 'Un compte existe déjà avec cet email' }); + } + + // Hasher le mot de passe + const hashedPassword = await hashPassword(input.password); + + // Créer l'utilisateur + await db.createUser({ + email: input.email, + password: hashedPassword, + name: input.name, + emailVerified: false, + role: input.role, + isActive: input.isActive, + }); return { success: true }; }), diff --git a/test-bcrypt.mjs b/test-bcrypt.mjs new file mode 100644 index 0000000..297e4da --- /dev/null +++ b/test-bcrypt.mjs @@ -0,0 +1,19 @@ +import bcrypt from 'bcrypt'; + +const password = 'admin123'; +const hash = '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK'; + +console.log('Testing bcrypt...'); +console.log('Password:', password); +console.log('Hash:', hash); + +bcrypt.compare(password, hash).then(result => { + console.log('Result:', result); + if (result) { + console.log('✅ Password is correct!'); + } else { + console.log('❌ Password is incorrect!'); + } +}).catch(err => { + console.error('Error:', err); +}); diff --git a/todo.md b/todo.md index 051b64c..a65d0ef 100644 --- a/todo.md +++ b/todo.md @@ -251,9 +251,13 @@ - [x] Modifier le système de context pour utiliser JWT - [x] Créer la page de connexion - [x] Créer la page d'inscription -- [ ] Modifier la page de gestion des utilisateurs (ajouter mot de passe) -- [ ] Mettre à jour useAuth pour utiliser la nouvelle authentification -- [ ] Tester la connexion et l'inscription +- [x] Modifier la page de gestion des utilisateurs (ajouter mot de passe) +- [x] Mettre à jour useAuth pour utiliser la nouvelle authentification +- [x] Tester le flux de déconnexion et reconnexion +- [x] Mettre à jour AdminUsers pour permettre la création d'utilisateurs avec mot de passe +- [x] Tester l'inscription de nouveaux utilisateurs +- [x] Vérifier que toutes les routes protégées nécessitent une authentification +- [x] Nettoyer le code (supprimer les références à openId si nécessaire) - [ ] Créer un guide de déploiement sans OAuth ## Correction des erreurs de compilation