From 9c63511371d566ee89a34957e47a7421bbedffff Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 7 Nov 2025 17:57:33 +0100 Subject: [PATCH] Adds permission management UI and logic Implements a Vue-based UI for managing user permissions, allowing administrators to grant and revoke access to expense accounts. Provides views for managing permissions by user and by account, along with dialogs for granting and revoking permissions. Integrates with the LNbits API to load accounts and permissions and to persist changes. --- static/js/permissions.js | 292 ++++++++++++++++++++++++++ templates/castle/permissions.html | 336 ++++++++++++++++++++++++++++++ 2 files changed, 628 insertions(+) create mode 100644 static/js/permissions.js create mode 100644 templates/castle/permissions.html diff --git a/static/js/permissions.js b/static/js/permissions.js new file mode 100644 index 0000000..43659ee --- /dev/null +++ b/static/js/permissions.js @@ -0,0 +1,292 @@ +window.app = Vue.createApp({ + el: '#vue', + mixins: [windowMixin], + data() { + return { + permissions: [], + accounts: [], + loading: false, + granting: false, + revoking: false, + activeTab: 'by-user', + showGrantDialog: false, + showRevokeDialog: false, + permissionToRevoke: null, + isSuperUser: false, + grantForm: { + user_id: '', + account_id: '', + permission_type: 'read', + notes: '', + expires_at: '' + }, + permissionTypeOptions: [ + { + value: 'read', + label: 'Read', + description: 'View account and balance' + }, + { + value: 'submit_expense', + label: 'Submit Expense', + description: 'Submit expenses to this account' + }, + { + value: 'manage', + label: 'Manage', + description: 'Full account management' + } + ] + } + }, + + computed: { + accountOptions() { + return this.accounts.map(acc => ({ + id: acc.id, + name: acc.name + })) + }, + + isGrantFormValid() { + return !!( + this.grantForm.user_id && + this.grantForm.account_id && + this.grantForm.permission_type + ) + }, + + permissionsByUser() { + const grouped = new Map() + for (const perm of this.permissions) { + if (!grouped.has(perm.user_id)) { + grouped.set(perm.user_id, []) + } + grouped.get(perm.user_id).push(perm) + } + return grouped + }, + + permissionsByAccount() { + const grouped = new Map() + for (const perm of this.permissions) { + if (!grouped.has(perm.account_id)) { + grouped.set(perm.account_id, []) + } + grouped.get(perm.account_id).push(perm) + } + return grouped + } + }, + + methods: { + async loadPermissions() { + if (!this.isSuperUser) { + this.$q.notify({ + type: 'warning', + message: 'Admin access required to view permissions', + timeout: 3000 + }) + return + } + + this.loading = true + try { + const response = await LNbits.api.request( + 'GET', + '/castle/api/v1/admin/permissions', + this.g.user.wallets[0].adminkey + ) + this.permissions = response.data + } catch (error) { + console.error('Failed to load permissions:', error) + this.$q.notify({ + type: 'negative', + message: 'Failed to load permissions', + caption: error.message || 'Unknown error', + timeout: 5000 + }) + } finally { + this.loading = false + } + }, + + async loadAccounts() { + try { + const response = await LNbits.api.request( + 'GET', + '/castle/api/v1/accounts', + this.g.user.wallets[0].inkey + ) + this.accounts = response.data + } catch (error) { + console.error('Failed to load accounts:', error) + this.$q.notify({ + type: 'negative', + message: 'Failed to load accounts', + caption: error.message || 'Unknown error', + timeout: 5000 + }) + } + }, + + async grantPermission() { + if (!this.isGrantFormValid) { + this.$q.notify({ + type: 'warning', + message: 'Please fill in all required fields', + timeout: 3000 + }) + return + } + + this.granting = true + try { + const payload = { + user_id: this.grantForm.user_id, + account_id: this.grantForm.account_id, + permission_type: this.grantForm.permission_type + } + + if (this.grantForm.notes) { + payload.notes = this.grantForm.notes + } + + if (this.grantForm.expires_at) { + payload.expires_at = new Date(this.grantForm.expires_at).toISOString() + } + + await LNbits.api.request( + 'POST', + '/castle/api/v1/admin/permissions', + this.g.user.wallets[0].adminkey, + payload + ) + + this.$q.notify({ + type: 'positive', + message: 'Permission granted successfully', + timeout: 3000 + }) + + this.showGrantDialog = false + this.resetGrantForm() + await this.loadPermissions() + } catch (error) { + console.error('Failed to grant permission:', error) + this.$q.notify({ + type: 'negative', + message: 'Failed to grant permission', + caption: error.message || 'Unknown error', + timeout: 5000 + }) + } finally { + this.granting = false + } + }, + + confirmRevokePermission(permission) { + this.permissionToRevoke = permission + this.showRevokeDialog = true + }, + + async revokePermission() { + if (!this.permissionToRevoke) return + + this.revoking = true + try { + await LNbits.api.request( + 'DELETE', + `/castle/api/v1/admin/permissions/${this.permissionToRevoke.id}`, + this.g.user.wallets[0].adminkey + ) + + this.$q.notify({ + type: 'positive', + message: 'Permission revoked successfully', + timeout: 3000 + }) + + this.showRevokeDialog = false + this.permissionToRevoke = null + await this.loadPermissions() + } catch (error) { + console.error('Failed to revoke permission:', error) + this.$q.notify({ + type: 'negative', + message: 'Failed to revoke permission', + caption: error.message || 'Unknown error', + timeout: 5000 + }) + } finally { + this.revoking = false + } + }, + + resetGrantForm() { + this.grantForm = { + user_id: '', + account_id: '', + permission_type: 'read', + notes: '', + expires_at: '' + } + }, + + getAccountName(accountId) { + const account = this.accounts.find(a => a.id === accountId) + return account ? account.name : accountId + }, + + getPermissionLabel(permissionType) { + const option = this.permissionTypeOptions.find(opt => opt.value === permissionType) + return option ? option.label : permissionType + }, + + getPermissionColor(permissionType) { + switch (permissionType) { + case 'read': + return 'blue' + case 'submit_expense': + return 'green' + case 'manage': + return 'red' + default: + return 'grey' + } + }, + + getPermissionIcon(permissionType) { + switch (permissionType) { + case 'read': + return 'visibility' + case 'submit_expense': + return 'add_circle' + case 'manage': + return 'admin_panel_settings' + default: + return 'security' + } + }, + + formatDate(dateString) { + if (!dateString) return '-' + const date = new Date(dateString) + return date.toLocaleString() + } + }, + + async created() { + // Check if user is super user + this.isSuperUser = this.g.user.super_user || false + + if (this.g.user.wallets && this.g.user.wallets.length > 0) { + await this.loadAccounts() + if (this.isSuperUser) { + await this.loadPermissions() + } + } + } +}) + +window.app.mount('#vue') diff --git a/templates/castle/permissions.html b/templates/castle/permissions.html new file mode 100644 index 0000000..74d327b --- /dev/null +++ b/templates/castle/permissions.html @@ -0,0 +1,336 @@ +{% extends "base.html" %} +{% from "macros.jinja" import window_vars with context %} + +{% block scripts %} +{{ window_vars(user) }} + +{% endblock %} + +{% block page %} +
+
+ + +
+
+
🔐 Permission Management
+

Manage user access to expense accounts

+
+
+ + Admin access required + +
+
+ + + + +
+ Admin Access Required: You must be a super user to manage permissions. +
+
+ + + + + + + + + + + + +
+ +
+ +
+ +

No permissions granted yet

+
+ +
+ + +
+ + User: {% raw %}{{ userId }}{% endraw %} +
+ + + + + + + + + {% raw %}{{ getAccountName(perm.account_id) }}{% endraw %} + + + {% raw %}{{ getPermissionLabel(perm.permission_type) }}{% endraw %} + + + + + Granted: {% raw %}{{ formatDate(perm.granted_at) }}{% endraw %} + + Expires: {% raw %}{{ formatDate(perm.expires_at) }}{% endraw %} + + + {% raw %}{{ perm.notes }}{% endraw %} + + + + + Revoke Permission + + + + +
+
+
+
+ + + +
+ +
+ +
+ +

No permissions granted yet

+
+ +
+ + +
+ + {% raw %}{{ getAccountName(accountId) }}{% endraw %} +
+ + + + + + + + + {% raw %}{{ perm.user_id }}{% endraw %} + + + {% raw %}{{ getPermissionLabel(perm.permission_type) }}{% endraw %} + + + + + Granted: {% raw %}{{ formatDate(perm.granted_at) }}{% endraw %} + + Expires: {% raw %}{{ formatDate(perm.expires_at) }}{% endraw %} + + + {% raw %}{{ perm.notes }}{% endraw %} + + + + + Revoke Permission + + + + +
+
+
+
+
+
+
+
+
+ + + + + +
Grant Account Permission
+
+ Grant a user permission to access an expense account. Permissions on parent accounts cascade to children. +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
Revoke Permission?
+
+ + +

Are you sure you want to revoke this permission? The user will immediately lose access.

+ + + + User + {% raw %}{{ permissionToRevoke.user_id }}{% endraw %} + + + + + Account + {% raw %}{{ getAccountName(permissionToRevoke.account_id) }}{% endraw %} + + + + + Permission Type + {% raw %}{{ getPermissionLabel(permissionToRevoke.permission_type) }}{% endraw %} + + + +
+ + + + + +
+
+{% endblock %}