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 %} +
Manage user access to expense accounts
+No permissions granted yet
+No permissions granted yet
+Are you sure you want to revoke this permission? The user will immediately lose access.
+