feat(mam-api): GET /auth/me + POST /auth/password
This commit is contained in:
parent
d75a0241eb
commit
0bbaf80d2a
2 changed files with 71 additions and 1 deletions
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue