new: dev: initialize the GTK client #11
This commit is contained in:
93
gtk-client/mineseeker/api/auth.py
Normal file
93
gtk-client/mineseeker/api/auth.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
This file is part of the SplendidBear Websites' projects.
|
||||
|
||||
Copyright (c) 2026 @ www.splendidbear.org
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mineseeker.api import client
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
"""Raised on authentication failure."""
|
||||
|
||||
|
||||
class TotpRequired(Exception):
|
||||
"""Raised when the server requires a TOTP code after password login."""
|
||||
|
||||
|
||||
def login(username: str, password: str) -> None:
|
||||
"""
|
||||
Authenticate with username + password via the dedicated JSON endpoint
|
||||
POST /api/auth/login, which bypasses the reCAPTCHA gate.
|
||||
|
||||
Raises:
|
||||
TotpRequired – server confirmed credentials but TOTP is required next.
|
||||
AuthError – credentials wrong, account inactive, or server error.
|
||||
"""
|
||||
session = client.get_session()
|
||||
resp = session.post(
|
||||
client.url("/api/auth/login"),
|
||||
json={"username": username, "password": password},
|
||||
# The endpoint sets a session cookie; follow any redirects
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
# Non-2xx means a hard server error (500 etc.) — let it propagate
|
||||
if resp.status_code >= 500:
|
||||
resp.raise_for_status()
|
||||
|
||||
data = resp.json()
|
||||
|
||||
if not data.get("success"):
|
||||
raise AuthError(data.get("error", "Login failed."))
|
||||
|
||||
if data.get("requiresTwoFactor"):
|
||||
raise TotpRequired()
|
||||
|
||||
|
||||
def submit_totp(code: str) -> None:
|
||||
"""
|
||||
Submit the 6-digit TOTP code after login() raises TotpRequired.
|
||||
|
||||
The scheb/2fa bundle processes POST /2fa_check directly as a firewall
|
||||
listener — no CSRF token required, no JSON body. The code goes as a
|
||||
form-encoded field named _auth_code, same as the browser form.
|
||||
The LoginCaptchaListener already skips /2fa_check paths.
|
||||
|
||||
Raises:
|
||||
AuthError – if the code is wrong or the session is no longer in
|
||||
IS_AUTHENTICATED_2FA_IN_PROGRESS state.
|
||||
"""
|
||||
session = client.get_session()
|
||||
resp = session.post(
|
||||
client.url("/2fa_check"),
|
||||
data={"_auth_code": code},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
allow_redirects=True,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
# If we land back on /2fa the code was wrong
|
||||
if "/2fa" in resp.url:
|
||||
raise AuthError("Invalid authentication code.")
|
||||
|
||||
|
||||
def login_as_guest() -> None:
|
||||
"""
|
||||
Start an anonymous session.
|
||||
A GET to the homepage is enough for Symfony to create a session and
|
||||
assign the anon_<session_id> identity used by ResolveUserNamesService.
|
||||
"""
|
||||
session = client.get_session()
|
||||
resp = session.get(client.url("/"), headers={"Accept": "text/html"})
|
||||
resp.raise_for_status()
|
||||
|
||||
|
||||
def logout() -> None:
|
||||
"""Discard the local session (client-side only)."""
|
||||
client.reset_session()
|
||||
Reference in New Issue
Block a user