feat: move /first_install to vue component (#3485)

This commit is contained in:
dni ⚡ 2025-11-10 11:17:29 +01:00 committed by GitHub
parent 8755984bd8
commit f8c58aef0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 205 additions and 166 deletions

View file

@ -1,150 +0,0 @@
{% extends "public.html" %} {% block page_container %}
<q-page-container>
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
{% block page %}
<div class="row q-col-gutter-md justify-center main">
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section class="grid">
<div>
<h6 class="q-my-none text-center">
<strong v-text="$t('welcome_lnbits')"></strong>
<p><span v-text="$t('setup_su_account')"></span></p>
</h6>
<br />
<q-form class="q-gutter-md">
<q-input
filled
v-model="loginData.username"
:label="$t('username')"
></q-input>
<q-input
filled
v-model.trim="loginData.password"
:type="loginData.isPwd ? 'password' : 'text'"
autocomplete="off"
:label="$t('password')"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
><template v-slot:append>
<q-icon
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwd = !loginData.isPwd"
/> </template
></q-input>
<q-input
filled
v-model.trim="loginData.passwordRepeat"
:type="loginData.isPwdRepeat ? 'password' : 'text'"
autocomplete="off"
:label="$t('password_repeat')"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || $t('invalid_password_repeat')]"
><template v-slot:append>
<q-icon
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template
></q-input>
<q-btn
@click="setPassword()"
unelevated
color="primary"
:label="$t('login')"
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
></q-btn>
</q-form>
</div>
<div class="hero-wrapper">
<div class="hero q-mx-auto"></div>
</div>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<style>
main {
display: flex;
flex-direction: column;
justify-content: center;
}
.grid {
display: block;
}
.hero-wrapper {
display: none;
}
.hero {
display: block;
height: 100%;
max-width: 250px;
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
@media (min-width: 992px) {
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
.hero-wrapper {
display: block;
position: relative;
height: 100%;
padding: 1rem;
}
}
</style>
<script>
window.app = Vue.createApp({
el: '#vue',
mixins: [window.windowMixin],
data: function () {
return {
loginData: {
isPwd: true,
isPwdRepeat: true,
username: '',
password: '',
passwordRepeat: ''
}
}
},
created() {
this.hasAdminUI = '{{ LNBITS_ADMIN_UI | tojson}}'
},
computed: {
checkPasswordsMatch() {
return this.loginData.password !== this.loginData.passwordRepeat
}
},
methods: {
async setPassword() {
try {
await LNbits.api.request(
'PUT',
'/api/v1/auth/first_install',
null,
{
username: this.loginData.username,
password: this.loginData.password,
password_repeat: this.loginData.passwordRepeat
}
)
window.location.href = '/admin'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
}
})
</script>
{% endblock %}
</q-page>
</q-page-container>
{% endblock %}

View file

@ -19,6 +19,7 @@ from lnbits.decorators import (
check_admin,
check_admin_ui,
check_extension_builder,
check_first_install,
check_user_exists,
)
from lnbits.helpers import check_callback_url, template_renderer
@ -96,19 +97,6 @@ async def get_user_wallet(
)
@generic_router.get("/first_install", response_class=HTMLResponse)
async def first_install(request: Request):
if not settings.first_install:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Super user account has already been configured.",
)
return template_renderer().TemplateResponse(
request,
"core/first_install.html",
)
@generic_router.get("/robots.txt", response_class=HTMLResponse)
async def robots():
data = """
@ -296,6 +284,7 @@ async def index(
@generic_router.get("/node/public")
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
async def index_public(request: Request) -> HTMLResponse:
return template_renderer().TemplateResponse(request, "index_public.html")

View file

@ -376,3 +376,11 @@ async def check_extension_builder(
HTTPStatus.FORBIDDEN,
"Extension Builder is disabled for non admin users.",
)
async def check_first_install():
if not settings.first_install:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Super user account has already been configured.",
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -341,3 +341,39 @@ video {
.q-dialog__inner--minimized {
padding: 12px;
}
.first-install {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.first-install .grid {
display: block;
}
.first-install .hero-wrapper {
display: none;
}
.first-install .hero {
display: block;
height: 100%;
max-width: 250px;
background-image: url(/static/images/logos/lnbits.svg);
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
@media (min-width: 992px) {
.first-install .grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
.first-install .hero-wrapper {
display: block;
position: relative;
height: 100%;
padding: 1rem;
}
}

View file

@ -188,6 +188,11 @@ const routes = [
path: '/extensions',
name: 'Extensions',
component: PageExtensions
},
{
path: '/first_install',
name: 'FirstInstall',
component: PageFirstInstall
}
]

View file

@ -0,0 +1,37 @@
window.PageFirstInstall = {
template: '#page-first-install',
mixins: [window.windowMixin],
data() {
return {
loginData: {
isPwd: true,
isPwdRepeat: true,
username: '',
password: '',
passwordRepeat: ''
}
}
},
computed: {
checkPasswordsMatch() {
return this.loginData.password !== this.loginData.passwordRepeat
}
},
methods: {
async setPassword() {
try {
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
username: this.loginData.username,
password: this.loginData.password,
password_repeat: this.loginData.passwordRepeat
})
window.location.href = '/admin'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
},
created() {
document.title = 'First Install - LNbits'
}
}

View file

@ -132,3 +132,40 @@ video {
.q-dialog__inner--minimized {
padding: 12px;
}
.first-install {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
.grid {
display: block;
}
.hero-wrapper {
display: none;
}
.hero {
display: block;
height: 100%;
max-width: 250px;
background-image: url(/static/images/logos/lnbits.svg);
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
@media (min-width: 992px) {
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
.hero-wrapper {
display: block;
position: relative;
height: 100%;
padding: 1rem;
}
}
}

View file

@ -45,6 +45,7 @@
"components": [
"js/pages/extensions_builder.js",
"js/pages/extensions.js",
"js/pages/first-install.js",
"js/pages/payments.js",
"js/pages/node.js",
"js/pages/node-public.js",

View file

@ -2,4 +2,4 @@
include('pages/audit.vue') %} {% include('pages/wallets.vue') %} {%
include('pages/users.vue') %} {% include('pages/admin.vue') %} {%
include('pages/account.vue') %} {% include('pages/extensions_builder.vue') %} {%
include('pages/extensions.vue') %}
include('pages/extensions.vue') %} {% include('pages/first-install.vue') %}

View file

@ -0,0 +1,75 @@
<template id="page-first-install">
<div class="row q-col-gutter-md justify-center main first-install">
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section class="grid">
<div>
<h6 class="q-my-none text-center">
<strong v-text="$t('welcome_lnbits')"></strong>
<p><span v-text="$t('setup_su_account')"></span></p>
</h6>
<br />
<q-form class="q-gutter-md">
<q-input
filled
v-model="loginData.username"
:label="$t('username')"
></q-input>
<q-input
filled
v-model.trim="loginData.password"
:type="loginData.isPwd ? 'password' : 'text'"
autocomplete="off"
:label="$t('password')"
:rules="[
val => !val || val.length >= 8 || $t('invalid_password')
]"
><template v-slot:append>
<q-icon
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwd = !loginData.isPwd"
/> </template
></q-input>
<q-input
filled
v-model.trim="loginData.passwordRepeat"
:type="loginData.isPwdRepeat ? 'password' : 'text'"
autocomplete="off"
:label="$t('password_repeat')"
:rules="[
val => !val || val.length >= 8 || $t('invalid_password'),
val =>
val === loginData.password || $t('invalid_password_repeat')
]"
><template v-slot:append>
<q-icon
:name="
loginData.isPwdRepeat ? 'visibility_off' : 'visibility'
"
class="cursor-pointer"
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template
></q-input>
<q-btn
@click="setPassword()"
unelevated
color="primary"
:label="$t('login')"
:disable="
checkPasswordsMatch ||
!loginData.username ||
!loginData.password ||
!loginData.passwordRepeat
"
></q-btn>
</q-form>
</div>
<div class="hero-wrapper">
<div class="hero q-mx-auto"></div>
</div>
</q-card-section>
</q-card>
</div>
</div>
</template>

View file

@ -97,6 +97,7 @@
"components": [
"js/pages/extensions_builder.js",
"js/pages/extensions.js",
"js/pages/first-install.js",
"js/pages/payments.js",
"js/pages/node.js",
"js/pages/node-public.js",