94 lines
2.8 KiB
Python
94 lines
2.8 KiB
Python
|
|
"""
|
|||
|
|
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()
|