Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53fee6896 | ||
|
|
f14dfd2522 | ||
|
|
526467747e | ||
|
|
b8d295a5b7 | ||
|
|
293b6267be | ||
|
|
20e70854a1 |
+11
-4
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}'.")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -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 |
@@ -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
|
||||||
|
|||||||
@@ -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,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 => {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user