Refactor code for improved readability and consistency
- Cleaned up whitespace and formatting in various files including http.py, language.py, logger.py, safe_exec.py, and SQL migration scripts. - Consolidated import statements and removed unnecessary blank lines. - Updated logging configuration for better clarity. - Enhanced the safe execution code with improved error handling and logging. - Removed commented-out code and unnecessary variables in backfill_zero_trades.py and other scripts. - Added a pyproject.toml for Ruff and Vulture configuration. - Introduced requirements-dev.txt for development dependencies. - Removed commented-out stock entries in init.sql for cleaner migration scripts.
This commit is contained in:
@@ -3,22 +3,24 @@ User Service - Multi-user management
|
||||
|
||||
Handles user CRUD operations, password hashing, and role management.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
from typing import Optional, Dict, Any, List
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.utils.db import get_db_connection
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# IANA timezone id subset check (e.g. Asia/Shanghai, America/New_York)
|
||||
_TIMEZONE_ID_RE = re.compile(r'^[A-Za-z0-9_/+\-.]+$')
|
||||
_TIMEZONE_ID_RE = re.compile(r"^[A-Za-z0-9_/+\-.]+$")
|
||||
|
||||
# Try to import bcrypt for secure password hashing
|
||||
try:
|
||||
import bcrypt
|
||||
|
||||
HAS_BCRYPT = True
|
||||
except ImportError:
|
||||
HAS_BCRYPT = False
|
||||
@@ -27,50 +29,60 @@ except ImportError:
|
||||
|
||||
class UserService:
|
||||
"""User management service"""
|
||||
|
||||
|
||||
# Available roles (ordered by privilege level)
|
||||
ROLES = ['viewer', 'user', 'manager', 'admin']
|
||||
|
||||
ROLES = ["viewer", "user", "manager", "admin"]
|
||||
|
||||
# Role permissions mapping
|
||||
ROLE_PERMISSIONS = {
|
||||
'viewer': ['dashboard', 'view'],
|
||||
'user': ['dashboard', 'view', 'indicator', 'backtest', 'strategy', 'portfolio'],
|
||||
'manager': ['dashboard', 'view', 'indicator', 'backtest', 'strategy', 'portfolio', 'settings'],
|
||||
'admin': ['dashboard', 'view', 'indicator', 'backtest', 'strategy', 'portfolio', 'settings', 'user_manage', 'credentials'],
|
||||
"viewer": ["dashboard", "view"],
|
||||
"user": ["dashboard", "view", "indicator", "backtest", "strategy", "portfolio"],
|
||||
"manager": ["dashboard", "view", "indicator", "backtest", "strategy", "portfolio", "settings"],
|
||||
"admin": [
|
||||
"dashboard",
|
||||
"view",
|
||||
"indicator",
|
||||
"backtest",
|
||||
"strategy",
|
||||
"portfolio",
|
||||
"settings",
|
||||
"user_manage",
|
||||
"credentials",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def hash_password(self, password: str) -> str:
|
||||
"""Hash password using bcrypt (preferred) or SHA256 (fallback)"""
|
||||
if HAS_BCRYPT:
|
||||
salt = bcrypt.gensalt(rounds=12)
|
||||
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
|
||||
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
||||
else:
|
||||
# Fallback to SHA256 with salt
|
||||
salt = os.urandom(16).hex()
|
||||
hashed = hashlib.sha256((password + salt).encode('utf-8')).hexdigest()
|
||||
hashed = hashlib.sha256((password + salt).encode("utf-8")).hexdigest()
|
||||
return f"sha256${salt}${hashed}"
|
||||
|
||||
|
||||
def verify_password(self, password: str, password_hash: str) -> bool:
|
||||
"""Verify password against hash"""
|
||||
if password_hash.startswith('$2b$') or password_hash.startswith('$2a$'):
|
||||
if password_hash.startswith("$2b$") or password_hash.startswith("$2a$"):
|
||||
# bcrypt hash
|
||||
if HAS_BCRYPT:
|
||||
try:
|
||||
return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
|
||||
return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode("utf-8"))
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
elif password_hash.startswith('sha256$'):
|
||||
elif password_hash.startswith("sha256$"):
|
||||
# SHA256 fallback hash
|
||||
parts = password_hash.split('$')
|
||||
parts = password_hash.split("$")
|
||||
if len(parts) != 3:
|
||||
return False
|
||||
salt = parts[1]
|
||||
stored_hash = parts[2]
|
||||
computed = hashlib.sha256((password + salt).encode('utf-8')).hexdigest()
|
||||
computed = hashlib.sha256((password + salt).encode("utf-8")).hexdigest()
|
||||
return computed == stored_hash
|
||||
return False
|
||||
|
||||
|
||||
def get_user_by_id(self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get user by ID"""
|
||||
try:
|
||||
@@ -82,7 +94,7 @@ class UserService:
|
||||
credits, vip_expires_at, timezone, last_login_at, created_at, updated_at
|
||||
FROM qd_users WHERE id = ?
|
||||
""",
|
||||
(user_id,)
|
||||
(user_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
@@ -90,7 +102,7 @@ class UserService:
|
||||
except Exception as e:
|
||||
logger.error(f"get_user_by_id failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get user by username (includes password_hash for auth)"""
|
||||
try:
|
||||
@@ -102,7 +114,7 @@ class UserService:
|
||||
status, role, timezone, last_login_at, created_at, updated_at
|
||||
FROM qd_users WHERE username = ?
|
||||
""",
|
||||
(username,)
|
||||
(username,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
@@ -110,7 +122,7 @@ class UserService:
|
||||
except Exception as e:
|
||||
logger.error(f"get_user_by_username failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get user by email (includes password_hash for auth)"""
|
||||
if not email:
|
||||
@@ -124,7 +136,7 @@ class UserService:
|
||||
status, role, timezone, last_login_at, created_at, updated_at
|
||||
FROM qd_users WHERE LOWER(email) = LOWER(?)
|
||||
""",
|
||||
(email,)
|
||||
(email,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
@@ -132,7 +144,7 @@ class UserService:
|
||||
except Exception as e:
|
||||
logger.error(f"get_user_by_email failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def authenticate(self, username: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Authenticate user with username/email and password.
|
||||
@@ -141,38 +153,35 @@ class UserService:
|
||||
"""
|
||||
# Try username first
|
||||
user = self.get_user_by_username(username)
|
||||
|
||||
|
||||
# If not found, try email (supports both username and email login)
|
||||
if not user:
|
||||
user = self.get_user_by_email(username)
|
||||
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if user.get('status') != 'active':
|
||||
|
||||
if user.get("status") != "active":
|
||||
logger.warning(f"Login attempt for disabled user: {username}")
|
||||
return None
|
||||
|
||||
password_hash = user.get('password_hash', '')
|
||||
|
||||
|
||||
password_hash = user.get("password_hash", "")
|
||||
|
||||
# Check if user has no password (code-login user)
|
||||
if not password_hash or password_hash.strip() == '':
|
||||
if not password_hash or password_hash.strip() == "":
|
||||
logger.info(f"Password login attempted for code-login user: {username}")
|
||||
# Return a special marker to indicate no password set
|
||||
# This allows the caller to provide a more specific error message
|
||||
return {'_no_password': True, **user}
|
||||
|
||||
return {"_no_password": True, **user}
|
||||
|
||||
if not self.verify_password(password, password_hash):
|
||||
return None
|
||||
|
||||
|
||||
# Update last login time
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
"UPDATE qd_users SET last_login_at = NOW() WHERE id = ?",
|
||||
(user['id'],)
|
||||
)
|
||||
cur.execute("UPDATE qd_users SET last_login_at = NOW() WHERE id = ?", (user["id"],))
|
||||
db.commit()
|
||||
affected = cur.rowcount
|
||||
cur.close()
|
||||
@@ -182,45 +191,42 @@ class UserService:
|
||||
logger.info(f"Updated last_login_at for user_id={user['id']}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update last_login_at for user_id={user.get('id')}: {e}")
|
||||
|
||||
|
||||
# Remove password_hash from return value
|
||||
user.pop('password_hash', None)
|
||||
user.pop("password_hash", None)
|
||||
return user
|
||||
|
||||
|
||||
def get_token_version(self, user_id: int) -> int:
|
||||
"""
|
||||
Get the user's current token version number.
|
||||
|
||||
|
||||
Args:
|
||||
user_id: user ID
|
||||
|
||||
|
||||
Returns:
|
||||
Current token version number, default is 1
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
"SELECT token_version FROM qd_users WHERE id = ?",
|
||||
(user_id,)
|
||||
)
|
||||
cur.execute("SELECT token_version FROM qd_users WHERE id = ?", (user_id,))
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
if row:
|
||||
return int(row.get('token_version') or 1)
|
||||
return int(row.get("token_version") or 1)
|
||||
return 1
|
||||
except Exception as e:
|
||||
logger.error(f"get_token_version failed: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
def increment_token_version(self, user_id: int) -> int:
|
||||
"""
|
||||
Increment the user's token version number and invalidate the old token.
|
||||
Used to implement single client login (kick out other devices).
|
||||
|
||||
|
||||
Args:
|
||||
user_id: user ID
|
||||
|
||||
|
||||
Returns:
|
||||
New token version number
|
||||
"""
|
||||
@@ -230,33 +236,30 @@ class UserService:
|
||||
# Increment token_version
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE qd_users
|
||||
UPDATE qd_users
|
||||
SET token_version = COALESCE(token_version, 0) + 1, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
""",
|
||||
(user_id,)
|
||||
(user_id,),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
# Get new token_version
|
||||
cur.execute(
|
||||
"SELECT token_version FROM qd_users WHERE id = ?",
|
||||
(user_id,)
|
||||
)
|
||||
cur.execute("SELECT token_version FROM qd_users WHERE id = ?", (user_id,))
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
new_version = int(row.get('token_version') or 1) if row else 1
|
||||
|
||||
new_version = int(row.get("token_version") or 1) if row else 1
|
||||
logger.info(f"Incremented token_version for user_id={user_id} to {new_version}")
|
||||
return new_version
|
||||
except Exception as e:
|
||||
logger.error(f"increment_token_version failed: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
def create_user(self, data: Dict[str, Any] = None, **kwargs) -> Optional[int]:
|
||||
"""
|
||||
Create a new user.
|
||||
|
||||
|
||||
Args:
|
||||
data: dict with user fields, OR use keyword arguments:
|
||||
username: str (required),
|
||||
@@ -267,7 +270,7 @@ class UserService:
|
||||
status: str (optional, default 'active'),
|
||||
email_verified: bool (optional, default False),
|
||||
referred_by: int (optional, referrer user ID)
|
||||
|
||||
|
||||
Returns:
|
||||
New user ID or None if failed
|
||||
"""
|
||||
@@ -276,99 +279,99 @@ class UserService:
|
||||
data = kwargs
|
||||
else:
|
||||
data = {**data, **kwargs}
|
||||
|
||||
username = (data.get('username') or '').strip()
|
||||
password = data.get('password') # Can be None for code-login users
|
||||
|
||||
|
||||
username = (data.get("username") or "").strip()
|
||||
password = data.get("password") # Can be None for code-login users
|
||||
|
||||
if not username:
|
||||
raise ValueError("Username is required")
|
||||
|
||||
|
||||
if len(username) < 3 or len(username) > 50:
|
||||
raise ValueError("Username must be 3-50 characters")
|
||||
|
||||
|
||||
# Password validation only if provided
|
||||
if password and len(password) < 6:
|
||||
raise ValueError("Password must be at least 6 characters")
|
||||
|
||||
|
||||
# Check if username already exists
|
||||
existing = self.get_user_by_username(username)
|
||||
if existing:
|
||||
raise ValueError("Username already exists")
|
||||
|
||||
|
||||
# Hash password or use empty string for code-login users
|
||||
password_hash = self.hash_password(password) if password else ''
|
||||
email = (data.get('email') or '').strip() or None
|
||||
nickname = (data.get('nickname') or '').strip() or username
|
||||
role = data.get('role', 'user')
|
||||
status = data.get('status', 'active')
|
||||
email_verified = data.get('email_verified', False)
|
||||
referred_by = data.get('referred_by') # Referrer user ID
|
||||
|
||||
password_hash = self.hash_password(password) if password else ""
|
||||
email = (data.get("email") or "").strip() or None
|
||||
nickname = (data.get("nickname") or "").strip() or username
|
||||
role = data.get("role", "user")
|
||||
status = data.get("status", "active")
|
||||
email_verified = data.get("email_verified", False)
|
||||
referred_by = data.get("referred_by") # Referrer user ID
|
||||
|
||||
if role not in self.ROLES:
|
||||
role = 'user'
|
||||
|
||||
role = "user"
|
||||
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO qd_users
|
||||
INSERT INTO qd_users
|
||||
(username, password_hash, email, nickname, role, status, email_verified, referred_by, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
""",
|
||||
(username, password_hash, email, nickname, role, status, email_verified, referred_by)
|
||||
(username, password_hash, email, nickname, role, status, email_verified, referred_by),
|
||||
)
|
||||
db.commit()
|
||||
user_id = cur.lastrowid
|
||||
cur.close()
|
||||
|
||||
|
||||
# For PostgreSQL, get the ID differently
|
||||
if user_id is None:
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT id FROM qd_users WHERE username = ?", (username,))
|
||||
row = cur.fetchone()
|
||||
user_id = row['id'] if row else None
|
||||
user_id = row["id"] if row else None
|
||||
cur.close()
|
||||
|
||||
|
||||
logger.info(f"Created user: {username} (id={user_id}, referred_by={referred_by})")
|
||||
return user_id
|
||||
except Exception as e:
|
||||
logger.error(f"create_user failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def update_user(self, user_id: int, data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Update user information.
|
||||
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
data: Fields to update (email, nickname, avatar, role, status)
|
||||
"""
|
||||
allowed_fields = ['email', 'nickname', 'avatar', 'role', 'status', 'timezone']
|
||||
allowed_fields = ["email", "nickname", "avatar", "role", "status", "timezone"]
|
||||
updates = []
|
||||
values = []
|
||||
|
||||
|
||||
for field in allowed_fields:
|
||||
if field in data:
|
||||
value = data[field]
|
||||
if field == 'role' and value not in self.ROLES:
|
||||
if field == "role" and value not in self.ROLES:
|
||||
continue
|
||||
if field == 'timezone':
|
||||
s = '' if value is None else str(value).strip()
|
||||
if field == "timezone":
|
||||
s = "" if value is None else str(value).strip()
|
||||
if s and (len(s) > 64 or not _TIMEZONE_ID_RE.match(s)):
|
||||
continue
|
||||
updates.append('timezone = ?')
|
||||
updates.append("timezone = ?")
|
||||
values.append(s)
|
||||
continue
|
||||
updates.append(f"{field} = ?")
|
||||
values.append(value)
|
||||
|
||||
|
||||
if not updates:
|
||||
return False
|
||||
|
||||
|
||||
updates.append("updated_at = NOW()")
|
||||
values.append(user_id)
|
||||
|
||||
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
@@ -380,49 +383,48 @@ class UserService:
|
||||
except Exception as e:
|
||||
logger.error(f"update_user failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def change_password(self, user_id: int, old_password: str, new_password: str) -> bool:
|
||||
"""Change user password (requires old password verification, except for users with no password)"""
|
||||
user = self.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
|
||||
# Get full user with password_hash
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT password_hash FROM qd_users WHERE id = ?", (user_id,))
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
|
||||
if not row:
|
||||
return False
|
||||
|
||||
password_hash = row.get('password_hash', '')
|
||||
|
||||
|
||||
password_hash = row.get("password_hash", "")
|
||||
|
||||
# If user has no password (code-login user), allow setting password without old password
|
||||
if not password_hash or password_hash.strip() == '':
|
||||
if not password_hash or password_hash.strip() == "":
|
||||
logger.info(f"Setting initial password for code-login user: {user_id}")
|
||||
return self.reset_password(user_id, new_password)
|
||||
|
||||
|
||||
# For users with existing password, verify old password
|
||||
if not self.verify_password(old_password, password_hash):
|
||||
return False
|
||||
|
||||
|
||||
return self.reset_password(user_id, new_password)
|
||||
|
||||
|
||||
def reset_password(self, user_id: int, new_password: str) -> bool:
|
||||
"""Reset user password (admin operation, no old password required)"""
|
||||
if len(new_password) < 6:
|
||||
raise ValueError("Password must be at least 6 characters")
|
||||
|
||||
|
||||
password_hash = self.hash_password(new_password)
|
||||
|
||||
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
"UPDATE qd_users SET password_hash = ?, updated_at = NOW() WHERE id = ?",
|
||||
(password_hash, user_id)
|
||||
"UPDATE qd_users SET password_hash = ?, updated_at = NOW() WHERE id = ?", (password_hash, user_id)
|
||||
)
|
||||
db.commit()
|
||||
cur.close()
|
||||
@@ -430,11 +432,11 @@ class UserService:
|
||||
except Exception as e:
|
||||
logger.error(f"reset_password failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def update_password(self, user_id: int, new_password: str) -> bool:
|
||||
"""Alias for reset_password - update user password without old password verification"""
|
||||
return self.reset_password(user_id, new_password)
|
||||
|
||||
|
||||
def delete_user(self, user_id: int) -> bool:
|
||||
"""Delete a user"""
|
||||
try:
|
||||
@@ -447,15 +449,15 @@ class UserService:
|
||||
except Exception as e:
|
||||
logger.error(f"delete_user failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def list_users(self, page: int = 1, page_size: int = 20, search: str = None) -> Dict[str, Any]:
|
||||
"""List all users with pagination and optional search"""
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
|
||||
|
||||
# Build WHERE clause for search
|
||||
where_clause = ""
|
||||
params = []
|
||||
@@ -463,12 +465,12 @@ class UserService:
|
||||
search_term = f"%{search.strip()}%"
|
||||
where_clause = "WHERE username LIKE ? OR email LIKE ? OR nickname LIKE ?"
|
||||
params = [search_term, search_term, search_term]
|
||||
|
||||
|
||||
# Get total count
|
||||
count_sql = f"SELECT COUNT(*) as count FROM qd_users {where_clause}"
|
||||
cur.execute(count_sql, tuple(params))
|
||||
total = cur.fetchone()['count']
|
||||
|
||||
total = cur.fetchone()["count"]
|
||||
|
||||
# Get users
|
||||
query_sql = f"""
|
||||
SELECT id, username, email, nickname, avatar, status, role,
|
||||
@@ -481,22 +483,22 @@ class UserService:
|
||||
cur.execute(query_sql, tuple(params + [page_size, offset]))
|
||||
users = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
|
||||
return {
|
||||
'items': users,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'total_pages': (total + page_size - 1) // page_size
|
||||
"items": users,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"list_users failed: {e}")
|
||||
return {'items': [], 'total': 0, 'page': 1, 'page_size': page_size, 'total_pages': 0}
|
||||
|
||||
return {"items": [], "total": 0, "page": 1, "page_size": page_size, "total_pages": 0}
|
||||
|
||||
def get_user_permissions(self, role: str) -> List[str]:
|
||||
"""Get permissions for a role"""
|
||||
return self.ROLE_PERMISSIONS.get(role, self.ROLE_PERMISSIONS['viewer'])
|
||||
|
||||
return self.ROLE_PERMISSIONS.get(role, self.ROLE_PERMISSIONS["viewer"])
|
||||
|
||||
def ensure_admin_exists(self):
|
||||
"""
|
||||
Ensure at least one admin user exists.
|
||||
@@ -506,24 +508,26 @@ class UserService:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT COUNT(*) as count FROM qd_users")
|
||||
count = cur.fetchone()['count']
|
||||
count = cur.fetchone()["count"]
|
||||
cur.close()
|
||||
|
||||
|
||||
if count == 0:
|
||||
# Create admin using env credentials
|
||||
admin_user = os.getenv('ADMIN_USER', 'admin')
|
||||
admin_password = os.getenv('ADMIN_PASSWORD', 'admin123')
|
||||
admin_email = os.getenv('ADMIN_EMAIL', 'admin@example.com')
|
||||
admin_user = os.getenv("ADMIN_USER", "admin")
|
||||
admin_password = os.getenv("ADMIN_PASSWORD", "admin123")
|
||||
admin_email = os.getenv("ADMIN_EMAIL", "admin@example.com")
|
||||
|
||||
self.create_user({
|
||||
'username': admin_user,
|
||||
'password': admin_password,
|
||||
'email': admin_email,
|
||||
'nickname': 'Administrator',
|
||||
'role': 'admin',
|
||||
'status': 'active',
|
||||
'email_verified': True # Admin email is pre-verified
|
||||
})
|
||||
self.create_user(
|
||||
{
|
||||
"username": admin_user,
|
||||
"password": admin_password,
|
||||
"email": admin_email,
|
||||
"nickname": "Administrator",
|
||||
"role": "admin",
|
||||
"status": "active",
|
||||
"email_verified": True, # Admin email is pre-verified
|
||||
}
|
||||
)
|
||||
logger.info(f"Created admin user: {admin_user} ({admin_email})")
|
||||
except Exception as e:
|
||||
logger.error(f"ensure_admin_exists failed: {e}")
|
||||
@@ -532,6 +536,7 @@ class UserService:
|
||||
# Global singleton
|
||||
_user_service = None
|
||||
|
||||
|
||||
def get_user_service() -> UserService:
|
||||
"""Get UserService singleton"""
|
||||
global _user_service
|
||||
|
||||
Reference in New Issue
Block a user