[feat] access control lists (with access tokens) (#2864)

This commit is contained in:
Vlad Stan
2025-01-16 17:25:27 +02:00
committed by GitHub
parent f415a92914
commit b164317121
25 changed files with 2131 additions and 67 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -482,6 +482,12 @@ body.body--dark .q-field--error .q-field__messages {
width: 500px;
}
.lnbits__table-bordered td,
.lnbits__table-bordered th {
border: 1px solid black;
border-collapse: collapse;
}
.q-table--dense th:first-child,
.q-table--dense td:first-child,
.q-table--dense .q-table__bottom {
+9
View File
@@ -85,9 +85,11 @@ window.localisation.en = {
cancel: 'Cancel',
scan: 'Scan',
read: 'Read',
write: 'Write',
pay: 'Pay',
memo: 'Memo',
date: 'Date',
path: 'Path',
processing_payment: 'Processing payment...',
not_enough_funds: 'Not enough funds!',
search_by_tag_memo_amount: 'Search by tag, memo, amount',
@@ -256,6 +258,13 @@ window.localisation.en = {
back: 'Back',
logout: 'Logout',
look_and_feel: 'Look and Feel',
api_token: 'API Token',
api_tokens: 'API Tokens',
access_control_list: 'Access Control List',
access_control_list_admin_warning:
'This is an admin account. The generated tokens will have admin privileges.',
new_api_acl: 'New Access Control List',
api_token_id: 'Token Id',
toggle_gradient: 'Toggle Gradient',
gradient_background: 'Gradient Background',
language: 'Language',
+264
View File
@@ -21,6 +21,60 @@ window.AccountPageLogic = {
newPasswordRepeat: null,
username: null,
pubkey: null
},
apiAcl: {
showNewAclDialog: false,
showPasswordDialog: false,
showNewTokenDialog: false,
data: [],
passwordGuardedFunction: null,
newAclName: '',
newTokenName: '',
password: '',
apiToken: null,
selectedTokenId: null,
columns: [
{
name: 'Name',
align: 'left',
label: this.$t('Name'),
field: 'Name',
sortable: false
},
{
name: 'path',
align: 'left',
label: this.$t('path'),
field: 'path',
sortable: false
},
{
name: 'read',
align: 'left',
label: this.$t('read'),
field: 'read',
sortable: false
},
{
name: 'write',
align: 'left',
label: this.$t('write'),
field: 'write',
sortable: false
}
],
pagination: {
rowsPerPage: 100,
page: 1
}
},
selectedApiAcl: {
id: null,
name: null,
endpoints: [],
token_id_list: [],
allRead: false,
allWrite: false
}
}
},
@@ -151,6 +205,215 @@ window.AccountPageLogic = {
newPassword: null,
newPasswordRepeat: null
}
},
newApiAclDialog() {
this.apiAcl.newAclName = null
this.apiAcl.showNewAclDialog = true
},
newTokenAclDialog() {
this.apiAcl.newTokenName = null
this.apiAcl.newTokenExpiry = null
this.apiAcl.showNewTokenDialog = true
},
handleApiACLSelected(aclId) {
this.selectedApiAcl = {
id: null,
name: null,
endpoints: [],
token_id_list: []
}
this.apiAcl.selectedTokenId = null
if (!aclId) {
return
}
setTimeout(() => {
const selectedApiAcl = this.apiAcl.data.find(t => t.id === aclId)
if (!this.selectedApiAcl) {
return
}
this.selectedApiAcl = {...selectedApiAcl}
this.selectedApiAcl.allRead = this.selectedApiAcl.endpoints.every(
e => e.read
)
this.selectedApiAcl.allWrite = this.selectedApiAcl.endpoints.every(
e => e.write
)
})
},
handleAllEndpointsReadAccess() {
this.selectedApiAcl.endpoints.forEach(
e => (e.read = this.selectedApiAcl.allRead)
)
},
handleAllEndpointsWriteAccess() {
this.selectedApiAcl.endpoints.forEach(
e => (e.write = this.selectedApiAcl.allWrite)
)
},
async getApiACLs() {
try {
const {data} = await LNbits.api.request('GET', '/api/v1/auth/acl', null)
this.apiAcl.data = data.access_control_list
} catch (e) {
LNbits.utils.notifyApiError(e)
}
},
askPasswordAndRunFunction(func) {
this.apiAcl.passwordGuardedFunction = func
this.apiAcl.showPasswordDialog = true
},
runPasswordGuardedFunction() {
this.apiAcl.showPasswordDialog = false
const func = this.apiAcl.passwordGuardedFunction
if (func) {
this[func]()
}
},
async addApiACL() {
if (!this.apiAcl.newAclName) {
this.$q.notify({
type: 'warning',
message: 'Name is required.'
})
return
}
try {
const {data} = await LNbits.api.request(
'PUT',
'/api/v1/auth/acl',
null,
{
id: this.apiAcl.newAclName,
name: this.apiAcl.newAclName,
password: this.apiAcl.password
}
)
this.apiAcl.data = data.access_control_list
const acl = this.apiAcl.data.find(
t => t.name === this.apiAcl.newAclName
)
this.handleApiACLSelected(acl.id)
this.apiAcl.showNewAclDialog = false
this.$q.notify({
type: 'positive',
message: 'Access Control List created.'
})
} catch (e) {
LNbits.utils.notifyApiError(e)
} finally {
this.apiAcl.name = ''
this.apiAcl.password = ''
}
this.apiAcl.showNewAclDialog = false
},
async updateApiACLs() {
try {
const {data} = await LNbits.api.request(
'PUT',
'/api/v1/auth/acl',
null,
{
id: this.user.id,
password: this.apiAcl.password,
...this.selectedApiAcl
}
)
this.apiAcl.data = data.access_control_list
} catch (e) {
LNbits.utils.notifyApiError(e)
} finally {
this.apiAcl.password = ''
}
},
async deleteApiACL() {
if (!this.selectedApiAcl.id) {
return
}
try {
await LNbits.api.request('DELETE', '/api/v1/auth/acl', null, {
id: this.selectedApiAcl.id,
password: this.apiAcl.password
})
this.$q.notify({
type: 'positive',
message: 'Access Control List deleted.'
})
} catch (e) {
LNbits.utils.notifyApiError(e)
} finally {
this.apiAcl.password = ''
}
this.apiAcl.data = this.apiAcl.data.filter(
t => t.id !== this.selectedApiAcl.id
)
this.handleApiACLSelected(this.apiAcl.data[0]?.id)
},
async generateApiToken() {
if (!this.selectedApiAcl.id) {
return
}
const expirationTimeMilliseconds =
new Date(this.apiAcl.newTokenExpiry) - new Date()
try {
const {data} = await LNbits.api.request(
'POST',
'/api/v1/auth/acl/token',
null,
{
acl_id: this.selectedApiAcl.id,
token_name: this.apiAcl.newTokenName,
password: this.apiAcl.password,
expiration_time_minutes: Math.trunc(
expirationTimeMilliseconds / 60000
)
}
)
this.apiAcl.apiToken = data.api_token
this.apiAcl.selectedTokenId = data.id
Quasar.Notify.create({
type: 'positive',
message: 'Token Generated.'
})
await this.getApiACLs()
this.handleApiACLSelected(this.selectedApiAcl.id)
this.apiAcl.showNewTokenDialog = false
} catch (e) {
LNbits.utils.notifyApiError(e)
} finally {
this.apiAcl.password = ''
}
},
async deleteToken() {
if (!this.apiAcl.selectedTokenId) {
return
}
try {
await LNbits.api.request('DELETE', '/api/v1/auth/acl/token', null, {
id: this.apiAcl.selectedTokenId,
acl_id: this.selectedApiAcl.id,
password: this.apiAcl.password
})
this.$q.notify({
type: 'positive',
message: 'Token deleted.'
})
this.selectedApiAcl.token_id_list =
this.selectedApiAcl.token_id_list.filter(
t => t.id !== this.apiAcl.selectedTokenId
)
this.apiAcl.selectedTokenId = null
} catch (e) {
LNbits.utils.notifyApiError(e)
} finally {
this.apiAcl.password = ''
}
}
},
async created() {
@@ -166,5 +429,6 @@ window.AccountPageLogic = {
if (hash) {
this.tab = hash
}
await this.getApiACLs()
}
}
+4 -9
View File
@@ -308,14 +308,9 @@ window.app.component('lnbits-notifications-btn', {
.subscribe(options)
.then(subscription => {
LNbits.api
.request(
'POST',
'/api/v1/webpush',
this.g.user.wallets[0].adminkey,
{
subscription: JSON.stringify(subscription)
}
)
.request('POST', '/api/v1/webpush', null, {
subscription: JSON.stringify(subscription)
})
.then(response => {
this.saveUserSubscribed(response.data.user)
this.isSubscribed = true
@@ -337,7 +332,7 @@ window.app.component('lnbits-notifications-btn', {
.request(
'DELETE',
'/api/v1/webpush?endpoint=' + btoa(subscription.endpoint),
this.g.user.wallets[0].adminkey
null
)
.then(() => {
this.removeUserSubscribed(this.g.user.id)
+6
View File
@@ -152,6 +152,12 @@ body.body--dark .q-field--error {
width: 500px;
}
.lnbits__table-bordered td,
.lnbits__table-bordered th {
border: 1px solid black;
border-collapse: collapse;
}
.q-table--dense {
th:first-child,
td:first-child,