Files

991 lines
27 KiB
Python

import os
import sys
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.db import Base, SessionLocal, get_engine
from app.models.rbac import AdminResource, AdminRole, AdminRoleResource, AdminUser, AdminUserRole, utcnow
from app.services.password import hash_password, validate_password
SUPER_CODE = "R_SUPER"
SUPER_NAME = "超级管理员"
PAGES = [
{
"name": "User",
"title": "用户管理",
"path": "/system/user",
"component": "/system/user",
"sort": 1,
"buttons": [
("user:add", "新增"),
("user:edit", "编辑"),
("user:reset-password", "重置密码"),
("user:disable", "停用"),
("user:cancel", "注销"),
],
},
{
"name": "Role",
"title": "角色管理",
"path": "/system/role",
"component": "/system/role",
"sort": 2,
"buttons": [
("role:add", "新增"),
("role:edit", "编辑"),
("role:disable", "停用"),
("role:grant", "授权"),
],
},
{
"name": "Menu",
"title": "菜单管理",
"path": "/system/menu",
"component": "/system/menu",
"sort": 3,
"buttons": [
("menu:add", "新增"),
("menu:edit", "编辑"),
("menu:delete", "删除"),
],
},
{
"name": "AuditLog",
"title": "操作日志",
"path": "/system/audit-log",
"component": "/system/audit-log",
"sort": 4,
"buttons": [],
},
{
"name": "LoginSecurity",
"title": "登录安全",
"path": "/system/login-security",
"component": "/system/login-security",
"sort": 5,
"buttons": [("security:edit", "编辑")],
},
{
"name": "File",
"title": "附件",
"path": "/system/file",
"component": "/system/file",
"sort": 6,
"buttons": [("file:upload", "上传"), ("file:delete", "删除")],
},
{
"name": "Site",
"title": "站点配置",
"path": "/system/site",
"component": "/system/site",
"sort": 7,
"buttons": [("site:edit", "编辑")],
},
{
"name": "SensitiveWord",
"title": "敏感词",
"path": "/system/sensitive-word",
"component": "/system/sensitive-word",
"sort": 8,
"buttons": [("word:add", "新增"), ("word:delete", "删除"), ("word:preview", "预览")],
},
]
def _find_resource(db: Session, parent_id: int | None, name: str) -> AdminResource | None:
stmt = select(AdminResource).where(AdminResource.name == name)
if parent_id is None:
stmt = stmt.where(AdminResource.parent_id.is_(None))
else:
stmt = stmt.where(AdminResource.parent_id == parent_id)
return db.scalars(stmt.order_by(AdminResource.id.asc())).first()
def _ensure_resource(
db: Session,
*,
parent_id: int | None,
resource_type: str,
name: str,
title: str,
path: str,
component: str,
icon: str,
permission: str,
sort: int,
visible: bool,
) -> AdminResource:
row = _find_resource(db, parent_id, name)
if row is None:
row = AdminResource(
parent_id=parent_id,
type=resource_type,
name=name,
title=title,
path=path,
component=component,
icon=icon,
permission=permission,
sort=sort,
visible=visible,
status="active",
)
db.add(row)
db.flush()
return row
row.parent_id = parent_id
row.type = resource_type
row.title = title
row.path = path
row.component = component
row.icon = icon
row.permission = permission
row.sort = sort
row.visible = visible
row.status = "active"
return row
HOME_MENUS = (
("HomeTemplate", "模板", "/home/template", "ri:layout-2-line", 1),
("HomeSlides", "轮播", "/home/slides", "ri:slideshow-3-line", 2),
("HomeArticles", "文章", "/home/articles", "ri:article-line", 3),
("HomeEntries", "入口与菜单", "/home/entries", "ri:apps-2-line", 4),
("HomeMarquees", "荣誉与会员", "/home/marquees", "ri:award-line", 5),
("HomeFooter", "页脚", "/home/footer", "ri:layout-bottom-line", 6),
)
ABOUT_ORG_MENUS = (
("AboutIntro", "协会简介", "/about/intro", "ri:building-line", 1),
("AboutOrg", "组织机构", "/about/org", "ri:node-tree", 2),
("AboutExperts", "专家委员会", "/about/experts", "ri:team-line", 3),
("AboutReports", "年度报告/工作计划", "/about/reports", "ri:file-list-3-line", 4),
("AboutBriefs", "工作简报", "/about/briefs", "ri:newspaper-line", 5),
)
def ensure_home_menu(db: Session) -> None:
directory = _ensure_resource(
db,
parent_id=None,
resource_type="directory",
name="Home",
title="首页配置",
path="/home",
component="",
icon="ri:home-4-line",
permission="",
sort=2,
visible=True,
)
legacy = _find_resource(db, directory.id, "HomeConfig")
if legacy is not None and _find_resource(db, directory.id, "HomeTemplate") is None:
legacy.name = "HomeTemplate"
db.flush()
pages = [
_ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name=name,
title=title,
path=path,
component=path,
icon=icon,
permission="",
sort=sort,
visible=True,
)
for name, title, path, icon, sort in HOME_MENUS
]
button = _ensure_resource(
db,
parent_id=pages[0].id,
resource_type="button",
name="home:edit",
title="保存",
path="",
component="",
icon="",
permission="home:edit",
sort=1,
visible=False,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
for resource_id in (directory.id, button.id, *(page.id for page in pages)):
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_party_menu(db: Session) -> None:
party = _ensure_resource(
db,
parent_id=None,
resource_type="menu",
name="PartyBuilding",
title="行业党建",
path="/party",
component="/party",
icon="iconoven:national-emblem",
permission="",
sort=5,
visible=True,
)
button = _ensure_resource(
db,
parent_id=party.id,
resource_type="button",
name="party:edit",
title="保存",
path="",
component="",
icon="",
permission="party:edit",
sort=1,
visible=False,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
for resource_id in (party.id, button.id):
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_expert_menu(db: Session) -> None:
directory = _ensure_resource(
db,
parent_id=None,
resource_type="directory",
name="ExpertService",
title="专家服务",
path="/expert",
component="",
icon="ri:customer-service-2-line",
permission="",
sort=6,
visible=True,
)
guides = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="ExpertGuides",
title="估价指南",
path="/expert/guides",
component="/expert/guides",
icon="ri:book-open-line",
permission="",
sort=1,
visible=True,
)
qa = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="ExpertQa",
title="专家答疑",
path="/expert/qa",
component="/expert/qa",
icon="ri:question-answer-line",
permission="",
sort=2,
visible=True,
)
edit_button = _ensure_resource(
db,
parent_id=guides.id,
resource_type="button",
name="expert:edit",
title="保存",
path="",
component="",
icon="",
permission="expert:edit",
sort=1,
visible=False,
)
reply_button = _ensure_resource(
db,
parent_id=qa.id,
resource_type="button",
name="expert:reply",
title="回复",
path="",
component="",
icon="",
permission="expert:reply",
sort=1,
visible=False,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
for resource_id in (directory.id, guides.id, qa.id, edit_button.id, reply_button.id):
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_directory_menu(db: Session) -> None:
directory = _ensure_resource(
db,
parent_id=None,
resource_type="menu",
name="InstitutionDirectory",
title="机构名录",
path="/institutions",
component="/institutions",
icon="ri:building-2-line",
permission="",
sort=8,
visible=True,
)
button = _ensure_resource(
db,
parent_id=directory.id,
resource_type="button",
name="directory:edit",
title="保存",
path="",
component="",
icon="",
permission="directory:edit",
sort=1,
visible=False,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
for resource_id in (directory.id, button.id):
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_library_menu(db: Session) -> None:
directory = _ensure_resource(
db,
parent_id=None,
resource_type="directory",
name="Library",
title="资料库",
path="/library",
component="",
icon="ri:folder-open-line",
permission="",
sort=9,
visible=True,
)
policies = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="LibraryPolicies",
title="政策分析",
path="/library/policies",
component="/library/policies",
icon="ri:file-text-line",
permission="",
sort=1,
visible=True,
)
cases = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="LibraryCases",
title="案例库",
path="/library/cases",
component="/library/cases",
icon="ri:archive-line",
permission="",
sort=2,
visible=True,
)
policy_button = _ensure_resource(
db,
parent_id=policies.id,
resource_type="button",
name="library:edit",
title="保存",
path="",
component="",
icon="",
permission="library:edit",
sort=1,
visible=False,
)
case_button = _ensure_resource(
db,
parent_id=cases.id,
resource_type="button",
name="library:edit",
title="保存",
path="",
component="",
icon="",
permission="library:edit",
sort=1,
visible=False,
)
uploads = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="LibraryUploads",
title="上传的案例库",
path="/library/uploads",
component="/library/uploads",
icon="ri:upload-2-line",
permission="",
sort=3,
visible=True,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
for resource_id in (directory.id, policies.id, cases.id, uploads.id, policy_button.id, case_button.id):
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_report_menu(db: Session) -> None:
menu = _ensure_resource(
db,
parent_id=None,
resource_type="menu",
name="ReportCenter",
title="报告中心",
path="/reports",
component="/reports",
icon="ri:file-list-3-line",
permission="",
sort=10,
visible=True,
)
button = _ensure_resource(
db,
parent_id=menu.id,
resource_type="button",
name="report:edit",
title="保存",
path="",
component="",
icon="",
permission="report:edit",
sort=1,
visible=False,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)).all()
)
for resource_id in (menu.id, button.id):
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_discipline_menu(db: Session) -> None:
directory = _ensure_resource(
db,
parent_id=None,
resource_type="directory",
name="IndustryDiscipline",
title="行业自律",
path="/discipline",
component="",
icon="ri:shield-check-line",
permission="",
sort=11,
visible=True,
)
complaints = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="DisciplineComplaints",
title="投诉举报",
path="/discipline/complaints",
component="/discipline/complaints",
icon="ri:feedback-line",
permission="",
sort=1,
visible=True,
)
warnings = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="DisciplineWarnings",
title="警示案例",
path="/discipline/warnings",
component="/discipline/warnings",
icon="ri:alarm-warning-line",
permission="",
sort=2,
visible=True,
)
integrity = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="DisciplineIntegrity",
title="诚信档案",
path="/discipline/integrity",
component="/discipline/integrity",
icon="ri:medal-line",
permission="",
sort=3,
visible=True,
)
results = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name="DisciplineResults",
title="结果公示",
path="/discipline/results",
component="/discipline/results",
icon="ri:megaphone-line",
permission="",
sort=4,
visible=True,
)
handle_button = _ensure_resource(
db,
parent_id=complaints.id,
resource_type="button",
name="discipline:handle",
title="处理",
path="",
component="",
icon="",
permission="discipline:handle",
sort=1,
visible=False,
)
edit_buttons = []
for parent in (warnings, integrity, results):
edit_buttons.append(
_ensure_resource(
db,
parent_id=parent.id,
resource_type="button",
name="discipline:edit",
title="保存",
path="",
component="",
icon="",
permission="discipline:edit",
sort=1,
visible=False,
)
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
resource_ids = [
directory.id,
complaints.id,
warnings.id,
integrity.id,
results.id,
handle_button.id,
*(button.id for button in edit_buttons),
]
for resource_id in resource_ids:
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_edu_menu(db: Session) -> None:
menu = _ensure_resource(
db,
parent_id=None,
resource_type="menu",
name="ContinuingEducation",
title="继续教育",
path="/edu",
component="/edu",
icon="ri:graduation-cap-line",
permission="",
sort=4,
visible=True,
)
button = _ensure_resource(
db,
parent_id=menu.id,
resource_type="button",
name="edu:edit",
title="保存",
path="",
component="",
icon="",
permission="edu:edit",
sort=1,
visible=False,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
for resource_id in (menu.id, button.id):
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_tech_menu(db: Session) -> None:
tech = _ensure_resource(
db,
parent_id=None,
resource_type="menu",
name="TechExchange",
title="技术交流",
path="/tech",
component="/tech",
icon="ri:discuss-line",
permission="",
sort=7,
visible=True,
)
button = _ensure_resource(
db,
parent_id=tech.id,
resource_type="button",
name="tech:edit",
title="保存",
path="",
component="",
icon="",
permission="tech:edit",
sort=1,
visible=False,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
for resource_id in (tech.id, button.id):
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def ensure_about_menu(db: Session) -> None:
org_dir = _ensure_resource(
db,
parent_id=None,
resource_type="directory",
name="AboutOrgDir",
title="组织概况",
path="/about",
component="",
icon="ri:building-4-line",
permission="",
sort=3,
visible=True,
)
org_pages = [
_ensure_resource(
db,
parent_id=org_dir.id,
resource_type="menu",
name=name,
title=title,
path=path,
component=path,
icon=icon,
permission="",
sort=sort,
visible=True,
)
for name, title, path, icon, sort in ABOUT_ORG_MENUS
]
qualifications = _ensure_resource(
db,
parent_id=None,
resource_type="menu",
name="AboutQualifications",
title="资质荣誉",
path="/about/qualifications",
component="/about/qualifications",
icon="ri:medal-line",
permission="",
sort=4,
visible=True,
)
contact = _ensure_resource(
db,
parent_id=None,
resource_type="menu",
name="AboutContact",
title="联系合作",
path="/about/contact",
component="/about/contact",
icon="ri:contacts-line",
permission="",
sort=6,
visible=True,
)
button = _ensure_resource(
db,
parent_id=org_pages[0].id,
resource_type="button",
name="about:edit",
title="保存",
path="",
component="",
icon="",
permission="about:edit",
sort=1,
visible=False,
)
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
db.flush()
return
linked = set(
db.scalars(
select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)
).all()
)
resource_ids = (
org_dir.id,
button.id,
qualifications.id,
contact.id,
*(page.id for page in org_pages),
)
for resource_id in resource_ids:
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
db.flush()
def seed_superuser(db: Session, username: str, password: str) -> None:
username = username.strip()
if not username:
raise HTTPException(status_code=400, detail="请填写用户名")
if len(username) > 64:
raise HTTPException(status_code=400, detail="用户名过长")
validate_password(password)
now = utcnow()
role = db.scalar(select(AdminRole).where(AdminRole.code == SUPER_CODE))
if role is None:
role = AdminRole(
code=SUPER_CODE,
name=SUPER_NAME,
description="系统内置角色",
status="active",
created_at=now,
updated_at=now,
)
db.add(role)
db.flush()
else:
role.name = SUPER_NAME
role.status = "active"
if not role.description:
role.description = "系统内置角色"
role.updated_at = now
directory = _ensure_resource(
db,
parent_id=None,
resource_type="directory",
name="System",
title="系统管理",
path="/system",
component="",
icon="ri:settings-3-line",
permission="",
sort=1,
visible=True,
)
resource_ids = [directory.id]
for page in PAGES:
menu = _ensure_resource(
db,
parent_id=directory.id,
resource_type="menu",
name=page["name"],
title=page["title"],
path=page["path"],
component=page["component"],
icon="",
permission="",
sort=page["sort"],
visible=True,
)
resource_ids.append(menu.id)
for index, (code, title) in enumerate(page["buttons"], start=1):
button = _ensure_resource(
db,
parent_id=menu.id,
resource_type="button",
name=code,
title=title,
path="",
component="",
icon="",
permission=code,
sort=index,
visible=False,
)
resource_ids.append(button.id)
linked = set(
db.scalars(select(AdminRoleResource.resource_id).where(AdminRoleResource.role_id == role.id)).all()
)
for resource_id in resource_ids:
if resource_id not in linked:
db.add(AdminRoleResource(role_id=role.id, resource_id=resource_id))
user = db.scalar(select(AdminUser).where(AdminUser.username == username))
password_hash = hash_password(password)
if user is None:
user = AdminUser(
username=username,
password_hash=password_hash,
display_name=username,
mobile_mask="",
status="active",
failed_login_count=0,
locked_until=None,
created_at=now,
updated_at=now,
)
db.add(user)
db.flush()
else:
user.password_hash = password_hash
user.status = "active"
user.failed_login_count = 0
user.locked_until = None
if not user.display_name:
user.display_name = username
user.updated_at = now
if db.get(AdminUserRole, (user.id, role.id)) is None:
db.add(AdminUserRole(user_id=user.id, role_id=role.id))
db.flush()
ensure_home_menu(db)
def main() -> int:
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
username = os.environ.get("BOOTSTRAP_ADMIN_USERNAME", "").strip()
password = os.environ.get("BOOTSTRAP_ADMIN_PASSWORD", "")
if username == "" or password == "":
print("缺少环境变量 BOOTSTRAP_ADMIN_USERNAME 或 BOOTSTRAP_ADMIN_PASSWORD", file=sys.stderr)
return 1
try:
validate_password(password)
except HTTPException as exc:
print(exc.detail, file=sys.stderr)
return 1
db: Session | None = None
try:
import app.models.content # noqa: F401
import app.models.security # noqa: F401
from app.models.content import ensure_site_defaults
from app.services.ip_guard import seed_ip_blacklist
engine = get_engine()
Base.metadata.create_all(bind=engine)
SessionLocal.configure(bind=engine)
db = SessionLocal()
ensure_site_defaults(db)
seed_ip_blacklist(db)
seed_superuser(db, username, password)
db.commit()
except RuntimeError as exc:
print(str(exc), file=sys.stderr)
return 1
except HTTPException as exc:
if db is not None:
db.rollback()
print(exc.detail, file=sys.stderr)
return 1
except Exception:
if db is not None:
db.rollback()
print("创建超级管理员失败", file=sys.stderr)
return 1
finally:
if db is not None:
db.close()
print(f"超级管理员已就绪:{username}")
return 0
if __name__ == "__main__":
raise SystemExit(main())