Compare commits

...
6 Commits
Author SHA1 Message Date
Tiago Vasconcelos f53fee6896 add onboard to wallet 2024-02-15 12:18:52 +00:00
Tiago Vasconcelos f14dfd2522 user onboarding 2024-02-15 09:23:09 +00:00
526467747e Add Keycloak SSO (#2272)
* feat: add `keycloak` SSO

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-02-14 09:23:37 +01:00
Vlad StanandGitHub b8d295a5b7 refactor: generalize SSO auth (#2263)
* refactor: first extraction of providers
* refactor: remove unused property
* refactor: extract `_find_auth_provider_class`
* fix: return type
* feat: prepare for `keycloak`
2024-02-14 07:57:50 +01:00
jackstar12andGitHub 293b6267be fix: correct invoice expiry for lndrest (#2271) 2024-02-13 15:14:09 +02:00
Vlad StanandGitHub 20e70854a1 fix: add installed extensions to the ext list if missing (#2261) 2024-02-12 15:33:28 +01:00
20 changed files with 393 additions and 111 deletions
+11 -4
View File
@@ -7,7 +7,7 @@
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available. # Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available.
# Warning: Enabling this will make LNbits ignore most configurations in file. Only the # Warning: Enabling this will make LNbits ignore most configurations in file. Only the
# configurations defined in `ReadOnlySettings` will still be read from the environment variables. # configurations defined in `ReadOnlySettings` will still be read from the environment variables.
# The rest of the settings will be stored in your database and you will be able to change them # The rest of the settings will be stored in your database and you will be able to change them
# only through the Admin UI. # only through the Admin UI.
# Disable this to make LNbits use this config file again. # Disable this to make LNbits use this config file again.
LNBITS_ADMIN_UI=false LNBITS_ADMIN_UI=false
@@ -107,21 +107,28 @@ LNTIPS_API_ENDPOINT=https://ln.tips
# Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value. # Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value.
AUTH_SECRET_KEY="" AUTH_SECRET_KEY=""
AUTH_TOKEN_EXPIRE_MINUTES=525600 AUTH_TOKEN_EXPIRE_MINUTES=525600
# Possible authorization methods: user-id-only, username-password, google-auth, github-auth # Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth
AUTH_ALLOWED_METHODS="user-id-only, username-password" AUTH_ALLOWED_METHODS="user-id-only, username-password"
# Set this flag if HTTP is used for OAuth # Set this flag if HTTP is used for OAuth
# OAUTHLIB_INSECURE_TRANSPORT="1" # OAUTHLIB_INSECURE_TRANSPORT="1"
# Google OAuth Config # Google OAuth Config
# Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token # Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET="" GOOGLE_CLIENT_SECRET=""
# GitHub OAuth Config # GitHub OAuth Config
# Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token # Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token
GITHUB_CLIENT_ID="" GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET="" GITHUB_CLIENT_SECRET=""
# Keycloak OAuth Config
# Make sure that the valid redirect URIs contain https://{domain}/api/v1/auth/keycloak/token
KEYCLOAK_CLIENT_ID=""
KEYCLOAK_CLIENT_SECRET=""
KEYCLOAK_DISCOVERY_URL=""
###################################### ######################################
# uvicorn variable, uncomment to allow https behind a proxy # uvicorn variable, uncomment to allow https behind a proxy
+2
View File
@@ -1097,6 +1097,8 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
return None return None
row_dict = dict(sets) row_dict = dict(sets)
row_dict.pop("super_user") row_dict.pop("super_user")
row_dict.pop("auth_all_methods")
admin_settings = AdminSettings( admin_settings = AdminSettings(
is_super_user=is_super_user, is_super_user=is_super_user,
lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources, lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources,
+37
View File
@@ -0,0 +1,37 @@
"""Keycloak SSO Login Helper
"""
from typing import Optional
import httpx
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
class KeycloakSSO(SSOBase):
"""Class providing login via Keycloak OAuth"""
provider = "keycloak"
scope = ["openid", "email", "profile"]
discovery_url = ""
async def openid_from_response(
self, response: dict, session: Optional["httpx.AsyncClient"] = None
) -> OpenID:
"""Return OpenID from user information provided by Keycloak"""
return OpenID(
email=response.get("email", ""),
provider=self.provider,
id=response.get("sub"),
first_name=response.get("given_name"),
last_name=response.get("family_name"),
display_name=response.get("name"),
picture=response.get("picture"),
)
async def get_discovery_document(self) -> DiscoveryDocument:
"""Get document containing handy urls"""
async with httpx.AsyncClient() as session:
response = await session.get(self.discovery_url)
content = response.json()
return content
@@ -85,7 +85,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="isSuperUser"> <div v-if="isSuperUser" id="funding-sources">
<lnbits-funding-sources <lnbits-funding-sources
:form-data="formData" :form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources" :allowed-funding-sources="settings.lnbits_allowed_funding_sources"
@@ -78,6 +78,41 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('keycloak-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">Keycloak Auth</strong>
<div class="row">
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_discovery_url"
label="Keycloak Discovey URL"
>
</q-input>
</div>
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_client_id"
label="Keycloak Client ID"
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token"
>
</q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_secret"
type="password"
label="Keycloak Client Secret"
>
</q-input>
</div>
</div>
</q-card-section>
<q-separator></q-separator> <q-separator></q-separator>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<br /> <br />
+6
View File
@@ -165,4 +165,10 @@
</q-dialog> </q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script> <script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
<style>
.introjs-tooltip-custom .introjs-tooltiptext,
.introjs-tooltip-header {
color: #111;
}
</style>
{% endblock %} {% endblock %}
+19
View File
@@ -263,6 +263,25 @@
<div><span v-text="$t('signin_with_github')"></span></div> <div><span v-text="$t('signin_with_github')"></span></div>
</q-btn> </q-btn>
</div> </div>
{%endif%} {% if "keycloak-auth" in LNBITS_AUTH_METHODS %}
<div class="col-12 full-width q-pa-sm">
<q-btn
href="/api/v1/auth/keycloak"
type="a"
outline
no-caps
color="grey"
rounded
class="full-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>
{%endif%} {%endif%}
</div> </div>
</q-card-section> </q-card-section>
+13 -2
View File
@@ -4,6 +4,12 @@
<!----> <!---->
{% block scripts %} {{ window_vars(user, wallet) }} {% block scripts %} {{ window_vars(user, wallet) }}
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script> <script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
<style>
.introjs-tooltip-custom .introjs-tooltiptext,
.introjs-tooltip-header {
color: #111;
}
</style>
{% endblock %} {% endblock %}
<!----> <!---->
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %} {% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
@@ -34,7 +40,7 @@
} : ''" } : ''"
> >
<q-card-section> <q-card-section>
<h3 class="q-my-none text-no-wrap"> <h3 class="q-my-none text-no-wrap" id="wallet-balance">
<strong v-text="formattedBalance"></strong> <strong v-text="formattedBalance"></strong>
<small>{{LNBITS_DENOMINATION}}</small> <small>{{LNBITS_DENOMINATION}}</small>
<lnbits-update-balance <lnbits-update-balance
@@ -68,7 +74,10 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md gt-sm"> <div
class="row q-pb-md q-px-md q-col-gutter-md gt-sm"
id="wallet-buttons"
>
<div class="col"> <div class="col">
<q-btn <q-btn
unelevated unelevated
@@ -165,6 +174,7 @@
:hide-header="mobileSimple" :hide-header="mobileSimple"
:hide-bottom="mobileSimple" :hide-bottom="mobileSimple"
@request="fetchPayments" @request="fetchPayments"
id="wallet-table"
> >
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
@@ -895,6 +905,7 @@
</q-dialog> </q-dialog>
<q-tabs <q-tabs
id="mobile-wallet-buttons"
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top" class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
active-class="px-0" active-class="px-0"
indicator-color="transparent" indicator-color="transparent"
+66 -86
View File
@@ -1,10 +1,9 @@
from typing import Optional import importlib
from typing import Callable, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse, RedirectResponse from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID from fastapi_sso.sso.base import OpenID, SSOBase
from fastapi_sso.sso.github import GithubSSO
from fastapi_sso.sso.google import GoogleSSO
from loguru import logger from loguru import logger
from starlette.status import ( from starlette.status import (
HTTP_400_BAD_REQUEST, HTTP_400_BAD_REQUEST,
@@ -94,72 +93,37 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.") raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.")
@auth_router.get("/api/v1/auth/google", description="Google SSO") @auth_router.get("/api/v1/auth/{provider}", description="SSO Provider")
async def login_with_google(request: Request, user_id: Optional[str] = None): async def login_with_sso_provider(
google_sso = _new_google_sso() request: Request, provider: str, user_id: Optional[str] = None
if not google_sso: ):
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'Google' not allowed.") provider_sso = _new_sso(provider)
if not provider_sso:
google_sso.redirect_uri = str(request.base_url) + "api/v1/auth/google/token"
with google_sso:
state = encrypt_internal_message(user_id)
return await google_sso.get_login_redirect(state=state)
@auth_router.get("/api/v1/auth/github", description="Github SSO")
async def login_with_github(request: Request, user_id: Optional[str] = None):
github_sso = _new_github_sso()
if not github_sso:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'GitHub' not allowed.")
github_sso.redirect_uri = str(request.base_url) + "api/v1/auth/github/token"
with github_sso:
state = decrypt_internal_message(user_id)
return await github_sso.get_login_redirect(state=state)
@auth_router.get(
"/api/v1/auth/google/token", description="Handle Google OAuth callback"
)
async def handle_google_token(request: Request) -> RedirectResponse:
google_sso = _new_google_sso()
if not google_sso:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'Google' not allowed.")
try:
with google_sso:
userinfo = await google_sso.verify_and_process(request)
assert userinfo is not None
user_id = decrypt_internal_message(google_sso.state)
request.session.pop("user", None)
return await _handle_sso_login(userinfo, user_id)
except HTTPException as e:
raise e
except ValueError as e:
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
except Exception as e:
logger.debug(e)
raise HTTPException( raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot authenticate user with Google Auth." HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed."
) )
provider_sso.redirect_uri = str(request.base_url) + f"api/v1/auth/{provider}/token"
with provider_sso:
state = encrypt_internal_message(user_id)
return await provider_sso.get_login_redirect(state=state)
@auth_router.get(
"/api/v1/auth/github/token", description="Handle Github OAuth callback" @auth_router.get("/api/v1/auth/{provider}/token", description="Handle OAuth callback")
) async def handle_oauth_token(request: Request, provider: str) -> RedirectResponse:
async def handle_github_token(request: Request) -> RedirectResponse: provider_sso = _new_sso(provider)
github_sso = _new_github_sso() if not provider_sso:
if not github_sso: raise HTTPException(
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'GitHub' not allowed.") HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed."
)
try: try:
with github_sso: with provider_sso:
userinfo = await github_sso.verify_and_process(request) userinfo = await provider_sso.verify_and_process(request)
assert userinfo is not None assert userinfo is not None
user_id = decrypt_internal_message(github_sso.state) user_id = decrypt_internal_message(provider_sso.state)
request.session.pop("user", None) request.session.pop("user", None)
return await _handle_sso_login(userinfo, user_id) return await _handle_sso_login(userinfo, user_id)
except HTTPException as e: except HTTPException as e:
raise e raise e
except ValueError as e: except ValueError as e:
@@ -167,7 +131,8 @@ async def handle_github_token(request: Request) -> RedirectResponse:
except Exception as e: except Exception as e:
logger.debug(e) logger.debug(e)
raise HTTPException( raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot authenticate user with GitHub Auth." HTTP_500_INTERNAL_SERVER_ERROR,
f"Cannot authenticate user with {provider} Auth.",
) )
@@ -340,29 +305,44 @@ def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
return response return response
def _new_google_sso() -> Optional[GoogleSSO]: def _new_sso(provider: str) -> Optional[SSOBase]:
if not settings.is_auth_method_allowed(AuthMethods.google_auth): try:
return None if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
if not settings.is_google_auth_configured: return None
logger.warning("Google Auth allowed but not configured.")
return None client_id = getattr(settings, f"{provider}_client_id", None)
return GoogleSSO( client_secret = getattr(settings, f"{provider}_client_secret", None)
settings.google_client_id, discovery_url = getattr(settings, f"{provider}_discovery_url", None)
settings.google_client_secret,
None, if not client_id or not client_secret:
allow_insecure_http=True, logger.warning(f"{provider} auth allowed but not configured.")
) return None
SSOProviderClass = _find_auth_provider_class(provider)
ssoProvider = SSOProviderClass(
client_id, client_secret, None, allow_insecure_http=True
)
if (
discovery_url
and getattr(ssoProvider, "discovery_url", discovery_url) != discovery_url
):
ssoProvider.discovery_url = discovery_url
return ssoProvider
except Exception as e:
logger.warning(e)
return None
def _new_github_sso() -> Optional[GithubSSO]: def _find_auth_provider_class(provider: str) -> Callable:
if not settings.is_auth_method_allowed(AuthMethods.github_auth): sso_modules = ["lnbits.core.sso", "fastapi_sso.sso"]
return None for module in sso_modules:
if not settings.is_github_auth_configured: try:
logger.warning("Github Auth allowed but not configured.") provider_module = importlib.import_module(f"{module}.{provider}")
return None ProviderClass = getattr(provider_module, f"{provider.title()}SSO")
return GithubSSO( if ProviderClass:
settings.github_client_id, return ProviderClass
settings.github_client_secret, except Exception:
None, pass
allow_insecure_http=True,
) raise ValueError(f"No SSO provider found for '{provider}'.")
+3 -4
View File
@@ -95,11 +95,10 @@ async def extensions_install(
installed_exts: List["InstallableExtension"] = await get_installed_extensions() installed_exts: List["InstallableExtension"] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts] installed_exts_ids = [e.id for e in installed_exts]
installable_exts: List[ installable_exts = await InstallableExtension.get_installable_extensions()
InstallableExtension installable_exts_ids = [e.id for e in installable_exts]
] = await InstallableExtension.get_installable_extensions()
installable_exts += [ installable_exts += [
e for e in installed_exts if e.id not in installed_exts_ids e for e in installed_exts if e.id not in installable_exts_ids
] ]
for e in installable_exts: for e in installable_exts:
+1 -1
View File
@@ -369,7 +369,7 @@ class LndRestNode(Node):
memo=invoice["memo"], memo=invoice["memo"],
pending=invoice["state"] == "OPEN", pending=invoice["state"] == "OPEN",
paid_at=invoice["settle_date"], paid_at=invoice["settle_date"],
expiry=invoice["creation_date"] + invoice["expiry"], expiry=int(invoice["creation_date"]) + int(invoice["expiry"]),
preimage=_decode_bytes(invoice["r_preimage"]), preimage=_decode_bytes(invoice["r_preimage"]),
bolt11=invoice["payment_request"], bolt11=invoice["payment_request"],
) )
+7 -7
View File
@@ -262,6 +262,7 @@ class AuthMethods(Enum):
username_and_password = "username-password" username_and_password = "username-password"
google_auth = "google-auth" google_auth = "google-auth"
github_auth = "github-auth" github_auth = "github-auth"
keycloak_auth = "keycloak-auth"
class AuthSettings(LNbitsSettings): class AuthSettings(LNbitsSettings):
@@ -282,18 +283,16 @@ class GoogleAuthSettings(LNbitsSettings):
google_client_id: str = Field(default="") google_client_id: str = Field(default="")
google_client_secret: str = Field(default="") google_client_secret: str = Field(default="")
@property
def is_google_auth_configured(self):
return self.google_client_id != "" and self.google_client_secret != ""
class GitHubAuthSettings(LNbitsSettings): class GitHubAuthSettings(LNbitsSettings):
github_client_id: str = Field(default="") github_client_id: str = Field(default="")
github_client_secret: str = Field(default="") github_client_secret: str = Field(default="")
@property
def is_github_auth_configured(self): class KeycloakAuthSettings(LNbitsSettings):
return self.github_client_id != "" and self.github_client_secret != "" keycloak_discovery_url: str = Field(default="")
keycloak_client_id: str = Field(default="")
keycloak_client_secret: str = Field(default="")
class EditableSettings( class EditableSettings(
@@ -309,6 +308,7 @@ class EditableSettings(
AuthSettings, AuthSettings,
GoogleAuthSettings, GoogleAuthSettings,
GitHubAuthSettings, GitHubAuthSettings,
KeycloakAuthSettings,
): ):
@validator( @validator(
"lnbits_admin_users", "lnbits_admin_users",
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -209,6 +209,7 @@ window.localisation.en = {
account_settings: 'Account Settings', account_settings: 'Account Settings',
signin_with_google: 'Sign in with Google', signin_with_google: 'Sign in with Google',
signin_with_github: 'Sign in with GitHub', signin_with_github: 'Sign in with GitHub',
signin_with_keycloak: 'Sign in with Keycloak',
username_or_email: 'Username or Email', username_or_email: 'Username or Email',
password: 'Password', password: 'Password',
password_config: 'Password Config', password_config: 'Password Config',
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+82 -1
View File
@@ -48,7 +48,8 @@ new Vue({
show: false show: false
}, },
tab: 'funding', tab: 'funding',
needsRestart: false needsRestart: false,
introJs: null
} }
}, },
created() { created() {
@@ -56,6 +57,32 @@ new Vue({
this.getAudit() this.getAudit()
this.balance = +'{{ balance|safe }}' this.balance = +'{{ balance|safe }}'
}, },
mounted() {
const url = window.location.href
const queryString = url.split('?')[1]
const queryObject = {}
if (queryString) {
for (const param of queryString.split('&')) {
const [key, value] = param.split('=')
queryObject[key] = value
}
}
if (queryObject.hasOwnProperty('first_use')) {
const scriptTag = document.createElement('script')
scriptTag.src = 'https://unpkg.com/intro.js/intro.js'
const linkTag = document.createElement('link')
linkTag.rel = 'stylesheet'
linkTag.href = 'https://unpkg.com/intro.js/introjs.css'
document.body.appendChild(scriptTag)
document.head.appendChild(linkTag)
scriptTag.onload = () => {
this.runOnboarding()
}
}
},
computed: { computed: {
lnbitsVersion() { lnbitsVersion() {
return LNBITS_VERSION return LNBITS_VERSION
@@ -68,6 +95,60 @@ new Vue({
} }
}, },
methods: { methods: {
runOnboarding() {
introJs()
.onbeforeexit(() => {
return window.history.replaceState(
null,
null,
window.location.pathname
)
})
.setOptions({
disableInteraction: true,
tooltipClass: 'introjs-tooltip-custom',
dontShowAgain: true,
steps: [
{
title: 'Welcome to LNbits',
intro: 'Start your tour!'
},
{
title: 'Add a funding source',
element: document.getElementById('funding-sources'),
intro:
'Select a Lightning Network funding source to add funds to your LNbits.'
},
{
title: 'Access your wallet',
element: document.getElementById('wallet-list'),
position: 'right',
intro:
'Wallets are the core of LNbits. They are the place where you can store your funds.'
},
{
title: 'Manage LNbits',
element: document.getElementById('admin-manage'),
position: 'right',
intro:
'This is the place where you can manage your LNbits. You can configure settings, install extensions, themes, view logs, manage a node, and more.'
},
{
title: 'Access extensions',
element: document.getElementById('extension-list'),
position: 'right',
intro:
'Extensions are the way to add functionality to your LNbits. You view and access them here.'
},
{
title: 'Farewell!',
intro: '<img src="static/images/logos/lnbits.svg" width=100%/>'
}
]
})
.start()
},
addAdminUser() { addAdminUser() {
let addUser = this.formAddAdmin let addUser = this.formAddAdmin
let admin_users = this.formData.lnbits_admin_users let admin_users = this.formData.lnbits_admin_users
+1 -1
View File
@@ -50,7 +50,7 @@ new Vue({
this.password, this.password,
this.passwordRepeat this.passwordRepeat
) )
window.location.href = '/wallet' window.location.href = '/wallet?first_use'
} catch (e) { } catch (e) {
LNbits.utils.notifyApiError(e) LNbits.utils.notifyApiError(e)
} }
+1 -1
View File
@@ -1,6 +1,6 @@
// update cache version every time there is a new deployment // update cache version every time there is a new deployment
// so the service worker reinitializes the cache // so the service worker reinitializes the cache
const CACHE_VERSION = 114 const CACHE_VERSION = 115
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-` const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => { const getApiKey = request => {
+100
View File
@@ -818,6 +818,105 @@ new Vue({
navigator.clipboard.readText().then(text => { navigator.clipboard.readText().then(text => {
this.$refs.textArea.value = text this.$refs.textArea.value = text
}) })
},
checkFirstUse() {
const url = window.location.href
const queryString = url.split('?')[1]
const queryObject = {}
if (queryString) {
for (const param of queryString.split('&')) {
const [key, value] = param.split('=')
queryObject[key] = value
}
}
if (queryObject.hasOwnProperty('first_use')) {
const scriptTag = document.createElement('script')
scriptTag.src = 'https://unpkg.com/intro.js/intro.js'
const linkTag = document.createElement('link')
linkTag.rel = 'stylesheet'
linkTag.href = 'https://unpkg.com/intro.js/introjs.css'
document.body.appendChild(scriptTag)
document.head.appendChild(linkTag)
scriptTag.onload = () => {
if (!this.g.visibleDrawer) {
this.g.visibleDrawer = true
}
this.runOnboarding()
}
}
},
runOnboarding() {
introJs()
.onbeforechange(step => {
if (!step.id || !this.mobileSimple) return
if (step.id === 'wallet-list' || step.id === 'admin-manage') {
this.g.visibleDrawer = true
} else {
this.g.visibleDrawer = false
}
})
// .onbeforeexit(() => {
// return window.history.replaceState(
// null,
// null,
// window.location.pathname
// )
// })
.setOptions({
disableInteraction: true,
tooltipClass: 'introjs-tooltip-custom',
dontShowAgain: true,
steps: [
{
title: 'Welcome to LNbits',
intro: 'Start your tour!'
},
{
title: 'Access your wallet',
element: document.getElementById('wallet-list'),
position: 'right',
intro:
'Wallets are the core of LNbits. They are the place where you can store your funds.'
},
{
title: 'Access extensions',
element: document.getElementById('admin-manage'),
position: 'right',
intro:
'Extensions are the way to add functionality to your LNbits. You view and access them here.'
},
{
title: 'Wallet balance',
element: document.getElementById('wallet-balance'),
position: 'right',
intro: 'Your wallet balance is displayed here.'
},
{
title: 'Send and receive payments',
element: document.getElementById(
this.mobileSimple ? 'mobile-wallet-buttons' : 'wallet-buttons'
),
position: 'bottom',
intro:
'Use these buttons to send and receive payments or scan a QR code.'
},
{
title: 'Transaction details',
element: document.getElementById('wallet-table'),
position: 'bottom',
intro:
'Here you can see all the transactions made in your wallet. You can also export them as a CSV file.'
},
{
title: 'Farewell!',
intro: '<img src="static/images/logos/lnbits.svg" width=100%/>'
}
]
})
.start()
} }
}, },
watch: { watch: {
@@ -865,6 +964,7 @@ new Vue({
this.onPaymentReceived(payment.payment_hash) this.onPaymentReceived(payment.payment_hash)
) )
eventReactionWebocket(wallet.id) eventReactionWebocket(wallet.id)
this.checkFirstUse()
} }
}) })
+6 -2
View File
@@ -168,13 +168,17 @@
show-if-above show-if-above
:elevated="$q.screen.lt.md" :elevated="$q.screen.lt.md"
> >
<lnbits-wallet-list></lnbits-wallet-list> <lnbits-wallet-list id="wallet-list"></lnbits-wallet-list>
<lnbits-manage <lnbits-manage
id="admin-manage"
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'" :show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
:show-node="'{{LNBITS_NODE_UI}}' == 'True'" :show-node="'{{LNBITS_NODE_UI}}' == 'True'"
></lnbits-manage> ></lnbits-manage>
<lnbits-extension-list class="q-pb-xl"></lnbits-extension-list> <lnbits-extension-list
id="extension-list"
class="q-pb-xl"
></lnbits-extension-list>
</q-drawer> </q-drawer>
{% endblock %} {% block page_container %} {% endblock %} {% block page_container %}
<q-page-container> <q-page-container>