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.
This commit is contained in:
padreug 2025-11-07 17:57:33 +01:00
parent 92c1649f3b
commit 9c63511371
2 changed files with 628 additions and 0 deletions

292
static/js/permissions.js Normal file
View file

@ -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')