施工(HEL-226): 实现登录门户与本机免密切换账号

用设备 Cookie 和授权表记住本机已验证账号,登录页按确认样图做成门户,不再把密码写进浏览器。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-08-29 11:24:39 +08:00
co-authored by Cursor multica-agent
parent 8a5e78f022
commit f0a1adf52f
29 changed files with 1583 additions and 89 deletions
+93
View File
@@ -234,3 +234,96 @@ class AccountRepositoryMixin:
(user_id,),
)
return cursor.rowcount > 0
def cleanup_expired_switch_grants(self, now: str) -> int:
with self.connect() as connection:
cursor = connection.execute(
"DELETE FROM account_switch_grants WHERE expires_at <= ?",
(now,),
)
return int(cursor.rowcount)
def list_switch_grants(self, device_hash: str, now: str) -> list[dict[str, Any]]:
with self.connect() as connection:
rows = connection.execute(
"""
SELECT u.id, u.username, u.role, u.llm_mode, u.membership_status,
u.membership_plan, u.membership_starts_at, u.membership_expires_at,
u.created_at, g.last_used_at, g.granted_at, g.expires_at
FROM account_switch_grants AS g
JOIN users AS u ON u.id = g.user_id
WHERE g.device_hash = ? AND g.expires_at > ?
ORDER BY g.last_used_at DESC, g.id DESC
""",
(device_hash, now),
).fetchall()
return [dict(row) for row in rows]
def get_switch_grant(self, device_hash: str, user_id: int) -> dict[str, Any] | None:
with self.connect() as connection:
row = connection.execute(
"""
SELECT device_hash, user_id, granted_at, last_used_at, expires_at
FROM account_switch_grants
WHERE device_hash = ? AND user_id = ?
""",
(device_hash, user_id),
).fetchone()
return dict(row) if row else None
def upsert_switch_grant(
self,
device_hash: str,
user_id: int,
granted_at: str,
last_used_at: str,
expires_at: str,
) -> None:
with self.connect() as connection:
connection.execute(
"""
INSERT INTO account_switch_grants
(device_hash, user_id, granted_at, last_used_at, expires_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(device_hash, user_id) DO UPDATE SET
granted_at = excluded.granted_at,
last_used_at = excluded.last_used_at,
expires_at = excluded.expires_at
""",
(device_hash, user_id, granted_at, last_used_at, expires_at),
)
def prune_switch_grants(self, device_hash: str, keep: int) -> int:
with self.connect() as connection:
rows = connection.execute(
"""
SELECT id FROM account_switch_grants
WHERE device_hash = ?
ORDER BY last_used_at DESC, id DESC
""",
(device_hash,),
).fetchall()
extra = [int(row["id"]) for row in rows[keep:]]
if not extra:
return 0
connection.execute(
f"DELETE FROM account_switch_grants WHERE id IN ({','.join('?' * len(extra))})",
extra,
)
return len(extra)
def delete_switch_grant(self, device_hash: str, user_id: int) -> bool:
with self.connect() as connection:
cursor = connection.execute(
"DELETE FROM account_switch_grants WHERE device_hash = ? AND user_id = ?",
(device_hash, user_id),
)
return cursor.rowcount > 0
def delete_switch_grants_for_user(self, user_id: int) -> int:
with self.connect() as connection:
cursor = connection.execute(
"DELETE FROM account_switch_grants WHERE user_id = ?",
(user_id,),
)
return int(cursor.rowcount)