From d8563147ebc9510d85f66eea3a86fe462a333ef4 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 25 Aug 2026 10:47:54 +0200 Subject: [PATCH 1/2] Only show resources owned by the user when creating policies --- controller/src/classes/OdrlController.ts | 5 + .../src/classes/utils/OdrlPolicyService.ts | 13 +++ controller/src/types/modules.ts | 5 + loama/src/components/policy-view/RuleForm.vue | 25 ++++- loama/src/lib/Usetomselect.ts | 99 ++++++++++++++++--- loama/src/lib/state.ts | 7 +- 6 files changed, 137 insertions(+), 17 deletions(-) diff --git a/controller/src/classes/OdrlController.ts b/controller/src/classes/OdrlController.ts index b603a54..b2aca70 100644 --- a/controller/src/classes/OdrlController.ts +++ b/controller/src/classes/OdrlController.ts @@ -53,6 +53,11 @@ export class ODRLController { + return new ODRLPolicyService(this.authorizationServerURL).fetchResources(); + } + + /** * Updates existing policies according to present rule changes * @param updates All requested changes diff --git a/controller/src/classes/utils/OdrlPolicyService.ts b/controller/src/classes/utils/OdrlPolicyService.ts index 986be95..8461e0a 100644 --- a/controller/src/classes/utils/OdrlPolicyService.ts +++ b/controller/src/classes/utils/OdrlPolicyService.ts @@ -24,6 +24,19 @@ export class ODRLPolicyService { return result; } + public async fetchResources(): Promise { + // TODO: should use well-known URL + const response = await authenticatedFetch(`${this.authorizationServerURL}/resources/`); + if (!response.ok) { + throw new Error(response.statusText); + } + const data = await response.json(); + if (!Array.isArray(data)) { + throw new Error('Expected an array of resources'); + } + return data; + } + public async fetchPolicies() { // Get all our policies const response = await authenticatedFetch(UMA_URL(this.authorizationServerURL), { diff --git a/controller/src/types/modules.ts b/controller/src/types/modules.ts index a3c5bea..7e4006e 100644 --- a/controller/src/types/modules.ts +++ b/controller/src/types/modules.ts @@ -15,6 +15,11 @@ export interface IController>(subject: T[K]): string; getOrCreateIndex(): Promise; + /** + * Returns all resources registered for the authorized user. + */ + getResources(): Promise; + updatePolicy(updates: RuleUpdate[]): Promise; getResourcePolicies(resourceUrl: string): Promise; diff --git a/loama/src/components/policy-view/RuleForm.vue b/loama/src/components/policy-view/RuleForm.vue index ece20f3..a41ec26 100644 --- a/loama/src/components/policy-view/RuleForm.vue +++ b/loama/src/components/policy-view/RuleForm.vue @@ -7,8 +7,9 @@ placeholder="webId of the person or app" /> - + + Access level @@ -58,7 +59,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'; import type { Rule, Constraint, RuleUpdate } from 'loama-controller'; import { levelForAction } from '@/lib/Accesslevel'; import { loadPurposes, PURPOSES } from '@/lib/Purposes'; -import { useTomSelectMultiple } from '@/lib/Usetomselect' +import { useTomSelectMultiple, useTomSelectSingle } from '@/lib/Usetomselect' import { usePodStore } from '@/lib/state'; import { useControllerStore } from '@/stores/useControllerStore'; import 'tom-select/dist/css/tom-select.css'; @@ -96,6 +97,7 @@ const form = reactive({ const errors = ref({ resourceIdentifier: false, action: false }); const confirmingDelete = ref(false); +const resourcesLoaded = ref(false); const purposesLoaded = ref(false); watch(() => form.action.length, (newLength) => { @@ -114,12 +116,27 @@ const subjectEditable = computed(() => { return editable.value && selectedPolicy.value?.type !== 'Agreement'; }); +const resourceSelectEl = ref(null); +const resourceModel = computed({ + get: () => form.resourceIdentifier, + set: (value) => { form.resourceIdentifier = value; }, +}); + const purposeSelectEl = ref(null); const purposesModel = computed({ get: () => form.purposes, set: (value) => { form.purposes = value; }, }); +useTomSelectSingle(resourceSelectEl, resourceModel, editable, () => ({ + options: podStore.resources.map((r) => ({ value: r, text: r })), + valueField: 'value', + labelField: 'text', + searchField: ['text'], + placeholder: 'Search resources…', + create: false, +})); + useTomSelectMultiple(purposeSelectEl, purposesModel, editable, { options: PURPOSES.options, optgroups: PURPOSES.groups, @@ -200,6 +217,8 @@ watch(() => [props.rule, props.mode], resetForm, { immediate: true }); onMounted(async () => { await loadPurposes(); purposesLoaded.value = true; + await podStore.loadResources(controllerStore.current); + resourcesLoaded.value = true; resetForm(); }); diff --git a/loama/src/lib/Usetomselect.ts b/loama/src/lib/Usetomselect.ts index 4fe3902..ca0ace7 100644 --- a/loama/src/lib/Usetomselect.ts +++ b/loama/src/lib/Usetomselect.ts @@ -12,10 +12,10 @@ import TomSelect from 'tom-select'; * @param settings TomSelect settings (options, optgroups, render, etc.) */ export function useTomSelectMultiple( - elRef: Ref, - modelValue: Ref, - editable: Ref, - settings: Record = {}, + elRef: Ref, + modelValue: Ref, + editable: Ref, + settings: Record = {}, ) { let instance: TomSelect | null = null; @@ -24,8 +24,8 @@ export function useTomSelectMultiple( const current = instance.getValue(); const currentArray = Array.isArray(current) ? current : [current]; const same = - currentArray.length === value.length && - currentArray.every((v, i) => v === value[i]); + currentArray.length === value.length && + currentArray.every((v, i) => v === value[i]); if (!same) instance.setValue(value, true); // true = silent, avoids feedback loop }; @@ -59,16 +59,89 @@ export function useTomSelectMultiple( // that starts false), so react to the ref itself rather than only // creating the instance once in onMounted. watch( - elRef, - (el) => { - destroy(); - if (el) create(el); - }, - { immediate: true }, + elRef, + (el) => { + destroy(); + if (el) create(el); + }, + { immediate: true }, ); watch(modelValue, applyValue, { deep: true }); watch(editable, applyEditable); onBeforeUnmount(destroy); -} \ No newline at end of file +} + +/** + * Same idea as useTomSelectMultiple, but binds to a plain element + * @param modelValue reactive single selected value, kept in sync both ways + * @param editable reactive boolean, toggles TomSelect enable/disable + * @param settings TomSelect settings (options, optgroups, render, etc.) + */ +export function useTomSelectSingle( + elRef: Ref, + modelValue: Ref, + editable: Ref, + settings: Record | (() => Record) = {}, +) { + let instance: TomSelect | null = null; + + // Support a factory so option lists reflect data that's still loading + // (e.g. async store data) at the time useTomSelectSingle() is called, + // rather than whatever snapshot existed during