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>
158 lines
4.4 KiB
JavaScript
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()
|
|
}
|
|
})
|