施工(HEL-226): 实现登录门户与本机免密切换账号
用设备 Cookie 和授权表记住本机已验证账号,登录页按确认样图做成门户,不再把密码写进浏览器。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
8a5e78f022
commit
f0a1adf52f
@@ -1,49 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import secrets
|
||||
from http import HTTPStatus
|
||||
|
||||
from backend.features.accounts.security import token_hash
|
||||
|
||||
|
||||
class AccountHttpMixin:
|
||||
def _device_hash(self) -> str:
|
||||
raw = self.device_token()
|
||||
return token_hash(raw) if raw else ""
|
||||
|
||||
def _ensure_device_token(self) -> str:
|
||||
return self.device_token() or secrets.token_urlsafe(32)
|
||||
|
||||
def _auth_success_headers(self, session_token: str, device_raw: str) -> list[tuple[str, str]]:
|
||||
return [
|
||||
("Set-Cookie", self.session_cookie(session_token)),
|
||||
("Set-Cookie", self.device_cookie(device_raw)),
|
||||
]
|
||||
|
||||
def _send_authenticated_session(self, result: dict, status: HTTPStatus, device_raw: str) -> None:
|
||||
self.send_json(
|
||||
{
|
||||
"ok": True,
|
||||
"authenticated": True,
|
||||
"user": result["user"],
|
||||
"csrf_token": result["csrf_token"],
|
||||
},
|
||||
status,
|
||||
self._auth_success_headers(result["session_token"], device_raw),
|
||||
)
|
||||
|
||||
def auth_register(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
device_raw = self._ensure_device_token()
|
||||
result = self.application_service.register_account(
|
||||
str(body.get("username") or ""),
|
||||
str(body.get("password") or ""),
|
||||
token_hash(device_raw),
|
||||
)
|
||||
self.send_json(
|
||||
{
|
||||
"ok": True,
|
||||
"authenticated": True,
|
||||
"user": result["user"],
|
||||
"csrf_token": result["csrf_token"],
|
||||
},
|
||||
HTTPStatus.CREATED,
|
||||
{"Set-Cookie": self.session_cookie(result["session_token"])},
|
||||
)
|
||||
self._send_authenticated_session(result, HTTPStatus.CREATED, device_raw)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
def auth_login(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
device_raw = self._ensure_device_token()
|
||||
result = self.application_service.login_account(
|
||||
str(body.get("username") or ""),
|
||||
str(body.get("password") or ""),
|
||||
token_hash(device_raw),
|
||||
)
|
||||
self.send_json(
|
||||
{
|
||||
"ok": True,
|
||||
"authenticated": True,
|
||||
"user": result["user"],
|
||||
"csrf_token": result["csrf_token"],
|
||||
},
|
||||
headers={"Set-Cookie": self.session_cookie(result["session_token"])},
|
||||
)
|
||||
self._send_authenticated_session(result, HTTPStatus.OK, device_raw)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.UNAUTHORIZED)
|
||||
|
||||
def auth_accounts(self) -> None:
|
||||
current_user_id = None
|
||||
if self.require_auth(send_error=False):
|
||||
current_user_id = int(self.auth_user["id"])
|
||||
payload = self.application_service.accounts.list_device_accounts(
|
||||
self._device_hash(),
|
||||
current_user_id,
|
||||
)
|
||||
self.send_json({"ok": True, **payload})
|
||||
|
||||
def auth_switch(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
try:
|
||||
user_id = int(body.get("user_id"))
|
||||
except (TypeError, ValueError):
|
||||
user_id = 0
|
||||
device_raw = self.device_token()
|
||||
result = self.application_service.accounts.switch_account(
|
||||
token_hash(device_raw) if device_raw else "",
|
||||
user_id,
|
||||
)
|
||||
self._send_authenticated_session(
|
||||
result,
|
||||
HTTPStatus.OK,
|
||||
device_raw or self._ensure_device_token(),
|
||||
)
|
||||
except PermissionError as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.UNAUTHORIZED)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
def auth_forget(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
try:
|
||||
user_id = int(body.get("user_id"))
|
||||
except (TypeError, ValueError):
|
||||
user_id = 0
|
||||
self.application_service.accounts.forget_account(self._device_hash(), user_id)
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
pass
|
||||
self.send_json({"ok": True})
|
||||
|
||||
def auth_me(self) -> None:
|
||||
service = self.application_service
|
||||
if not self.require_auth(send_error=False):
|
||||
@@ -55,6 +114,15 @@ class AccountHttpMixin:
|
||||
}
|
||||
)
|
||||
return
|
||||
headers = None
|
||||
if not self.device_token():
|
||||
device_raw = secrets.token_urlsafe(32)
|
||||
service.accounts.remember_account(
|
||||
token_hash(device_raw),
|
||||
int(self.auth_user["id"]),
|
||||
fresh=True,
|
||||
)
|
||||
headers = [("Set-Cookie", self.device_cookie(device_raw))]
|
||||
self.send_json(
|
||||
{
|
||||
"ok": True,
|
||||
@@ -66,15 +134,19 @@ class AccountHttpMixin:
|
||||
"membership": service.membership(),
|
||||
},
|
||||
"csrf_token": str(self.auth_user["csrf_token"]),
|
||||
}
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
def auth_logout(self) -> None:
|
||||
raw_token = self.session_token()
|
||||
user_id = int(getattr(self, "auth_user", {}).get("id") or 0)
|
||||
if raw_token:
|
||||
from backend.features.accounts.security import token_hash
|
||||
|
||||
self.application_service.database.delete_session(token_hash(raw_token))
|
||||
self.application_service.accounts.revoke_current_device_grant(
|
||||
self._device_hash(),
|
||||
user_id,
|
||||
)
|
||||
self.send_json(
|
||||
{"ok": True},
|
||||
headers={"Set-Cookie": self.session_cookie("", clear=True)},
|
||||
|
||||
Reference in New Issue
Block a user