Replaces the user ID input field with a user selection dropdown, allowing administrators to search and select users for permission management. This simplifies the process of assigning permissions and improves user experience. Fetches Castle users via a new API endpoint and filters them based on search input. Only users with Castle accounts (receivables, payables, equity, or permissions) are listed.
348 lines
8.5 KiB
JavaScript
348 lines
8.5 KiB
JavaScript
window.app = Vue.createApp({
|
|
el: '#vue',
|
|
mixins: [windowMixin],
|
|
data() {
|
|
return {
|
|
permissions: [],
|
|
accounts: [],
|
|
users: [],
|
|
filteredUsers: [],
|
|
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
|
|
}))
|
|
},
|
|
|
|
userOptions() {
|
|
const users = this.filteredUsers.length > 0 ? this.filteredUsers : this.users
|
|
return users.map(user => ({
|
|
id: user.id,
|
|
username: user.username || user.id,
|
|
label: user.username ? `${user.username} (${user.id.substring(0, 8)}...)` : user.id
|
|
}))
|
|
},
|
|
|
|
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 loadUsers() {
|
|
if (!this.isSuperUser) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/admin/castle-users',
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.users = response.data || []
|
|
this.filteredUsers = []
|
|
} catch (error) {
|
|
console.error('Failed to load users:', error)
|
|
this.$q.notify({
|
|
type: 'negative',
|
|
message: 'Failed to load users',
|
|
caption: error.message || 'Unknown error',
|
|
timeout: 5000
|
|
})
|
|
}
|
|
},
|
|
|
|
filterUsers(val, update) {
|
|
if (val === '') {
|
|
update(() => {
|
|
this.filteredUsers = []
|
|
})
|
|
return
|
|
}
|
|
|
|
update(() => {
|
|
const needle = val.toLowerCase()
|
|
this.filteredUsers = this.users.filter(user => {
|
|
const username = user.username || ''
|
|
const userId = user.id || ''
|
|
return username.toLowerCase().includes(needle) || userId.toLowerCase().includes(needle)
|
|
})
|
|
})
|
|
},
|
|
|
|
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 Promise.all([
|
|
this.loadPermissions(),
|
|
this.loadUsers()
|
|
])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
window.app.mount('#vue')
|