feat(server): 添加默认管理员自动创建功能
- 在 .env.example 中添加默认管理员相关配置项 - 在 docker-compose.yml 中添加默认管理员环境变量映射 - 在 server/app/core/config.py 中定义默认管理员配置 - 创建 server/app/db/bootstrap.py 文件实现默认管理员创建逻辑 - 在 server/app/main.py 的生命周期中集成默认管理员确保功能 - 更新 README.md 文档说明新的管理员配置方式 新配置项包括:DEFAULT_ADMIN_ENABLED、DEFAULT_ADMIN_PHONE、 DEFAULT_ADMIN_EMAIL、DEFAULT_ADMIN_PASSWORD、DEFAULT_ADMIN_NICKNAME 和 DEFAULT_ADMIN_SYNC_PASSWORD。
This commit is contained in:
@@ -69,6 +69,18 @@ SENTRY_DSN=
|
||||
LOG_LEVEL=INFO
|
||||
# 是否输出 JSON 格式日志。
|
||||
LOG_JSON=false
|
||||
# 是否在后端启动时自动创建默认管理员。
|
||||
DEFAULT_ADMIN_ENABLED=true
|
||||
# 默认管理员手机号,可用于登录管理端。
|
||||
DEFAULT_ADMIN_PHONE=13900000001
|
||||
# 默认管理员邮箱,也可用于登录管理端。
|
||||
DEFAULT_ADMIN_EMAIL=admin@ciyuan.local
|
||||
# 默认管理员密码,生产环境必须修改。
|
||||
DEFAULT_ADMIN_PASSWORD=admin123456
|
||||
# 默认管理员昵称。
|
||||
DEFAULT_ADMIN_NICKNAME=系统管理员
|
||||
# 启动时是否把已有默认管理员密码同步为 DEFAULT_ADMIN_PASSWORD。
|
||||
DEFAULT_ADMIN_SYNC_PASSWORD=true
|
||||
|
||||
# 管理端前端构建配置
|
||||
# 管理端构建阶段 Node 基础镜像。
|
||||
|
||||
@@ -86,6 +86,12 @@ services:
|
||||
SENTRY_DSN: "${SENTRY_DSN}"
|
||||
LOG_LEVEL: "${LOG_LEVEL}"
|
||||
LOG_JSON: "${LOG_JSON}"
|
||||
DEFAULT_ADMIN_ENABLED: "${DEFAULT_ADMIN_ENABLED}"
|
||||
DEFAULT_ADMIN_PHONE: "${DEFAULT_ADMIN_PHONE}"
|
||||
DEFAULT_ADMIN_EMAIL: "${DEFAULT_ADMIN_EMAIL}"
|
||||
DEFAULT_ADMIN_PASSWORD: "${DEFAULT_ADMIN_PASSWORD}"
|
||||
DEFAULT_ADMIN_NICKNAME: "${DEFAULT_ADMIN_NICKNAME}"
|
||||
DEFAULT_ADMIN_SYNC_PASSWORD: "${DEFAULT_ADMIN_SYNC_PASSWORD}"
|
||||
expose:
|
||||
- "${SERVER_INTERNAL_PORT}"
|
||||
volumes:
|
||||
|
||||
+5
-3
@@ -1,6 +1,8 @@
|
||||
# 后端默认管理员账号
|
||||
|
||||
- 管理后台登录账号(手机号):`13900000001`
|
||||
- 管理后台登录密码:`demo123456`
|
||||
后端启动时会根据根目录 `.env` 自动确保默认管理员存在。
|
||||
|
||||
> 说明:以上为 `seed_demo_data.py` 初始化的演示管理员账号(role=`admin`)。
|
||||
- 管理后台登录账号:`DEFAULT_ADMIN_PHONE` 或 `DEFAULT_ADMIN_EMAIL`
|
||||
- 管理后台登录密码:`DEFAULT_ADMIN_PASSWORD`
|
||||
|
||||
默认模板值为 `13900000001 / admin123456`。生产环境请修改根目录 `.env`。
|
||||
|
||||
@@ -34,6 +34,13 @@ class Settings(BaseSettings):
|
||||
LOG_JSON: bool = False
|
||||
LOG_LEVEL: str = "INFO"
|
||||
|
||||
DEFAULT_ADMIN_ENABLED: bool = True
|
||||
DEFAULT_ADMIN_PHONE: str = "13900000001"
|
||||
DEFAULT_ADMIN_EMAIL: str = "admin@ciyuan.local"
|
||||
DEFAULT_ADMIN_PASSWORD: str = "admin123456"
|
||||
DEFAULT_ADMIN_NICKNAME: str = "系统管理员"
|
||||
DEFAULT_ADMIN_SYNC_PASSWORD: bool = True
|
||||
|
||||
class Config:
|
||||
env_file = _env_file
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
from app.db.session import sync_engine
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def ensure_default_admin() -> None:
|
||||
if not settings.DEFAULT_ADMIN_ENABLED:
|
||||
return
|
||||
|
||||
phone = settings.DEFAULT_ADMIN_PHONE.strip() or None
|
||||
email = settings.DEFAULT_ADMIN_EMAIL.strip() or None
|
||||
if not phone and not email:
|
||||
raise RuntimeError("DEFAULT_ADMIN_PHONE or DEFAULT_ADMIN_EMAIL is required")
|
||||
|
||||
filters = []
|
||||
if phone:
|
||||
filters.append(User.phone == phone)
|
||||
if email:
|
||||
filters.append(User.email == email)
|
||||
|
||||
with Session(sync_engine) as session:
|
||||
user = session.execute(select(User).where(or_(*filters))).scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
user = User(
|
||||
phone=phone,
|
||||
email=email,
|
||||
password_hash=get_password_hash(settings.DEFAULT_ADMIN_PASSWORD),
|
||||
nickname=settings.DEFAULT_ADMIN_NICKNAME,
|
||||
identity="both",
|
||||
role="admin",
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
return
|
||||
|
||||
changed = False
|
||||
expected = {
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"nickname": settings.DEFAULT_ADMIN_NICKNAME,
|
||||
"identity": "both",
|
||||
"role": "admin",
|
||||
"is_active": True,
|
||||
}
|
||||
for field, value in expected.items():
|
||||
if value is not None and getattr(user, field) != value:
|
||||
setattr(user, field, value)
|
||||
changed = True
|
||||
|
||||
if settings.DEFAULT_ADMIN_SYNC_PASSWORD and not verify_password(
|
||||
settings.DEFAULT_ADMIN_PASSWORD,
|
||||
user.password_hash,
|
||||
):
|
||||
user.password_hash = get_password_hash(settings.DEFAULT_ADMIN_PASSWORD)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
session.add(user)
|
||||
session.commit()
|
||||
@@ -14,6 +14,7 @@ from app.api.v1.router import v1_router
|
||||
from app.core.config import settings
|
||||
from app.core.deps import get_current_active_user, get_db
|
||||
from app.core.storage import init_storage
|
||||
from app.db.bootstrap import ensure_default_admin
|
||||
from app.db.migrations import run_startup_migrations
|
||||
from app.models.spot import Spot
|
||||
from app.models.tag import Tag
|
||||
@@ -44,6 +45,7 @@ logger = logging.getLogger(__name__)
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await asyncio.to_thread(run_startup_migrations)
|
||||
await asyncio.to_thread(ensure_default_admin)
|
||||
app.state.redis = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
|
||||
app.state.storage = init_storage()
|
||||
yield
|
||||
|
||||
Reference in New Issue
Block a user