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:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { api } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
|
||||
@@ -8,28 +9,64 @@ interface Settings {
|
||||
relays: string[];
|
||||
public_url: string;
|
||||
admin_pubkey: string | null;
|
||||
login_allowlist_enabled: boolean;
|
||||
login_allowlist: string[];
|
||||
}
|
||||
|
||||
const auth = useAuthStore();
|
||||
const form = reactive({ blossom_url: '', relays: '', public_url: '' });
|
||||
const form = reactive({
|
||||
blossom_url: '',
|
||||
relays: '',
|
||||
public_url: '',
|
||||
login_allowlist_enabled: false,
|
||||
login_allowlist: '',
|
||||
});
|
||||
const saved = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
/** Accepts npub or raw hex, one per line; returns lowercase hex. Throws on anything invalid. */
|
||||
function parseAllowlist(text: string): string[] {
|
||||
return text
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
if (line.startsWith('npub1')) {
|
||||
const decoded = nip19.decode(line);
|
||||
if (decoded.type !== 'npub') throw new Error(`not an npub: ${line}`);
|
||||
return decoded.data;
|
||||
}
|
||||
if (!/^[0-9a-f]{64}$/i.test(line)) throw new Error(`not a valid npub or hex pubkey: ${line}`);
|
||||
return line.toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const s = await api.get<Settings>('/api/settings');
|
||||
form.blossom_url = s.blossom_url;
|
||||
form.relays = s.relays.join('\n');
|
||||
form.public_url = s.public_url;
|
||||
form.login_allowlist_enabled = s.login_allowlist_enabled;
|
||||
form.login_allowlist = s.login_allowlist.map((pk) => nip19.npubEncode(pk)).join('\n');
|
||||
});
|
||||
|
||||
async function save() {
|
||||
error.value = '';
|
||||
saved.value = false;
|
||||
let allowlist: string[];
|
||||
try {
|
||||
allowlist = parseAllowlist(form.login_allowlist);
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.put('/api/settings', {
|
||||
blossom_url: form.blossom_url,
|
||||
relays: form.relays.split('\n').map((r) => r.trim()).filter(Boolean),
|
||||
public_url: form.public_url,
|
||||
login_allowlist_enabled: form.login_allowlist_enabled,
|
||||
login_allowlist: allowlist,
|
||||
});
|
||||
saved.value = true;
|
||||
} catch (err) {
|
||||
@@ -62,6 +99,24 @@ async function save() {
|
||||
<input id="set-public" v-model="form.public_url" class="input" type="url" required />
|
||||
<p class="mt-1 text-xs text-white/30">Used in RSS feed links. Must be reachable by podcast apps.</p>
|
||||
</div>
|
||||
<div class="border-t border-white/10 pt-4">
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input v-model="form.login_allowlist_enabled" type="checkbox" />
|
||||
Restrict logins to an allowlist
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-white/30">
|
||||
When enabled, only the admin and pubkeys listed below can log in. Everyone else is
|
||||
rejected at login (existing sessions aren't revoked).
|
||||
</p>
|
||||
<textarea
|
||||
id="set-allowlist"
|
||||
v-model="form.login_allowlist"
|
||||
class="input mt-2 font-mono text-sm"
|
||||
rows="6"
|
||||
placeholder="npub1... (one per line, npub or hex)"
|
||||
:disabled="!form.login_allowlist_enabled"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="!auth.isAdmin" class="rounded-lg bg-amber-500/20 border border-amber-500/40 p-3 text-sm text-amber-200">
|
||||
Only the admin (first account to log in) can change settings.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user