diff --git a/examples/restricted-api.html b/examples/restricted-api.html index da3e8de..9a5494f 100644 --- a/examples/restricted-api.html +++ b/examples/restricted-api.html @@ -82,20 +82,22 @@ case 'auth-error': showStatus(`⚠ ${data.message || 'Authentication failed'}`, data.cancelled ? 'info' : 'error'); - if (data.cancelled) { - hideAuthIframe(); - currentApiCall = null; - } + // Don't hide iframe on error - let user retry break; case 'auth-cancelled': showStatus(`Operation cancelled: ${data.message || ''}`, 'info'); + // Don't hide iframe - deprecated message type + break; + + case 'auth-back': + showStatus('User clicked Back', 'info'); hideAuthIframe(); currentApiCall = null; break; case 'auth-close-request': - // Iframe wants to be closed + // Iframe wants to be closed (legacy) hideAuthIframe(); break; } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3f3ec29..c9455a0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -2,28 +2,184 @@
- -
+ +
-

Loading...

+

{{ loadingMessage }}

+
+
+
+

🔒 Authentication Required

+

You need to authenticate to access this page.

+
+ +
+
@@ -32,4 +188,10 @@ onMounted(async () => { .loading-spinner { width: 40px; height: 40px; border: 4px solid var(--color-border); border-top: 4px solid var(--color-primary); border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-container p { color: var(--color-text-muted); margin: 0; } + +.message-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; padding: 2rem; } +.message-content { text-align: center; max-width: 480px; } +.message-content h2 { margin: 0 0 1rem; color: var(--color-heading); } +.message-content p { color: var(--color-text-muted); margin: 0 0 1.5rem; } +.message-content .button-row { display: flex; gap: 0.75rem; justify-content: center; } diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css index 3232cbf..9a5b650 100644 --- a/frontend/src/assets/style.css +++ b/frontend/src/assets/style.css @@ -715,3 +715,21 @@ th { padding: 1.5rem; } } + +/* Auth iframe overlay styles */ +body:has(#auth-iframe) { + overflow: hidden; +} + +#auth-iframe { + border: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + color-scheme: auto; + backdrop-filter: blur(4px) brightness(0.7); + -webkit-backdrop-filter: blur(4px) brightness(0.7); +} diff --git a/frontend/src/components/ProfileView.vue b/frontend/src/components/ProfileView.vue index dee765a..76e25bf 100644 --- a/frontend/src/components/ProfileView.vue +++ b/frontend/src/components/ProfileView.vue @@ -173,6 +173,11 @@ const saveName = async () => { try { saving.value = true const res = await fetch('/auth/api/user/display-name', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ display_name: name }) }) + if (res.status === 401) { + authStore.authRequired = true + authStore.showMessage('Authentication required', 'error') + return + } const data = await res.json() if (!res.ok || data.detail) throw new Error(data.detail || 'Update failed') showNameDialog.value = false diff --git a/frontend/src/components/RegistrationLinkModal.vue b/frontend/src/components/RegistrationLinkModal.vue index 27c2c3b..6b6e4a8 100644 --- a/frontend/src/components/RegistrationLinkModal.vue +++ b/frontend/src/components/RegistrationLinkModal.vue @@ -64,6 +64,9 @@ import { ref, onMounted, watch, computed, nextTick } from 'vue' import QRCode from 'qrcode/lib/browser' import { formatDate } from '@/utils/helpers' +import { useAuthStore } from '@/stores/auth' + +const authStore = useAuthStore() const props = defineProps({ endpoint: { type: String, required: true }, @@ -90,6 +93,11 @@ const expirationMessage = computed(() => { async function fetchLink() { try { const res = await fetch(props.endpoint, { method: 'POST' }) + if (res.status === 401) { + authStore.authRequired = true + emit('close') + return + } const data = await res.json() if (data.detail) throw new Error(data.detail) url.value = data.url diff --git a/frontend/src/components/RestrictedAuth.vue b/frontend/src/components/RestrictedAuth.vue index a1276b8..530548e 100644 --- a/frontend/src/components/RestrictedAuth.vue +++ b/frontend/src/components/RestrictedAuth.vue @@ -116,21 +116,13 @@ async function fetchSettings() { async function fetchUserInfo() { try { const res = await fetch('/auth/api/user-info', { method: 'POST' }) - if (!res.ok) { - const payload = await safeParseJson(res) - showMessage(payload.detail || 'Unable to load user session info.', 'error', 2000) - return - } + if (!res.ok) return userInfo.value = await res.json() // In login mode, if the user is authenticated but still here, they lack permissions. // In reauth mode, being authenticated is expected - we just need re-verification. - if (isAuthenticated.value && props.mode !== 'reauth') { - showMessage('Permission Denied', 'error', 2000) - emit('forbidden', userInfo.value) - } + if (isAuthenticated.value && props.mode !== 'reauth') emit('forbidden', userInfo.value) } catch (error) { console.error('Failed to load user info', error) - showMessage('Could not contact the authentication server', 'error', 2000) } } diff --git a/frontend/src/restricted-api/RestrictedApiApp.vue b/frontend/src/restricted-api/RestrictedApiApp.vue index 7dd6cce..6edc4ae 100644 --- a/frontend/src/restricted-api/RestrictedApiApp.vue +++ b/frontend/src/restricted-api/RestrictedApiApp.vue @@ -5,15 +5,8 @@ @forbidden="handleForbidden" @logout="handleLogout" @auth-error="handleAuthError" - > - - + @back="handleBack" + /> diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index a6fa653..0fbf693 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -7,6 +7,7 @@ export const useAuthStore = defineStore('auth', { // Auth State userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info} isLoading: false, + authRequired: false, // Flag to trigger auth iframe // Settings settings: null, @@ -25,6 +26,9 @@ export const useAuthStore = defineStore('auth', { setLoading(flag) { this.isLoading = !!flag }, + clearAuthRequired() { + this.authRequired = false + }, showMessage(message, type = 'info', duration = 3000) { this.status = { message, @@ -89,9 +93,9 @@ export const useAuthStore = defineStore('auth', { } catch (_) { // ignore JSON parse errors (unlikely) } - if (response.status === 401 && result?.detail) { - this.showMessage(result.detail, 'error', 5000) - throw new Error(result.detail) + if (response.status === 401) { + this.authRequired = true + throw new Error(result?.detail || 'Authentication required') } if (result?.detail) { // Other error style @@ -102,7 +106,11 @@ export const useAuthStore = defineStore('auth', { console.log('User info loaded:', result) }, async deleteCredential(uuid) { - const response = await fetch(`/auth/api/user/credential/${uuid}`, {method: 'Delete'}) + const response = await fetch(`/auth/api/user/credential/${uuid}`, {method: 'Delete'}) + if (response.status === 401) { + this.authRequired = true + throw new Error('Authentication required') + } const result = await response.json() if (result.detail) throw new Error(`Server: ${result.detail}`) @@ -111,6 +119,10 @@ export const useAuthStore = defineStore('auth', { async terminateSession(sessionId) { try { const res = await fetch(`/auth/api/user/session/${sessionId}`, { method: 'DELETE' }) + if (res.status === 401) { + this.authRequired = true + throw new Error('Authentication required') + } let payload = null try { payload = await res.json()