This commit is contained in:
dni ⚡
2024-10-17 10:37:32 +02:00
parent 1ed581d2ac
commit a58a698918
5 changed files with 88 additions and 87 deletions
+9 -9
View File
@@ -38,9 +38,9 @@
</div> </div>
<div class="col"> <div class="col">
<q-img <q-img
v-if="user.config.picture" v-if="user.extra.picture"
style="max-width: 100px" style="max-width: 100px"
:src="user.config.picture" :src="user.extra.picture"
class="float-right" class="float-right"
></q-img> ></q-img>
</div> </div>
@@ -133,9 +133,9 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<q-img <q-img
v-if="user.config.picture" v-if="user.extra.picture"
style="max-width: 100px" style="max-width: 100px"
:src="user.config.picture" :src="user.extra.picture"
class="float-right" class="float-right"
></q-img> ></q-img>
</div> </div>
@@ -236,9 +236,9 @@
</div> </div>
</q-card-section> </q-card-section>
<q-card-section v-if="user.config"> <q-card-section v-if="user.extra">
<q-input <q-input
v-model="user.config.first_name" v-model="user.extra.first_name"
:label="$t('first_name')" :label="$t('first_name')"
filled filled
dense dense
@@ -246,7 +246,7 @@
> >
</q-input> </q-input>
<q-input <q-input
v-model="user.config.last_name" v-model="user.extra.last_name"
:label="$t('last_name')" :label="$t('last_name')"
filled filled
dense dense
@@ -254,7 +254,7 @@
> >
</q-input> </q-input>
<q-input <q-input
v-model="user.config.provider" v-model="user.extra.provider"
:label="$t('auth_provider')" :label="$t('auth_provider')"
filled filled
dense dense
@@ -263,7 +263,7 @@
> >
</q-input> </q-input>
<q-input <q-input
v-model="user.config.picture" v-model="user.extra.picture"
:label="$t('picture')" :label="$t('picture')"
filled filled
class="q-mb-md" class="q-mb-md"
+72 -70
View File
@@ -76,26 +76,16 @@ async def nostr_login(request: Request) -> JSONResponse:
raise HTTPException( raise HTTPException(
HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed." HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed."
) )
event = _nostr_nip98_event(request)
try: account = await get_account_by_pubkey(event["pubkey"])
event = _nostr_nip98_event(request) if not account:
account = await get_account_by_pubkey(event["pubkey"]) account = Account(
if not account: id=uuid4().hex,
account = Account( pubkey=event["pubkey"],
id=uuid4().hex, extra=UserExtra(provider="nostr"),
pubkey=event["pubkey"], )
extra=UserExtra(provider="nostr"), await create_account(account)
) return _auth_success_response(account.username or "", account.id, account.email)
await create_account(account)
return _auth_success_response(account.username or "", account.id, account.email)
except HTTPException as exc:
raise exc
except AssertionError as exc:
raise HTTPException(HTTPStatus.UNAUTHORIZED, str(exc)) from exc
except Exception as exc:
logger.warning(exc)
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot login.") from exc
@auth_router.post("/usr", description="Login via the User ID") @auth_router.post("/usr", description="Login via the User ID")
@@ -139,23 +129,15 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons
detail=f"Login by '{provider}' not allowed.", detail=f"Login by '{provider}' not allowed.",
) )
try: with provider_sso:
with provider_sso: userinfo = await provider_sso.verify_and_process(request)
userinfo = await provider_sso.verify_and_process(request) if not userinfo:
assert userinfo is not None raise HTTPException(
user_id = decrypt_internal_message(provider_sso.state) status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid user info."
request.session.pop("user", None) )
return await _handle_sso_login(userinfo, user_id) user_id = decrypt_internal_message(provider_sso.state)
except HTTPException as exc: request.session.pop("user", None)
raise exc return await _handle_sso_login(userinfo, user_id)
except ValueError as exc:
raise HTTPException(HTTPStatus.FORBIDDEN, str(exc)) from exc
except Exception as exc:
logger.debug(exc)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Cannot authenticate user with {provider} Auth.",
) from exc
@auth_router.post("/logout") @auth_router.post("/logout")
@@ -191,6 +173,11 @@ async def register(data: CreateUser) -> JSONResponse:
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid username." status_code=HTTPStatus.BAD_REQUEST, detail="Invalid username."
) )
if await get_account_by_username(data.username):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists."
)
if data.email and not is_valid_email_address(data.email): if data.email and not is_valid_email_address(data.email):
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid email.") raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid email.")
@@ -212,17 +199,19 @@ async def update_pubkey(
) -> Optional[User]: ) -> Optional[User]:
if data.user_id != user.id: if data.user_id != user.id:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
if (
data.pubkey
and data.pubkey != user.pubkey
and await get_account_by_pubkey(data.pubkey)
):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Public key already in use."
)
account = await get_account(user.id) account = await get_account(user.id)
if not account: if not account:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Account not found." status_code=HTTPStatus.NOT_FOUND, detail="Account not found."
) )
account_existing = await get_account_by_pubkey(data.pubkey)
if account_existing and account_existing.id != account.id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Public key already in use."
)
_validate_auth_timeout(payload.auth_time) _validate_auth_timeout(payload.auth_time)
account.pubkey = normalize_public_key(data.pubkey) account.pubkey = normalize_public_key(data.pubkey)
await update_account(account) await update_account(account)
@@ -239,7 +228,11 @@ async def update_password(
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user ID." status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user ID."
) )
if data.username and await get_account_by_username(data.username): if (
data.username
and user.username != data.username
and await get_account_by_username(data.username)
):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists." status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists."
) )
@@ -324,16 +317,19 @@ async def update(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail="Email mismatch.", detail="Email mismatch.",
) )
account = await get_account(user.id) if (
if not account: data.username
raise HTTPException( and user.username != data.username
status_code=HTTPStatus.NOT_FOUND, detail="Account not found." and await get_account_by_username(data.username)
) ):
if data.username and await get_account_by_username(data.username):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists." status_code=HTTPStatus.BAD_REQUEST, detail="Username already exists."
) )
if data.email and await get_account_by_email(data.email): if (
data.email
and data.email != user.email
and await get_account_by_email(data.email)
):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Email already exists." status_code=HTTPStatus.BAD_REQUEST, detail="Email already exists."
) )
@@ -473,37 +469,43 @@ def _find_auth_provider_class(provider: str) -> Callable:
def _nostr_nip98_event(request: Request) -> dict: def _nostr_nip98_event(request: Request) -> dict:
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
assert auth_header, "Nostr Auth header missing." if not auth_header:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Nostr Auth header missing.")
scheme, token = auth_header.split() scheme, token = auth_header.split()
assert scheme.lower() == "nostr", "Authorization header is not nostr." if scheme.lower() != "nostr":
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.")
event = None event = None
try: try:
event_json = base64.b64decode(token.encode("ascii")) event_json = base64.b64decode(token.encode("ascii"))
event = json.loads(event_json) event = json.loads(event_json)
except Exception as exc: except Exception as exc:
logger.warning(exc) logger.warning(exc)
if not event:
assert event, "Nostr login event cannot be parsed." raise HTTPException(
HTTPStatus.BAD_REQUEST, "Nostr login event cannot be parsed."
assert verify_event(event), "Nostr login event is not valid." )
if not verify_event(event):
assert event["kind"] == 27_235, "Invalid event kind." raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr login event is not valid.")
if event["kind"] != 27_235:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid event kind.")
auth_threshold = settings.auth_credetials_update_threshold auth_threshold = settings.auth_credetials_update_threshold
assert ( if abs(time() - event["created_at"]) > auth_threshold:
abs(time() - event["created_at"]) < auth_threshold raise HTTPException(
), f"More than {auth_threshold} seconds have passed since the event was signed." HTTPStatus.BAD_REQUEST,
f"{auth_threshold} seconds have passed since the event was signed.",
)
method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None) method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None)
assert method, "Tag 'method' is missing." if not method:
assert method.upper() == "POST", "Incorrect value for tag 'method'." raise HTTPException(HTTPStatus.BAD_REQUEST, "Tag 'method' is missing.")
if method.upper() != "POST":
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid value for tag 'method'.")
url = next((v for k, v in event["tags"] if k == "u"), None) url = next((v for k, v in event["tags"] if k == "u"), None)
assert url, "Tag 'u' for URL is missing." if not url:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Tag 'u' for URL is missing.")
accepted_urls = [f"{u}/nostr" for u in settings.nostr_absolute_request_urls] accepted_urls = [f"{u}/nostr" for u in settings.nostr_absolute_request_urls]
assert url in accepted_urls, f"Incorrect value for tag 'u': '{url}'." if url not in accepted_urls:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid value for tag 'u'.")
return event return event
+2 -2
View File
@@ -1,6 +1,6 @@
import json import json
import re import re
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Type from typing import Any, Optional, Type
@@ -184,7 +184,7 @@ def is_valid_username(username: str) -> bool:
def create_access_token(data: dict): def create_access_token(data: dict):
expire = datetime.utcnow() + timedelta(minutes=settings.auth_token_expire_minutes) expire = datetime.now(UTC) + timedelta(minutes=settings.auth_token_expire_minutes)
to_encode = data.copy() to_encode = data.copy()
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.auth_secret_key, "HS256") return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
+2 -2
View File
@@ -92,7 +92,7 @@ window.app = Vue.createApp({
user_id: this.user.id, user_id: this.user.id,
username: this.user.username, username: this.user.username,
email: this.user.email, email: this.user.email,
config: this.user.config extra: this.user.extra
} }
) )
this.user = data this.user = data
@@ -183,7 +183,7 @@ window.app = Vue.createApp({
const {data} = await LNbits.api.getAuthenticatedUser() const {data} = await LNbits.api.getAuthenticatedUser()
this.user = data this.user = data
this.hasUsername = !!data.username this.hasUsername = !!data.username
if (!this.user.config) this.user.config = {} if (!this.user.extra) this.user.extra = {}
} catch (e) { } catch (e) {
LNbits.utils.notifyApiError(e) LNbits.utils.notifyApiError(e)
} }
+3 -4
View File
@@ -1,18 +1,18 @@
# ruff: noqa: E402 # ruff: noqa: E402
import asyncio import asyncio
from time import time
import uvloop import uvloop
from asgi_lifespan import LifespanManager
from lnbits.wallets.fake import FakeWallet from lnbits.wallets.fake import FakeWallet
uvloop.install() asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
from time import time
from uuid import uuid4 from uuid import uuid4
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from asgi_lifespan import LifespanManager
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
@@ -112,7 +112,6 @@ async def user_alan():
username="alan", username="alan",
) )
account.hash_password("secret1234") account.hash_password("secret1234")
await create_account(account)
yield account yield account