From 0bbaf80d2a11fb756bdd95111b6fa5fa936b6d30 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Wed, 27 May 2026 14:42:53 -0400 Subject: [PATCH] feat(mam-api): GET /auth/me + POST /auth/password --- services/mam-api/src/routes/auth.js | 28 ++++++++++++++- services/mam-api/test/routes/auth.test.js | 44 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/services/mam-api/src/routes/auth.js b/services/mam-api/src/routes/auth.js index 269f58b..529e5ce 100644 --- a/services/mam-api/src/routes/auth.js +++ b/services/mam-api/src/routes/auth.js @@ -1,6 +1,6 @@ import express from 'express'; import pool from '../db/pool.js'; -import { DEV_USER_ID } from '../middleware/auth.js'; +import { DEV_USER_ID, requireAuth } from '../middleware/auth.js'; import { hashPassword, comparePassword } from '../auth/passwords.js'; const DUMMY_PASSWORD_HASH = '$2b$12$gSeC58PregWedNFK/8Q61OephUo.JJ7EUs0LCTdnJV5AzCS5qQH7K'; @@ -108,5 +108,31 @@ router.post('/logout', (req, res) => { }); }); +// GET /api/v1/auth/me +router.get('/me', requireAuth, (req, res) => { + res.json({ id: req.user.id, username: req.user.username, display_name: req.user.display_name }); +}); + +// POST /api/v1/auth/password { current_password, new_password } +router.post('/password', requireAuth, async (req, res, next) => { + try { + const { current_password, new_password } = req.body || {}; + if (!current_password || !new_password) return badRequest(res, 'current_password and new_password required'); + if (new_password.length < MIN_PASSWORD_LEN) return badRequest(res, 'new password must be at least ' + MIN_PASSWORD_LEN + ' characters'); + + const { rows } = await pool.query(`SELECT password_hash FROM users WHERE id = $1`, [req.user.id]); + if (!rows.length) return res.status(401).json({ error: 'unauthorized' }); + if (!(await comparePassword(current_password, rows[0].password_hash))) { + return badRequest(res, 'current password is incorrect'); + } + const newHash = await hashPassword(new_password); + await pool.query( + `UPDATE users SET password_hash = $1, password_updated_at = NOW() WHERE id = $2`, + [newHash, req.user.id] + ); + res.status(204).end(); + } catch (err) { next(err); } +}); + export default router; export { realUserCount }; diff --git a/services/mam-api/test/routes/auth.test.js b/services/mam-api/test/routes/auth.test.js index 07a7566..3d9824c 100644 --- a/services/mam-api/test/routes/auth.test.js +++ b/services/mam-api/test/routes/auth.test.js @@ -198,3 +198,47 @@ test('POST /auth/logout destroys the session row and the cookie no longer unlock assert.equal(meRes.status, 401); } finally { await close(); await pool.end(); } }); + +test('GET /auth/me returns the authed user', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => { + const pool = await setupTestDb(); + const hash = await hashPassword('correct-horse-battery'); + await pool.query(`INSERT INTO users (username, password_hash, display_name) VALUES ('alice', $1, 'Alice')`, [hash]); + const { baseUrl, close } = await appWithSessionAndMe(pool); + try { + const loginRes = await fetch(baseUrl + '/api/v1/auth/login', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: 'alice', password: 'correct-horse-battery' }), + }); + const cookie = (loginRes.headers.get('set-cookie') || '').split(';')[0]; + const me = await fetch(baseUrl + '/api/v1/auth/me', { headers: { cookie } }); + assert.equal(me.status, 200); + const body = await me.json(); + assert.equal(body.username, 'alice'); + assert.equal(body.display_name, 'Alice'); + } finally { await close(); await pool.end(); } +}); + +test('POST /auth/password rotates the password when current is correct', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => { + const pool = await setupTestDb(); + const hash = await hashPassword('correct-horse-battery'); + await pool.query(`INSERT INTO users (username, password_hash) VALUES ('alice', $1)`, [hash]); + const { baseUrl, close } = await appWithSessionAndMe(pool); + try { + const loginRes = await fetch(baseUrl + '/api/v1/auth/login', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: 'alice', password: 'correct-horse-battery' }), + }); + const cookie = (loginRes.headers.get('set-cookie') || '').split(';')[0]; + const change = await fetch(baseUrl + '/api/v1/auth/password', { + method: 'POST', headers: { 'Content-Type': 'application/json', cookie }, + body: JSON.stringify({ current_password: 'correct-horse-battery', new_password: 'brand-new-passphrase' }), + }); + assert.equal(change.status, 204); + // Wrong current → 400 + const wrong = await fetch(baseUrl + '/api/v1/auth/password', { + method: 'POST', headers: { 'Content-Type': 'application/json', cookie }, + body: JSON.stringify({ current_password: 'no', new_password: 'another-good-passphrase' }), + }); + assert.equal(wrong.status, 400); + } finally { await close(); await pool.end(); } +});