Enables user selection for permissions

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.
This commit is contained in:
padreug 2025-11-07 23:06:24 +01:00
parent fc12dae435
commit d6a1c6e5b3
3 changed files with 142 additions and 7 deletions

View file

@ -5,6 +5,8 @@ window.app = Vue.createApp({
return { return {
permissions: [], permissions: [],
accounts: [], accounts: [],
users: [],
filteredUsers: [],
loading: false, loading: false,
granting: false, granting: false,
revoking: false, revoking: false,
@ -48,6 +50,15 @@ window.app = Vue.createApp({
})) }))
}, },
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() { isGrantFormValid() {
return !!( return !!(
this.grantForm.user_id && this.grantForm.user_id &&
@ -130,6 +141,48 @@ window.app = Vue.createApp({
} }
}, },
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() { async grantPermission() {
if (!this.isGrantFormValid) { if (!this.isGrantFormValid) {
this.$q.notify({ this.$q.notify({
@ -283,7 +336,10 @@ window.app = Vue.createApp({
if (this.g.user.wallets && this.g.user.wallets.length > 0) { if (this.g.user.wallets && this.g.user.wallets.length > 0) {
await this.loadAccounts() await this.loadAccounts()
if (this.isSuperUser) { if (this.isSuperUser) {
await this.loadPermissions() await Promise.all([
this.loadPermissions(),
this.loadUsers()
])
} }
} }
} }

View file

@ -187,19 +187,33 @@
</q-card-section> </q-card-section>
<q-card-section class="q-gutter-md"> <q-card-section class="q-gutter-md">
<!-- User ID --> <!-- User -->
<q-input <q-select
v-model="grantForm.user_id" v-model="grantForm.user_id"
label="User ID *" label="User *"
hint="Wallet ID of the user" hint="Search and select a user"
:options="userOptions"
option-value="id"
option-label="label"
emit-value
map-options
use-input
@filter="filterUsers"
outlined outlined
dense dense
:rules="[val => !!val || 'User ID is required']" :rules="[val => !!val || 'User is required']"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="person"></q-icon> <q-icon name="person"></q-icon>
</template> </template>
</q-input> <template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No users found
</q-item-section>
</q-item>
</template>
</q-select>
<!-- Account --> <!-- Account -->
<q-select <q-select

View file

@ -1339,6 +1339,71 @@ async def api_get_all_users(
return users return users
@castle_api_router.get("/api/v1/admin/castle-users")
async def api_get_castle_users(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[dict]:
"""
Get all users who have Castle accounts (receivables, payables, equity, or permissions).
This returns only users who are actively using Castle, not all LNbits users.
Admin only.
"""
from lnbits.core.crud.users import get_user
from .crud import get_all_equity_eligible_users
# Get all user-specific accounts (Receivable/Payable/Equity)
all_accounts = await get_all_accounts()
user_accounts = [acc for acc in all_accounts if acc.user_id is not None]
# Get all users who have permissions
all_permissions = []
for account in all_accounts:
account_perms = await get_account_permissions(account.id)
all_permissions.extend(account_perms)
# Get all equity-eligible users
equity_users = await get_all_equity_eligible_users()
# Collect unique user IDs
user_ids = set()
# Add users with accounts
for acc in user_accounts:
user_ids.add(acc.user_id)
# Add users with permissions
for perm in all_permissions:
user_ids.add(perm.user_id)
# Add equity-eligible users
for equity in equity_users:
user_ids.add(equity.user_id)
# Build user list with enriched data
users = []
for user_id in user_ids:
# Get user details from core
user = await get_user(user_id)
# Use username if available, otherwise use user_id
username = user.username if user and user.username else None
# Get user's wallet setting if exists
user_wallet = await get_user_wallet(user_id)
users.append({
"id": user_id,
"user_id": user_id, # Compatibility with existing code
"username": username,
"user_wallet_id": user_wallet.user_wallet_id if user_wallet else None,
})
# Sort by username (None values last)
users.sort(key=lambda x: (x["username"] is None, x["username"] or "", x["user_id"]))
return users
@castle_api_router.get("/api/v1/user/wallet") @castle_api_router.get("/api/v1/user/wallet")
async def api_get_user_wallet( async def api_get_user_wallet(
user: User = Depends(check_user_exists), user: User = Depends(check_user_exists),