Harden admin auth gate and remove body backdrop
This commit is contained in:
@@ -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',
|
||||
|
||||
68
src/App.vue
68
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) => {
|
||||
</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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user