This commit is contained in:
dni ⚡ 2024-10-01 09:51:54 +02:00 committed by Vlad Stan
parent 3d5730492f
commit 5f32975a56
5 changed files with 83 additions and 86 deletions

View File

@ -38,9 +38,9 @@
</div>
<div class="col">
<q-img
v-if="user.config.picture"
v-if="user.extra.picture"
style="max-width: 100px"
:src="user.config.picture"
:src="user.extra.picture"
class="float-right"
></q-img>
</div>
@ -133,9 +133,9 @@
<div class="row">
<div class="col">
<q-img
v-if="user.config.picture"
v-if="user.extra.picture"
style="max-width: 100px"
:src="user.config.picture"
:src="user.extra.picture"
class="float-right"
></q-img>
</div>
@ -236,9 +236,9 @@
</div>
</q-card-section>
<q-card-section v-if="user.config">
<q-card-section v-if="user.extra">
<q-input
v-model="user.config.first_name"
v-model="user.extra.first_name"
:label="$t('first_name')"
filled
dense
@ -246,7 +246,7 @@
>
</q-input>
<q-input
v-model="user.config.last_name"
v-model="user.extra.last_name"
:label="$t('last_name')"
filled
dense
@ -254,7 +254,7 @@
>
</q-input>
<q-input
v-model="user.config.provider"
v-model="user.extra.provider"
:label="$t('auth_provider')"
filled
dense
@ -263,7 +263,7 @@
>
</q-input>
<q-input
v-model="user.config.picture"
v-model="user.extra.picture"
:label="$t('picture')"
filled
class="q-mb-md"

View File

@ -76,26 +76,16 @@ async def nostr_login(request: Request) -> JSONResponse:
raise HTTPException(
HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed."
)
try:
event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"])
if not account:
account = Account(
id=uuid4().hex,
pubkey=event["pubkey"],
extra=UserExtra(provider="nostr"),
)
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
event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"])
if not account:
account = Account(
id=uuid4().hex,
pubkey=event["pubkey"],
extra=UserExtra(provider="nostr"),
)
await create_account(account)
return _auth_success_response(account.username or "", account.id, account.email)
@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.",
)
try:
with provider_sso:
userinfo = await provider_sso.verify_and_process(request)
assert userinfo is not None
user_id = decrypt_internal_message(provider_sso.state)
request.session.pop("user", None)
return await _handle_sso_login(userinfo, user_id)
except HTTPException as exc:
raise exc
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
with provider_sso:
userinfo = await provider_sso.verify_and_process(request)
if not userinfo:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid user info."
)
user_id = decrypt_internal_message(provider_sso.state)
request.session.pop("user", None)
return await _handle_sso_login(userinfo, user_id)
@auth_router.post("/logout")
@ -191,6 +173,11 @@ async def register(data: CreateUser) -> JSONResponse:
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):
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid email.")
@ -212,17 +199,19 @@ async def update_pubkey(
) -> Optional[User]:
if data.user_id != 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)
if not account:
raise HTTPException(
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)
account.pubkey = normalize_public_key(data.pubkey)
await update_account(account)
@ -301,16 +290,19 @@ async def update(
status_code=HTTPStatus.BAD_REQUEST,
detail="Email mismatch.",
)
account = await get_account(user.id)
if not account:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Account not found."
)
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(
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(
status_code=HTTPStatus.BAD_REQUEST, detail="Email already exists."
)
@ -462,37 +454,43 @@ def _find_auth_provider_class(provider: str) -> Callable:
def _nostr_nip98_event(request: Request) -> dict:
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()
assert scheme.lower() == "nostr", "Authorization header is not nostr."
if scheme.lower() != "nostr":
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.")
event = None
try:
event_json = base64.b64decode(token.encode("ascii"))
event = json.loads(event_json)
except Exception as exc:
logger.warning(exc)
assert event, "Nostr login event cannot be parsed."
assert verify_event(event), "Nostr login event is not valid."
assert event["kind"] == 27_235, "Invalid event kind."
if not event:
raise HTTPException(
HTTPStatus.BAD_REQUEST, "Nostr login event cannot be parsed."
)
if not verify_event(event):
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
assert (
abs(time() - event["created_at"]) < auth_threshold
), f"More than {auth_threshold} seconds have passed since the event was signed."
if abs(time() - event["created_at"]) > auth_threshold:
raise HTTPException(
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)
assert method, "Tag 'method' is missing."
assert method.upper() == "POST", "Incorrect value for tag 'method'."
if not 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)
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]
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

View File

@ -1,6 +1,6 @@
import json
import re
from datetime import datetime, timedelta
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any, Optional, Type
@ -184,7 +184,7 @@ def is_valid_username(username: str) -> bool:
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.update({"exp": expire})
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")

View File

@ -92,7 +92,7 @@ window.app = Vue.createApp({
user_id: this.user.id,
username: this.user.username,
email: this.user.email,
config: this.user.config
extra: this.user.extra
}
)
this.user = data
@ -183,7 +183,7 @@ window.app = Vue.createApp({
const {data} = await LNbits.api.getAuthenticatedUser()
this.user = data
this.hasUsername = !!data.username
if (!this.user.config) this.user.config = {}
if (!this.user.extra) this.user.extra = {}
} catch (e) {
LNbits.utils.notifyApiError(e)
}

View File

@ -1,16 +1,16 @@
# ruff: noqa: E402
import asyncio
from time import time
import uvloop
from asgi_lifespan import LifespanManager
uvloop.install()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
from time import time
from uuid import uuid4
import pytest
import pytest_asyncio
from asgi_lifespan import LifespanManager
from fastapi.testclient import TestClient
from httpx import ASGITransport, AsyncClient
@ -92,7 +92,6 @@ async def user_alan():
username="alan",
)
account.hash_password("secret1234")
await create_account(account)
yield account