Promoted from the door-portal scratch repo to its own repo for install via the aiolabs catalog. Authenticates a tapped Bolt Card via boltcards /verify, authorizes against per-door grants, and fires the door's local Home Assistant webhook to unlock a Z-Wave lock. Fails closed; logs every attempt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
3.8 KiB
JavaScript
141 lines
3.8 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
|
|
},
|
|
async createDoor() {
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
'/access/api/v1/doors',
|
|
this.adminkey,
|
|
this.doorDialog.data
|
|
)
|
|
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()
|
|
}
|
|
})
|