Revamp the UI on Login / Register page (#2919)

This commit is contained in:
Tiago Vasconcelos 2025-02-07 07:43:19 +00:00 committed by GitHub
parent 736699af94
commit 9a1d707f54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1125 additions and 667 deletions

View File

@ -74,8 +74,8 @@
></q-input>
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_badge')"></span></p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8">
@ -97,6 +97,17 @@
</div>
</div>
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_image')"></span></p>
<q-input
filled
type="text"
tip="Custom Image"
v-model.trim="formData.lnbits_custom_image"
:label="$t('ui_custom_image_label')"
:hint="$t('ui_custom_image_hint')"
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">

File diff suppressed because it is too large Load Diff

View File

@ -428,14 +428,14 @@ async def hex_to_uuid4(hex_value: str):
@generic_router.get("/lnurlwallet", response_class=RedirectResponse)
async def lnurlwallet(request: Request):
async def lnurlwallet(request: Request, lightning: str = ""):
"""
If a user doesn't have a Lightning Network wallet and scans the LNURLw QR code with
their smartphone camera, or a QR scanner app, they can follow the link provided to
claim their satoshis and get an instant LNbits wallet! lnbits/withdraw docs
"""
lightning_param = request.query_params.get("lightning")
lightning_param = lightning
if not lightning_param:
return {"status": "ERROR", "reason": "lightning parameter not provided."}
if not settings.lnbits_allow_new_accounts:
@ -459,20 +459,19 @@ async def lnurlwallet(request: Request):
)
account = await create_user_account()
wallet = await create_wallet(user_id=account.id)
_, payment_request = await create_invoice(
payment = await create_invoice(
wallet_id=wallet.id,
amount=data1.get("maxWithdrawable") / 1000,
memo=data1.get("defaultDescription", "lnurl wallet withdraw"),
)
url = data1.get("callback")
params = {"k1": data1.get("k1"), "pr": payment_request}
params = {"k1": data1.get("k1"), "pr": payment.bolt11}
callback = url + ("&" if urlparse(url).query else "?") + urlencode(params)
res2 = await client.get(callback, timeout=2)
res2.raise_for_status()
return RedirectResponse(
f"/wallet?usr={account.id}&wal={wallet.id}",
f"/wallet?wal={wallet.id}",
)

View File

@ -76,6 +76,7 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
settings.lnbits_show_home_page_elements
)
t.env.globals["LNBITS_CUSTOM_IMAGE"] = settings.lnbits_custom_image
t.env.globals["LNBITS_CUSTOM_BADGE"] = settings.lnbits_custom_badge
t.env.globals["LNBITS_CUSTOM_BADGE_COLOR"] = settings.lnbits_custom_badge_color
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options

View File

