Skip to content

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.

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 ​

Authorization - Data  Model

API ​

Extend existing get_context routine to return privilege data.

./db/src/database/odbvue/package_bodies/pck_app.sql ​

plsql
-- ...
  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 ​

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:

ValueDescription
alwaysPage always appears in navigation, regardless of authentication state
when-authenticatedPage only appears when user is logged in
when-unauthenticatedPage only appears when user is not logged in
with-rolePage only appears when user has one of the specified roles
neverPage never appears in navigation (e.g., error pages, authentication pages)

Access ​

Controls whether a user can actually access the page:

ValueDescription
alwaysPage is always accessible to anyone
when-authenticatedPage requires user to be logged in
when-unauthenticatedPage only accessible to users not logged in
with-rolePage requires user to have one of the specified roles
neverPage is never accessible

Page Meta Definition ​

Page meta can be set using definePage() in .vue files:

ts
<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:

vue
<route lang="json">
{
  "meta": {
    "title": "Admin",
    "visibility": "with-role",
    "access": "with-role",
    "roles": ["admin"]
  }
}
</route>

Or in .md files using frontmatter:

md
---
title: About
description: Learn more about our platform
icon: $mdiInformation
color: '#FA8531'
visibility: always
access: always
---

# About

Page content here...

Apply meta to pages ​

PagePathVisibilityAccessRoles
Home/alwaysalways-
About/aboutalwaysalways-
Login/loginneverwhen-unauthenticated-
Sign Up/signupneverwhen-unauthenticated-
Recover Password/recover-passwordneverwhen-unauthenticated-
Reset Password/reset-password/[token]neveralways-
Confirm Email/confirm-email/[id]neveralways-
Sandbox/sandboxalwayswhen-authenticated-
Admin/adminwith-rolewith-roleadmin
Not Found/[...path]neveralways-

Guard method in Navigation Store

@/stores/app/navigation.ts ​

source
ts
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 ​

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!