feat(auth): add an admin-managed login allowlist
Lets the admin restrict which pubkeys may log in, enforced server-side at /api/auth/login before a session is issued. Disabled by default; the admin and the bootstrap (no-admin-claimed-yet) case always pass. Manageable via the existing settings UI/API (npub or hex, one per line). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,78 @@ describe('auth', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('login allowlist', () => {
|
||||
const sk2 = generateSecretKey();
|
||||
const pk2 = getPublicKey(sk2);
|
||||
|
||||
function nip98Header2(url: string, method: string): string {
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: 27235,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: '',
|
||||
tags: [['u', url], ['method', method], ['nonce', Math.random().toString(36).slice(2)]],
|
||||
},
|
||||
sk2,
|
||||
);
|
||||
return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`;
|
||||
}
|
||||
|
||||
it('rejects a non-listed pubkey once the allowlist is enabled, admin still logs in', async () => {
|
||||
const enable = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
payload: { login_allowlist_enabled: true, login_allowlist: [] },
|
||||
});
|
||||
expect(enable.statusCode).toBe(200);
|
||||
|
||||
const blocked = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
headers: { authorization: nip98Header2('http://localhost:8095/api/auth/login', 'POST') },
|
||||
});
|
||||
expect(blocked.statusCode).toBe(403);
|
||||
|
||||
const adminStillIn = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
headers: { authorization: nip98Header('http://localhost:8095/api/auth/login', 'POST') },
|
||||
});
|
||||
expect(adminStillIn.statusCode).toBe(200);
|
||||
expect(adminStillIn.json().isAdmin).toBe(true);
|
||||
});
|
||||
|
||||
it('allows a pubkey once it is added to the allowlist', async () => {
|
||||
const update = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
payload: { login_allowlist: [pk2] },
|
||||
});
|
||||
expect(update.statusCode).toBe(200);
|
||||
expect(update.json().login_allowlist).toEqual([pk2]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
headers: { authorization: nip98Header2('http://localhost:8095/api/auth/login', 'POST') },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json().pubkey).toBe(pk2);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Leave the allowlist disabled so later describe blocks aren't affected.
|
||||
await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
payload: { login_allowlist_enabled: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('podcasts, episodes, feed', () => {
|
||||
let podcastId: string;
|
||||
const sha = 'c'.repeat(64);
|
||||
|
||||
Reference in New Issue
Block a user