Authorization
Concepts
Role-Based Authorization
A security mechanism that restricts access to resources or functionality based on a user's role within an application. It ensures users can only perform actions or view content permitted for their assigned roles.
Navigation Guards
A feature in front-end frameworks that controls access to routes or pages based on conditions such as authentication, user roles, or application state, ensuring users navigate only to authorized or appropriate areas.
Page Meta
Metadata associated with a web page, such as titles, descriptions, and keywords, used to improve SEO, accessibility, and user experience. Page meta's are often set dynamically based on the content or purpose of the page.
Data model

API
Extend existing get_context routine to return privilege data.
./db/src/database/odbvue/package_bodies/pck_app.sql
-- ...
OPEN r_user FOR SELECT
uuid AS "uuid",
username AS "username",
fullname AS "fullname",
created AS "created",
coalesce((
SELECT
JSON_ARRAYAGG(
JSON_OBJECT(
'role' VALUE r.role,
'permission' VALUE p.permission,
'validfrom' VALUE p.valid_from,
'validto' VALUE p.valid_to
)
)
FROM
app_permissions p
JOIN app_roles r ON r.id = p.id_role
JOIN app_users u ON u.id = p.id_user
WHERE
u.uuid = v_uuid
),
'[]') AS "{}privileges"
FROM
app_users
WHERE
uuid = v_uuid
AND v_uuid IS NOT NULL;
-- ...Store
User data type with privileges in Application Main store.
@/stores/index.ts
// ..
type ContextResponse = {
version: string
user?: {
uuid: string
username: string
fullname: string
created: string
privileges: {
role: string
permission: string
validfrom: string
validto: string
}[]
}[]
consents: {
id: string
language: string
name: string
created: string
}[]
}
// ..
const defaultUser = {
uuid: '',
username: '',
fullname: '',
created: '',
privileges: [] as {
role: string
permission: string
validfrom: string
validto: string
}[],
}
// ..Pages & Page Meta's
Visibility & Access Props
Meta properties control page visibility and access permissions:
Visibility
Determines whether a page link/item appears in navigation menus and UI:
| Value | Description |
|---|---|
always | Page always appears in navigation, regardless of authentication state |
when-authenticated | Page only appears when user is logged in |
when-unauthenticated | Page only appears when user is not logged in |
with-role | Page only appears when user has one of the specified roles |
never | Page never appears in navigation (e.g., error pages, authentication pages) |
Access
Controls whether a user can actually access the page:
| Value | Description |
|---|---|
always | Page is always accessible to anyone |
when-authenticated | Page requires user to be logged in |
when-unauthenticated | Page only accessible to users not logged in |
with-role | Page requires user to have one of the specified roles |
never | Page is never accessible |
Page Meta Definition
Page meta can be set using definePage() in .vue files:
<script setup lang="ts">
definePage({
meta: {
title: 'Page Title',
description: 'Page description',
icon: '$mdiIcon',
color: '#HEXCOLOR',
visibility: 'always',
access: 'when-authenticated',
roles: ['admin', 'editor'] // required for 'with-role'
}
})
</script>Or in .vue files using <route> block:
<route lang="json">
{
"meta": {
"title": "Admin",
"visibility": "with-role",
"access": "with-role",
"roles": ["admin"]
}
}
</route>Or in .md files using frontmatter:
---
title: About
description: Learn more about our platform
icon: $mdiInformation
color: '#FA8531'
visibility: always
access: always
---
# About
Page content here...Apply meta to pages
| Page | Path | Visibility | Access | Roles |
|---|---|---|---|---|
| Home | / | always | always | - |
| About | /about | always | always | - |
| Login | /login | never | when-unauthenticated | - |
| Sign Up | /signup | never | when-unauthenticated | - |
| Recover Password | /recover-password | never | when-unauthenticated | - |
| Reset Password | /reset-password/[token] | never | always | - |
| Confirm Email | /confirm-email/[id] | never | always | - |
| Sandbox | /sandbox | always | when-authenticated | - |
| Admin | /admin | with-role | with-role | admin |
| Not Found | /[...path] | never | always | - |
Navigation guards
Guard method in Navigation Store
@/stores/app/navigation.ts
source
import { defineStore, acceptHMRUpdate } from 'pinia'
import { useRouter, useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'
import { computed, ref, type ComputedRef } from 'vue'
import { useAppStore } from '../index'
const routeValueToString = (value: unknown): string => {
if (Array.isArray(value)) return routeValueToString(value[0])
if (typeof value === 'string') return value
if (typeof value === 'number') return String(value)
if (typeof value === 'boolean') return value ? 'true' : 'false'
return ''
}
export const computedRouteParam = (
route: RouteLocationNormalizedLoaded,
name: string,
): ComputedRef<string> => {
return computed(() => routeValueToString((route.params as Record<string, unknown>)[name]))
}
export const computedRouteQuery = (
route: RouteLocationNormalizedLoaded,
name: string,
): ComputedRef<string> => {
return computed(() => routeValueToString(route.query[name]))
}
export const computedRouteParams = (
route: RouteLocationNormalizedLoaded,
): ComputedRef<Record<string, string>> => {
return computed(() => {
const merged: Record<string, string> = {}
for (const [key, value] of Object.entries(route.params as Record<string, unknown>)) {
const v = routeValueToString(value)
if (v) merged[key] = v
}
for (const [key, value] of Object.entries(route.query)) {
const v = routeValueToString(value)
if (v) merged[key] = v
}
return merged
})
}
export const useRouteParams = (): {
pathParams: ComputedRef<Record<string, string>>
queryParams: ComputedRef<Record<string, string>>
routeParams: ComputedRef<Record<string, string>>
param: (name: string) => ComputedRef<string>
query: (name: string) => ComputedRef<string>
} => {
const route = useRoute()
const pathParams = computed(() => {
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(route.params as Record<string, unknown>)) {
const v = routeValueToString(value)
if (v) result[key] = v
}
return result
})
const queryParams = computed(() => {
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(route.query)) {
const v = routeValueToString(value)
if (v) result[key] = v
}
return result
})
const routeParams = computed(() => ({ ...pathParams.value, ...queryParams.value }))
const param = (name: string) => computedRouteParam(route, name)
const query = (name: string) => computedRouteQuery(route, name)
return { pathParams, queryParams, routeParams, param, query }
}
type Visibility = 'always' | 'when-authenticated' | 'when-unauthenticated' | 'never' | 'with-role'
type Access = 'always' | 'when-authenticated' | 'when-unauthenticated' | 'never' | 'with-role'
type Page = {
path: string
level: number
children: boolean
title: string
description: string
icon: string
color: string
visibility: Visibility
access: Access
roles: string[]
}
export const useNavigationStore = defineStore('navigation', () => {
const router = useRouter()
const routes = router.getRoutes()
const route = useRoute()
const breadcrumb = ref('')
const allPages: Page[] = routes.map((r) => {
const path = r.path
return {
path,
level: path == '/' ? 0 : path.split('/').length - 1,
children: routes.find((rr) => rr.path.includes(path) && rr.path !== path) !== undefined,
title:
r.meta?.title?.toString() ||
path
.split('/')
.at(-1)
?.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ') ||
'',
description: r.meta?.description?.toString() || '',
icon: (r.meta?.icon as string) || '$mdiMinus',
color: (r.meta?.color as string) || '',
visibility: (r.meta?.visibility as Visibility) || 'never',
access: (r.meta?.access as Access) || 'never',
roles: (r.meta?.roles as string[]) || [],
}
})
const title = computed(() => (path: string) => {
const page = allPages.find((p) => p.path === path)
return page ? page.title : ''
})
const breadcrumbs = computed(() => {
const paths = ['', ...route.path.split('/').filter(Boolean)].map((_, i, arr) => {
const p = arr.slice(1, i + 1).join('/')
return '/' + p
})
const crumbs = allPages
.filter((p) => p.path !== '/:path(.*)')
.filter((p) => paths.includes(p.path))
.sort((a, b) => a.level - b.level)
.map((p) => ({ title: p.title, disabled: route.path === p.path, href: p.path, icon: p.icon }))
if (breadcrumb.value)
crumbs.push({
title: breadcrumb.value,
disabled: true,
href: '',
icon: '',
})
return crumbs
})
const hasVisibility = (page: Page): boolean => {
const app = useAppStore()
if (page.visibility === 'always') return true
if (page.visibility === 'when-authenticated' && app.auth.isAuthenticated) return true
if (page.visibility === 'when-unauthenticated' && !app.auth.isAuthenticated) return true
if (page.visibility === 'with-role' && app.auth.isAuthenticated) {
return app.user.privileges.some(
(privilege: { role: string; permission: string; validfrom: string; validto: string }) =>
page.roles.some((role) => privilege.role.toLowerCase() === role.toLowerCase()),
)
}
return false
}
const pages: ComputedRef<Page[]> = computed(() => {
return allPages.filter((p) => p.level < 2 && hasVisibility(p))
})
const guard = computed(() => (pathToCheck: string) => {
const app = useAppStore()
const page = allPages.find((p) => {
const regexPath = new RegExp('^' + p.path.replace(/:[^/]+/g, '[^/]+') + '$')
return regexPath.test(pathToCheck)
})
if (!page) return false
if (page.access === 'always') return true
if (page.access === 'when-authenticated' && app.auth.isAuthenticated) return true
if (page.access === 'when-unauthenticated' && !app.auth.isAuthenticated) return true
if (page.access === 'with-role' && app.auth.isAuthenticated) {
const hasRole = app.user.privileges.some(
(privilege: { role: string; permission: string; validfrom: string; validto: string }) =>
page.roles.some((role) => privilege.role.toLowerCase() === role.toLowerCase()),
)
if (hasRole) return true
}
return hasVisibility(page) ? '/login' : '/'
})
return {
pages,
title,
breadcrumb,
breadcrumbs,
guard,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useNavigationStore, import.meta.hot))
}Guard in Router
@/router/index.ts
// ...
router.beforeEach(async (to) => {
const app = useAppStore()
app.ui.clearMessages()
const result: string | boolean = app.navigation.guard(to.path)
if (result) {
const appTitle = title || 'OdbVue'
const pageTitle = app.navigation.title(to.path)
const documentTitle = pageTitle ? `${appTitle} - ${pageTitle}` : appTitle
useHead({ title: documentTitle })
} else {
window.scrollTo(0, 0)
app.ui.setError('unauthorized')
}
return result === '/login' ? { path: result, query: { redirect: to.path } } : result
})
// ...WARNING
Client side navigation guard does not prevent from authorization flaws, always apply navigation guards in back-end using pck_api_auth.role and pck_api_auth.perm!