Feat: audit (#2779)
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ window.localisation.en = {
|
||||
site_customisation: 'Site Customisation',
|
||||
funding: 'Funding',
|
||||
users: 'Users',
|
||||
audit: 'Audit',
|
||||
apps: 'Apps',
|
||||
channels: 'Channels',
|
||||
transactions: 'Transactions',
|
||||
|
||||
@@ -42,6 +42,9 @@ window.app = Vue.createApp({
|
||||
formAllowedIPs: '',
|
||||
formBlockedIPs: '',
|
||||
nostrAcceptedUrl: '',
|
||||
formAddIncludePath: '',
|
||||
formAddExcludePath: '',
|
||||
formAddIncludeResponseCode: '',
|
||||
isSuperUser: false,
|
||||
wallet: {},
|
||||
cancel: {},
|
||||
@@ -103,6 +106,58 @@ window.app = Vue.createApp({
|
||||
let allowed_users = this.formData.lnbits_allowed_users
|
||||
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
|
||||
},
|
||||
addIncludePath() {
|
||||
if (!this.formAddIncludePath) {
|
||||
return
|
||||
}
|
||||
const paths = this.formData.lnbits_audit_include_paths
|
||||
if (!paths.includes(this.formAddIncludePath)) {
|
||||
this.formData.lnbits_audit_include_paths = [
|
||||
...paths,
|
||||
this.formAddIncludePath
|
||||
]
|
||||
}
|
||||
this.formAddIncludePath = ''
|
||||
},
|
||||
removeIncludePath(path) {
|
||||
this.formData.lnbits_audit_include_paths =
|
||||
this.formData.lnbits_audit_include_paths.filter(p => p !== path)
|
||||
},
|
||||
addExcludePath() {
|
||||
if (!this.formAddExcludePath) {
|
||||
return
|
||||
}
|
||||
const paths = this.formData.lnbits_audit_exclude_paths
|
||||
if (!paths.includes(this.formAddExcludePath)) {
|
||||
this.formData.lnbits_audit_exclude_paths = [
|
||||
...paths,
|
||||
this.formAddExcludePath
|
||||
]
|
||||
}
|
||||
this.formAddExcludePath = ''
|
||||
},
|
||||
|
||||
removeExcludePath(path) {
|
||||
this.formData.lnbits_audit_exclude_paths =
|
||||
this.formData.lnbits_audit_exclude_paths.filter(p => p !== path)
|
||||
},
|
||||
addIncludeResponseCode() {
|
||||
if (!this.formAddIncludeResponseCode) {
|
||||
return
|
||||
}
|
||||
const codes = this.formData.lnbits_audit_http_response_codes
|
||||
if (!codes.includes(this.formAddIncludeResponseCode)) {
|
||||
this.formData.lnbits_audit_http_response_codes = [
|
||||
...codes,
|
||||
this.formAddIncludeResponseCode
|
||||
]
|
||||
}
|
||||
this.formAddIncludeResponseCode = ''
|
||||
},
|
||||
removeIncludeResponseCode(code) {
|
||||
this.formData.lnbits_audit_http_response_codes =
|
||||
this.formData.lnbits_audit_http_response_codes.filter(c => c !== code)
|
||||
},
|
||||
addExtensionsManifest() {
|
||||
const addManifest = this.formAddExtensionsManifest.trim()
|
||||
const manifests = this.formData.lnbits_extensions_manifests
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
auditEntries: [],
|
||||
searchData: {
|
||||
user_id: '',
|
||||
ip_address: '',
|
||||
request_type: '',
|
||||
component: '',
|
||||
request_method: '',
|
||||
response_code: '',
|
||||
path: ''
|
||||
},
|
||||
searchOptions: {
|
||||
component: [],
|
||||
request_method: [],
|
||||
response_code: []
|
||||
},
|
||||
auditTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'created_at',
|
||||
align: 'center',
|
||||
label: 'Date',
|
||||
field: 'created_at',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
align: 'left',
|
||||
label: 'Duration (sec)',
|
||||
field: 'duration',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'component',
|
||||
align: 'left',
|
||||
label: 'Component',
|
||||
field: 'component',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'request_method',
|
||||
align: 'left',
|
||||
label: 'Method',
|
||||
field: 'request_method',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'response_code',
|
||||
align: 'left',
|
||||
label: 'Code',
|
||||
field: 'response_code',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'user_id',
|
||||
align: 'left',
|
||||
label: 'User Id',
|
||||
field: 'user_id',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'ip_address',
|
||||
align: 'left',
|
||||
label: 'IP Address',
|
||||
field: 'ip_address',
|
||||
sortable: false
|
||||
},
|
||||
|
||||
{
|
||||
name: 'path',
|
||||
align: 'left',
|
||||
label: 'Path',
|
||||
field: 'path',
|
||||
sortable: false
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
sortBy: 'created_at',
|
||||
rowsPerPage: 10,
|
||||
page: 1,
|
||||
descending: true,
|
||||
rowsNumber: 10
|
||||
},
|
||||
search: null,
|
||||
hideEmpty: true,
|
||||
loading: false
|
||||
},
|
||||
auditDetailsDialog: {
|
||||
data: null,
|
||||
show: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.fetchAudit()
|
||||
},
|
||||
mounted() {
|
||||
this.initCharts()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchAudit(props) {
|
||||
try {
|
||||
const params = LNbits.utils.prepareFilterQuery(this.auditTable, props)
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/audit/api/v1?${params}`
|
||||
)
|
||||
|
||||
this.auditTable.pagination.rowsNumber = data.total
|
||||
this.auditEntries = data.data
|
||||
await this.fetchAuditStats(props)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.auditTable.loading = false
|
||||
}
|
||||
},
|
||||
async fetchAuditStats(props) {
|
||||
try {
|
||||
const params = LNbits.utils.prepareFilterQuery(this.auditTable, props)
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/audit/api/v1/stats?${params}`
|
||||
)
|
||||
|
||||
const request_methods = data.request_method.map(rm => rm.field)
|
||||
this.searchOptions.request_method = [
|
||||
...new Set(this.searchOptions.request_method.concat(request_methods))
|
||||
]
|
||||
this.requestMethodChart.data.labels = request_methods
|
||||
this.requestMethodChart.data.datasets[0].data = data.request_method.map(
|
||||
rm => rm.total
|
||||
)
|
||||
this.requestMethodChart.update()
|
||||
|
||||
const response_codes = data.response_code.map(rm => rm.field)
|
||||
this.searchOptions.response_code = [
|
||||
...new Set(this.searchOptions.response_code.concat(response_codes))
|
||||
]
|
||||
this.responseCodeChart.data.labels = response_codes
|
||||
this.responseCodeChart.data.datasets[0].data = data.response_code.map(
|
||||
rm => rm.total
|
||||
)
|
||||
this.responseCodeChart.update()
|
||||
|
||||
const components = data.component.map(rm => rm.field)
|
||||
this.searchOptions.component = [
|
||||
...new Set(this.searchOptions.component.concat(components))
|
||||
]
|
||||
this.componentUseChart.data.labels = components
|
||||
this.componentUseChart.data.datasets[0].data = data.component.map(
|
||||
rm => rm.total
|
||||
)
|
||||
|
||||
this.componentUseChart.update()
|
||||
|
||||
this.longDurationChart.data.labels = data.long_duration.map(
|
||||
rm => rm.field
|
||||
)
|
||||
this.longDurationChart.data.datasets[0].data = data.long_duration.map(
|
||||
rm => rm.total
|
||||
)
|
||||
|
||||
this.longDurationChart.update()
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
async searchAuditBy(fieldName, fieldValue) {
|
||||
if (fieldName) {
|
||||
this.searchData[fieldName] = fieldValue
|
||||
}
|
||||
// remove empty fields
|
||||
this.auditTable.filter = Object.entries(this.searchData).reduce(
|
||||
(a, [k, v]) => (v ? ((a[k] = v), a) : a),
|
||||
{}
|
||||
)
|
||||
|
||||
await this.fetchAudit()
|
||||
},
|
||||
showDetailsDialog(entry) {
|
||||
const details = JSON.parse(entry?.request_details || '')
|
||||
try {
|
||||
if (details.body) {
|
||||
details.body = JSON.parse(details.body)
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
this.auditDetailsDialog.data = JSON.stringify(details, null, 4)
|
||||
this.auditDetailsDialog.show = true
|
||||
},
|
||||
formatDate: function (value) {
|
||||
return Quasar.date.formatDate(new Date(value), 'YYYY-MM-DD HH:mm')
|
||||
},
|
||||
shortify(value) {
|
||||
valueLength = (value || '').length
|
||||
if (valueLength <= 10) {
|
||||
return value
|
||||
}
|
||||
return `${value.substring(0, 5)}...${value.substring(valueLength - 5, valueLength)}`
|
||||
},
|
||||
initCharts() {
|
||||
this.responseCodeChart = new Chart(
|
||||
this.$refs.responseCodeChart.getContext('2d'),
|
||||
{
|
||||
type: 'doughnut',
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
text: 'HTTP Response Codes'
|
||||
}
|
||||
},
|
||||
onClick: (_, elements, chart) => {
|
||||
if (elements[0]) {
|
||||
const i = elements[0].index
|
||||
this.searchAuditBy('response_code', chart.data.labels[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [20, 10],
|
||||
backgroundColor: [
|
||||
'rgb(100, 99, 200)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(255, 5, 86)',
|
||||
'rgb(25, 205, 86)',
|
||||
'rgb(255, 205, 250)'
|
||||
]
|
||||
}
|
||||
],
|
||||
labels: []
|
||||
}
|
||||
}
|
||||
)
|
||||
this.requestMethodChart = new Chart(
|
||||
this.$refs.requestMethodChart.getContext('2d'),
|
||||
{
|
||||
type: 'bar',
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
onClick: (_, elements, chart) => {
|
||||
if (elements[0]) {
|
||||
const i = elements[0].index
|
||||
this.searchAuditBy('request_method', chart.data.labels[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(255, 5, 86)',
|
||||
'rgb(25, 205, 86)',
|
||||
'rgb(255, 205, 250)'
|
||||
],
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
this.componentUseChart = new Chart(
|
||||
this.$refs.componentUseChart.getContext('2d'),
|
||||
{
|
||||
type: 'pie',
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'xxx'
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Components'
|
||||
}
|
||||
},
|
||||
onClick: (_, elements, chart) => {
|
||||
if (elements[0]) {
|
||||
const i = elements[0].index
|
||||
this.searchAuditBy('component', chart.data.labels[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(255, 5, 86)',
|
||||
'rgb(25, 205, 86)',
|
||||
'rgb(255, 205, 250)',
|
||||
'rgb(100, 205, 250)',
|
||||
'rgb(120, 205, 250)',
|
||||
'rgb(140, 205, 250)',
|
||||
'rgb(160, 205, 250)'
|
||||
],
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
this.longDurationChart = new Chart(
|
||||
this.$refs.longDurationChart.getContext('2d'),
|
||||
{
|
||||
type: 'bar',
|
||||
options: {
|
||||
responsive: true,
|
||||
indexAxis: 'y',
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Long Duration'
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick: (_, elements, chart) => {
|
||||
if (elements[0]) {
|
||||
const i = elements[0].index
|
||||
this.searchAuditBy('path', chart.data.labels[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: '',
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(255, 5, 86)',
|
||||
'rgb(25, 205, 86)',
|
||||
'rgb(255, 205, 250)',
|
||||
'rgb(100, 205, 250)',
|
||||
'rgb(120, 205, 250)',
|
||||
'rgb(140, 205, 250)',
|
||||
'rgb(160, 205, 250)'
|
||||
],
|
||||
hoverOffset: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -125,7 +125,7 @@ window.app.component('lnbits-extension-list', {
|
||||
|
||||
window.app.component('lnbits-manage', {
|
||||
template: '#lnbits-manage',
|
||||
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers'],
|
||||
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
|
||||
methods: {
|
||||
isActive: function (path) {
|
||||
return window.location.pathname === path
|
||||
|
||||
Reference in New Issue
Block a user