Harden admin auth gate and remove body backdrop

This commit is contained in:
Dorian
2026-05-15 18:51:10 -05:00
parent 1e73bbf2c0
commit a37ada3146
3 changed files with 54 additions and 50 deletions

View File

@@ -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',

View File

@@ -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) => {
</div>
</section>
<section v-if="isAdminLikeRoute && !isAdminAuthenticated" class="admin-login-area">
<section v-if="isAdminLikeRoute && !adminSessionChecked" class="admin-login-area">
<div class="mx-auto flex min-h-svh w-full max-w-lg flex-col justify-center px-5 py-16">
<div class="admin-login-card text-center">
<img class="mx-auto h-5 w-auto" src="/images/header-logo.svg" alt="L484" />
<p class="section-kicker mt-8">{{ isEditRoute ? 'Site Editor' : 'Admin Panel' }}</p>
<h1 class="mt-3 text-3xl font-black uppercase leading-none">Checking access</h1>
</div>
</div>
</section>
<section v-else-if="isAdminLikeRoute && !isAdminAuthenticated" class="admin-login-area">
<div class="mx-auto flex min-h-svh w-full max-w-lg flex-col justify-center px-5 py-16">
<div class="admin-login-card">
<img class="mx-auto h-5 w-auto" src="/images/header-logo.svg" alt="L484" />

View File

@@ -41,19 +41,6 @@ body {
background: #0a0a0a;
}
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -3;
background:
linear-gradient(90deg, rgba(0, 0, 0, 0.84), rgba(0, 0, 0, 0.42) 48%, rgba(0, 0, 0, 0.84)),
var(--safe-area-bg-image, #000);
background-position: center;
background-size: cover;
pointer-events: none;
}
.intro-header > div {
padding-top: calc(1.25rem + env(safe-area-inset-top));
}
@@ -73,21 +60,6 @@ body.menu-open {
min-height: 100svh;
}
.hero-fold::before {
content: "";
position: fixed;
inset: calc(env(safe-area-inset-top) * -1) 0 calc(env(safe-area-inset-bottom) * -1);
z-index: -2;
display: none;
background-image:
linear-gradient(90deg, rgba(0, 0, 0, 0.84), rgba(0, 0, 0, 0.42) 48%, rgba(0, 0, 0, 0.84)),
var(--safe-area-bg-image);
background-position: center;
background-size: cover;
transform: scale(1.04);
pointer-events: none;
}
.hero-bg {
opacity: 0;
filter: saturate(1.08) contrast(1.08);
@@ -266,10 +238,6 @@ body.menu-open {
padding-bottom: 0.5rem;
}
.hero-fold::before {
display: block;
}
.public-nav {
display: none;
}