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 express from 'express';
|
||||||
import pool from '../db/pool.js';
|
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';
|
import { hashPassword, comparePassword } from '../auth/passwords.js';
|
||||||
|
|
||||||
const DUMMY_PASSWORD_HASH = '$2b$12$gSeC58PregWedNFK/8Q61OephUo.JJ7EUs0LCTdnJV5AzCS5qQH7K';
|
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 default router;
|
||||||
export { realUserCount };
|
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);
|
assert.equal(meRes.status, 401);
|
||||||
} finally { await close(); await pool.end(); }
|
} 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