fixup!
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user