@ -215,7 +215,6 @@ class InstalledExtensionsSettings(LNbitsSettings):
class ExchangeHistorySettings(LNbitsSettings):
lnbits_exchange_rate_history: list[dict] = Field(default=[])
def append_exchange_rate_datapoint(self, rates: dict, max_size: int):
@ -250,6 +249,9 @@ class ThemesSettings(LNbitsSettings):
]
)
lnbits_custom_logo: Optional[str] = Field(default=None)
lnbits_custom_image: Optional[str] = Field(
default="/static/images/logos/lnbits.svg"
)
lnbits_ad_space_title: str = Field(default="Supported by")
lnbits_ad_space: str = Field(
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
@ -272,7 +274,6 @@ class OpsSettings(LNbitsSettings):
class FeeSettings(LNbitsSettings):
lnbits_reserve_fee_min: int = Field(default=2000)
lnbits_reserve_fee_percent: float = Field(default=1.0)
lnbits_service_fee: float = Field(default=0)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -263,13 +263,14 @@ window.localisation.en = {
enable_server_log: 'Enable Server Log',
coming_soon: 'Feature coming soon',
session_has_expired: 'Your session has expired. Please login again.',
instant_access_question: 'Want instant access?',
instant_access_question: 'or instant access',
login_with_user_id: 'Login with user ID',
or: 'or',
create_new_wallet: 'Create New Wallet',
login_to_account: 'Login to your account',
create_account: 'Create account',
account_settings: 'Account Settings',
signin_with_oauth: 'or Login with',
signin_with_nostr: 'Continue with Nostr',
signin_with_google: 'Sign in with Google',
signin_with_github: 'Sign in with GitHub',
@ -458,6 +459,9 @@ window.localisation.en = {
denomination_hint: 'The name for the FakeWallet token',
ui_qr_code_logo: 'QR Code Logo',
ui_qr_code_logo_hint: 'URL to logo image in QR code',
ui_custom_image: 'Custom Image',
ui_custom_image_label: 'URL to custom image',
ui_custom_image_hint: 'Image showed at homepage/login',
ui_custom_badge: 'Custom Badge',
ui_custom_badge_label:
"Custom Badge 'USE WITH CAUTION - LNbits wallet is still in BETA'",

View File

@ -744,7 +744,6 @@ window.windowMixin = {
)
.onOk(async () => {
try {
this.$q.localStorage.remove('lnbits.disclaimerShown')
await LNbits.api.logout()
window.location = '/'
} catch (e) {

View File

@ -498,3 +498,123 @@ window.app.component('lnbits-update-balance', {
}
}
})
window.app.component('user-id-only', {
template: '#user-id-only',
mixins: [window.windowMixin],
props: {
allowed_new_users: Boolean,
authAction: String,
authMethod: String,
usr: String,
wallet: String
},
data() {
return {
user: this.usr,
walletName: this.wallet
}
},
methods: {
showLogin(method) {
this.$emit('show-login', method)
},
showRegister(method) {
this.$emit('show-register', method)
},
loginUsr() {
this.$emit('update:user', this.user)
this.$emit('login-usr')
},
createWallet() {
this.$emit('update:wallet', this.walletName)
this.$emit('create-wallet')
}
},
computed: {
showInstantLogin() {
// do not show if authmethod is 'username-password' and authAction is 'register'
return (
this.authMethod !== 'username-password' ||
this.authAction !== 'register'
)
}
},
created() {}
})
window.app.component('username-password', {
template: '#username-password',
mixins: [window.windowMixin],
props: {
allowed_new_users: Boolean,
authMethods: Array,
authAction: String,
username: String,
password_1: String,
password_2: String,
resetKey: String
},
data() {
return {
oauth: [
'nostr-auth-nip98',
'google-auth',
'github-auth',
'keycloak-auth'
],
username: this.userName,
password: this.password_1,
passwordRepeat: this.password_2,
reset_key: this.resetKey
}
},
methods: {
login() {
this.$emit('update:userName', this.username)
this.$emit('update:password_1', this.password)
this.$emit('login')
},
register() {
this.$emit('update:userName', this.username)
this.$emit('update:password_1', this.password)
this.$emit('update:password_2', this.passwordRepeat)
this.$emit('register')
},
reset() {
this.$emit('update:resetKey', this.reset_key)
this.$emit('update:passeord_1', this.password)
this.$emit('update:password_2', this.passwordRepeat)
this.$emit('reset')
},
validateUsername(val) {
const usernameRegex = new RegExp(
'^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$'
)
return usernameRegex.test(val)
}
},
computed: {
showOauth() {
return this.oauth.some(m => this.authMethods.includes(m))
}
},
created() {
console.log('username-password created', this.passwordRepeat)
}
})
window.app.component('separator-text', {
template: '#separator-text',
props: {
text: String,
uppercase: {
type: Boolean,
default: false
},
color: {
type: String,
default: 'grey'
}
}
})

View File

@ -9,7 +9,9 @@ window.app = Vue.createApp({
description: ''
},
isUserAuthorized: false,
authAction: 'login',
authAction: Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
? 'login'
: 'register',
authMethod: 'username-password',
usr: '',
username: '',
@ -18,7 +20,9 @@ window.app = Vue.createApp({
password: '',
passwordRepeat: '',
walletName: '',
signup: false
signup: false,
slide: 1,
autoplay: true
}
},
computed: {
@ -170,15 +174,10 @@ window.app = Vue.createApp({
message: 'Processing...',
icon: null
})
},
validateUsername(val) {
const usernameRegex = new RegExp(
'^(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]$'
)
return usernameRegex.test(val)
}
},
created() {
console.log(Quasar.LocalStorage.getItem('lnbits.disclaimerShown'))
this.description = SITE_DESCRIPTION
this.isUserAuthorized = !!this.$q.cookies.get('is_lnbits_user_authorized')
if (this.isUserAuthorized) {

View File

@ -847,18 +847,18 @@ window.WalletPageLogic = {
}
},
mounted() {
if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
if (!Quasar.LocalStorage.getItem('lnbits.disclaimerShown')) {
this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true)
this.$q.localStorage.set('lnbits.reactions', 'confettiTop')
Quasar.LocalStorage.setItem('lnbits.disclaimerShown', true)
Quasar.LocalStorage.setItem('lnbits.reactions', 'confettiTop')
}
if (this.$q.localStorage.getItem('lnbits.isPrioritySwapped')) {
this.isPrioritySwapped = this.$q.localStorage.getItem(
if (Quasar.LocalStorage.getItem('lnbits.isPrioritySwapped')) {
this.isPrioritySwapped = Quasar.LocalStorage.getItem(
'lnbits.isPrioritySwapped'
)
} else {
this.isPrioritySwapped = false
this.$q.localStorage.setItem('lnbits.isPrioritySwapped', false)
Quasar.LocalStorage.setItem('lnbits.isPrioritySwapped', false)
}
}
}

View File

@ -1054,3 +1054,329 @@
</q-dialog>
</span>
</template>
<template id="user-id-only">
<div v-if="authAction === 'login' && authMethod === 'user-id-only'">
<q-card-section class="q-pb-none">
<div class="text-center text-h6">
<span v-text="$t('login_with_user_id')"></span>
</div>
</q-card-section>
<q-card-section>
<q-form @submit="loginUsr" class="q-gutter-md">
<q-input
dense
filled
v-model="user"
label="usr"
type="password"
></q-input>
<q-card-actions vertical align="center" class="q-pa-none">
<q-btn
color="primary"
:disable="user == ''"
type="submit"
:label="$t('login')"
class="full-width q-mb-sm"
></q-btn>
<q-btn
@click="showLogin('username-password')"
outline
color="grey"
:label="$t('back')"
class="full-width"
></q-btn>
</q-card-actions>
</q-form>
</q-card-section>
</div>
<div v-if="authAction === 'register' && authMethod === 'user-id-only'">
<q-card-section class="q-pb-none">
<div class="text-center text-h6">
<span v-text="$t('create_new_wallet')"></span>
</div>
</q-card-section>
<q-card-section>
<q-form @submit="createWallet" class="q-gutter-md">
<q-input
dense
filled
v-model="walletName"
:label="$t('name_your_wallet', {name: '{{ SITE_TITLE }} *'})"
></q-input>
<q-card-actions vertical align="center" class="q-pa-none">
<q-btn
color="primary"
:disable="walletName == ''"
type="submit"
:label="$t('add_wallet')"
class="full-width q-mb-sm"
></q-btn>
<q-btn
@click="showLogin('username-password')"
outline
color="grey"
:label="$t('back')"
class="full-width"
></q-btn>
</q-card-actions>
</q-form>
</q-card-section>
</div>
<q-card-section v-show="showInstantLogin">
<div>
<separator-text :text="$t('instant_access_question')"></separator-text>
<div class="text-body2 text-center q-mt-md">
<q-badge
@click="showLogin('user-id-only')"
color="accent"
class="cursor-pointer"
rounded
>
<strong>
<q-icon name="account_circle" size="xs"></q-icon>
<span v-text="$t('login_with_user_id')"></span> </strong
></q-badge>
<div v-if="allowed_new_users" class="inline-block">
<span v-text="$t('or')" class="q-mx-sm text-grey"></span>
<q-badge
@click="showRegister('user-id-only')"
color="accent"
class="cursor-pointer"
rounded
>
<strong>
<q-icon name="add" size="xs"></q-icon>
<span v-text="$t('create_new_wallet')"></span>
</strong>
</q-badge>
</div>
</div>
</div>
</q-card-section>
</template>
<template id="username-password">
<q-card-section class="q-pb-none">
<div class="text-center text-h6 q-mb-sm q-mt-none q-pt-none">
<span
v-if="authAction === 'login'"
v-text="$t('login_to_account')"
></span>
<span
v-if="authAction === 'register'"
v-text="$t('create_account')"
></span>
<span v-if="authAction === 'reset'" v-text="$t('reset_password')"></span>
</div>
</q-card-section>
<!-- LOGIN -->
<q-card-section v-if="authAction === 'login'">
<q-form @submit="login" class="q-gutter-sm">
<q-input
dense
filled
v-model="username"
name="username"
:label="$t('username_or_email') + ' *'"
></q-input>
<q-input
dense
filled
v-model="password"
name="password"
:label="$t('password') + ' *'"
type="password"
></q-input>
<div class="row justify-end">
<q-btn
:disable="!username || !password"
color="primary"
type="submit"
:label="$t('login')"
class="full-width"
></q-btn>
</div>
</q-form>
</q-card-section>
<!-- REGISTER -->
<q-card-section v-if="authAction === 'register'">
<q-form @submit="register" class="q-gutter-sm">
<q-input
dense
filled
required
v-model="username"
:label="$t('username') + ' *'"
:rules="[val => validateUsername(val) || $t('invalid_username')]"
></q-input>
<q-input
dense
filled
v-model="password"
:label="$t('password') + ' *'"
type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<q-input
dense
filled
v-model="passwordRepeat"
:label="$t('password_repeat') + ' *'"
type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<div class="row justify-end">
<q-btn
unelevated
color="primary"
:disable="
!password ||
!passwordRepeat ||
!username ||
password !== passwordRepeat
"
type="submit"
class="full-width"
:label="$t('create_account')"
></q-btn>
</div>
</q-form>
</q-card-section>
<slot></slot>
<!-- RESET -->
<q-card-section v-if="authAction === 'reset'">
<q-form @submit="reset" class="q-gutter-sm">
<q-input
dense
filled
required
:disable="true"
v-model="reset_key"
:label="$t('reset_key') + ' *'"
></q-input>
<q-input
dense
filled
v-model="password"
:label="$t('password') + ' *'"
type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<q-input
dense
filled
v-model="passwordRepeat"
:label="$t('password_repeat') + ' *'"
type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<div class="row justify-end">
<q-btn
unelevated
color="primary"
:disable="
!password ||
!passwordRepeat ||
!reset_key ||
password !== passwordRepeat
"
type="submit"
class="full-width"
:label="$t('reset_password')"
></q-btn>
</div>
</q-form>
</q-card-section>
<!-- OAUTH -->
<q-card-section v-if="showOauth">
<separator-text :text="$t('signin_with_oauth')"></separator-text>
<div class="flex justify-center q-mt-md" style="gap: 1rem">
<q-btn
v-if="authMethods.includes('nostr-auth-nip98')"
@click="signInWithNostr"
outline
no-caps
color="grey"
padding="sm md"
>
<div class="row items-center no-wrap">
<q-avatar size="32px">
<q-img
class="bg-primary"
:src="`{{ static_url_for('static', 'images/logos/nostr.svg') }}`"
></q-img>
</q-avatar>
</div>
<q-tooltip>
<span v-text="$t('signin_with_nostr')"></span>
</q-tooltip>
</q-btn>
<q-btn
v-if="authMethods.includes('github-auth')"
href="/api/v1/auth/github"
type="a"
outline
no-caps
color="grey"
padding="sm md"
>
<div class="row items-center no-wrap">
<q-avatar size="32px">
<q-img
:src="`{{ static_url_for('static', 'images/github-logo.png') }}`"
:style="$q.dark.isActive ? 'filter: grayscale(1) invert(1)' : ''"
></q-img>
</q-avatar>
</div>
<q-tooltip><span v-text="$t('signin_with_github')"></span></q-tooltip>
</q-btn>
<q-btn
v-if="authMethods.includes('google-auth')"
href="/api/v1/auth/google"
type="a"
outline
no-caps
color="grey"
padding="sm md"
>
<div class="row items-center no-wrap">
<q-avatar size="32px">
<q-img
:src="`{{ static_url_for('static', 'images/google-logo.png') }}`"
></q-img>
</q-avatar>
</div>
<q-tooltip>
<span v-text="$t('signin_with_google')"></span>
</q-tooltip>
</q-btn>
<q-btn
v-if="authMethods.includes('keycloak-auth')"
href="/api/v1/auth/keycloak"
type="a"
outline
no-caps
color="grey"
class="btn-fixed-width"
>
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="`{{ static_url_for('static', 'images/keycloak-logo.png') }}`"
></q-img>
</q-avatar>
<div><span v-text="$t('signin_with_keycloak')"></span></div>
</q-btn>
</div>
</q-card-section>
</template>
<template id="separator-text">
<div class="row fit no-wrap items-center">
<q-separator class="col q-mr-sm"></q-separator>
<div class="col-grow q-mx-sm">
<span :class="`text-h6 text-${color}`" v-text="text"></span>
</div>
<q-separator class="col q-ml-sm"></q-separator>
</div>
</template>