Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

279 lines
9.7 KiB
Python

"""Default SQLite implementation of user service."""
import sqlite3
from datetime import datetime, timezone
from uuid import uuid4
from invokeai.app.services.auth.password_utils import hash_password, validate_password_strength, verify_password
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
from invokeai.app.services.users.users_base import UserServiceBase
from invokeai.app.services.users.users_common import UserCreateRequest, UserDTO, UserUpdateRequest
class UserService(UserServiceBase):
"""SQLite-based user service."""
def __init__(self, db: SqliteDatabase):
"""Initialize user service.
Args:
db: SQLite database instance
"""
self._db = db
def create(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Create a new user."""
# Validate password strength
if strict_password_checking:
is_valid, error_msg = validate_password_strength(user_data.password)
if not is_valid:
raise ValueError(error_msg)
elif not user_data.password:
raise ValueError("Password cannot be empty")
# Check if email already exists
if self.get_by_email(user_data.email) is not None:
raise ValueError(f"User with email {user_data.email} already exists")
user_id = str(uuid4())
password_hash = hash_password(user_data.password)
with self._db.transaction() as cursor:
try:
cursor.execute(
"""
INSERT INTO users (user_id, email, display_name, password_hash, is_admin)
VALUES (?, ?, ?, ?, ?)
""",
(user_id, user_data.email, user_data.display_name, password_hash, user_data.is_admin),
)
except sqlite3.IntegrityError as e:
raise ValueError(f"Failed to create user: {e}") from e
user = self.get(user_id)
if user is None:
raise RuntimeError("Failed to retrieve created user")
return user
def get(self, user_id: str) -> UserDTO | None:
"""Get user by ID."""
with self._db.transaction() as cursor:
cursor.execute(
"""
SELECT user_id, email, display_name, is_admin, is_active, created_at, updated_at, last_login_at
FROM users
WHERE user_id = ?
""",
(user_id,),
)
row = cursor.fetchone()
if row is None:
return None
return UserDTO(
user_id=row[0],
email=row[1],
display_name=row[2],
is_admin=bool(row[3]),
is_active=bool(row[4]),
created_at=datetime.fromisoformat(row[5]),
updated_at=datetime.fromisoformat(row[6]),
last_login_at=datetime.fromisoformat(row[7]) if row[7] else None,
)
def get_by_email(self, email: str) -> UserDTO | None:
"""Get user by email."""
with self._db.transaction() as cursor:
cursor.execute(
"""
SELECT user_id, email, display_name, is_admin, is_active, created_at, updated_at, last_login_at
FROM users
WHERE email = ?
""",
(email,),
)
row = cursor.fetchone()
if row is None:
return None
return UserDTO(
user_id=row[0],
email=row[1],
display_name=row[2],
is_admin=bool(row[3]),
is_active=bool(row[4]),
created_at=datetime.fromisoformat(row[5]),
updated_at=datetime.fromisoformat(row[6]),
last_login_at=datetime.fromisoformat(row[7]) if row[7] else None,
)
def update(self, user_id: str, changes: UserUpdateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Update user."""
# Check if user exists
user = self.get(user_id)
if user is None:
raise ValueError(f"User {user_id} not found")
# Validate password if provided
if changes.password is not None:
if strict_password_checking:
is_valid, error_msg = validate_password_strength(changes.password)
if not is_valid:
raise ValueError(error_msg)
elif not changes.password:
raise ValueError("Password cannot be empty")
# Build update query dynamically based on provided fields
updates: list[str] = []
params: list[str | bool | int] = []
if changes.display_name is not None:
updates.append("display_name = ?")
params.append(changes.display_name)
if changes.password is not None:
updates.append("password_hash = ?")
params.append(hash_password(changes.password))
if changes.is_admin is not None:
updates.append("is_admin = ?")
params.append(changes.is_admin)
if changes.is_active is not None:
updates.append("is_active = ?")
params.append(changes.is_active)
if not updates:
return user
params.append(user_id)
query = f"UPDATE users SET {', '.join(updates)} WHERE user_id = ?"
with self._db.transaction() as cursor:
cursor.execute(query, params)
updated_user = self.get(user_id)
if updated_user is None:
raise RuntimeError("Failed to retrieve updated user")
return updated_user
def delete(self, user_id: str) -> None:
"""Delete user."""
user = self.get(user_id)
if user is None:
raise ValueError(f"User {user_id} not found")
with self._db.transaction() as cursor:
cursor.execute("DELETE FROM users WHERE user_id = ?", (user_id,))
def authenticate(self, email: str, password: str) -> UserDTO | None:
"""Authenticate user credentials."""
with self._db.transaction() as cursor:
cursor.execute(
"""
SELECT user_id, email, display_name, password_hash, is_admin, is_active, created_at, updated_at, last_login_at
FROM users
WHERE email = ?
""",
(email,),
)
row = cursor.fetchone()
if row is None:
return None
password_hash = row[3]
if not verify_password(password, password_hash):
return None
# Update last login time
with self._db.transaction() as cursor:
cursor.execute(
"UPDATE users SET last_login_at = ? WHERE user_id = ?",
(datetime.now(timezone.utc).isoformat(), row[0]),
)
return UserDTO(
user_id=row[0],
email=row[1],
display_name=row[2],
is_admin=bool(row[4]),
is_active=bool(row[5]),
created_at=datetime.fromisoformat(row[6]),
updated_at=datetime.fromisoformat(row[7]),
last_login_at=datetime.now(timezone.utc),
)
def has_admin(self) -> bool:
"""Check if any admin user exists."""
with self._db.transaction() as cursor:
cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = TRUE AND is_active = TRUE")
row = cursor.fetchone()
count = row[0] if row else 0
return bool(count > 0)
def create_admin(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Create an admin user (for initial setup)."""
if self.has_admin():
raise ValueError("Admin user already exists")
# Force is_admin to True
admin_data = UserCreateRequest(
email=user_data.email,
display_name=user_data.display_name,
password=user_data.password,
is_admin=True,
)
return self.create(admin_data, strict_password_checking=strict_password_checking)
def list_users(self, limit: int = 100, offset: int = 0) -> list[UserDTO]:
"""List all users."""
with self._db.transaction() as cursor:
cursor.execute(
"""
SELECT user_id, email, display_name, is_admin, is_active, created_at, updated_at, last_login_at
FROM users
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""",
(limit, offset),
)
rows = cursor.fetchall()
return [
UserDTO(
user_id=row[0],
email=row[1],
display_name=row[2],
is_admin=bool(row[3]),
is_active=bool(row[4]),
created_at=datetime.fromisoformat(row[5]),
updated_at=datetime.fromisoformat(row[6]),
last_login_at=datetime.fromisoformat(row[7]) if row[7] else None,
)
for row in rows
]
def get_admin_email(self) -> str | None:
"""Get the email address of the first active admin user."""
with self._db.transaction() as cursor:
cursor.execute(
"""
SELECT email FROM users
WHERE is_admin = TRUE AND is_active = TRUE
ORDER BY created_at ASC
LIMIT 1
""",
)
row = cursor.fetchone()
return row[0] if row else None
def count_admins(self) -> int:
"""Count active admin users."""
with self._db.transaction() as cursor:
cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = TRUE AND is_active = TRUE")
row = cursor.fetchone()
return int(row[0]) if row else 0