Files

137 lines
5.4 KiB
Python

from __future__ import annotations
from datetime import datetime
import secrets
from typing import Optional
from sqlalchemy import inspect, select, text
from sqlalchemy.orm import Session
from app.core.config import AppConfig
from app.core.db import Database
from app.models.session import SessionRecord
from app.models.user import User
from app.services.auth import AuthService
def _generate_initial_password() -> str:
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+"
while True:
password = "".join(secrets.choice(alphabet) for _ in range(12))
if (
any(char.islower() for char in password)
and any(char.isupper() for char in password)
and any(char.isdigit() for char in password)
and any(char in "!@#$%^&*()_+" for char in password)
):
return password
def ensure_admin_user(session: Session, auth_service: AuthService) -> Optional[str]:
existing = session.execute(select(User).where(User.username == "admin")).scalar_one_or_none()
if existing:
return None
password = _generate_initial_password()
admin = User(
username="admin",
password_hash=auth_service.hash_password(password),
must_change_password=True,
failed_login_count=0,
locked_until=None,
last_login=None,
)
session.add(admin)
return password
def ensure_session_note_column(database: Database) -> None:
inspector = inspect(database._engine)
if "sessions" not in inspector.get_table_names():
return
columns = {column["name"] for column in inspector.get_columns("sessions")}
if "note" in columns:
return
with database._engine.begin() as connection:
connection.execute(text("ALTER TABLE sessions ADD COLUMN note TEXT"))
def ensure_connection_arch_columns(database: Database) -> None:
inspector = inspect(database._engine)
if "connections" not in inspector.get_table_names():
return
columns = {column["name"] for column in inspector.get_columns("connections")}
with database._engine.begin() as connection:
if "remote_arch" not in columns:
connection.execute(text("ALTER TABLE connections ADD COLUMN remote_arch VARCHAR(64)"))
if "remote_os" not in columns:
connection.execute(text("ALTER TABLE connections ADD COLUMN remote_os VARCHAR(64)"))
if "enhance_prompt_shown" not in columns:
connection.execute(text("ALTER TABLE connections ADD COLUMN enhance_prompt_shown BOOLEAN DEFAULT 0"))
def ensure_session_enhanced_columns(database: Database) -> None:
inspector = inspect(database._engine)
if "sessions" not in inspector.get_table_names():
return
columns = {column["name"] for column in inspector.get_columns("sessions")}
with database._engine.begin() as connection:
if "enhanced_enabled" not in columns:
connection.execute(text("ALTER TABLE sessions ADD COLUMN enhanced_enabled BOOLEAN DEFAULT 0"))
if "enhanced_fingerprint" not in columns:
connection.execute(text("ALTER TABLE sessions ADD COLUMN enhanced_fingerprint VARCHAR(128)"))
if "tmux_binary_path" not in columns:
connection.execute(text("ALTER TABLE sessions ADD COLUMN tmux_binary_path VARCHAR(255)"))
if "disconnected_at" not in columns:
connection.execute(text("ALTER TABLE sessions ADD COLUMN disconnected_at DATETIME"))
if "auto_retry_count" not in columns:
connection.execute(text("ALTER TABLE sessions ADD COLUMN auto_retry_count INTEGER DEFAULT 0"))
if "retry_cycle_count" not in columns:
connection.execute(text("ALTER TABLE sessions ADD COLUMN retry_cycle_count INTEGER DEFAULT 0"))
if "allow_auto_retry" not in columns:
connection.execute(text("ALTER TABLE sessions ADD COLUMN allow_auto_retry BOOLEAN DEFAULT 1"))
def ensure_session_order_column(database: Database) -> None:
inspector = inspect(database._engine)
if "sessions" not in inspector.get_table_names():
return
columns = {column["name"] for column in inspector.get_columns("sessions")}
with database._engine.begin() as connection:
if "session_order" not in columns:
connection.execute(text("ALTER TABLE sessions ADD COLUMN session_order INTEGER"))
# 为历史数据补齐顺序(按 started_at 排序)
with database.session() as db_session:
records = db_session.execute(
select(SessionRecord).order_by(SessionRecord.user_id, SessionRecord.started_at)
).scalars().all()
last_order_by_user: dict[int, int] = {}
for record in records:
if record.session_order and record.session_order > 0:
last_order_by_user[record.user_id] = max(
last_order_by_user.get(record.user_id, 0),
record.session_order,
)
for record in records:
if record.session_order and record.session_order > 0:
continue
next_order = last_order_by_user.get(record.user_id, 0) + 1
record.session_order = next_order
last_order_by_user[record.user_id] = next_order
def is_user_locked(user: User, auth_service: AuthService) -> bool:
return auth_service.is_locked(user)
def lockout_until(user: User, config: AppConfig) -> Optional[datetime]:
if user.locked_until is None:
return None
return user.locked_until