Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac0256c366 | ||
|
|
6439437e8b |
@@ -5,7 +5,7 @@
|
|||||||
<ProfileView v-if="store.currentView === 'profile'" />
|
<ProfileView v-if="store.currentView === 'profile'" />
|
||||||
<DeviceLinkView v-if="store.currentView === 'device-link'" />
|
<DeviceLinkView v-if="store.currentView === 'device-link'" />
|
||||||
<ResetView v-if="store.currentView === 'reset'" />
|
<ResetView v-if="store.currentView === 'reset'" />
|
||||||
<PermissionDeniedView v-if="store.currentView === 'permission-denied'" />
|
<PermissionDeniedView v-if="store.currentView === 'permission-denied'" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,9 +44,9 @@ onMounted(async () => {
|
|||||||
if (reset) {
|
if (reset) {
|
||||||
store.resetToken = reset
|
store.resetToken = reset
|
||||||
// Remove query param to avoid lingering in history / clipboard
|
// Remove query param to avoid lingering in history / clipboard
|
||||||
const targetPath = '/auth/'
|
const targetPath = '/auth/'
|
||||||
const currentPath = location.pathname.endsWith('/') ? location.pathname : location.pathname + '/'
|
const currentPath = location.pathname.endsWith('/') ? location.pathname : location.pathname + '/'
|
||||||
history.replaceState(null, '', currentPath.startsWith('/auth') ? '/auth/' : targetPath)
|
history.replaceState(null, '', currentPath.startsWith('/auth') ? '/auth/' : targetPath)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await store.loadUserInfo()
|
await store.loadUserInfo()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
|
||||||
|
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
||||||
import CredentialList from '@/components/CredentialList.vue'
|
import CredentialList from '@/components/CredentialList.vue'
|
||||||
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
||||||
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
||||||
@@ -318,6 +319,27 @@ const pageHeading = computed(() => {
|
|||||||
return (authStore.settings?.rp_name || 'Passkey') + ' Admin'
|
return (authStore.settings?.rp_name || 'Passkey') + ' Admin'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Breadcrumb entries for admin app.
|
||||||
|
const breadcrumbEntries = computed(() => {
|
||||||
|
const entries = [
|
||||||
|
{ label: 'Auth', href: '/auth/' },
|
||||||
|
{ label: 'Admin', href: '/auth/admin/' }
|
||||||
|
]
|
||||||
|
// Determine organization for user view if selectedOrg not explicitly chosen.
|
||||||
|
let orgForUser = null
|
||||||
|
if (selectedUser.value) {
|
||||||
|
orgForUser = orgs.value.find(o => o.uuid === selectedUser.value.org_uuid) || null
|
||||||
|
}
|
||||||
|
const orgToShow = selectedOrg.value || orgForUser
|
||||||
|
if (orgToShow) {
|
||||||
|
entries.push({ label: orgToShow.display_name, href: `#org/${orgToShow.uuid}` })
|
||||||
|
}
|
||||||
|
if (selectedUser.value) {
|
||||||
|
entries.push({ label: selectedUser.value.display_name || 'User', href: `#user/${selectedUser.value.uuid}` })
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
})
|
||||||
|
|
||||||
watch(selectedUser, async (u) => {
|
watch(selectedUser, async (u) => {
|
||||||
if (!u) { userDetail.value = null; return }
|
if (!u) { userDetail.value = null; return }
|
||||||
try {
|
try {
|
||||||
@@ -432,17 +454,8 @@ async function submitDialog() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>
|
<h1>{{ pageHeading }}</h1>
|
||||||
{{ pageHeading }}
|
<Breadcrumbs :entries="breadcrumbEntries" />
|
||||||
<a href="/auth/" class="back-link" title="Back to User App">User</a>
|
|
||||||
<a
|
|
||||||
v-if="info?.is_global_admin && (selectedOrg || selectedUser)"
|
|
||||||
@click.prevent="goOverview"
|
|
||||||
href="#overview"
|
|
||||||
class="nav-link"
|
|
||||||
title="Back to overview"
|
|
||||||
>Overview</a>
|
|
||||||
</h1>
|
|
||||||
<div v-if="loading">Loading…</div>
|
<div v-if="loading">Loading…</div>
|
||||||
<div v-else-if="error" class="error">{{ error }}</div>
|
<div v-else-if="error" class="error">{{ error }}</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@@ -454,9 +467,8 @@ async function submitDialog() {
|
|||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
|
||||||
<!-- Removed user-specific info (current org, effective permissions, admin flags) -->
|
|
||||||
|
|
||||||
<!-- Overview Page -->
|
|
||||||
<div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card">
|
<div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card">
|
||||||
<h2>Organizations</h2>
|
<h2>Organizations</h2>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -484,8 +496,6 @@ async function submitDialog() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Detail Page -->
|
|
||||||
<div v-if="selectedUser" class="card user-detail">
|
<div v-if="selectedUser" class="card user-detail">
|
||||||
<UserBasicInfo
|
<UserBasicInfo
|
||||||
v-if="userDetail && !userDetail.error"
|
v-if="userDetail && !userDetail.error"
|
||||||
@@ -519,7 +529,7 @@ async function submitDialog() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Organization Detail Page -->
|
|
||||||
<div v-else-if="selectedOrg" class="card">
|
<div v-else-if="selectedOrg" class="card">
|
||||||
<h2 class="org-title" :title="selectedOrg.uuid">
|
<h2 class="org-title" :title="selectedOrg.uuid">
|
||||||
<span class="org-name">{{ selectedOrg.display_name }}</span>
|
<span class="org-name">{{ selectedOrg.display_name }}</span>
|
||||||
@@ -533,7 +543,7 @@ async function submitDialog() {
|
|||||||
class="perm-matrix-grid"
|
class="perm-matrix-grid"
|
||||||
:style="{ gridTemplateColumns: 'minmax(180px, 1fr) ' + selectedOrg.roles.map(()=> '2.2rem').join(' ') + ' 2.2rem' }"
|
:style="{ gridTemplateColumns: 'minmax(180px, 1fr) ' + selectedOrg.roles.map(()=> '2.2rem').join(' ') + ' 2.2rem' }"
|
||||||
>
|
>
|
||||||
<!-- Headers -->
|
|
||||||
<div class="grid-head perm-head">Permission</div>
|
<div class="grid-head perm-head">Permission</div>
|
||||||
<div
|
<div
|
||||||
v-for="r in selectedOrg.roles"
|
v-for="r in selectedOrg.roles"
|
||||||
@@ -545,7 +555,7 @@ async function submitDialog() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid-head role-head add-role-head" title="Add role" @click="createRole(selectedOrg)" role="button">➕</div>
|
<div class="grid-head role-head add-role-head" title="Add role" @click="createRole(selectedOrg)" role="button">➕</div>
|
||||||
|
|
||||||
<!-- Data Rows -->
|
|
||||||
<template v-for="pid in selectedOrg.permissions" :key="pid">
|
<template v-for="pid in selectedOrg.permissions" :key="pid">
|
||||||
<div class="perm-name" :title="pid">{{ permissionDisplayName(pid) }}</div>
|
<div class="perm-name" :title="pid">{{ permissionDisplayName(pid) }}</div>
|
||||||
<div
|
<div
|
||||||
@@ -583,14 +593,14 @@ async function submitDialog() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="r.users.length > 0">
|
<template v-if="r.users.length > 0">
|
||||||
<ul class="user-list">
|
<ul class="user-list">
|
||||||
<li
|
<li
|
||||||
v-for="u in r.users"
|
v-for="u in r.users"
|
||||||
:key="u.uuid"
|
:key="u.uuid"
|
||||||
class="user-chip"
|
class="user-chip"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="e => onUserDragStart(e, u, selectedOrg.uuid)"
|
@dragstart="e => onUserDragStart(e, u, selectedOrg.uuid)"
|
||||||
@click="openUser(u)"
|
@click="openUser(u)"
|
||||||
:title="u.uuid"
|
:title="u.uuid"
|
||||||
>
|
>
|
||||||
<span class="name">{{ u.display_name }}</span>
|
<span class="name">{{ u.display_name }}</span>
|
||||||
@@ -606,7 +616,7 @@ async function submitDialog() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card">
|
<div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card">
|
||||||
<h2>All Permissions</h2>
|
<h2>All Permissions</h2>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button v-if="!showCreatePermission" @click="showCreatePermission = true">+ Create Permission</button>
|
<button v-if="!showCreatePermission" @click="showCreatePermission = true">+ Create Permission</button>
|
||||||
|
|||||||
42
frontend/src/components/Breadcrumbs.vue
Normal file
42
frontend/src/components/Breadcrumbs.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
// Props:
|
||||||
|
// entries: Array<{ label:string, href:string }>
|
||||||
|
// showHome: include leading home icon (defaults true)
|
||||||
|
// homeHref: home link target (default '/')
|
||||||
|
const props = defineProps({
|
||||||
|
entries: { type: Array, default: () => [] },
|
||||||
|
showHome: { type: Boolean, default: true },
|
||||||
|
homeHref: { type: String, default: '/' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const crumbs = computed(() => {
|
||||||
|
const base = props.showHome ? [{ label: '🏠', href: props.homeHref }] : []
|
||||||
|
return [...base, ...props.entries]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="breadcrumbs" aria-label="Breadcrumb" v-if="crumbs.length">
|
||||||
|
<ol>
|
||||||
|
<li v-for="(c, idx) in crumbs" :key="idx">
|
||||||
|
<a :href="c.href">{{ c.label }}</a>
|
||||||
|
<span v-if="idx < crumbs.length - 1" class="sep"> — </span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.breadcrumbs { margin: .25rem 0 .5rem; line-height:1.2; }
|
||||||
|
.breadcrumbs ol { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; }
|
||||||
|
.breadcrumbs li { display: inline-flex; align-items: center; }
|
||||||
|
.breadcrumbs a { text-decoration: none; color: #0366d6; padding: 0 .15rem; border-radius:4px; }
|
||||||
|
.breadcrumbs a:hover, .breadcrumbs a:focus { text-decoration: underline; }
|
||||||
|
.breadcrumbs .sep { color: #888; margin: 0 .1rem; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.breadcrumbs a { color: #4ea3ff; }
|
||||||
|
.breadcrumbs .sep { color: #aaa; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="view active">
|
<div class="view active">
|
||||||
<h1>👋 Welcome! <a v-if="isAdmin" href="/auth/admin/" class="admin-link" title="Admin Console">Admin</a></h1>
|
<h1>👋 Welcome!</h1>
|
||||||
|
<Breadcrumbs :entries="[{ label: 'Auth', href: '/auth/' }, ...(isAdmin ? [{ label: 'Admin', href: '/auth/admin/' }] : [])]" />
|
||||||
<UserBasicInfo
|
<UserBasicInfo
|
||||||
v-if="authStore.userInfo?.user"
|
v-if="authStore.userInfo?.user"
|
||||||
:name="authStore.userInfo.user.user_name"
|
:name="authStore.userInfo.user.user_name"
|
||||||
@@ -9,7 +10,7 @@
|
|||||||
:created-at="authStore.userInfo.user.created_at"
|
:created-at="authStore.userInfo.user.created_at"
|
||||||
:last-seen="authStore.userInfo.user.last_seen"
|
:last-seen="authStore.userInfo.user.last_seen"
|
||||||
:loading="authStore.isLoading"
|
:loading="authStore.isLoading"
|
||||||
update-endpoint="/auth/api/user/display-name"
|
update-endpoint="/auth/api/user/display-name"
|
||||||
@saved="authStore.loadUserInfo()"
|
@saved="authStore.loadUserInfo()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { formatDate } from '@/utils/helpers'
|
import { formatDate } from '@/utils/helpers'
|
||||||
import passkey from '@/utils/passkey'
|
import passkey from '@/utils/passkey'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "passkey"
|
name = "passkey"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
description = "Passkey Authentication for Web Services"
|
description = "Passkey Authentication for Web Services"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Leo Vasanko"},
|
{name = "Leo Vasanko"},
|
||||||
|
|||||||
Reference in New Issue
Block a user