From 0e6bc8c640b5a7c94a11f60077eb841fc81bd5cd Mon Sep 17 00:00:00 2001 From: Bert Hausmans Date: Wed, 20 May 2026 22:45:21 +0200 Subject: [PATCH] feat(auth): password hashing service --- package-lock.json | 18 ++++++++++++++++++ packages/backend/package.json | 2 ++ .../src/services/auth/passwords.test.ts | 19 +++++++++++++++++++ .../backend/src/services/auth/passwords.ts | 15 +++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 packages/backend/src/services/auth/passwords.test.ts create mode 100644 packages/backend/src/services/auth/passwords.ts diff --git a/package-lock.json b/package-lock.json index 8cdfd7f..908255a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2005,6 +2005,13 @@ "@babel/types": "^7.28.2" } }, + "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/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -2559,6 +2566,15 @@ "node": ">=6.0.0" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/better-sqlite3": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", @@ -7838,6 +7854,7 @@ "version": "0.1.0", "dependencies": { "@flashcard/shared": "*", + "bcryptjs": "^3.0.3", "better-sqlite3": "^11.0.0", "drizzle-orm": "^0.33.0", "express": "^4.19.0", @@ -7846,6 +7863,7 @@ "zod": "^3.23.0" }, "devDependencies": { + "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.0", "@types/express": "^4.17.0", "@types/multer": "^1.4.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 902964d..4736b46 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@flashcard/shared": "*", + "bcryptjs": "^3.0.3", "better-sqlite3": "^11.0.0", "drizzle-orm": "^0.33.0", "express": "^4.19.0", @@ -25,6 +26,7 @@ "zod": "^3.23.0" }, "devDependencies": { + "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.0", "@types/express": "^4.17.0", "@types/multer": "^1.4.0", diff --git a/packages/backend/src/services/auth/passwords.test.ts b/packages/backend/src/services/auth/passwords.test.ts new file mode 100644 index 0000000..ea6cf32 --- /dev/null +++ b/packages/backend/src/services/auth/passwords.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { hashPassword, verifyPassword } from './passwords.js'; + +describe('passwords', () => { + it('hashes a password and verifies it', async () => { + const hash = await hashPassword('correcthorse'); + expect(hash).toMatch(/^\$2[aby]\$/); + expect(await verifyPassword('correcthorse', hash)).toBe(true); + }); + + it('rejects a wrong password', async () => { + const hash = await hashPassword('correcthorse'); + expect(await verifyPassword('wrong', hash)).toBe(false); + }); + + it('returns false on malformed hash', async () => { + expect(await verifyPassword('x', 'not-a-bcrypt-hash')).toBe(false); + }); +}); diff --git a/packages/backend/src/services/auth/passwords.ts b/packages/backend/src/services/auth/passwords.ts new file mode 100644 index 0000000..9a2beeb --- /dev/null +++ b/packages/backend/src/services/auth/passwords.ts @@ -0,0 +1,15 @@ +import bcrypt from 'bcryptjs'; + +const COST = 12; + +export async function hashPassword(plain: string): Promise { + return bcrypt.hash(plain, COST); +} + +export async function verifyPassword(plain: string, hash: string): Promise { + try { + return await bcrypt.compare(plain, hash); + } catch { + return false; + } +}