window.app = Vue.createApp({ el: '#vue', mixins: [windowMixin], data() { return { permissions: [], accounts: [], users: [], filteredUsers: [], equityEligibleUsers: [], loading: false, granting: false, revoking: false, grantingEquity: false, revokingEquity: false, activeTab: 'by-user', showGrantDialog: false, showRevokeDialog: false, showGrantEquityDialog: false, showRevokeEquityDialog: false, showBulkGrantDialog: false, showBulkGrantErrors: false, permissionToRevoke: null, equityToRevoke: null, bulkGranting: false, bulkGrantResults: null, isSuperUser: false, grantForm: { user_id: '', account_id: '', permission_type: 'submit_expense', notes: '', expires_at: '' }, grantEquityForm: { user_id: '', notes: '' }, bulkGrantForm: { user_ids: [], account_id: '', permission_type: 'submit_expense', 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, is_virtual: acc.is_virtual || false })) }, 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 ) }, isBulkGrantFormValid() { return !!( this.bulkGrantForm.user_ids && this.bulkGrantForm.user_ids.length > 0 && this.bulkGrantForm.account_id && this.bulkGrantForm.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 { // Admin permissions UI needs to see virtual accounts to grant permissions on them const response = await LNbits.api.request( 'GET', '/castle/api/v1/accounts?exclude_virtual=false', 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: 'submit_expense', notes: '', expires_at: '' } }, async bulkGrantPermissions() { if (!this.isBulkGrantFormValid) { this.$q.notify({ type: 'warning', message: 'Please fill in all required fields', timeout: 3000 }) return } this.bulkGranting = true this.bulkGrantResults = null try { const payload = { user_ids: this.bulkGrantForm.user_ids, account_id: this.bulkGrantForm.account_id, permission_type: this.bulkGrantForm.permission_type } if (this.bulkGrantForm.notes) { payload.notes = this.bulkGrantForm.notes } if (this.bulkGrantForm.expires_at) { payload.expires_at = new Date(this.bulkGrantForm.expires_at).toISOString() } const response = await LNbits.api.request( 'POST', '/castle/api/v1/admin/permissions/bulk-grant', this.g.user.wallets[0].adminkey, payload ) this.bulkGrantResults = response.data // Show success notification const message = this.bulkGrantResults.failure_count > 0 ? `Bulk grant completed: ${this.bulkGrantResults.success_count} succeeded, ${this.bulkGrantResults.failure_count} failed` : `Successfully granted permissions to ${this.bulkGrantResults.success_count} users` this.$q.notify({ type: this.bulkGrantResults.failure_count > 0 ? 'warning' : 'positive', message: message, timeout: 5000 }) // Reload permissions to show new grants await this.loadPermissions() // Don't close dialog immediately if there were failures // (so user can review errors) if (this.bulkGrantResults.failure_count === 0) { setTimeout(() => { this.closeBulkGrantDialog() }, 2000) } } catch (error) { console.error('Failed to bulk grant permissions:', error) this.$q.notify({ type: 'negative', message: 'Failed to bulk grant permissions', caption: error.message || 'Unknown error', timeout: 5000 }) } finally { this.bulkGranting = false } }, closeBulkGrantDialog() { this.showBulkGrantDialog = false this.resetBulkGrantForm() this.bulkGrantResults = null }, resetBulkGrantForm() { this.bulkGrantForm = { user_ids: [], account_id: '', permission_type: 'submit_expense', 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 loadEquityEligibleUsers() { if (!this.isSuperUser) { return } try { const response = await LNbits.api.request( 'GET', '/castle/api/v1/admin/equity-eligibility', this.g.user.wallets[0].adminkey ) this.equityEligibleUsers = response.data || [] } catch (error) { console.error('Failed to load equity-eligible users:', error) this.$q.notify({ type: 'negative', message: 'Failed to load equity-eligible users', caption: error.message || 'Unknown error', timeout: 5000 }) } }, async grantEquityEligibility() { if (!this.grantEquityForm.user_id) { this.$q.notify({ type: 'warning', message: 'Please select a user', timeout: 3000 }) return } this.grantingEquity = true try { const payload = { user_id: this.grantEquityForm.user_id, is_equity_eligible: true } if (this.grantEquityForm.notes) { payload.notes = this.grantEquityForm.notes } await LNbits.api.request( 'POST', '/castle/api/v1/admin/equity-eligibility', this.g.user.wallets[0].adminkey, payload ) this.$q.notify({ type: 'positive', message: 'Equity eligibility granted successfully', timeout: 3000 }) this.showGrantEquityDialog = false this.resetGrantEquityForm() await this.loadEquityEligibleUsers() } catch (error) { console.error('Failed to grant equity eligibility:', error) this.$q.notify({ type: 'negative', message: 'Failed to grant equity eligibility', caption: error.message || 'Unknown error', timeout: 5000 }) } finally { this.grantingEquity = false } }, confirmRevokeEquity(equity) { this.equityToRevoke = equity this.showRevokeEquityDialog = true }, async revokeEquityEligibility() { if (!this.equityToRevoke) return this.revokingEquity = true try { await LNbits.api.request( 'DELETE', `/castle/api/v1/admin/equity-eligibility/${this.equityToRevoke.user_id}`, this.g.user.wallets[0].adminkey ) this.$q.notify({ type: 'positive', message: 'Equity eligibility revoked successfully', timeout: 3000 }) this.showRevokeEquityDialog = false this.equityToRevoke = null await this.loadEquityEligibleUsers() } catch (error) { console.error('Failed to revoke equity eligibility:', error) this.$q.notify({ type: 'negative', message: 'Failed to revoke equity eligibility', caption: error.message || 'Unknown error', timeout: 5000 }) } finally { this.revokingEquity = false } }, resetGrantEquityForm() { this.grantEquityForm = { user_id: '', notes: '' } } }, 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(), this.loadEquityEligibleUsers() ]) } } } }) window.app.mount('#vue')