From a37ada3146c07be9e149cb860519cd53f90e0949 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 15 May 2026 18:51:10 -0500 Subject: [PATCH] Harden admin auth gate and remove body backdrop --- server/server.js | 4 +-- src/App.vue | 68 ++++++++++++++++++++++++++++++++++++------------ src/style.css | 32 ----------------------- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/server/server.js b/server/server.js index 39cec16..6dcfbb4 100644 --- a/server/server.js +++ b/server/server.js @@ -352,7 +352,7 @@ const requireAdmin = (req, res) => { return false } if (!isAdminPubkey(getAuthPubkey(req))) { - json(res, 403, { error: 'Admin access required.' }) + json(res, 403, { error: 'This npub is not an admin. Please request access and we will authorise it if you are permissioned.' }) return false } return true @@ -1093,7 +1093,7 @@ const handleApi = async (req, res) => { if (req.method === 'GET' && url.pathname === '/api/admin/events') { if (!adminApiEnabled()) return json(res, 404, { error: 'Admin API is disabled on this deployment.' }) const pubkey = cleanText(url.searchParams.get('pubkey'), 80).toLowerCase() - if (!isAdminPubkey(pubkey)) return json(res, 403, { error: 'Admin access required.' }) + if (!isAdminPubkey(pubkey)) return json(res, 403, { error: 'This npub is not an admin. Please request access and we will authorise it if you are permissioned.' }) res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store', diff --git a/src/App.vue b/src/App.vue index 255ea54..3deaca3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -214,6 +214,8 @@ const adminUser = ref('') const adminError = ref('') const adminLoginMethod = ref('') const isAdminLoading = ref(false) +const isAdminSessionChecking = ref(false) +const adminSessionChecked = ref(false) const isAdminMenuOpen = ref(false) const mobileMenuOpen = ref(false) let isSigning = false @@ -272,7 +274,7 @@ const isEventsRoute = computed(() => currentPath.value === '/events') const isAdminRoute = computed(() => currentPath.value === '/admin') const isEditRoute = computed(() => currentPath.value === '/edit') const isAdminLikeRoute = computed(() => isAdminRoute.value || isEditRoute.value) -const isAdminAuthenticated = computed(() => Boolean(adminUser.value)) +const isAdminAuthenticated = computed(() => adminSessionChecked.value && Boolean(adminUser.value)) const pendingAdminRequests = computed(() => adminAccessRequests.value.filter((request) => request.status === 'requested')) const adminTabs = computed(() => [ { id: 'requested', label: 'Requested', count: members.value.filter((member) => !paymentForMember(member.membershipId) && !['suspended', 'revoked', 'expired'].includes(member.accessStatus || member.status)).length, icon: 'icon-admin-requested' }, @@ -476,7 +478,7 @@ const sha256Hex = async (value) => { return bytesToHex(sha256(new TextEncoder().encode(value))) } -const adminHeaders = () => (adminUser.value ? { Authorization: `Bearer ${adminUser.value}` } : {}) +const adminHeaders = (pubkey = adminUser.value) => (pubkey ? { Authorization: `Bearer ${pubkey}` } : {}) const fetchJson = async (url, options = {}) => { const response = await fetch(url, { @@ -708,12 +710,23 @@ const loadMembers = async () => { } } -const loadAdminSession = () => { +const adminNotAuthorizedMessage = 'This npub is not an admin. Please request access and we will authorise it if you are permissioned.' + +const loadAdminSession = async () => { + adminSessionChecked.value = false + isAdminSessionChecking.value = isAdminLikeRoute.value try { const stored = JSON.parse(localStorage.getItem(ADMIN_AUTH_KEY) || 'null') - adminUser.value = stored?.pubkey?.toLowerCase() || '' + const storedPubkey = stored?.pubkey?.toLowerCase() || '' + if (!storedPubkey) return + await verifyAdminSession(storedPubkey) + adminUser.value = storedPubkey } catch { adminUser.value = '' + localStorage.removeItem(ADMIN_AUTH_KEY) + } finally { + adminSessionChecked.value = true + isAdminSessionChecking.value = false } } @@ -1661,10 +1674,18 @@ const setAdminSession = (pubkey) => { localStorage.setItem(ADMIN_AUTH_KEY, JSON.stringify({ pubkey: normalized, lastLogin: new Date().toISOString() })) } -const verifyAdminSession = async () => { - const data = await fetchJson('/api/admin/access-requests', { headers: adminHeaders() }) - adminAccessRequests.value = Array.isArray(data.requests) ? data.requests : [] - adminIsMaster.value = Boolean(data.isMasterAdmin) +const verifyAdminSession = async (pubkey = adminUser.value) => { + try { + const data = await fetchJson('/api/admin/access-requests', { headers: adminHeaders(pubkey) }) + adminAccessRequests.value = Array.isArray(data.requests) ? data.requests : [] + adminIsMaster.value = Boolean(data.isMasterAdmin) + return data + } catch (error) { + if (error instanceof Error && /admin access required|403/i.test(error.message)) { + throw new Error(adminNotAuthorizedMessage) + } + throw error + } } const loginAdminWithExtension = async () => { @@ -1673,8 +1694,9 @@ const loginAdminWithExtension = async () => { isAdminLoading.value = true try { - setAdminSession(await loginWithExtension()) - await verifyAdminSession() + const pubkey = await loginWithExtension() + await verifyAdminSession(pubkey) + setAdminSession(pubkey) await loadMembers() connectAdminEvents() } catch (error) { @@ -1694,8 +1716,9 @@ const loginAdminWithRemoteApp = async () => { sessionStorage.setItem('l484-admin-return-path', isEditRoute.value ? '/edit' : '/admin') try { - setAdminSession(await loginWithRemoteApp()) - await verifyAdminSession() + const pubkey = await loginWithRemoteApp() + await verifyAdminSession(pubkey) + setAdminSession(pubkey) await loadMembers() connectAdminEvents() sessionStorage.removeItem(SIGNER_LOGIN_CONTEXT_KEY) @@ -1715,8 +1738,9 @@ const resumePendingAdminRemoteSignin = async () => { isAdminLoading.value = true try { - setAdminSession(await resumeRemoteAppLogin()) - await verifyAdminSession() + const pubkey = await resumeRemoteAppLogin() + await verifyAdminSession(pubkey) + setAdminSession(pubkey) await loadMembers() connectAdminEvents() sessionStorage.removeItem(SIGNER_LOGIN_CONTEXT_KEY) @@ -1738,6 +1762,8 @@ const logoutAdmin = () => { disconnectAdminEvents() adminUser.value = '' adminIsMaster.value = false + adminSessionChecked.value = true + isAdminSessionChecking.value = false isAdminMenuOpen.value = false localStorage.removeItem(ADMIN_AUTH_KEY) } @@ -2141,7 +2167,7 @@ onMounted(async () => { window.addEventListener('appinstalled', handlePwaInstalled) await loadAppConfig() await loadSiteContent() - loadAdminSession() + await loadAdminSession() await loadMembers() connectAdminEvents() if (appConfig.value.mode !== 'legacy') loadBitcoinPrice() @@ -2551,7 +2577,17 @@ watch(mobileMenuOpen, (open) => { -
+ + +