refactor: move /admin into vue component (#3466)

This commit is contained in:
dni ⚡ 2025-11-07 18:10:59 +01:00 committed by GitHub
parent 2fecec2623
commit a40306f5cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1444 additions and 1235 deletions

View file

@ -1,302 +0,0 @@
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
{% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn
:label="$t('save')"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges">
<span v-text="$t('save_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="checkChanges"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
:label="$t('restart')"
color="primary"
@click="restartServer"
class="q-ml-md"
>
<q-tooltip v-if="needsRestart">
<span v-text="$t('restart_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="needsRestart"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
:label="$t('download_backup')"
flat
@click="downloadBackup"
></q-btn>
<q-btn
flat
v-if="isSuperUser"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip>
<span v-text="$t('reset_defaults_tooltip')"></span>
</q-tooltip>
</q-btn>
</div>
<div></div>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
<div class="col q-gutter-y-md">
<q-card>
<!-- Mobile: Horizontal tabs at top -->
<q-tabs
v-if="$q.screen.lt.md"
@update:model-value="showExchangeProvidersTab"
v-model="tab"
dense
active-color="primary"
inline-label
class="text-primary"
>
<q-tab name="funding" icon="account_balance_wallet" label="Fund" />
<q-tab name="security" icon="security" label="Sec" />
<q-tab name="server" icon="settings" label="Srv" />
<q-tab name="exchange_providers" icon="swap_horiz" label="Exch" />
<q-tab name="fiat_providers" icon="account_balance" label="Fiat" />
<q-tab name="extensions" icon="extension" label="Ext" />
<q-tab name="notifications" icon="notifications" label="Not" />
<q-tab name="audit" icon="receipt_long" label="Aud" />
<q-tab name="site_customisation" icon="language" label="Site" />
<q-tab name="library" icon="image" label="Lib" />
</q-tabs>
<!-- Desktop: Vertical sidebar with splitter -->
<q-splitter v-if="$q.screen.gt.sm">
<template v-slot:before>
<q-tabs
@update:model-value="showExchangeProvidersTab"
v-model="tab"
vertical
active-color="primary"
>
<q-tab
name="funding"
icon="account_balance_wallet"
:label="$q.screen.gt.sm ? $t('funding') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('funding')"></span></q-tooltip
></q-tab>
<q-tab
name="security"
icon="security"
:label="$q.screen.gt.sm ? $t('security') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('security')"></span></q-tooltip
></q-tab>
<q-tab
name="server"
icon="price_change"
:label="$q.screen.gt.sm ? $t('payments') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('payments')"></span></q-tooltip
></q-tab>
<q-tab
name="exchange_providers"
icon="show_chart"
:label="$q.screen.gt.sm ? $t('exchanges') : null"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('exchanges')"></span></q-tooltip
></q-tab>
<q-tab
name="fiat_providers"
icon="credit_score"
:label="$q.screen.gt.sm ? $t('fiat_providers') : null"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('fiat_providers')"></span></q-tooltip
></q-tab>
<q-tab
name="users"
icon="group"
:label="$q.screen.gt.sm ? $t('users') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('users')"></span></q-tooltip
></q-tab>
<q-tab
name="extensions"
icon="extension"
:label="$q.screen.gt.sm ? $t('extensions') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('extensions')"></span></q-tooltip
></q-tab>
<q-tab
name="notifications"
icon="notifications"
:label="$q.screen.gt.sm ? $t('notifications') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('notifications')"></span></q-tooltip
></q-tab>
<q-tab
name="audit"
icon="playlist_add_check_circle"
:label="$q.screen.gt.sm ? $t('audit') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('audit')"></span></q-tooltip
></q-tab>
<q-tab
name="library"
icon="image"
:label="$q.screen.gt.sm ? $t('library') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('library')"></span></q-tooltip
></q-tab>
<q-tab
style="word-break: break-all"
name="site_customisation"
icon="language"
:label="$q.screen.gt.sm ? $t('site_customisation') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('site_customisation')"></span></q-tooltip
></q-tab>
</q-tabs>
</template>
<template v-slot:after>
<q-form name="settings_form" id="settings_form">
<q-scroll-area style="height: 100vh">
<q-tab-panels
v-model="tab"
animated
vertical
scroll
transition-prev="jump-up"
transition-next="jump-up"
>
{% include "admin/_tab_funding.html" %} {% include
"admin/_tab_users.html" %} {% include "admin/_tab_server.html"
%} {% include "admin/_tab_exchange_providers.html" %}{% include
"admin/_tab_fiat_providers.html" %} {% include
"admin/_tab_extensions.html" %} {% include
"admin/_tab_notifications.html" %} {% include
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
%}{% include "admin/_tab_audit.html"%}{% include
"admin/_tab_library.html"%}
</q-tab-panels>
</q-scroll-area>
</q-form>
</template>
</q-splitter>
<!-- Mobile: Full-width content without sidebar -->
<q-form
v-if="$q.screen.lt.md"
name="settings_form"
id="settings_form_mobile"
>
<q-scroll-area style="height: 70vh">
<q-tab-panels
v-model="tab"
animated
vertical
scroll
transition-prev="jump-up"
transition-next="jump-up"
style="overflow-x: hidden; padding: 8px"
>
{% include "admin/_tab_funding.html" %} {% include
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %} {%
include "admin/_tab_exchange_providers.html" %}{% include
"admin/_tab_fiat_providers.html" %} {% include
"admin/_tab_extensions.html" %} {% include
"admin/_tab_notifications.html" %} {% include
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
%}{% include "admin/_tab_audit.html"%}{% include
"admin/_tab_library.html"%}
</q-tab-panels>
</q-scroll-area>
</q-form>
</q-card>
</div>
</div>
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<div class="q-mb-md">
<strong v-text="$t('create_ticker_converter')"></strong>
</div>
<div class="row">
<div class="col-12 q-mb-md">
<q-select
filled
dense
v-model="exchangeData.convertFromTicker"
label="From Currency"
:options="{{ currencies | safe }}"
></q-select>
</div>
<div class="col-12">
<q-input
v-model="exchangeData.convertToTicker"
dense
filled
label="New Ticker"
hint="This ticker will be used for the exchange API calls."
>
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
@click="addExchangeTickerConversion()"
label="Add Ticker Conversion"
color="primary"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
{% endblock %}

View file

@ -20,9 +20,8 @@ from lnbits.core.services.extensions import get_valid_extensions
from lnbits.decorators import check_admin, check_user_exists from lnbits.decorators import check_admin, check_user_exists
from lnbits.helpers import check_callback_url, template_renderer from lnbits.helpers import check_callback_url, template_renderer
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.wallets import get_funding_source
from ...utils.exchange_rates import allowed_currencies, currencies from ...utils.exchange_rates import allowed_currencies
from ..crud import ( from ..crud import (
create_wallet, create_wallet,
get_db_versions, get_db_versions,
@ -407,32 +406,12 @@ async def manifest(request: Request, usr: str):
} }
@generic_router.get("/admin", response_class=HTMLResponse)
async def admin_index(request: Request, user: User = Depends(check_admin)):
if not settings.lnbits_admin_ui:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
funding_source = get_funding_source()
_, balance = await funding_source.status()
return template_renderer().TemplateResponse(
request,
"admin/index.html",
{
"user": user.json(),
"balance": balance,
"currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request),
},
)
@generic_router.get("/payments", response_class=HTMLResponse) @generic_router.get("/payments", response_class=HTMLResponse)
@generic_router.get("/wallets", response_class=HTMLResponse) @generic_router.get("/wallets", response_class=HTMLResponse)
async def empty_index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
request, request,
"empty.html", "index.html",
{ {
"user": user.json(), "user": user.json(),
}, },
@ -442,10 +421,16 @@ async def empty_index(request: Request, user: User = Depends(check_user_exists))
@generic_router.get("/users", response_class=HTMLResponse) @generic_router.get("/users", response_class=HTMLResponse)
@generic_router.get("/audit", response_class=HTMLResponse) @generic_router.get("/audit", response_class=HTMLResponse)
@generic_router.get("/node", response_class=HTMLResponse) @generic_router.get("/node", response_class=HTMLResponse)
async def empty_admin_index(request: Request, admin: User = Depends(check_admin)): @generic_router.get("/admin", response_class=HTMLResponse)
async def index_admin(request: Request, admin: User = Depends(check_admin)):
if not settings.lnbits_admin_ui:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail="Admin UI is disabled."
)
return template_renderer().TemplateResponse( return template_renderer().TemplateResponse(
request, request,
"empty.html", "index.html",
{ {
"user": admin.json(), "user": admin.json(),
}, },
@ -453,8 +438,8 @@ async def empty_admin_index(request: Request, admin: User = Depends(check_admin)
@generic_router.get("/node/public", response_class=HTMLResponse) @generic_router.get("/node/public", response_class=HTMLResponse)
async def empty_public(request: Request): async def index_public(request: Request):
return template_renderer().TemplateResponse(request, "empty_public.html") return template_renderer().TemplateResponse(request, "index_public.html")
@generic_router.get("/uuidv4/{hex_value}") @generic_router.get("/uuidv4/{hex_value}")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,696 +0,0 @@
window.AdminPageLogic = {
mixins: [windowMixin],
data() {
return {
settings: {},
logs: [],
library_images: [],
serverlogEnabled: false,
lnbits_theme_options: [
'classic',
'bitcoin',
'flamingo',
'cyber',
'freedom',
'mint',
'autumn',
'monochrome',
'salvador'
],
reactionOptions: [
'none',
'confettiBothSides',
'confettiFireworks',
'confettiStars',
'confettiTop'
],
globalBorderOptions: [
'retro-border',
'hard-border',
'neon-border',
'no-border'
],
auditData: {},
statusData: {},
statusDataTable: {
columns: [
{
name: 'date',
align: 'left',
label: this.$t('date'),
field: 'date'
},
{
name: 'message',
align: 'left',
label: this.$t('memo'),
field: 'message'
}
]
},
formData: {
lnbits_exchange_rate_providers: []
},
chartReady: false,
formAddAdmin: '',
formAddUser: '',
formAddStripeUser: '',
hideInputToggle: true,
formAddExtensionsManifest: '',
nostrNotificationIdentifier: '',
emailNotificationAddress: '',
formAllowedIPs: '',
formCallbackUrlRule: '',
formBlockedIPs: '',
nostrAcceptedUrl: '',
formAddIncludePath: '',
formAddExcludePath: '',
formAddIncludeResponseCode: '',
isSuperUser: false,
wallet: {},
cancel: {},
colors: [
'primary',
'secondary',
'accent',
'positive',
'negative',
'info',
'warning',
'red',
'yellow',
'orange'
],
tab: 'funding',
needsRestart: false,
exchangesTable: {
columns: [
{
name: 'name',
align: 'left',
label: 'Exchange Name',
field: 'name',
sortable: true
},
{
name: 'api_url',
align: 'left',
label: 'URL',
field: 'api_url',
sortable: false
},
{
name: 'path',
align: 'left',
label: 'JSON Path',
field: 'path',
sortable: false
},
{
name: 'exclude_to',
align: 'left',
label: 'Exclude Currencies',
field: 'exclude_to',
sortable: false
},
{
name: 'ticker_conversion',
align: 'left',
label: 'Ticker Conversion',
field: 'ticker_conversion',
sortable: false
}
],
pagination: {
sortBy: 'name',
rowsPerPage: 100,
page: 1,
rowsNumber: 100
},
search: null,
hideEmpty: true
},
exchangeData: {
selectedProvider: null,
showTickerConversion: false,
convertFromTicker: null,
convertToTicker: null
}
}
},
async created() {
await this.getSettings()
await this.getAudit()
await this.getUploadedImages()
this.balance = +'{{ balance|safe }}'
const hash = window.location.hash.replace('#', '')
if (hash === 'exchange_providers') {
this.showExchangeProvidersTab(hash)
}
if (hash) {
this.tab = hash
}
},
computed: {
lnbitsVersion() {
return LNBITS_VERSION
},
checkChanges() {
return !_.isEqual(this.settings, this.formData)
},
updateAvailable() {
return LNBITS_VERSION !== this.statusData.version
}
},
methods: {
addAdminUser() {
let addUser = this.formAddAdmin
let admin_users = this.formData.lnbits_admin_users
if (addUser && addUser.length && !admin_users.includes(addUser)) {
this.formData.lnbits_admin_users = [...admin_users, addUser]
this.formAddAdmin = ''
}
},
removeAdminUser(user) {
let admin_users = this.formData.lnbits_admin_users
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
},
addAllowedUser() {
let addUser = this.formAddUser
let allowed_users = this.formData.lnbits_allowed_users
if (addUser && addUser.length && !allowed_users.includes(addUser)) {
this.formData.lnbits_allowed_users = [...allowed_users, addUser]
this.formAddUser = ''
}
},
removeAllowedUser(user) {
let allowed_users = this.formData.lnbits_allowed_users
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
},
addStripeAllowedUser() {
const addUser = this.formAddStripeUser || ''
if (
addUser.length &&
!this.formData.stripe_limits.allowed_users.includes(addUser)
) {
this.formData.stripe_limits.allowed_users = [
...this.formData.stripe_limits.allowed_users,
addUser
]
this.formAddStripeUser = ''
}
},
removeStripeAllowedUser(user) {
this.formData.stripe_limits.allowed_users =
this.formData.stripe_limits.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
if (
addManifest &&
addManifest.length &&
!manifests.includes(addManifest)
) {
this.formData.lnbits_extensions_manifests = [...manifests, addManifest]
this.formAddExtensionsManifest = ''
}
},
removeExtensionsManifest(manifest) {
const manifests = this.formData.lnbits_extensions_manifests
this.formData.lnbits_extensions_manifests = manifests.filter(
m => m !== manifest
)
},
addNostrNotificationIdentifier() {
const identifer = this.nostrNotificationIdentifier.trim()
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
if (identifer && identifer.length && !identifiers.includes(identifer)) {
this.formData.lnbits_nostr_notifications_identifiers = [
...identifiers,
identifer
]
this.nostrNotificationIdentifier = ''
}
},
removeNostrNotificationIdentifier(identifer) {
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
this.formData.lnbits_nostr_notifications_identifiers = identifiers.filter(
m => m !== identifer
)
},
addEmailNotificationAddress() {
const email = this.emailNotificationAddress.trim()
const emails = this.formData.lnbits_email_notifications_to_emails
if (email && email.length && !emails.includes(email)) {
this.formData.lnbits_email_notifications_to_emails = [...emails, email]
this.emailNotificationAddress = ''
}
},
removeEmailNotificationAddress(email) {
const emails = this.formData.lnbits_email_notifications_to_emails
this.formData.lnbits_email_notifications_to_emails = emails.filter(
m => m !== email
)
},
hideInputsToggle() {
this.hideInputToggle = !this.hideInputToggle
},
async toggleServerLog() {
this.serverlogEnabled = !this.serverlogEnabled
if (this.serverlogEnabled) {
const wsProto = location.protocol !== 'http:' ? 'wss://' : 'ws://'
const digestHex = await LNbits.utils.digestMessage(this.g.user.id)
const localUrl =
wsProto +
document.domain +
':' +
location.port +
'/api/v1/ws/' +
digestHex
this.ws = new WebSocket(localUrl)
this.ws.addEventListener('message', async ({data}) => {
this.logs.push(data.toString())
const scrollArea = this.$refs.logScroll
if (scrollArea) {
const scrollTarget = scrollArea.getScrollTarget()
const duration = 0
scrollArea.setScrollPosition(scrollTarget.scrollHeight, duration)
}
})
} else {
this.ws.close()
}
},
addAllowedIPs() {
const allowedIPs = this.formAllowedIPs.trim()
const allowed_ips = this.formData.lnbits_allowed_ips
if (
allowedIPs &&
allowedIPs.length &&
!allowed_ips.includes(allowedIPs)
) {
this.formData.lnbits_allowed_ips = [...allowed_ips, allowedIPs]
this.formAllowedIPs = ''
}
},
removeAllowedIPs(allowed_ip) {
const allowed_ips = this.formData.lnbits_allowed_ips
this.formData.lnbits_allowed_ips = allowed_ips.filter(
a => a !== allowed_ip
)
},
addBlockedIPs() {
const blockedIPs = this.formBlockedIPs.trim()
const blocked_ips = this.formData.lnbits_blocked_ips
if (
blockedIPs &&
blockedIPs.length &&
!blocked_ips.includes(blockedIPs)
) {
this.formData.lnbits_blocked_ips = [...blocked_ips, blockedIPs]
this.formBlockedIPs = ''
}
},
removeBlockedIPs(blocked_ip) {
const blocked_ips = this.formData.lnbits_blocked_ips
this.formData.lnbits_blocked_ips = blocked_ips.filter(
b => b !== blocked_ip
)
},
addCallbackUrlRule() {
const allowedCallback = this.formCallbackUrlRule.trim()
const allowedCallbacks = this.formData.lnbits_callback_url_rules
if (
allowedCallback &&
allowedCallback.length &&
!allowedCallbacks.includes(allowedCallback)
) {
this.formData.lnbits_callback_url_rules = [
...allowedCallbacks,
allowedCallback
]
this.formCallbackUrlRule = ''
}
},
removeCallbackUrlRule(allowedCallback) {
const allowedCallbacks = this.formData.lnbits_callback_url_rules
this.formData.lnbits_callback_url_rules = allowedCallbacks.filter(
a => a !== allowedCallback
)
},
addNostrUrl() {
const url = this.nostrAcceptedUrl.trim()
this.removeNostrUrl(url)
this.formData.nostr_absolute_request_urls.push(url)
this.nostrAcceptedUrl = ''
},
removeNostrUrl(url) {
this.formData.nostr_absolute_request_urls =
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
},
addExchangeProvider() {
this.formData.lnbits_exchange_rate_providers = [
{
name: '',
api_url: '',
path: '',
exclude_to: []
},
...this.formData.lnbits_exchange_rate_providers
]
},
removeExchangeProvider(provider) {
this.formData.lnbits_exchange_rate_providers =
this.formData.lnbits_exchange_rate_providers.filter(p => p !== provider)
},
removeExchangeTickerConversion(provider, ticker) {
provider.ticker_conversion = provider.ticker_conversion.filter(
t => t !== ticker
)
this.touchSettings()
},
addExchangeTickerConversion() {
if (!this.exchangeData.selectedProvider) {
return
}
this.exchangeData.selectedProvider.ticker_conversion.push(
`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`
)
this.touchSettings()
this.exchangeData.showTickerConversion = false
},
showTickerConversionDialog(provider) {
this.exchangeData.convertFromTicker = null
this.exchangeData.convertToTicker = null
this.exchangeData.selectedProvider = provider
this.exchangeData.showTickerConversion = true
},
getDefaultSetting(fieldName) {
LNbits.api
.request(
'GET',
`/admin/api/v1/settings/default?field_name=${fieldName}`
)
.then(response => {
this.formData[fieldName] = response.data.default_value
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
restartServer() {
LNbits.api
.request('GET', '/admin/api/v1/restart/')
.then(response => {
this.$q.notify({
type: 'positive',
message: 'Success! Restarted Server',
icon: null
})
this.needsRestart = false
})
.catch(LNbits.utils.notifyApiError)
},
formatDate(date) {
return moment
.utc(date * 1000)
.local()
.fromNow()
},
sendTestEmail() {
LNbits.api
.request(
'GET',
'/admin/api/v1/testemail',
this.g.user.wallets[0].adminkey
)
.then(response => {
if (response.data.status === 'error') {
throw new Error(response.data.message)
}
this.$q.notify({
message: 'Test email sent!',
color: 'positive'
})
})
.catch(error => {
this.$q.notify({
message: error.message,
color: 'negative'
})
})
},
getAudit() {
LNbits.api
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
.then(response => {
this.auditData = response.data
})
.catch(LNbits.utils.notifyApiError)
},
getExchangeRateHistory() {
LNbits.api
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
.then(response => {
this.initExchangeChart(response.data)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
async getSettings() {
await LNbits.api
.request(
'GET',
'/admin/api/v1/settings',
this.g.user.wallets[0].adminkey
)
.then(response => {
this.isSuperUser = response.data.is_super_user || false
this.settings = response.data
this.formData = {...this.settings}
})
.catch(LNbits.utils.notifyApiError)
},
updateSettings() {
const data = _.omit(this.formData, [
'is_super_user',
'lnbits_allowed_funding_sources',
'touch'
])
LNbits.api
.request(
'PUT',
'/admin/api/v1/settings',
this.g.user.wallets[0].adminkey,
data
)
.then(response => {
this.needsRestart =
this.settings.lnbits_backend_wallet_class !==
this.formData.lnbits_backend_wallet_class
this.settings = this.formData
this.formData = _.clone(this.settings)
Quasar.Notify.create({
type: 'positive',
message: `Success! Settings changed! ${
this.needsRestart ? 'Restart required!' : ''
}`,
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
},
deleteSettings() {
LNbits.utils
.confirmDialog('Are you sure you want to restore settings to default?')
.onOk(() => {
LNbits.api
.request('DELETE', '/admin/api/v1/settings')
.then(response => {
Quasar.Notify.create({
type: 'positive',
message:
'Success! Restored settings to defaults. Restarting...',
icon: null
})
this.$q.localStorage.clear()
})
.catch(LNbits.utils.notifyApiError)
})
},
onImageInput(e) {
const file = e.target.files[0]
if (file) {
this.uploadImage(file)
}
},
uploadImage(file) {
const formData = new FormData()
formData.append('file', file)
LNbits.api
.request(
'POST',
'/admin/api/v1/images',
this.g.user.wallets[0].adminkey,
formData,
{headers: {'Content-Type': 'multipart/form-data'}}
)
.then(() => {
this.$q.notify({
type: 'positive',
message: 'Image uploaded!',
icon: null
})
this.getUploadedImages()
})
.catch(LNbits.utils.notifyApiError)
},
getUploadedImages() {
LNbits.api
.request('GET', '/admin/api/v1/images', this.g.user.wallets[0].inkey)
.then(response => {
this.library_images = response.data.map(image => ({
...image,
url: `${window.origin}/${image.directory}/${image.filename}`
}))
})
.catch(LNbits.utils.notifyApiError)
},
deleteImage(filename) {
LNbits.utils
.confirmDialog('Are you sure you want to delete this image?')
.onOk(() => {
LNbits.api
.request(
'DELETE',
`/admin/api/v1/images/${filename}`,
this.g.user.wallets[0].adminkey
)
.then(() => {
this.$q.notify({
type: 'positive',
message: 'Image deleted!',
icon: null
})
this.getUploadedImages()
})
.catch(LNbits.utils.notifyApiError)
})
},
checkFiatProvider(providerName) {
LNbits.api
.request('PUT', `/api/v1/fiat/check/${providerName}`)
.then(response => {
response
const data = response.data
Quasar.Notify.create({
type: data.success ? 'positive' : 'warning',
message: data.message,
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
},
downloadBackup() {
window.open('/admin/api/v1/backup', '_blank')
},
showExchangeProvidersTab(tabName) {
if (tabName === 'exchange_providers') {
this.getExchangeRateHistory()
}
},
touchSettings() {
this.formData.touch = null
},
initExchangeChart(data) {
const xValues = data.map(d =>
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
)
const exchanges = [
...this.formData.lnbits_exchange_rate_providers,
{name: 'LNbits'}
]
const datasets = exchanges.map(exchange => ({
label: exchange.name,
data: data.map(d => d.rates[exchange.name]),
pointStyle: true,
borderWidth: exchange.name === 'LNbits' ? 4 : 1,
tension: 0.4
}))
this.exchangeRatesChart = new Chart(
this.$refs.exchangeRatesChart.getContext('2d'),
{
type: 'line',
options: {
plugins: {
legend: {
display: false
}
}
},
data: {
labels: xValues,
datasets
}
}
)
}
}
}

View file

@ -0,0 +1,66 @@
window.app.component('lnbits-admin-audit', {
props: ['form-data'],
template: '#lnbits-admin-audit',
mixins: [window.windowMixin],
data() {
return {
formAddIncludePath: '',
formAddExcludePath: '',
formAddIncludeResponseCode: ''
}
},
methods: {
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)
}
}
})

View file

@ -0,0 +1,159 @@
window.app.component('lnbits-admin-exchange-providers', {
props: ['form-data'],
template: '#lnbits-admin-exchange-providers',
mixins: [window.windowMixin],
data() {
return {
exchangeData: {
selectedProvider: null,
showTickerConversion: false,
convertFromTicker: null,
convertToTicker: null
},
exchangesTable: {
columns: [
{
name: 'name',
align: 'left',
label: 'Exchange Name',
field: 'name',
sortable: true
},
{
name: 'api_url',
align: 'left',
label: 'URL',
field: 'api_url',
sortable: false
},
{
name: 'path',
align: 'left',
label: 'JSON Path',
field: 'path',
sortable: false
},
{
name: 'exclude_to',
align: 'left',
label: 'Exclude Currencies',
field: 'exclude_to',
sortable: false
},
{
name: 'ticker_conversion',
align: 'left',
label: 'Ticker Conversion',
field: 'ticker_conversion',
sortable: false
}
],
pagination: {
sortBy: 'name',
rowsPerPage: 100,
page: 1,
rowsNumber: 100
},
search: null,
hideEmpty: true
}
}
},
mounted() {
this.getExchangeRateHistory()
},
created() {
const hash = window.location.hash.replace('#', '')
if (hash === 'exchange_providers') {
this.showExchangeProvidersTab(hash)
}
},
methods: {
getExchangeRateHistory() {
LNbits.api
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
.then(response => {
this.initExchangeChart(response.data)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
showExchangeProvidersTab(tabName) {
if (tabName === 'exchange_providers') {
this.getExchangeRateHistory()
}
},
addExchangeProvider() {
this.formData.lnbits_exchange_rate_providers = [
{
name: '',
api_url: '',
path: '',
exclude_to: []
},
...this.formData.lnbits_exchange_rate_providers
]
},
removeExchangeProvider(provider) {
this.formData.lnbits_exchange_rate_providers =
this.formData.lnbits_exchange_rate_providers.filter(p => p !== provider)
},
removeExchangeTickerConversion(provider, ticker) {
provider.ticker_conversion = provider.ticker_conversion.filter(
t => t !== ticker
)
this.formData.touch = null
},
addExchangeTickerConversion() {
if (!this.exchangeData.selectedProvider) {
return
}
this.exchangeData.selectedProvider.ticker_conversion.push(
`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`
)
this.formData.touch = null
this.exchangeData.showTickerConversion = false
},
showTickerConversionDialog(provider) {
this.exchangeData.convertFromTicker = null
this.exchangeData.convertToTicker = null
this.exchangeData.selectedProvider = provider
this.exchangeData.showTickerConversion = true
},
initExchangeChart(data) {
const xValues = data.map(d =>
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
)
const exchanges = [
...this.formData.lnbits_exchange_rate_providers,
{name: 'LNbits'}
]
const datasets = exchanges.map(exchange => ({
label: exchange.name,
data: data.map(d => d.rates[exchange.name]),
pointStyle: true,
borderWidth: exchange.name === 'LNbits' ? 4 : 1,
tension: 0.4
}))
this.exchangeRatesChart = new Chart(
this.$refs.exchangeRatesChart.getContext('2d'),
{
type: 'line',
options: {
plugins: {
legend: {
display: false
}
}
},
data: {
labels: xValues,
datasets
}
}
)
}
}
})

View file

@ -0,0 +1,30 @@
window.app.component('lnbits-admin-extensions', {
props: ['form-data'],
template: '#lnbits-admin-extensions',
mixins: [window.windowMixin],
data() {
return {
formAddExtensionsManifest: ''
}
},
methods: {
addExtensionsManifest() {
const addManifest = this.formAddExtensionsManifest.trim()
const manifests = this.formData.lnbits_extensions_manifests
if (
addManifest &&
addManifest.length &&
!manifests.includes(addManifest)
) {
this.formData.lnbits_extensions_manifests = [...manifests, addManifest]
this.formAddExtensionsManifest = ''
}
},
removeExtensionsManifest(manifest) {
const manifests = this.formData.lnbits_extensions_manifests
this.formData.lnbits_extensions_manifests = manifests.filter(
m => m !== manifest
)
}
}
})

View file

@ -0,0 +1,44 @@
window.app.component('lnbits-admin-fiat-providers', {
props: ['form-data'],
template: '#lnbits-admin-fiat-providers',
mixins: [window.windowMixin],
data() {
return {
formAddStripeUser: '',
hideInputToggle: true
}
},
methods: {
addStripeAllowedUser() {
const addUser = this.formAddStripeUser || ''
if (
addUser.length &&
!this.formData.stripe_limits.allowed_users.includes(addUser)
) {
this.formData.stripe_limits.allowed_users = [
...this.formData.stripe_limits.allowed_users,
addUser
]
this.formAddStripeUser = ''
}
},
removeStripeAllowedUser(user) {
this.formData.stripe_limits.allowed_users =
this.formData.stripe_limits.allowed_users.filter(u => u !== user)
},
checkFiatProvider(providerName) {
LNbits.api
.request('PUT', `/api/v1/fiat/check/${providerName}`)
.then(response => {
response
const data = response.data
Quasar.Notify.create({
type: data.success ? 'positive' : 'warning',
message: data.message,
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
}
}
})

View file

@ -1,5 +1,5 @@
window.app.component('lnbits-funding-sources', { window.app.component('lnbits-admin-funding-sources', {
template: '#lnbits-funding-sources', template: '#lnbits-admin-funding-sources',
mixins: [window.windowMixin], mixins: [window.windowMixin],
props: ['form-data', 'allowed-funding-sources'], props: ['form-data', 'allowed-funding-sources'],
methods: { methods: {

View file

@ -0,0 +1,24 @@
window.app.component('lnbits-admin-funding', {
props: ['is-super-user', 'form-data', 'settings'],
template: '#lnbits-admin-funding',
mixins: [window.windowMixin],
data() {
return {
auditData: []
}
},
created() {
this.getAudit()
},
methods: {
getAudit() {
LNbits.api
// TODO: should not use admin key here
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
.then(response => {
this.auditData = response.data
})
.catch(LNbits.utils.notifyApiError)
}
}
})

View file

@ -0,0 +1,74 @@
window.app.component('lnbits-admin-library', {
props: ['form-data'],
template: '#lnbits-admin-library',
mixins: [window.windowMixin],
data() {
return {
library_images: []
}
},
async created() {
await this.getUploadedImages()
},
methods: {
onImageInput(e) {
const file = e.target.files[0]
if (file) {
this.uploadImage(file)
}
},
uploadImage(file) {
const formData = new FormData()
formData.append('file', file)
LNbits.api
.request(
'POST',
'/admin/api/v1/images',
this.g.user.wallets[0].adminkey,
formData,
{headers: {'Content-Type': 'multipart/form-data'}}
)
.then(() => {
this.$q.notify({
type: 'positive',
message: 'Image uploaded!',
icon: null
})
this.getUploadedImages()
})
.catch(LNbits.utils.notifyApiError)
},
getUploadedImages() {
LNbits.api
.request('GET', '/admin/api/v1/images', this.g.user.wallets[0].inkey)
.then(response => {
this.library_images = response.data.map(image => ({
...image,
url: `${window.origin}/${image.directory}/${image.filename}`
}))
})
.catch(LNbits.utils.notifyApiError)
},
deleteImage(filename) {
LNbits.utils
.confirmDialog('Are you sure you want to delete this image?')
.onOk(() => {
LNbits.api
.request(
'DELETE',
`/admin/api/v1/images/${filename}`,
this.g.user.wallets[0].adminkey
)
.then(() => {
this.$q.notify({
type: 'positive',
message: 'Image deleted!',
icon: null
})
this.getUploadedImages()
})
.catch(LNbits.utils.notifyApiError)
})
}
}
})

View file

@ -0,0 +1,67 @@
window.app.component('lnbits-admin-notifications', {
props: ['form-data'],
template: '#lnbits-admin-notifications',
mixins: [window.windowMixin],
data() {
return {
nostrNotificationIdentifier: '',
emailNotificationAddress: ''
}
},
methods: {
sendTestEmail() {
LNbits.api
.request(
'GET',
'/admin/api/v1/testemail',
this.g.user.wallets[0].adminkey
)
.then(response => {
if (response.data.status === 'error') {
throw new Error(response.data.message)
}
this.$q.notify({
message: 'Test email sent!',
color: 'positive'
})
})
.catch(error => {
this.$q.notify({
message: error.message,
color: 'negative'
})
})
},
addNostrNotificationIdentifier() {
const identifer = this.nostrNotificationIdentifier.trim()
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
if (identifer && identifer.length && !identifiers.includes(identifer)) {
this.formData.lnbits_nostr_notifications_identifiers = [
...identifiers,
identifer
]
this.nostrNotificationIdentifier = ''
}
},
removeNostrNotificationIdentifier(identifer) {
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
this.formData.lnbits_nostr_notifications_identifiers = identifiers.filter(
m => m !== identifer
)
},
addEmailNotificationAddress() {
const email = this.emailNotificationAddress.trim()
const emails = this.formData.lnbits_email_notifications_to_emails
if (email && email.length && !emails.includes(email)) {
this.formData.lnbits_email_notifications_to_emails = [...emails, email]
this.emailNotificationAddress = ''
}
},
removeEmailNotificationAddress(email) {
const emails = this.formData.lnbits_email_notifications_to_emails
this.formData.lnbits_email_notifications_to_emails = emails.filter(
m => m !== email
)
}
}
})

View file

@ -0,0 +1,111 @@
window.app.component('lnbits-admin-security', {
props: ['form-data'],
template: '#lnbits-admin-security',
mixins: [window.windowMixin],
data() {
return {
logs: [],
formBlockedIPs: '',
serverlogEnabled: false,
nostrAcceptedUrl: '',
formAllowedIPs: '',
formCallbackUrlRule: ''
}
},
created() {},
methods: {
addAllowedIPs() {
const allowedIPs = this.formAllowedIPs.trim()
const allowed_ips = this.formData.lnbits_allowed_ips
if (
allowedIPs &&
allowedIPs.length &&
!allowed_ips.includes(allowedIPs)
) {
this.formData.lnbits_allowed_ips = [...allowed_ips, allowedIPs]
this.formAllowedIPs = ''
}
},
removeAllowedIPs(allowed_ip) {
const allowed_ips = this.formData.lnbits_allowed_ips
this.formData.lnbits_allowed_ips = allowed_ips.filter(
a => a !== allowed_ip
)
},
addBlockedIPs() {
const blockedIPs = this.formBlockedIPs.trim()
const blocked_ips = this.formData.lnbits_blocked_ips
if (
blockedIPs &&
blockedIPs.length &&
!blocked_ips.includes(blockedIPs)
) {
this.formData.lnbits_blocked_ips = [...blocked_ips, blockedIPs]
this.formBlockedIPs = ''
}
},
removeBlockedIPs(blocked_ip) {
const blocked_ips = this.formData.lnbits_blocked_ips
this.formData.lnbits_blocked_ips = blocked_ips.filter(
b => b !== blocked_ip
)
},
addCallbackUrlRule() {
const allowedCallback = this.formCallbackUrlRule.trim()
const allowedCallbacks = this.formData.lnbits_callback_url_rules
if (
allowedCallback &&
allowedCallback.length &&
!allowedCallbacks.includes(allowedCallback)
) {
this.formData.lnbits_callback_url_rules = [
...allowedCallbacks,
allowedCallback
]
this.formCallbackUrlRule = ''
}
},
removeCallbackUrlRule(allowedCallback) {
const allowedCallbacks = this.formData.lnbits_callback_url_rules
this.formData.lnbits_callback_url_rules = allowedCallbacks.filter(
a => a !== allowedCallback
)
},
addNostrUrl() {
const url = this.nostrAcceptedUrl.trim()
this.removeNostrUrl(url)
this.formData.nostr_absolute_request_urls.push(url)
this.nostrAcceptedUrl = ''
},
removeNostrUrl(url) {
this.formData.nostr_absolute_request_urls =
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
},
async toggleServerLog() {
this.serverlogEnabled = !this.serverlogEnabled
if (this.serverlogEnabled) {
const wsProto = location.protocol !== 'http:' ? 'wss://' : 'ws://'
const digestHex = await LNbits.utils.digestMessage(this.g.user.id)
const localUrl =
wsProto +
document.domain +
':' +
location.port +
'/api/v1/ws/' +
digestHex
this.ws = new WebSocket(localUrl)
this.ws.addEventListener('message', async ({data}) => {
this.logs.push(data.toString())
const scrollArea = this.$refs.logScroll
if (scrollArea) {
const scrollTarget = scrollArea.getScrollTarget()
const duration = 0
scrollArea.setScrollPosition(scrollTarget.scrollHeight, duration)
}
})
} else {
this.ws.close()
}
}
}
})

View file

@ -0,0 +1,13 @@
window.app.component('lnbits-admin-server', {
props: ['form-data'],
template: '#lnbits-admin-server',
mixins: [window.windowMixin],
data() {
return {
currencies: []
}
},
async created() {
this.currencies = await LNbits.api.getCurrencies()
}
})

View file

@ -0,0 +1,46 @@
window.app.component('lnbits-admin-site-customisation', {
props: ['form-data'],
template: '#lnbits-admin-site-customisation',
mixins: [window.windowMixin],
data() {
return {
lnbits_theme_options: [
'classic',
'bitcoin',
'flamingo',
'cyber',
'freedom',
'mint',
'autumn',
'monochrome',
'salvador'
],
colors: [
'primary',
'secondary',
'accent',
'positive',
'negative',
'info',
'warning',
'red',
'yellow',
'orange'
],
reactionOptions: [
'none',
'confettiBothSides',
'confettiFireworks',
'confettiStars',
'confettiTop'
],
globalBorderOptions: [
'retro-border',
'hard-border',
'neon-border',
'no-border'
]
}
},
methods: {}
})

View file

@ -0,0 +1,37 @@
window.app.component('lnbits-admin-users', {
props: ['form-data'],
template: '#lnbits-admin-users',
mixins: [window.windowMixin],
data() {
return {
formAddUser: '',
formAddAdmin: ''
}
},
methods: {
addAllowedUser() {
let addUser = this.formAddUser
let allowed_users = this.formData.lnbits_allowed_users
if (addUser && addUser.length && !allowed_users.includes(addUser)) {
this.formData.lnbits_allowed_users = [...allowed_users, addUser]
this.formAddUser = ''
}
},
removeAllowedUser(user) {
let allowed_users = this.formData.lnbits_allowed_users
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
},
addAdminUser() {
let addUser = this.formAddAdmin
let admin_users = this.formData.lnbits_admin_users
if (addUser && addUser.length && !admin_users.includes(addUser)) {
this.formData.lnbits_admin_users = [...admin_users, addUser]
this.formAddAdmin = ''
}
},
removeAdminUser(user) {
let admin_users = this.formData.lnbits_admin_users
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
}
}
})

View file

@ -139,15 +139,6 @@ const routes = [
} }
} }
}, },
{
path: '/admin',
name: 'Admin',
component: DynamicComponent,
props: {
fetchUrl: '/admin',
scripts: ['/static/js/admin.js']
}
},
{ {
path: '/extensions', path: '/extensions',
name: 'Extensions', name: 'Extensions',
@ -204,6 +195,11 @@ const routes = [
path: '/users', path: '/users',
name: 'Users', name: 'Users',
component: PageUsers component: PageUsers
},
{
path: '/admin',
name: 'Admin',
component: PageAdmin
} }
] ]

View file

@ -0,0 +1,122 @@
window.PageAdmin = {
template: '#page-admin',
mixins: [windowMixin],
data() {
return {
tab: 'funding',
settings: {},
formData: {
lnbits_exchange_rate_providers: [],
lnbits_audit_exclude_paths: [],
lnbits_audit_include_paths: [],
lnbits_audit_http_response_codes: []
},
isSuperUser: false,
needsRestart: false
}
},
async created() {
await this.getSettings()
const hash = window.location.hash.replace('#', '')
if (hash) {
this.tab = hash
}
},
computed: {
checkChanges() {
return !_.isEqual(this.settings, this.formData)
}
},
methods: {
getDefaultSetting(fieldName) {
LNbits.api
.request(
'GET',
`/admin/api/v1/settings/default?field_name=${fieldName}`
)
.then(response => {
this.formData[fieldName] = response.data.default_value
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
restartServer() {
LNbits.api
.request('GET', '/admin/api/v1/restart/')
.then(response => {
this.$q.notify({
type: 'positive',
message: 'Success! Restarted Server',
icon: null
})
this.needsRestart = false
})
.catch(LNbits.utils.notifyApiError)
},
async getSettings() {
await LNbits.api
.request(
'GET',
'/admin/api/v1/settings',
this.g.user.wallets[0].adminkey
)
.then(response => {
this.isSuperUser = response.data.is_super_user || false
this.settings = response.data
this.formData = {...this.settings}
})
.catch(LNbits.utils.notifyApiError)
},
updateSettings() {
const data = _.omit(this.formData, [
'is_super_user',
'lnbits_allowed_funding_sources',
'touch'
])
LNbits.api
.request(
'PUT',
'/admin/api/v1/settings',
this.g.user.wallets[0].adminkey,
data
)
.then(response => {
this.needsRestart =
this.settings.lnbits_backend_wallet_class !==
this.formData.lnbits_backend_wallet_class
this.settings = this.formData
this.formData = _.clone(this.settings)
Quasar.Notify.create({
type: 'positive',
message: `Success! Settings changed! ${
this.needsRestart ? 'Restart required!' : ''
}`,
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
},
deleteSettings() {
LNbits.utils
.confirmDialog('Are you sure you want to restore settings to default?')
.onOk(() => {
LNbits.api
.request('DELETE', '/admin/api/v1/settings')
.then(response => {
Quasar.Notify.create({
type: 'positive',
message:
'Success! Restored settings to defaults. Restarting...',
icon: null
})
this.$q.localStorage.clear()
})
.catch(LNbits.utils.notifyApiError)
})
},
downloadBackup() {
window.open('/admin/api/v1/backup', '_blank')
}
}
}

View file

@ -125,6 +125,12 @@ window.windowMixin = {
}) })
} }
}, },
formatDate(date) {
return moment
.utc(date * 1000)
.local()
.fromNow()
},
formatBalance(amount) { formatBalance(amount) {
if (LNBITS_DENOMINATION != 'sats') { if (LNBITS_DENOMINATION != 'sats') {
return LNbits.utils.formatCurrency(amount / 100, LNBITS_DENOMINATION) return LNbits.utils.formatCurrency(amount / 100, LNBITS_DENOMINATION)

View file

@ -49,9 +49,21 @@
"js/pages/audit.js", "js/pages/audit.js",
"js/pages/wallets.js", "js/pages/wallets.js",
"js/pages/users.js", "js/pages/users.js",
"js/pages/admin.js",
"js/components/admin/lnbits-admin-funding.js",
"js/components/admin/lnbits-admin-funding-sources.js",
"js/components/admin/lnbits-admin-fiat-providers.js",
"js/components/admin/lnbits-admin-exchange-providers.js",
"js/components/admin/lnbits-admin-security.js",
"js/components/admin/lnbits-admin-users.js",
"js/components/admin/lnbits-admin-server.js",
"js/components/admin/lnbits-admin-extensions.js",
"js/components/admin/lnbits-admin-notifications.js",
"js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-library.js",
"js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-qrcode.js", "js/components/lnbits-qrcode.js",
"js/components/lnbits-qrcode-lnurl.js", "js/components/lnbits-qrcode-lnurl.js",
"js/components/lnbits-funding-sources.js",
"js/components/extension-settings.js", "js/components/extension-settings.js",
"js/components/data-fields.js", "js/components/data-fields.js",
"js/components/payment-list.js", "js/components/payment-list.js",

View file

@ -1,3 +1,16 @@
{% include('components/admin/funding.vue') %} {%
include('components/admin/funding_sources.vue') %} {%
include('components/admin/fiat_providers.vue') %} {%
include('components/admin/exchange_providers.vue') %} {%
include('components/admin/security.vue') %} {%
include('components/admin/users.vue') %} {%
include('components/admin/site_customisation.vue') %} {%
include('components/admin/audit.vue') %} {%
include('components/admin/extensions.vue') %} {%
include('components/admin/library.vue') %} {%
include('components/admin/notifications.vue') %} {%
include('components/admin/server.vue') %}
<template id="lnbits-wallet-list"> <template id="lnbits-wallet-list">
<q-list <q-list
v-if="g.user && g.user.wallets.length" v-if="g.user && g.user.wallets.length"
@ -1232,92 +1245,6 @@
</q-btn> </q-btn>
</template> </template>
<template id="lnbits-funding-sources">
<div class="funding-sources">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('funding_sources')"></span>
<q-btn
round
flat
@click="this.hideInput = !this.hideInput"
:icon="this.hideInput ? 'visibility_off' : 'visibility'"
></q-btn>
</h6>
<div class="row">
<div class="col-12">
<p>Active Funding<small> (Requires server restart)</small></p>
<q-select
filled
v-model="formData.lnbits_backend_wallet_class"
hint="Select the active funding wallet"
:options="sortedAllowedFundingSources"
:option-label="item => getFundingSourceLabel(item)"
></q-select>
</div>
</div>
<q-list
class="q-mt-md"
v-for="(fund, idx) in allowedFundingSources"
:key="idx"
>
<div
v-if="
fundingSources.get(fund) &&
fund === formData.lnbits_backend_wallet_class
"
>
<div
class="row"
v-for="([key, prop], i) in Object.entries(fundingSources.get(fund))"
:key="i"
>
<div class="col-12">
<q-input
v-model="formData[key]"
filled
class="q-mt-sm"
:type="hideInput ? 'password' : 'text'"
:label="prop.label"
:hint="prop.hint"
:readonly="prop.readonly || false"
>
<q-btn
v-if="prop.copy"
@click="copyText(formData[key])"
icon="content_copy"
class="cursor-pointer"
color="grey"
flat
dense
></q-btn>
<q-btn
v-if="prop.qrcode"
@click="showQRValue(formData[key])"
icon="qr_code"
class="cursor-pointer"
color="grey"
flat
dense
></q-btn>
</q-input>
</div>
</div>
</div>
</q-list>
<q-dialog v-model="showQRDialog">
<q-card class="q-pa-md">
<q-card-section>
<lnbits-qrcode :value="qrValue"></lnbits-qrcode>
</q-card-section>
<q-card-actions align="right">
<q-btn flat :label="$t('close')" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>
<template id="user-id-only"> <template id="user-id-only">
<div v-if="authAction === 'login' && authMethod === 'user-id-only'"> <div v-if="authAction === 'login' && authMethod === 'user-id-only'">
<q-card-section class="q-pb-none"> <q-card-section class="q-pb-none">

View file

@ -1,7 +1,6 @@
<q-tab-panel name="audit"> <template id="lnbits-admin-audit">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">Audit</h6> <h6 class="q-my-none q-mb-sm">Audit</h6>
<div class="row q-mb-lg"> <div class="row q-mb-lg">
<div class="col-md-6 col-sm-12 q-pr-sm"> <div class="col-md-6 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple> <q-item tag="label" v-ripple>
@ -50,8 +49,12 @@
<span v-text="$t('audit_record_warning')"></span> <span v-text="$t('audit_record_warning')"></span>
<br /> <br />
<ul> <ul>
<li><span v-text="$t('audit_record_req_warning_1')"></span></li> <li>
<li><span v-text="$t('audit_record_req_warning_2')"></span></li> <span v-text="$t('audit_record_req_warning_1')"></span>
</li>
<li>
<span v-text="$t('audit_record_req_warning_2')"></span>
</li>
</ul> </ul>
<br /> <br />
<span v-text="$t('audit_record_use')"></span> <span v-text="$t('audit_record_use')"></span>
@ -133,7 +136,15 @@
multiple multiple
:hint="$t('audit_http_methods_hint')" :hint="$t('audit_http_methods_hint')"
:label="$t('audit_http_methods_label')" :label="$t('audit_http_methods_label')"
:options="['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']" :options="[
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'HEAD',
'OPTIONS'
]"
></q-select> ></q-select>
</div> </div>
<div class="col-md-6 col-sm-12 q-pr-sm"> <div class="col-md-6 col-sm-12 q-pr-sm">
@ -225,4 +236,4 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </template>

View file

@ -1,4 +1,4 @@
<q-tab-panel name="exchange_providers"> <template id="lnbits-admin-exchange-providers">
<h6 class="q-my-none q-mb-sm"> <h6 class="q-my-none q-mb-sm">
<span v-text="$t('exchange_providers')"></span> <span v-text="$t('exchange_providers')"></span>
</h6> </h6>
@ -109,7 +109,7 @@
dense dense
filled filled
v-model="props.row.name" v-model="props.row.name"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
type="text" type="text"
> >
</q-input> </q-input>
@ -119,7 +119,7 @@
dense dense
filled filled
v-model="props.row.api_url" v-model="props.row.api_url"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
type="text" type="text"
> >
</q-input </q-input
@ -129,7 +129,7 @@
dense dense
filled filled
v-model="props.row.path" v-model="props.row.path"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
type="text" type="text"
> >
</q-input> </q-input>
@ -139,9 +139,9 @@
filled filled
dense dense
v-model="props.row.exclude_to" v-model="props.row.exclude_to"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
multiple multiple
:options="{{ currencies | safe }}" :options="currencies"
></q-select> ></q-select>
</q-td> </q-td>
<q-td> <q-td>
@ -155,7 +155,7 @@
> >
</q-btn> </q-btn>
<q-chip <q-chip
v-for="ticker, index in props.row.ticker_conversion" v-for="(ticker, index) in props.row.ticker_conversion"
:key="ticker" :key="ticker"
removable removable
dense dense
@ -198,4 +198,47 @@
</div> </div>
<div class="col-md-8 col-sm-12"></div> <div class="col-md-8 col-sm-12"></div>
</div> </div>
</q-tab-panel>
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<div class="q-mb-md">
<strong v-text="$t('create_ticker_converter')"></strong>
</div>
<div class="row">
<div class="col-12 q-mb-md">
<q-select
filled
dense
v-model="exchangeData.convertFromTicker"
label="From Currency"
:options="currencies"
></q-select>
</div>
<div class="col-12">
<q-input
v-model="exchangeData.convertToTicker"
dense
filled
label="New Ticker"
hint="This ticker will be used for the exchange API calls."
>
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
@click="addExchangeTickerConversion()"
label="Add Ticker Conversion"
color="primary"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
</template>

View file

@ -1,4 +1,4 @@
<q-tab-panel name="extensions"> <template id="lnbits-admin-extensions">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<div> <div>
<h6 class="q-my-none"> <h6 class="q-my-none">
@ -140,4 +140,4 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </template>

View file

@ -1,14 +1,13 @@
<q-tab-panel name="fiat_providers"> <template id="lnbits-admin-fiat-providers">
<h6 class="q-my-none q-mb-sm"> <h6 class="q-my-none q-mb-sm">
<span v-text="$t('fiat_providers')"></span> <span v-text="$t('fiat_providers')"></span>
<q-btn <q-btn
round round
flat flat
@click="hideInputsToggle()" @click="hideInputToggle = !hideInputToggle"
:icon="hideInputToggle ? 'visibility_off' : 'visibility'" :icon="hideInputToggle ? 'visibility_off' : 'visibility'"
></q-btn> ></q-btn>
</h6> </h6>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<q-list bordered class="rounded-borders"> <q-list bordered class="rounded-borders">
@ -16,9 +15,7 @@
<template v-slot:header> <template v-slot:header>
<q-item-section avatar> <q-item-section avatar>
<q-avatar> <q-avatar>
<img <img src="/static/images/stripe_logo.ico" />
:src="'{{ static_url_for('static', 'images/stripe_logo.ico') }}'"
/>
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>
@ -103,7 +100,9 @@
<q-card-section> <q-card-section>
<span v-text="$t('webhook_events_list')"></span> <span v-text="$t('webhook_events_list')"></span>
<ul> <ul>
<li><code>checkout.session.completed</code></li> <li>
<code>checkout.session.completed</code>
</li>
- the user completed the checkout process - the user completed the checkout process
<li><code>invoice.paid</code></li> <li><code>invoice.paid</code></li>
- the invoice was successfully paid (for subscriptions) - the invoice was successfully paid (for subscriptions)
@ -120,7 +119,7 @@
type="number" type="number"
min="0" min="0"
v-model="formData.stripe_limits.service_fee_percent" v-model="formData.stripe_limits.service_fee_percent"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
:label="$t('service_fee_label')" :label="$t('service_fee_label')"
:hint="$t('service_fee_hint')" :hint="$t('service_fee_hint')"
></q-input> ></q-input>
@ -132,7 +131,7 @@
type="number" type="number"
min="0" min="0"
v-model="formData.stripe_limits.service_max_fee_sats" v-model="formData.stripe_limits.service_max_fee_sats"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
:label="$t('service_fee_max')" :label="$t('service_fee_max')"
:hint="$t('service_fee_max_hint')" :hint="$t('service_fee_max_hint')"
></q-input> ></q-input>
@ -143,7 +142,7 @@
class="q-ma-sm" class="q-ma-sm"
type="text" type="text"
v-model="formData.stripe_limits.service_fee_wallet_id" v-model="formData.stripe_limits.service_fee_wallet_id"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
:label="$t('fee_wallet_label')" :label="$t('fee_wallet_label')"
:hint="$t('fee_wallet_hint')" :hint="$t('fee_wallet_hint')"
></q-input> ></q-input>
@ -161,7 +160,7 @@
type="number" type="number"
min="0" min="0"
v-model="formData.stripe_limits.service_min_amount_sats" v-model="formData.stripe_limits.service_min_amount_sats"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
:label="$t('min_incoming_payment_amount')" :label="$t('min_incoming_payment_amount')"
:hint="$t('min_incoming_payment_amount_desc')" :hint="$t('min_incoming_payment_amount_desc')"
></q-input> ></q-input>
@ -173,7 +172,7 @@
type="number" type="number"
min="0" min="0"
v-model="formData.stripe_limits.service_max_amount_sats" v-model="formData.stripe_limits.service_max_amount_sats"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
:label="$t('max_incoming_payment_amount')" :label="$t('max_incoming_payment_amount')"
:hint="$t('max_incoming_payment_amount_desc')" :hint="$t('max_incoming_payment_amount_desc')"
></q-input> ></q-input>
@ -183,7 +182,7 @@
filled filled
class="q-ma-sm" class="q-ma-sm"
v-model="formData.stripe_limits.service_faucet_wallet_id" v-model="formData.stripe_limits.service_faucet_wallet_id"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
:label="$t('faucest_wallet_id')" :label="$t('faucest_wallet_id')"
:hint="$t('faucest_wallet_id_hint')" :hint="$t('faucest_wallet_id_hint')"
></q-input> ></q-input>
@ -196,12 +195,20 @@
<ul> <ul>
<li> <li>
<span <span
v-text="$t('faucest_wallet_desc_1', {provider: 'stripe'})" v-text="
$t('faucest_wallet_desc_1', {
provider: 'stripe'
})
"
></span> ></span>
</li> </li>
<li> <li>
<span <span
v-text="$t('faucest_wallet_desc_2', {provider: 'stripe'})" v-text="
$t('faucest_wallet_desc_2', {
provider: 'stripe'
})
"
></span> ></span>
</li> </li>
<li> <li>
@ -209,7 +216,11 @@
</li> </li>
<li> <li>
<span <span
v-text="$t('faucest_wallet_desc_4', {provider: 'stripe'})" v-text="
$t('faucest_wallet_desc_4', {
provider: 'stripe'
})
"
></span> ></span>
</li> </li>
<li> <li>
@ -227,10 +238,14 @@
<q-input <q-input
filled filled
v-model="formAddStripeUser" v-model="formAddStripeUser"
@keydown.enter="addAllowedUser" @keydown.enter="addStripeAllowedUser"
type="text" type="text"
:label="$t('allowed_users_label')" :label="$t('allowed_users_label')"
:hint="$t('allowed_users_hint_feature', {feature: 'Stripe'})" :hint="
$t('allowed_users_hint_feature', {
feature: 'Stripe'
})
"
> >
<q-btn <q-btn
@click="addStripeAllowedUser" @click="addStripeAllowedUser"
@ -242,7 +257,7 @@
<div> <div>
<q-chip <q-chip
v-for="user in formData.stripe_limits.allowed_users" v-for="user in formData.stripe_limits.allowed_users"
@update:model-value="touchSettings()" @update:model-value="formData.touch = null"
:key="user" :key="user"
removable removable
@remove="removeStripeAllowedUser(user)" @remove="removeStripeAllowedUser(user)"
@ -264,9 +279,7 @@
<template v-slot:header> <template v-slot:header>
<q-item-section avatar> <q-item-section avatar>
<q-avatar> <q-avatar>
<img <img src="/static/images/square_logo.png" />
:src="'{{ static_url_for('static', 'images/square_logo.png') }}'"
/>
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>
@ -284,4 +297,4 @@
</q-list> </q-list>
</div> </div>
</div> </div>
</q-tab-panel> </template>

View file

@ -1,4 +1,4 @@
<q-tab-panel name="funding"> <template id="lnbits-admin-funding">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<h6 class="q-my-none"> <h6 class="q-my-none">
<span v-text="$t('wallets_management')"></span> <span v-text="$t('wallets_management')"></span>
@ -12,45 +12,67 @@
</p> </p>
<ul> <ul>
<li <li
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})" v-text="
$t('funding_source', {
wallet_class: settings.lnbits_backend_wallet_class
})
"
></li> ></li>
<li <li
v-text="$t('node_balance', {balance: (auditData.node_balance_sats || 0).toLocaleString()})" v-text="
$t('node_balance', {
balance: (auditData.node_balance_sats || 0).toLocaleString()
})
"
></li> ></li>
<li <li
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_sats || 0).toLocaleString()})" v-text="
$t('lnbits_balance', {
balance: (auditData.lnbits_balance_sats || 0).toLocaleString()
})
"
></li> ></li>
<li <li
v-text="$t('funding_reserve_percent', { v-text="
percent: auditData.lnbits_balance_sats > 0 $t('funding_reserve_percent', {
? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2) percent:
: 100 auditData.lnbits_balance_sats > 0
})" ? (
(auditData.node_balance_sats /
auditData.lnbits_balance_sats) *
100
).toFixed(2)
: 100
})
"
></li> ></li>
</ul> </ul>
<br /> <br />
</div> </div>
<div class="col"> <div class="col">
{% if LNBITS_NODE_UI_AVAILABLE %} <div v-if="LNBITS_NODE_UI">
<p><span v-text="$t('node_management')"></span></p> <p>
<q-toggle <span v-text="$t('node_management')"></span>
:label="$t('toggle_node_ui')" </p>
v-model="formData.lnbits_node_ui" <q-toggle
></q-toggle> :label="$t('toggle_node_ui')"
<q-toggle v-model="formData.lnbits_node_ui"
v-if="formData.lnbits_node_ui" ></q-toggle>
:label="$t('toggle_public_node_ui')" <q-toggle
v-model="formData.lnbits_public_node_ui" v-if="formData.lnbits_node_ui"
></q-toggle> :label="$t('toggle_public_node_ui')"
<br /> v-model="formData.lnbits_public_node_ui"
<q-toggle ></q-toggle>
v-if="formData.lnbits_node_ui" <br />
:label="$t('toggle_transactions_node_ui')" <q-toggle
v-model="formData.lnbits_node_ui_transactions" v-if="formData.lnbits_node_ui"
></q-toggle> :label="$t('toggle_transactions_node_ui')"
{% else %} v-model="formData.lnbits_node_ui_transactions"
<p><span v-text="$t('node_management_not_supported')"></span></p> ></q-toggle>
{% endif %} </div>
<p v-if="!LNBITS_NODE_UI">
<span v-text="$t('node_management_not_supported')"></span>
</p>
</div> </div>
</div> </div>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
@ -66,7 +88,9 @@
</q-input> </q-input>
</div> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve_percent')"></span></p> <p>
<span v-text="$t('fee_reserve_percent')"></span>
</p>
<q-input <q-input
type="number" type="number"
filled filled
@ -88,7 +112,9 @@
</q-input> </q-input>
</div> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<p><span v-text="$t('payment_wait_time')"></span></p> <p>
<span v-text="$t('payment_wait_time')"></span>
</p>
<q-input <q-input
type="number" type="number"
filled filled
@ -102,7 +128,7 @@
</div> </div>
</div> </div>
<div v-if="isSuperUser"> <div v-if="isSuperUser">
<lnbits-funding-sources <lnbits-admin-funding-sources
:form-data="formData" :form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources" :allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/> />
@ -215,4 +241,4 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </template>

View file

@ -0,0 +1,85 @@
<template id="lnbits-admin-funding-sources">
<div class="funding-sources">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('funding_sources')"></span>
<q-btn
round
flat
@click="this.hideInput = !this.hideInput"
:icon="this.hideInput ? 'visibility_off' : 'visibility'"
></q-btn>
</h6>
<div class="row">
<div class="col-12">
<p>Active Funding<small> (Requires server restart)</small></p>
<q-select
filled
v-model="formData.lnbits_backend_wallet_class"
hint="Select the active funding wallet"
:options="sortedAllowedFundingSources"
:option-label="item => getFundingSourceLabel(item)"
></q-select>
</div>
</div>
<q-list
class="q-mt-md"
v-for="(fund, idx) in allowedFundingSources"
:key="idx"
>
<div
v-if="
fundingSources.get(fund) &&
fund === formData.lnbits_backend_wallet_class
"
>
<div
class="row"
v-for="([key, prop], i) in Object.entries(fundingSources.get(fund))"
:key="i"
>
<div class="col-12">
<q-input
v-model="formData[key]"
filled
class="q-mt-sm"
:type="hideInput ? 'password' : 'text'"
:label="prop.label"
:hint="prop.hint"
:readonly="prop.readonly || false"
>
<q-btn
v-if="prop.copy"
@click="copyText(formData[key])"
icon="content_copy"
class="cursor-pointer"
color="grey"
flat
dense
></q-btn>
<q-btn
v-if="prop.qrcode"
@click="showQRValue(formData[key])"
icon="qr_code"
class="cursor-pointer"
color="grey"
flat
dense
></q-btn>
</q-input>
</div>
</div>
</div>
</q-list>
<q-dialog v-model="showQRDialog">
<q-card class="q-pa-md">
<q-card-section>
<lnbits-qrcode :value="qrValue"></lnbits-qrcode>
</q-card-section>
<q-card-actions align="right">
<q-btn flat :label="$t('close')" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>

View file

@ -1,4 +1,4 @@
<q-tab-panel name="library"> <template id="lnbits-admin-library">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm"> <h6 class="q-my-none q-mb-sm">
<span v-text="$t('image_library')"></span> <span v-text="$t('image_library')"></span>
@ -64,4 +64,4 @@
<div v-if="library_images.length === 0" class="q-pa-xl"> <div v-if="library_images.length === 0" class="q-pa-xl">
<div class="text-subtitle2 text-grey">No images uploaded yet.</div> <div class="text-subtitle2 text-grey">No images uploaded yet.</div>
</div> </div>
</q-tab-panel> </template>

View file

@ -1,4 +1,4 @@
<q-tab-panel name="notifications"> <template id="lnbits-admin-notifications">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm"> <h6 class="q-my-none q-mb-sm">
<span v-text="$t('notifications_configure')"></span> <span v-text="$t('notifications_configure')"></span>
@ -478,7 +478,9 @@
type="number" type="number"
min="0" min="0"
filled filled
v-model="formData.lnbits_notification_incoming_payment_amount_sats" v-model="
formData.lnbits_notification_incoming_payment_amount_sats
"
/> />
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -501,11 +503,13 @@
type="number" type="number"
min="0" min="0"
filled filled
v-model="formData.lnbits_notification_outgoing_payment_amount_sats" v-model="
formData.lnbits_notification_outgoing_payment_amount_sats
"
/> />
</q-item-section> </q-item-section>
</q-item> </q-item>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </template>

View file

@ -1,6 +1,8 @@
<q-tab-panel name="security"> <template id="lnbits-admin-security">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<h6 class="q-my-none"><span v-text="$t('server_management')"></span></h6> <h6 class="q-my-none">
<span v-text="$t('server_management')"></span>
</h6>
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<p><span v-text="$t('base_url')"></span></p> <p><span v-text="$t('base_url')"></span></p>
@ -34,7 +36,10 @@
:hint="$t('auth_allowed_methods_hint')" :hint="$t('auth_allowed_methods_hint')"
:label="$t('auth_allowed_methods_label')" :label="$t('auth_allowed_methods_label')"
:options="formData.auth_all_methods" :options="formData.auth_all_methods"
:option-label="option => option.length > 25 ? option.substring(0, 22) + '...' : option" :option-label="
option =>
option.length > 25 ? option.substring(0, 22) + '...' : option
"
></q-select> ></q-select>
</div> </div>
</div> </div>
@ -206,7 +211,11 @@
dense dense
flat flat
color="primary" color="primary"
:label="(serverlogEnabled) ? $t('disable_server_log') : $t('enable_server_log')" :label="
serverlogEnabled
? $t('disable_server_log')
: $t('enable_server_log')
"
></q-btn> ></q-btn>
</div> </div>
<br /> <br />
@ -291,7 +300,7 @@
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<q-select <q-select
filled filled
:options="[$t('second'),$t('minute'),$t('hour')]" :options="[$t('second'), $t('minute'), $t('hour')]"
v-model="formData.lnbits_rate_limit_unit" v-model="formData.lnbits_rate_limit_unit"
:label="$t('time_unit')" :label="$t('time_unit')"
></q-select> ></q-select>
@ -337,4 +346,4 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </template>

View file

@ -1,37 +1,51 @@
<q-tab-panel name="server"> <template id="lnbits-admin-server">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<div> <div>
<h6 class="q-my-none"><span v-text="$t('currency_settings')"></span></h6> <h6 class="q-my-none">
<span v-text="$t('currency_settings')"></span>
</h6>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<p><span v-text="$t('allowed_currencies')"></span></p> <p>
<span v-text="$t('allowed_currencies')"></span>
</p>
<q-select <q-select
filled filled
v-model="formData.lnbits_allowed_currencies" v-model="formData.lnbits_allowed_currencies"
multiple multiple
:hint="$t('allowed_currencies_hint')" :hint="$t('allowed_currencies_hint')"
:label="$t('allowed_currencies')" :label="$t('allowed_currencies')"
:options="{{ currencies | safe }}" :options="currencies"
></q-select> ></q-select>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<p><span v-text="$t('default_account_currency')"></span></p> <p>
<span v-text="$t('default_account_currency')"></span>
</p>
<q-select <q-select
filled filled
v-model="formData.lnbits_default_accounting_currency" v-model="formData.lnbits_default_accounting_currency"
clearable clearable
:hint="$t('default_account_currency_hint')" :hint="$t('default_account_currency_hint')"
:label="$t('currency')" :label="$t('currency')"
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}" :options="
formData.lnbits_allowed_currencies?.length
? formData.lnbits_allowed_currencies
: currencies
"
></q-select> ></q-select>
</div> </div>
</div> </div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator> <q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('payments')"></span></h6> <h6 class="q-my-none">
<span v-text="$t('payments')"></span>
</h6>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<p><span v-text="$t('max_outgoing_payment_amount')"></span></p> <p>
<span v-text="$t('max_outgoing_payment_amount')"></span>
</p>
<q-input <q-input
filled filled
type="number" type="number"
@ -44,7 +58,9 @@
</div> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<p><span v-text="$t('max_incoming_payment_amount')"></span></p> <p>
<span v-text="$t('max_incoming_payment_amount')"></span>
</p>
<q-input <q-input
filled filled
type="number" type="number"
@ -59,7 +75,9 @@
</div> </div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator> <q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('wallet_limiter')"></span></h6> <h6 class="q-my-none">
<span v-text="$t('wallet_limiter')"></span>
</h6>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<q-input <q-input
@ -95,7 +113,9 @@
</div> </div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator> <q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none"><span v-text="$t('service_fee')"></span></h6> <h6 class="q-my-none">
<span v-text="$t('service_fee')"></span>
</h6>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<p><span v-text="$t('service_fee')"></span></p> <p><span v-text="$t('service_fee')"></span></p>
@ -131,7 +151,9 @@
<br /> <br />
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<p><span v-text="$t('disable_fee_internal')"></span></p> <p>
<span v-text="$t('disable_fee_internal')"></span>
</p>
<q-item tag="label" v-ripple> <q-item tag="label" v-ripple>
<q-item-section> <q-item-section>
<q-item-label v-text="$t('disable_fee')"></q-item-label> <q-item-label v-text="$t('disable_fee')"></q-item-label>
@ -155,4 +177,4 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </template>

View file

@ -1,6 +1,8 @@
<q-tab-panel name="site_customisation"> <template id="lnbits-admin-site-customisation">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<h6 class="q-my-none"><span v-text="$t('ui_management')"></span></h6> <h6 class="q-my-none">
<span v-text="$t('ui_management')"></span>
</h6>
<br /> <br />
<div> <div>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
@ -10,7 +12,9 @@
filled filled
type="text" type="text"
v-model="formData.lnbits_site_title" v-model="formData.lnbits_site_title"
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')" :label="
$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')
"
></q-input> ></q-input>
<br /> <br />
</div> </div>
@ -28,13 +32,19 @@
<q-toggle <q-toggle
:tip="$t('ui_toggle_elements_tip')" :tip="$t('ui_toggle_elements_tip')"
v-model="formData.lnbits_show_home_page_elements" v-model="formData.lnbits_show_home_page_elements"
:label="formData.lnbits_show_home_page_elements ? $t('ui_elements_enable') : $t('ui_elements_disable')" :label="
formData.lnbits_show_home_page_elements
? $t('ui_elements_enable')
: $t('ui_elements_disable')
"
></q-toggle> ></q-toggle>
</div> </div>
</div> </div>
<div> <div>
<p><span v-text="$t('ui_site_description')"></span></p> <p>
<span v-text="$t('ui_site_description')"></span>
</p>
<q-input <q-input
v-model="formData.lnbits_site_description" v-model="formData.lnbits_site_description"
filled filled
@ -45,7 +55,9 @@
<br /> <br />
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<p><span v-text="$t('ui_default_wallet_name')"></span></p> <p>
<span v-text="$t('ui_default_wallet_name')"></span>
</p>
<q-input <q-input
filled filled
type="text" type="text"
@ -151,7 +163,11 @@
</q-input> </q-input>
<q-toggle <q-toggle
v-model="formData.lnbits_ad_space_enabled" v-model="formData.lnbits_ad_space_enabled"
:label="formData.lnbits_ad_space_enabled ? $t('ads_enabled') : $t('ads_disabled')" :label="
formData.lnbits_ad_space_enabled
? $t('ads_enabled')
: $t('ads_disabled')
"
/> />
<br /> <br />
</div> </div>
@ -207,4 +223,4 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </template>

View file

@ -1,4 +1,4 @@
<q-tab-panel name="users"> <template id="lnbits-admin-users">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm"> <h6 class="q-my-none q-mb-sm">
<span v-text="$t('user_management')"></span> <span v-text="$t('user_management')"></span>
@ -80,4 +80,4 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-tab-panel> </template>

View file

@ -1,3 +1,3 @@
{% include('pages/payments.vue') %} {% include('pages/node.vue') %} {% {% include('pages/payments.vue') %} {% include('pages/node.vue') %} {%
include('pages/audit.vue') %} {% include('pages/wallets.vue') %} {% include('pages/audit.vue') %} {% include('pages/wallets.vue') %} {%
include('pages/users.vue') %} include('pages/users.vue') %} {% include('pages/admin.vue') %}

View file

@ -0,0 +1,247 @@
<template id="page-admin">
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn
:label="$t('save')"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges">
<span v-text="$t('save_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="checkChanges"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
:label="$t('restart')"
color="primary"
@click="restartServer"
class="q-ml-md"
>
<q-tooltip v-if="needsRestart">
<span v-text="$t('restart_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="needsRestart"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
:label="$t('download_backup')"
flat
@click="downloadBackup"
></q-btn>
<q-btn
flat
v-if="isSuperUser"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip>
<span v-text="$t('reset_defaults_tooltip')"></span>
</q-tooltip>
</q-btn>
</div>
<div></div>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
<div class="col q-gutter-y-md">
<q-card>
<q-tabs
v-if="$q.screen.lt.md"
v-model="tab"
dense
active-color="primary"
inline-label
class="text-primary"
>
<q-tab name="funding" icon="account_balance_wallet" label="Fund" />
<q-tab name="security" icon="security" label="Sec" />
<q-tab name="server" icon="settings" label="Srv" />
<q-tab name="exchange_providers" icon="swap_horiz" label="Exch" />
<q-tab name="fiat_providers" icon="account_balance" label="Fiat" />
<q-tab name="extensions" icon="extension" label="Ext" />
<q-tab name="notifications" icon="notifications" label="Not" />
<q-tab name="audit" icon="receipt_long" label="Aud" />
<q-tab name="site_customisation" icon="language" label="Site" />
<q-tab name="library" icon="image" label="Lib" />
</q-tabs>
<q-splitter>
<template v-slot:before>
<q-tabs v-model="tab" vertical active-color="primary">
<q-tab
name="funding"
icon="account_balance_wallet"
:label="$q.screen.gt.sm ? $t('funding') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('funding')"></span></q-tooltip
></q-tab>
<q-tab
name="security"
icon="security"
:label="$q.screen.gt.sm ? $t('security') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('security')"></span></q-tooltip
></q-tab>
<q-tab
name="server"
icon="price_change"
:label="$q.screen.gt.sm ? $t('payments') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('payments')"></span></q-tooltip
></q-tab>
<q-tab
name="exchange_providers"
icon="show_chart"
:label="$q.screen.gt.sm ? $t('exchanges') : null"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('exchanges')"></span></q-tooltip
></q-tab>
<q-tab
name="fiat_providers"
icon="credit_score"
:label="$q.screen.gt.sm ? $t('fiat_providers') : null"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('fiat_providers')"></span></q-tooltip
></q-tab>
<q-tab
name="users"
icon="group"
:label="$q.screen.gt.sm ? $t('users') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('users')"></span></q-tooltip
></q-tab>
<q-tab
name="extensions"
icon="extension"
:label="$q.screen.gt.sm ? $t('extensions') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('extensions')"></span></q-tooltip
></q-tab>
<q-tab
name="notifications"
icon="notifications"
:label="$q.screen.gt.sm ? $t('notifications') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('notifications')"></span></q-tooltip
></q-tab>
<q-tab
name="audit"
icon="playlist_add_check_circle"
:label="$q.screen.gt.sm ? $t('audit') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('audit')"></span></q-tooltip
></q-tab>
<q-tab
name="library"
icon="image"
:label="$q.screen.gt.sm ? $t('library') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('library')"></span></q-tooltip
></q-tab>
<q-tab
style="word-break: break-all"
name="site_customisation"
icon="language"
:label="$q.screen.gt.sm ? $t('site_customisation') : null"
@update="val => (tab = val.name)"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('site_customisation')"></span></q-tooltip
></q-tab>
</q-tabs>
</template>
<template v-slot:after>
<q-form name="settings_form" id="settings_form">
<q-scroll-area style="height: 100vh">
<q-tab-panels
v-model="tab"
animated
vertical
scroll
transition-prev="jump-up"
transition-next="jump-up"
>
<q-tab-panel name="funding">
<lnbits-admin-funding
:is-super-user="isSuperUser"
:settings="settings"
:form-data="formData"
></lnbits-admin-funding>
</q-tab-panel>
<q-tab-panel name="fiat_providers">
<lnbits-admin-fiat-providers :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="users">
<lnbits-admin-users :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="server">
<lnbits-admin-server :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="exchange_providers">
<lnbits-admin-exchange-providers :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="extensions">
<lnbits-admin-extensions :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="notifications">
<lnbits-admin-notifications :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="security">
<lnbits-admin-security :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="site_customisation">
<lnbits-admin-site-customisation :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="audit">
<lnbits-admin-audit :form-data="formData" />
</q-tab-panel>
<q-tab-panel name="library">
<lnbits-admin-library :form-data="formData" />
</q-tab-panel>
</q-tab-panels>
</q-scroll-area>
</q-form>
</template>
</q-splitter>
</q-card>
</div>
</div>
</template>

View file

@ -101,9 +101,21 @@
"js/pages/audit.js", "js/pages/audit.js",
"js/pages/wallets.js", "js/pages/wallets.js",
"js/pages/users.js", "js/pages/users.js",
"js/pages/admin.js",
"js/components/admin/lnbits-admin-funding.js",
"js/components/admin/lnbits-admin-funding-sources.js",
"js/components/admin/lnbits-admin-fiat-providers.js",
"js/components/admin/lnbits-admin-exchange-providers.js",
"js/components/admin/lnbits-admin-security.js",
"js/components/admin/lnbits-admin-users.js",
"js/components/admin/lnbits-admin-server.js",
"js/components/admin/lnbits-admin-extensions.js",
"js/components/admin/lnbits-admin-notifications.js",
"js/components/admin/lnbits-admin-site-customisation.js",
"js/components/admin/lnbits-admin-library.js",
"js/components/admin/lnbits-admin-audit.js",
"js/components/lnbits-qrcode.js", "js/components/lnbits-qrcode.js",
"js/components/lnbits-qrcode-lnurl.js", "js/components/lnbits-qrcode-lnurl.js",
"js/components/lnbits-funding-sources.js",
"js/components/extension-settings.js", "js/components/extension-settings.js",
"js/components/data-fields.js", "js/components/data-fields.js",
"js/components/payment-list.js", "js/components/payment-list.js",