From 07739dfb61adc879853539f82388b14fa6247744 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 20 Aug 2026 13:39:34 +0200 Subject: [PATCH 1/2] Provide the option to highlight a specific grant --- documentation/access-requests.md | 32 ++++ .../components/access-grants/AccessGrant.vue | 164 ++++++++++++++---- 2 files changed, 158 insertions(+), 38 deletions(-) diff --git a/documentation/access-requests.md b/documentation/access-requests.md index c43179e..ba3c32b 100644 --- a/documentation/access-requests.md +++ b/documentation/access-requests.md @@ -93,6 +93,38 @@ curl --location 'http://localhost:4000/uma/policies' --header 'Authorization: We 3. Verify that the request appears in Alice's **Accepted** tab. 4. Verify that the request appears in Bob's **Accepted** tab. +## Deep-linking to a specific access request (Grant Access page) + +External clients can redirect a resource owner directly to the **Grant Access** page with a specific incoming request pre-filtered, so they immediately see the one they need to act on. + +### How it works + +Append a `request` query parameter whose value is the URL-encoded UID of the access request: + +``` +/access-grants/?request= +``` + +**Example:** + +``` +http://localhost:5173/access-grants/?request=http%3A%2F%2Fexample.org%2Fc432df06-9bae-4a20-a21d-0e30833552b0 +``` + +When the parameter is present the page will: + +1. Show a **purple filter banner** at the top: *"Active filter: showing only the request you were directed to."* +2. Display **only the matching request**, with the Accept / Deny buttons available as normal. +3. Provide a **"Show all requests"** button inside the banner to clear the filter and return to the full grouped view (Requested / Accepted / Denied). + +If the UID in the parameter does not match any known request, a *"The requested access request could not be found."* message is shown instead. + +### Usage notes + +- The filter is applied purely client-side via the Vue Router query parameter; no server changes are required. +- Clicking **"Show all requests"** removes the `request` parameter from the URL via `router.replace`, so the browser history is not polluted with the filtered URL. +- The feature is intentionally **only on the Grant Access page** (`/access-grants/`). The Request Access page (`/access-requests/`) does not support this parameter. + ## Known bugs and limitations * **UI State Sync:** When you update a policy on a selected resource, the interface does not visually refresh until you manually deselect and reselect that resource. diff --git a/loama/src/components/access-grants/AccessGrant.vue b/loama/src/components/access-grants/AccessGrant.vue index 9c64f36..0ae9176 100644 --- a/loama/src/components/access-grants/AccessGrant.vue +++ b/loama/src/components/access-grants/AccessGrant.vue @@ -1,10 +1,13 @@ @@ -190,4 +240,42 @@ button.deny:hover { text-align: center; padding: 1rem; } + +.filter-banner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + background-color: color-mix(in srgb, var(--solid-purple) 10%, white); + border: 1.5px solid var(--solid-purple); + border-radius: var(--base-corner); + padding: 0.75rem 1.25rem; + font-size: calc(var(--base-unit) * 1.75); + color: var(--solid-purple); +} + +.clear-filter-button { + background: none; + border: 1.5px solid var(--solid-purple); + color: var(--solid-purple); + border-radius: var(--base-corner); + padding: 0.4rem 0.9rem; + font-weight: 600; + font-size: calc(var(--base-unit) * 1.75); + cursor: pointer; + white-space: nowrap; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.clear-filter-button:hover { + background-color: var(--solid-purple); + color: white; +} + +.access-request-item.highlighted { + border-radius: var(--base-corner); + outline: 2.5px solid var(--solid-purple); + outline-offset: 2px; + background-color: color-mix(in srgb, var(--solid-purple) 5%, white); +} From 570a16d6cc1ca7bec0802b03b39b7df9b0498f23 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 20 Aug 2026 14:06:41 +0200 Subject: [PATCH 2/2] Add return URL query parameter to access grants --- documentation/access-requests.md | 17 +++++++-- .../components/access-grants/AccessGrant.vue | 35 +++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/documentation/access-requests.md b/documentation/access-requests.md index ba3c32b..d0ebf7b 100644 --- a/documentation/access-requests.md +++ b/documentation/access-requests.md @@ -99,10 +99,13 @@ External clients can redirect a resource owner directly to the **Grant Access** ### How it works -Append a `request` query parameter whose value is the URL-encoded UID of the access request: +Append: + +- a `request` query parameter whose value is the URL-encoded UID of the access request +- an optional `returnUrl` query parameter whose value is the URL-encoded callback URL of the originating client ``` -/access-grants/?request= +/access-grants/?request=&returnUrl= ``` **Example:** @@ -111,6 +114,10 @@ Append a `request` query parameter whose value is the URL-encoded UID of the acc http://localhost:5173/access-grants/?request=http%3A%2F%2Fexample.org%2Fc432df06-9bae-4a20-a21d-0e30833552b0 ``` +```text +http://localhost:5173/access-grants/?request=http%3A%2F%2Fexample.org%2Fc432df06-9bae-4a20-a21d-0e30833552b0&returnUrl=https%3A%2F%2Fclient.example%2Fgrant-callback +``` + When the parameter is present the page will: 1. Show a **purple filter banner** at the top: *"Active filter: showing only the request you were directed to."* @@ -119,11 +126,15 @@ When the parameter is present the page will: If the UID in the parameter does not match any known request, a *"The requested access request could not be found."* message is shown instead. +When `returnUrl` is provided, clicking **Accept** or **Deny** redirects the user to that URL only in the filtered deep-link flow (the `request` query is active and matches the acted item). The redirect appends: + +- `request=` +- `decision=accepted` or `decision=denied` + ### Usage notes - The filter is applied purely client-side via the Vue Router query parameter; no server changes are required. - Clicking **"Show all requests"** removes the `request` parameter from the URL via `router.replace`, so the browser history is not polluted with the filtered URL. -- The feature is intentionally **only on the Grant Access page** (`/access-grants/`). The Request Access page (`/access-requests/`) does not support this parameter. ## Known bugs and limitations diff --git a/loama/src/components/access-grants/AccessGrant.vue b/loama/src/components/access-grants/AccessGrant.vue index 0ae9176..d6362ee 100644 --- a/loama/src/components/access-grants/AccessGrant.vue +++ b/loama/src/components/access-grants/AccessGrant.vue @@ -10,8 +10,40 @@ const route = useRoute(); const router = useRouter(); const accessRequests: Ref = ref([]); +const highlightedUid = computed(() => + typeof route.query.request === 'string' ? route.query.request : null +); + +const returnUrl = computed(() => + typeof route.query.returnUrl === 'string' ? route.query.returnUrl : null +); + +const buildReturnLocation = (requestID: string, status: 'accepted' | 'denied'): string | null => { + // Redirect callbacks are only enabled for deep-linked filtered requests. + if (!highlightedUid.value || highlightedUid.value !== requestID) return null; + if (!returnUrl.value) return null; + + try { + const target = new URL(returnUrl.value); + // Never allow javascript: style redirects from query parameters. + if (target.protocol === 'javascript:') return null; + target.searchParams.set('request', requestID); + target.searchParams.set('decision', status); + return target.toString(); + } catch { + return null; + } +}; + const updateStatus = async (requestID: string, status: 'accepted' | 'denied') => { await controllerStore.current.handleAccessRequest(requestID, status); + + const returnLocation = buildReturnLocation(requestID, status); + if (returnLocation) { + window.location.assign(returnLocation); + return; + } + await fetchAccessRequests(); }; @@ -28,9 +60,6 @@ onMounted(async () => { onBeforeUnmount(() => clearInterval(interval)); -const highlightedUid = computed(() => - typeof route.query.request === 'string' ? route.query.request : null -); const filteredRequests = computed(() => highlightedUid.value