access/static/js/index.js
Patrick Mulligan 2bc4a01edc feat(ui): edit doors from the admin page (add Edit action)
The PUT /api/v1/doors/{id} endpoint already existed and preserves the
controller token when it isn't resent, but the admin UI only exposed
create / reveal-token / delete. Add a per-row Edit (pencil) button that
reuses the door dialog as create-or-update (saveDoor: POST when new, PUT
when editing), so a door's Home Assistant webhook URL / boltcards base /
timeout can be changed without deleting + recreating (which would churn
the controller token the reader uses). Bump to 0.0.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 23:56:52 +02:00

158 lines
4.4 KiB
JavaScript

// LNbits `access` extension admin app.
// Manages doors (resources), grants (card → door permissions), and shows the
// access log. The reader hits /access/api/v1/check with a door controller token;
// this UI is admin-only (wallet admin key).
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
data() {
return {
doors: [],
grants: [],
logs: [],
doorCols: [
{ name: 'name', label: 'Name', field: 'name' },
{ name: 'id', label: 'ID', field: 'id' },
{ name: 'enabled', label: 'State', field: 'enabled' },
{ name: 'actions', label: '', field: 'actions' }
],
grantCols: [
{ name: 'door', label: 'Door', field: 'door_id' },
{ name: 'external_id', label: 'Card', field: 'external_id' },
{ name: 'label', label: 'Label', field: 'label' },
{ name: 'actions', label: '', field: 'actions' }
],
logCols: [
{ name: 'time', label: 'Time', field: 'time' },
{ name: 'door', label: 'Door', field: 'door_id' },
{ name: 'decision', label: 'Decision', field: 'decision' },
{ name: 'reason', label: 'Reason', field: 'reason' }
],
doorDialog: { show: false, data: { unlock_timeout_ms: 2500 } },
grantDialog: { show: false, data: {} }
}
},
computed: {
adminkey() {
return this.g.user.wallets[0].adminkey
},
inkey() {
return this.g.user.wallets[0].inkey
},
doorOptions() {
return this.doors.map(d => ({ label: d.name, value: d.id }))
}
},
methods: {
doorName(id) {
const d = this.doors.find(x => x.id === id)
return d ? d.name : id
},
async loadAll() {
await Promise.all([this.loadDoors(), this.loadGrants(), this.loadLogs()])
},
async loadDoors() {
const { data } = await LNbits.api.request(
'GET',
'/access/api/v1/doors',
this.inkey
)
this.doors = data
},
async loadGrants() {
const { data } = await LNbits.api.request(
'GET',
'/access/api/v1/grants',
this.inkey
)
this.grants = data
},
async loadLogs() {
const { data } = await LNbits.api.request(
'GET',
'/access/api/v1/logs',
this.inkey
)
this.logs = data
},
editDoor(door) {
// Prefill the dialog for an update. Drop controller_token so it's left
// untouched server-side (api_door_update keeps it when not resent).
const { controller_token, ...data } = door
this.doorDialog = { show: true, data }
},
async saveDoor() {
// POST to create, PUT to update an existing door (data.id present).
const d = this.doorDialog.data
try {
if (d.id) {
await LNbits.api.request(
'PUT',
`/access/api/v1/doors/${d.id}`,
this.adminkey,
d
)
} else {
await LNbits.api.request(
'POST',
'/access/api/v1/doors',
this.adminkey,
d
)
}
this.doorDialog = { show: false, data: { unlock_timeout_ms: 2500 } }
await this.loadDoors()
} catch (e) {
LNbits.utils.notifyApiError(e)
}
},
async deleteDoor(id) {
try {
await LNbits.api.request(
'DELETE',
`/access/api/v1/doors/${id}`,
this.adminkey
)
await this.loadAll()
} catch (e) {
LNbits.utils.notifyApiError(e)
}
},
showToken(door) {
this.$q.dialog({
title: `${door.name} — controller token`,
message: `Set this as X-Controller-Token on the reader for doorId "${door.id}":<br><br><code>${door.controller_token}</code>`,
html: true
})
},
async createGrant() {
try {
await LNbits.api.request(
'POST',
'/access/api/v1/grants',
this.adminkey,
this.grantDialog.data
)
this.grantDialog = { show: false, data: {} }
await this.loadGrants()
} catch (e) {
LNbits.utils.notifyApiError(e)
}
},
async deleteGrant(id) {
try {
await LNbits.api.request(
'DELETE',
`/access/api/v1/grants/${id}`,
this.adminkey
)
await this.loadGrants()
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
},
created() {
this.loadAll()
}
})