feat(mam-api): POST /auth/logout

This commit is contained in:
Zac Gaetano 2026-05-27 14:38:05 -04:00
parent bcfc19e530
commit d75a0241eb
2 changed files with 32 additions and 0 deletions

View file

@ -98,5 +98,15 @@ router.post('/login', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// POST /api/v1/auth/logout — destroys the session and clears the cookie.
router.post('/logout', (req, res) => {
if (!req.session) return res.status(204).end();
req.session.destroy(err => {
if (err) console.error('[auth] session destroy failed:', err.message);
res.clearCookie('dragonflight.sid', { path: '/' });
res.status(204).end();
});
});
export default router; export default router;
export { realUserCount }; export { realUserCount };

View file

@ -176,3 +176,25 @@ test('POST /auth/login with wrong password → 401 + generic message (no enumera
assert.equal(e2, 'invalid credentials'); // identical message — no enumeration assert.equal(e2, 'invalid credentials'); // identical message — no enumeration
} finally { await close(); await pool.end(); } } finally { await close(); await pool.end(); }
}); });
test('POST /auth/logout destroys the session row and the cookie no longer unlocks /me', { 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 logoutRes = await fetch(baseUrl + '/api/v1/auth/logout', {
method: 'POST', headers: { cookie },
});
assert.equal(logoutRes.status, 204);
const meRes = await fetch(baseUrl + '/api/v1/protected/me', { headers: { cookie } });
assert.equal(meRes.status, 401);
} finally { await close(); await pool.end(); }
});