Unify host profile view as framed dialog, add profile iframe mode
HostProfileView now renders the same centered frame card as the login flows, whether shown full-page at /auth/ (host mode) or inside the new #mode=profile restricted iframe. The component self-fetches its data when the parent does not provide it, and emits back/logout so each context reacts appropriately: the full page reloads, the iframe posts auth-back / auth-logout to the host.
This commit is contained in:
@@ -1,92 +1,115 @@
|
||||
<template>
|
||||
<section class="view-root view-root--wide host-view" data-view="host-profile">
|
||||
<header class="view-header">
|
||||
<h1>{{ headingTitle }}</h1>
|
||||
<p class="view-lede">{{ subheading }}</p>
|
||||
</header>
|
||||
<div class="view-root host-profile" data-view="host-profile">
|
||||
<div class="surface surface--tight">
|
||||
<header class="view-header center">
|
||||
<h1>{{ headingTitle }}</h1>
|
||||
<p class="view-lede">{{ subheading }}</p>
|
||||
</header>
|
||||
|
||||
<section class="section-block" ref="userInfoSection">
|
||||
<div class="section-body">
|
||||
<UserBasicInfo
|
||||
v-if="ctx"
|
||||
:name="ctx.user.display_name"
|
||||
:avatar-url="authStore.userInfo.user.avatar_url"
|
||||
:visits="authStore.userInfo.user.visits"
|
||||
:created-at="authStore.userInfo.user.created_at"
|
||||
:last-seen="authStore.userInfo.user.last_seen"
|
||||
:email="ctx.user.email"
|
||||
:telephone="ctx.user.telephone"
|
||||
:org-display-name="orgDisplayName"
|
||||
:role-name="roleDisplayName"
|
||||
:can-edit="false"
|
||||
/>
|
||||
<p v-else class="empty-state">
|
||||
{{ initializing ? 'Loading your account…' : 'No active session found.' }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block">
|
||||
<div class="section-body host-actions">
|
||||
<div class="button-row" ref="buttonRow" @keydown="handleButtonRowKeydown">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
@click="goBack"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
:disabled="authStore.isLoading"
|
||||
@click="logout"
|
||||
>
|
||||
{{ authStore.isLoading ? 'Signing out…' : 'Logout' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="authSiteUrl"
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
:disabled="authStore.isLoading"
|
||||
@click="goToAuthSite"
|
||||
>
|
||||
Full Profile
|
||||
</button>
|
||||
<section class="section-block">
|
||||
<div class="section-body">
|
||||
<UserBasicInfo
|
||||
v-if="sessionCtx && info"
|
||||
:name="sessionCtx.user.display_name"
|
||||
:avatar-url="info.user.avatar_url"
|
||||
:visits="info.user.visits"
|
||||
:created-at="info.user.created_at"
|
||||
:last-seen="info.user.last_seen"
|
||||
:email="sessionCtx.user.email"
|
||||
:telephone="sessionCtx.user.telephone"
|
||||
:org-display-name="orgDisplayName"
|
||||
:role-name="roleDisplayName"
|
||||
:can-edit="false"
|
||||
/>
|
||||
<p v-else class="empty-state">
|
||||
{{ loading ? 'Loading your account…' : 'No active session found.' }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="note"><strong>Logout</strong> from {{ currentHost }}, or access your <strong>Full Profile</strong> at {{ authSiteHost }} (you may need to sign in again).</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="section-block">
|
||||
<div class="section-body host-actions">
|
||||
<div class="button-row" ref="buttonRow" @keydown="handleButtonRowKeydown">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
@click="$emit('back')"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
v-if="sessionCtx"
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
:disabled="busy"
|
||||
@click="logout"
|
||||
>
|
||||
{{ busy ? 'Signing out…' : 'Logout' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
:disabled="busy"
|
||||
@click="goToAuthSite"
|
||||
>
|
||||
Full Profile
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="isRemoteAuthSite" class="note"><strong>Logout</strong> from {{ currentHost }}, or access your <strong>Full Profile</strong> at {{ authSiteHost }} (you may need to sign in again).</p>
|
||||
<p v-else class="note"><strong>Logout</strong> from {{ currentHost }}, or open your <strong>Full Profile</strong>.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { goBack } from '@/utils/helpers'
|
||||
import { getSettings } from '@/utils/settings'
|
||||
import { fetchJson, settings as paskiaSettings } from 'paskia'
|
||||
import { updateThemeFromSession } from '@/utils/theme'
|
||||
import { getDirection, navigateButtonRow } from '@/utils/keynav'
|
||||
|
||||
defineProps({
|
||||
initializing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
// Data may be provided by the parent (full-page /auth/ app already loaded it
|
||||
// into the store); otherwise the component fetches it itself (restricted iframe).
|
||||
const props = defineProps({
|
||||
ctx: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
userInfo: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
settings: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const emit = defineEmits(['back', 'logout'])
|
||||
|
||||
const inIframe = window.parent !== window
|
||||
const currentHost = window.location.host
|
||||
|
||||
const fetchedCtx = ref(null)
|
||||
const fetchedInfo = ref(null)
|
||||
const fetchedSettings = ref(null)
|
||||
const loading = ref(!(props.ctx && props.userInfo))
|
||||
const busy = ref(false)
|
||||
|
||||
// Template refs for navigation
|
||||
const userInfoSection = ref(null)
|
||||
const buttonRow = ref(null)
|
||||
|
||||
const ctx = computed(() => authStore.userInfo || null)
|
||||
const orgDisplayName = computed(() => ctx.value?.org?.display_name ?? '')
|
||||
const roleDisplayName = computed(() => ctx.value?.role?.display_name ?? '')
|
||||
const sessionCtx = computed(() => props.ctx || fetchedCtx.value)
|
||||
const info = computed(() => props.userInfo || fetchedInfo.value)
|
||||
const settingsData = computed(() => props.settings || fetchedSettings.value)
|
||||
const orgDisplayName = computed(() => sessionCtx.value?.org?.display_name ?? '')
|
||||
const roleDisplayName = computed(() => sessionCtx.value?.role?.display_name ?? '')
|
||||
|
||||
const headingTitle = computed(() => {
|
||||
const service = authStore.settings?.rp_name
|
||||
const service = settingsData.value?.rp_name
|
||||
return service ? `${service} account` : 'Account overview'
|
||||
})
|
||||
|
||||
@@ -94,11 +117,17 @@ const subheading = computed(() => {
|
||||
return `You're signed in to ${currentHost}.`
|
||||
})
|
||||
|
||||
const authSiteHost = computed(() => authStore.settings?.auth_host || '')
|
||||
const authSiteHost = computed(() => settingsData.value?.auth_host || '')
|
||||
// Normalize for comparison (lowercase, strip default ports), matching App.vue
|
||||
const normalizeHost = (raw) => (raw || '').trim().toLowerCase().replace(/:80$/, '').replace(/:443$/, '')
|
||||
const isRemoteAuthSite = computed(() => {
|
||||
return !!authSiteHost.value && normalizeHost(authSiteHost.value) !== normalizeHost(currentHost)
|
||||
})
|
||||
const authSiteUrl = computed(() => {
|
||||
const host = authSiteHost.value
|
||||
if (!host) return ''
|
||||
let path = authStore.settings?.ui_base_path ?? '/auth/'
|
||||
// Fall back to the current host when no separate auth host is configured;
|
||||
// the full profile is at ui_base_path either way.
|
||||
const host = authSiteHost.value || currentHost
|
||||
let path = settingsData.value?.ui_base_path ?? '/auth/'
|
||||
if (!path.startsWith('/')) path = `/${path}`
|
||||
if (!path.endsWith('/')) path = `${path}/`
|
||||
const protocol = window.location.protocol || 'https:'
|
||||
@@ -107,11 +136,27 @@ const authSiteUrl = computed(() => {
|
||||
|
||||
const goToAuthSite = () => {
|
||||
if (!authSiteUrl.value) return
|
||||
window.location.href = authSiteUrl.value
|
||||
// Inside an iframe, open the full profile in a new window and close the
|
||||
// frame (auth-back) so the host page regains focus.
|
||||
if (inIframe) {
|
||||
window.open(authSiteUrl.value, '_blank')
|
||||
emit('back')
|
||||
} else {
|
||||
window.location.href = authSiteUrl.value
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
await authStore.logout()
|
||||
if (busy.value) return
|
||||
busy.value = true
|
||||
try {
|
||||
await fetchJson('/auth/api/logout', { method: 'POST', timeout: paskiaSettings.auth_ms })
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
}
|
||||
// The parent decides how to react: the full-page app reloads, the iframe
|
||||
// host receives auth-logout and closes the frame.
|
||||
emit('logout')
|
||||
}
|
||||
|
||||
// Keyboard navigation for button row
|
||||
@@ -124,7 +169,39 @@ const handleButtonRowKeydown = (event) => {
|
||||
if (direction === 'left' || direction === 'right') {
|
||||
navigateButtonRow(buttonRow.value, event.target, direction, { itemSelector: 'button' })
|
||||
}
|
||||
// Up does nothing (no elements above to navigate to)
|
||||
// Down does nothing (no elements below to navigate to)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!props.settings) {
|
||||
getSettings().then((data) => { fetchedSettings.value = data })
|
||||
}
|
||||
if (props.ctx && props.userInfo) return
|
||||
try {
|
||||
const [validateData, infoData] = await Promise.all([
|
||||
fetchJson('/auth/api/validate', { method: 'POST', timeout: paskiaSettings.auth_ms }),
|
||||
fetchJson('/auth/api/user-info', { method: 'GET', timeout: paskiaSettings.auth_ms })
|
||||
])
|
||||
fetchedCtx.value = validateData.ctx
|
||||
fetchedInfo.value = infoData
|
||||
updateThemeFromSession(validateData.ctx)
|
||||
} catch (error) {
|
||||
if (error.status !== 401 && error.status !== 403) {
|
||||
console.error('Failed to load account summary:', error)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.view-root.host-profile { min-height: 100vh; align-items: center; justify-content: center; padding: 2rem 1rem; }
|
||||
.surface.surface--tight {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user