From 24c799f08eaa5930f9831c113e4cb681cf65a529 Mon Sep 17 00:00:00 2001 From: SZCJW <792430652@qq.com> Date: Wed, 4 Mar 2026 01:08:00 +0800 Subject: [PATCH] init --- config/settings.py | 8 +- src/api/auth_routes.py | 495 ++++++++++++++++++++++++++++++++++------ src/database/init_db.py | 157 ++++++++++--- src/models/database.py | 93 +++++++- static/style.css | 140 ++++++++++++ static/vue-app.js | 281 ++++++++++++++--------- 6 files changed, 960 insertions(+), 214 deletions(-) diff --git a/config/settings.py b/config/settings.py index dfc4cd4..fb7dda7 100644 --- a/config/settings.py +++ b/config/settings.py @@ -69,7 +69,13 @@ class Settings: # JWT配置 self.SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production') self.ALGORITHM = os.getenv('ALGORITHM', 'HS256') - self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', '1440')) # 24小时 + self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', '1440')) + + # 管理员账户配置 + self.ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin') + self.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123') + self.ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@gemold.com') + self.ADMIN_FULL_NAME = os.getenv('ADMIN_FULL_NAME', '系统管理员') @property def DATABASE_URL(self) -> str: diff --git a/src/api/auth_routes.py b/src/api/auth_routes.py index 476d26d..92efbc9 100644 --- a/src/api/auth_routes.py +++ b/src/api/auth_routes.py @@ -1,40 +1,33 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession -from pydantic import BaseModel, EmailStr -from typing import Optional +from pydantic import BaseModel +from typing import Optional, List from datetime import timedelta +from sqlalchemy import select from database.database import get_db_session from services.auth_service import ( authenticate_user, create_access_token, - create_user, - get_user_by_username, - get_user_by_email, get_current_active_user, - get_current_admin_user + get_password_hash ) -from models.database import User +from models.database import User, Role, Permission, UserRole, RolePermission from config.settings import settings +from utils.logger import get_logger +logger = get_logger(__name__) router = APIRouter(prefix="/api/auth", tags=["认证"]) -class UserCreate(BaseModel): - username: str - email: EmailStr - password: str - full_name: Optional[str] = None - - class UserResponse(BaseModel): id: int username: str email: str full_name: Optional[str] is_active: bool - is_superuser: bool + roles: List[str] class Config: from_attributes = True @@ -51,6 +44,63 @@ class LoginRequest(BaseModel): password: str +class RoleCreate(BaseModel): + code: str + name: str + description: Optional[str] = None + + +class RoleResponse(BaseModel): + id: int + code: str + name: str + description: Optional[str] + is_system: bool + permissions: List[str] + + class Config: + from_attributes = True + + +class PermissionCreate(BaseModel): + code: str + name: str + module: Optional[str] = None + description: Optional[str] = None + + +class PermissionResponse(BaseModel): + id: int + code: str + name: str + module: Optional[str] + description: Optional[str] + + class Config: + from_attributes = True + + +class UserCreate(BaseModel): + username: str + email: str + password: str + full_name: Optional[str] = None + role_ids: List[int] = [] + + +class UserUpdate(BaseModel): + email: Optional[str] = None + full_name: Optional[str] = None + is_active: Optional[bool] = None + role_ids: Optional[List[int]] = None + + +def check_admin(user: User) -> bool: + if not user.is_superuser: + raise HTTPException(status_code=403, detail="需要管理员权限") + return True + + @router.post("/login", response_model=Token) async def login( form_data: OAuth2PasswordRequestForm = Depends(), @@ -72,7 +122,14 @@ async def login( return Token( access_token=access_token, token_type="bearer", - user=UserResponse.from_orm(user) + user=UserResponse( + id=user.id, + username=user.username, + email=user.email, + full_name=user.full_name, + is_active=user.is_active, + roles=[r.code for r in user.roles] + ) ) @@ -96,39 +153,29 @@ async def login_json( return Token( access_token=access_token, token_type="bearer", - user=UserResponse.from_orm(user) + user=UserResponse( + id=user.id, + username=user.username, + email=user.email, + full_name=user.full_name, + is_active=user.is_active, + roles=[r.code for r in user.roles] + ) ) -@router.post("/register", response_model=UserResponse, status_code=201) -async def register( - user_data: UserCreate, - db_session: AsyncSession = Depends(get_db_session) -): - existing_user = await get_user_by_username(db_session, user_data.username) - if existing_user: - raise HTTPException(status_code=400, detail="用户名已存在") - - existing_email = await get_user_by_email(db_session, user_data.email) - if existing_email: - raise HTTPException(status_code=400, detail="邮箱已被注册") - - user = await create_user( - db_session=db_session, - username=user_data.username, - email=user_data.email, - password=user_data.password, - full_name=user_data.full_name - ) - - return UserResponse.from_orm(user) - - @router.get("/me", response_model=UserResponse) async def get_current_user_info( current_user: User = Depends(get_current_active_user) ): - return UserResponse.from_orm(current_user) + return UserResponse( + id=current_user.id, + username=current_user.username, + email=current_user.email, + full_name=current_user.full_name, + is_active=current_user.is_active, + roles=[r.code for r in current_user.roles] + ) @router.post("/logout") @@ -136,58 +183,368 @@ async def logout(): return {"message": "已登出"} -@router.get("/users", response_model=list[UserResponse]) +@router.get("/users", response_model=List[UserResponse]) async def list_users( db_session: AsyncSession = Depends(get_db_session), - admin_user: User = Depends(get_current_admin_user) + current_user: User = Depends(get_current_active_user) ): - from sqlalchemy import select - result = await db_session.execute(select(User)) + check_admin(current_user) + result = await db_session.execute( + select(User).options( + select(User).options() + ) + ) users = result.scalars().all() - return [UserResponse.from_orm(u) for u in users] + return [ + UserResponse( + id=u.id, + username=u.username, + email=u.email, + full_name=u.full_name, + is_active=u.is_active, + roles=[r.code for r in u.roles] + ) for u in users + ] -@router.put("/users/{user_id}/toggle-active", response_model=UserResponse) -async def toggle_user_active( - user_id: int, +@router.post("/users", response_model=UserResponse, status_code=201) +async def create_user( + user_data: UserCreate, db_session: AsyncSession = Depends(get_db_session), - admin_user: User = Depends(get_current_admin_user) + current_user: User = Depends(get_current_active_user) ): - from sqlalchemy import select - result = await db_session.execute(select(User).where(User.id == user_id)) - user = result.scalar_one_or_none() + check_admin(current_user) - if not user: - raise HTTPException(status_code=404, detail="用户不存在") + existing = await db_session.execute( + select(User).where(User.username == user_data.username) + ) + if existing.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="用户名已存在") - if user.id == admin_user.id: - raise HTTPException(status_code=400, detail="不能禁用自己的账户") + existing_email = await db_session.execute( + select(User).where(User.email == user_data.email) + ) + if existing_email.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="邮箱已存在") + + user = User( + username=user_data.username, + email=user_data.email, + hashed_password=get_password_hash(user_data.password), + full_name=user_data.full_name, + is_active=True + ) + db_session.add(user) + await db_session.flush() + + for role_id in user_data.role_ids: + user_role = UserRole(user_id=user.id, role_id=role_id) + db_session.add(user_role) - user.is_active = not user.is_active await db_session.commit() await db_session.refresh(user) - return UserResponse.from_orm(user) + logger.info(f"管理员 {current_user.username} 创建了用户 {user.username}") + + return UserResponse( + id=user.id, + username=user.username, + email=user.email, + full_name=user.full_name, + is_active=user.is_active, + roles=[r.code for r in user.roles] + ) -@router.put("/users/{user_id}/toggle-admin", response_model=UserResponse) -async def toggle_user_admin( +@router.put("/users/{user_id}", response_model=UserResponse) +async def update_user( user_id: int, + user_data: UserUpdate, db_session: AsyncSession = Depends(get_db_session), - admin_user: User = Depends(get_current_admin_user) + current_user: User = Depends(get_current_active_user) ): - from sqlalchemy import select + check_admin(current_user) + result = await db_session.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() - if not user: raise HTTPException(status_code=404, detail="用户不存在") - if user.id == admin_user.id: - raise HTTPException(status_code=400, detail="不能修改自己的管理员权限") + if user_data.email is not None: + user.email = user_data.email + if user_data.full_name is not None: + user.full_name = user_data.full_name + if user_data.is_active is not None: + user.is_active = user_data.is_active + + if user_data.role_ids is not None: + await db_session.execute( + select(UserRole).where(UserRole.user_id == user_id) + ) + for ur in (await db_session.execute(select(UserRole).where(UserRole.user_id == user_id))).scalars().all(): + await db_session.delete(ur) + + for role_id in user_data.role_ids: + user_role = UserRole(user_id=user.id, role_id=role_id) + db_session.add(user_role) - user.is_superuser = not user.is_superuser await db_session.commit() await db_session.refresh(user) - return UserResponse.from_orm(user) + logger.info(f"管理员 {current_user.username} 更新了用户 {user.username}") + + return UserResponse( + id=user.id, + username=user.username, + email=user.email, + full_name=user.full_name, + is_active=user.is_active, + roles=[r.code for r in user.roles] + ) + + +@router.delete("/users/{user_id}") +async def delete_user( + user_id: int, + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + + result = await db_session.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + if user.id == current_user.id: + raise HTTPException(status_code=400, detail="不能删除自己的账户") + + username = user.username + await db_session.delete(user) + await db_session.commit() + + logger.info(f"管理员 {current_user.username} 删除了用户 {username}") + + return {"message": "用户已删除"} + + +@router.put("/users/{user_id}/reset-password") +async def reset_user_password( + user_id: int, + new_password: str, + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + + result = await db_session.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + user.hashed_password = get_password_hash(new_password) + await db_session.commit() + + logger.info(f"管理员 {current_user.username} 重置了用户 {user.username} 的密码") + + return {"message": "密码已重置"} + + +@router.get("/roles", response_model=List[RoleResponse]) +async def list_roles( + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + result = await db_session.execute(select(Role)) + roles = result.scalars().all() + return [ + RoleResponse( + id=r.id, + code=r.code, + name=r.name, + description=r.description, + is_system=r.is_system, + permissions=[p.code for p in r.permissions] + ) for r in roles + ] + + +@router.post("/roles", response_model=RoleResponse, status_code=201) +async def create_role( + role_data: RoleCreate, + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + + existing = await db_session.execute( + select(Role).where(Role.code == role_data.code) + ) + if existing.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="角色编码已存在") + + role = Role( + code=role_data.code, + name=role_data.name, + description=role_data.description + ) + db_session.add(role) + await db_session.commit() + await db_session.refresh(role) + + logger.info(f"管理员 {current_user.username} 创建了角色 {role.code}") + + return RoleResponse( + id=role.id, + code=role.code, + name=role.name, + description=role.description, + is_system=role.is_system, + permissions=[] + ) + + +@router.put("/roles/{role_id}", response_model=RoleResponse) +async def update_role( + role_id: int, + role_data: RoleCreate, + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + + result = await db_session.execute(select(Role).where(Role.id == role_id)) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + if role.is_system: + raise HTTPException(status_code=400, detail="系统角色不能修改") + + role.name = role_data.name + role.description = role_data.description + await db_session.commit() + await db_session.refresh(role) + + return RoleResponse( + id=role.id, + code=role.code, + name=role.name, + description=role.description, + is_system=role.is_system, + permissions=[p.code for p in role.permissions] + ) + + +@router.delete("/roles/{role_id}") +async def delete_role( + role_id: int, + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + + result = await db_session.execute(select(Role).where(Role.id == role_id)) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + if role.is_system: + raise HTTPException(status_code=400, detail="系统角色不能删除") + + await db_session.delete(role) + await db_session.commit() + + logger.info(f"管理员 {current_user.username} 删除了角色 {role.code}") + + return {"message": "角色已删除"} + + +@router.put("/roles/{role_id}/permissions") +async def set_role_permissions( + role_id: int, + permission_ids: List[int], + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + + result = await db_session.execute(select(Role).where(Role.id == role_id)) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + for rp in (await db_session.execute(select(RolePermission).where(RolePermission.role_id == role_id))).scalars().all(): + await db_session.delete(rp) + + for perm_id in permission_ids: + rp = RolePermission(role_id=role_id, permission_id=perm_id) + db_session.add(rp) + + await db_session.commit() + + logger.info(f"管理员 {current_user.username} 更新了角色 {role.code} 的权限") + + return {"message": "权限已更新"} + + +@router.get("/permissions", response_model=List[PermissionResponse]) +async def list_permissions( + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + result = await db_session.execute(select(Permission)) + permissions = result.scalars().all() + return [PermissionResponse.from_orm(p) for p in permissions] + + +@router.post("/permissions", response_model=PermissionResponse, status_code=201) +async def create_permission( + perm_data: PermissionCreate, + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + + existing = await db_session.execute( + select(Permission).where(Permission.code == perm_data.code) + ) + if existing.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="权限编码已存在") + + permission = Permission( + code=perm_data.code, + name=perm_data.name, + module=perm_data.module, + description=perm_data.description + ) + db_session.add(permission) + await db_session.commit() + await db_session.refresh(permission) + + logger.info(f"管理员 {current_user.username} 创建了权限 {permission.code}") + + return PermissionResponse.from_orm(permission) + + +@router.delete("/permissions/{permission_id}") +async def delete_permission( + permission_id: int, + db_session: AsyncSession = Depends(get_db_session), + current_user: User = Depends(get_current_active_user) +): + check_admin(current_user) + + result = await db_session.execute(select(Permission).where(Permission.id == permission_id)) + permission = result.scalar_one_or_none() + if not permission: + raise HTTPException(status_code=404, detail="权限不存在") + + await db_session.delete(permission) + await db_session.commit() + + logger.info(f"管理员 {current_user.username} 删除了权限 {permission.code}") + + return {"message": "权限已删除"} diff --git a/src/database/init_db.py b/src/database/init_db.py index edf7dbc..260de7f 100644 --- a/src/database/init_db.py +++ b/src/database/init_db.py @@ -1,42 +1,108 @@ -# database/init_db.py import asyncio -from database.database import db_manager -from utils.logger import get_logger from sqlalchemy import select -from models.database import User +from database.database import db_manager +from models.database import User, Role, Permission, UserRole, RolePermission from services.auth_service import get_password_hash +from config.settings import settings +from utils.logger import get_logger logger = get_logger(__name__) +DEFAULT_PERMISSIONS = [ + {"code": "view_dashboard", "name": "查看仪表盘", "module": "dashboard"}, + {"code": "view_moldinsight", "name": "使用模具分析", "module": "moldinsight"}, + {"code": "upload_file", "name": "上传文件", "module": "moldinsight"}, + {"code": "view_history", "name": "查看历史记录", "module": "moldinsight"}, + {"code": "view_inventory", "name": "查看库存", "module": "inventory"}, + {"code": "manage_inventory", "name": "管理库存", "module": "inventory"}, + {"code": "view_products", "name": "查看产品", "module": "inventory"}, + {"code": "manage_products", "name": "管理产品", "module": "inventory"}, + {"code": "view_suppliers", "name": "查看供应商", "module": "inventory"}, + {"code": "manage_suppliers", "name": "管理供应商", "module": "inventory"}, + {"code": "view_customers", "name": "查看客户", "module": "inventory"}, + {"code": "manage_customers", "name": "管理客户", "module": "inventory"}, + {"code": "view_users", "name": "查看用户", "module": "admin"}, + {"code": "manage_users", "name": "管理用户", "module": "admin"}, + {"code": "manage_roles", "name": "管理角色", "module": "admin"}, +] -async def create_default_admin(): - """创建默认管理员账户""" - try: - async with db_manager.session() as session: - result = await session.execute( - select(User).where(User.username == "admin") - ) - existing_admin = result.scalar_one_or_none() - - if existing_admin: - logger.info("管理员账户已存在") - return - - admin = User( - username="admin", - email="admin@gemold.com", - hashed_password=get_password_hash("admin123"), - full_name="系统管理员", - is_active=True, - is_superuser=True - ) - - session.add(admin) - await session.commit() - logger.info("默认管理员账户创建成功 (用户名: admin, 密码: admin123)") - - except Exception as e: - logger.error(f"创建默认管理员失败: {e}") +DEFAULT_ROLES = [ + {"code": "admin", "name": "管理员", "description": "系统管理员,拥有所有权限", "is_system": True, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "manage_inventory", "view_products", "manage_products", "view_suppliers", "manage_suppliers", "view_customers", "manage_customers", "view_users", "manage_users", "manage_roles"]}, + {"code": "user", "name": "普通用户", "description": "普通用户,可使用模具分析和查看库存", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers"]}, + {"code": "viewer", "name": "只读用户", "description": "只读用户,只能查看数据", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers"]}, +] + + +async def init_permissions(session): + """初始化权限""" + result = await session.execute(select(Permission)) + existing_perms = result.scalars().all() + + if existing_perms: + logger.info("权限已初始化") + return + + perm_map = {} + for perm_data in DEFAULT_PERMISSIONS: + perm = Permission(**perm_data) + session.add(perm) + await session.flush() + perm_map[perm.code] = perm.id + + logger.info(f"创建了 {len(DEFAULT_PERMISSIONS)} 个权限") + return perm_map + + +async def init_roles(session, perm_map): + """初始化角色""" + result = await session.execute(select(Role)) + existing_roles = result.scalars().all() + + if existing_roles: + logger.info("角色已初始化") + return + + for role_data in DEFAULT_ROLES: + perm_ids = [perm_map[code] for code in role_data.pop("permissions")] + role = Role(**role_data) + session.add(role) + await session.flush() + + for perm_id in perm_ids: + rp = RolePermission(role_id=role.id, permission_id=perm_id) + session.add(rp) + + logger.info(f"创建了 {len(DEFAULT_ROLES)} 个角色") + + +async def create_admin_user(session): + """创建默认管理员""" + result = await session.execute(select(User).where(User.username == settings.ADMIN_USERNAME)) + existing_admin = result.scalar_one_or_none() + + if existing_admin: + logger.info("管理员账户已存在") + return + + admin = User( + username=settings.ADMIN_USERNAME, + email=settings.ADMIN_EMAIL, + hashed_password=get_password_hash(settings.ADMIN_PASSWORD), + full_name=settings.ADMIN_FULL_NAME, + is_active=True + ) + session.add(admin) + await session.flush() + + result = await session.execute(select(Role).where(Role.code == "admin")) + admin_role = result.scalar_one_or_none() + + if admin_role: + user_role = UserRole(user_id=admin.id, role_id=admin_role.id) + session.add(user_role) + + await session.commit() + logger.info(f"创建了管理员账户: {settings.ADMIN_USERNAME}") async def init_database(): @@ -44,13 +110,36 @@ async def init_database(): try: await db_manager.connect() await db_manager.create_tables() - await create_default_admin() + + async with db_manager.session() as session: + perm_map = await init_permissions(session) + await init_roles(session, perm_map) + await create_admin_user(session) + logger.info("数据库初始化完成") + print("=" * 60) + print("数据库初始化成功!") + print("=" * 60) + print(f"管理员用户名: {settings.ADMIN_USERNAME}") + print(f"管理员密码: {settings.ADMIN_PASSWORD}") + print(f"管理员邮箱: {settings.ADMIN_EMAIL}") + print("=" * 60) + print("可以在 .env 文件中修改管理员配置:") + print(" ADMIN_USERNAME") + print(" ADMIN_PASSWORD") + print(" ADMIN_EMAIL") + print(" ADMIN_FULL_NAME") + print("=" * 60) + return True except Exception as e: logger.error(f"数据库初始化失败: {e}") + print(f"数据库初始化失败: {e}") return False + finally: + await db_manager.disconnect() + if __name__ == "__main__": - asyncio.run(init_database()) \ No newline at end of file + asyncio.run(init_database()) diff --git a/src/models/database.py b/src/models/database.py index 5b43895..c100e13 100644 --- a/src/models/database.py +++ b/src/models/database.py @@ -17,15 +17,102 @@ class User(Base): hashed_password = Column(String(255), nullable=False) full_name = Column(String(100)) is_active = Column(Boolean, default=True) - is_superuser = Column(Boolean, default=False) created_at = Column(DateTime, default=func.now()) last_login = Column(DateTime, nullable=True) - # 关联关系 stp_files = relationship("STPFile", back_populates="user") + user_roles = relationship("UserRole", back_populates="user", cascade="all, delete-orphan") + + @property + def roles(self): + return [ur.role for ur in self.user_roles] + + @property + def is_superuser(self): + return any(r.code == 'admin' for r in self.roles) + + def has_permission(self, permission_code: str) -> bool: + if self.is_superuser: + return True + for role in self.roles: + for perm in role.permissions: + if perm.code == permission_code: + return True + return False def __repr__(self): - return f"" + return f"" + + +class Role(Base): + """角色表""" + __tablename__ = "roles" + + id = Column(Integer, primary_key=True, index=True) + code = Column(String(50), unique=True, index=True, nullable=False) + name = Column(String(100), nullable=False) + description = Column(Text, nullable=True) + is_system = Column(Boolean, default=False) + created_at = Column(DateTime, default=func.now()) + + user_roles = relationship("UserRole", back_populates="role", cascade="all, delete-orphan") + role_permissions = relationship("RolePermission", back_populates="role", cascade="all, delete-orphan") + + @property + def permissions(self): + return [rp.permission for rp in self.role_permissions] + + def __repr__(self): + return f"" + + +class Permission(Base): + """权限表""" + __tablename__ = "permissions" + + id = Column(Integer, primary_key=True, index=True) + code = Column(String(100), unique=True, index=True, nullable=False) + name = Column(String(100), nullable=False) + module = Column(String(50), nullable=True) + description = Column(Text, nullable=True) + created_at = Column(DateTime, default=func.now()) + + role_permissions = relationship("RolePermission", back_populates="permission", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + +class UserRole(Base): + """用户角色关联表""" + __tablename__ = "user_roles" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + role_id = Column(Integer, ForeignKey("roles.id"), nullable=False, index=True) + created_at = Column(DateTime, default=func.now()) + + user = relationship("User", back_populates="user_roles") + role = relationship("Role", back_populates="user_roles") + + def __repr__(self): + return f"" + + +class RolePermission(Base): + """角色权限关联表""" + __tablename__ = "role_permissions" + + id = Column(Integer, primary_key=True, index=True) + role_id = Column(Integer, ForeignKey("roles.id"), nullable=False, index=True) + permission_id = Column(Integer, ForeignKey("permissions.id"), nullable=False, index=True) + created_at = Column(DateTime, default=func.now()) + + role = relationship("Role", back_populates="role_permissions") + permission = relationship("Permission", back_populates="role_permissions") + + def __repr__(self): + return f"" class STPFile(Base): """STP源文件元数据表""" diff --git a/static/style.css b/static/style.css index 0a62696..4223bc6 100644 --- a/static/style.css +++ b/static/style.css @@ -870,6 +870,141 @@ a:hover { color: var(--text-secondary); } +/* ================================ + 模态框 + ================================ */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.2s ease; +} + +.modal-content { + background: var(--bg-primary); + border-radius: var(--radius-lg); + width: 90%; + max-width: 500px; + max-height: 90vh; + overflow: auto; + animation: slideUp 0.3s ease; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-6); + border-bottom: 1px solid var(--border-light); +} + +.modal-header h2 { + font-size: var(--text-xl); + font-weight: 600; +} + +.modal-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--text-secondary); + font-size: 24px; + cursor: pointer; + border-radius: var(--radius-md); + transition: all 0.2s; +} + +.modal-close:hover { + background: var(--gray-100); + color: var(--text-primary); +} + +.modal-body { + padding: var(--space-6); +} + +.modal-footer { + display: flex; + gap: var(--space-3); + justify-content: flex-end; + padding: var(--space-6); + border-top: 1px solid var(--border-light); +} + +/* ================================ + 复选框组 + ================================ */ +.checkbox-group { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); +} + +.checkbox-label { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + background: var(--gray-100); + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.2s; +} + +.checkbox-label:hover { + background: var(--gray-200); +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; +} + +/* ================================ + 按钮变体 + ================================ */ +.btn-error { + background: var(--error); + color: white; + border: none; + padding: var(--space-2) var(--space-4); + border-radius: var(--radius-md); + font-size: var(--text-sm); + cursor: pointer; + transition: all 0.2s; +} + +.btn-error:hover { + background: #dc2626; +} + +.btn-secondary { + background: var(--gray-200); + color: var(--text-primary); + border: none; + padding: var(--space-2) var(--space-4); + border-radius: var(--radius-md); + font-size: var(--text-sm); + cursor: pointer; + transition: all 0.2s; +} + +.btn-secondary:hover { + background: var(--gray-300); +} + /* ================================ 进度条 ================================ */ @@ -1489,6 +1624,11 @@ a:hover { color: var(--primary-700); } +.auth-tip { + color: var(--text-tertiary); + font-size: var(--text-sm); +} + .page-container { max-width: 1200px; margin: 0 auto; diff --git a/static/vue-app.js b/static/vue-app.js index 8928c27..7e2f497 100644 --- a/static/vue-app.js +++ b/static/vue-app.js @@ -271,11 +271,8 @@ const LoginView = { setup() { const router = useRouter(); const state = reactive({ - isLogin: true, username: '', password: '', - email: '', - full_name: '', loading: false, error: '' }); @@ -296,51 +293,24 @@ const LoginView = { state.error = ''; try { - if (state.isLogin) { - const res = await fetch('/api/auth/login/json', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: state.username, - password: state.password - }) - }); - - if (!res.ok) { - const error = await res.json(); - throw new Error(error.detail || '登录失败'); - } - - const data = await res.json(); - saveAuth(data.access_token, data.user); - addNotification('登录成功', 'success'); - router.push('/'); - } else { - if (!state.email) { - state.error = '请填写邮箱'; - state.loading = false; - return; - } - - const res = await fetch('/api/auth/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: state.username, - password: state.password, - email: state.email, - full_name: state.full_name || null - }) - }); - - if (!res.ok) { - const error = await res.json(); - throw new Error(error.detail || '注册失败'); - } - - addNotification('注册成功,请登录', 'success'); - state.isLogin = true; + const res = await fetch('/api/auth/login/json', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: state.username, + password: state.password + }) + }); + + if (!res.ok) { + const error = await res.json(); + throw new Error(error.detail || '登录失败'); } + + const data = await res.json(); + saveAuth(data.access_token, data.user); + addNotification('登录成功', 'success'); + router.push('/'); } catch (e) { state.error = e.message; addNotification(e.message, 'error'); @@ -356,8 +326,8 @@ const LoginView = {
-

{{ state.isLogin ? '登录' : '注册' }}

-

{{ state.isLogin ? '登录到 Gemold 系统' : '创建新账户' }}

+

登录

+

登录到 Gemold 系统

@@ -371,47 +341,25 @@ const LoginView = { />
-
- - -
- -
- - -
-
{{ state.error }}
@@ -566,7 +514,17 @@ const UsersView = { const router = useRouter(); const state = reactive({ users: [], - loading: true + roles: [], + loading: true, + showUserModal: false, + editingUser: null, + userForm: { + username: '', + email: '', + password: '', + full_name: '', + role_ids: [] + } }); const loadUsers = async () => { @@ -579,43 +537,123 @@ const UsersView = { } }; - const toggleActive = async (user) => { + const loadRoles = async () => { try { - const updated = await apiRequest(`/api/auth/users/${user.id}/toggle-active`, { method: 'PUT' }); - const index = state.users.findIndex(u => u.id === user.id); - if (index > -1) state.users[index] = updated; - addNotification(`用户 ${user.username} 已${updated.is_active ? '启用' : '禁用'}`, 'success'); + state.roles = await apiRequest('/api/auth/roles'); } catch (e) { - handleApiError(e, '切换用户状态'); + handleApiError(e, '加载角色列表'); } }; - const toggleAdmin = async (user) => { + const openUserModal = (user = null) => { + state.editingUser = user; + if (user) { + state.userForm = { + username: user.username, + email: user.email, + password: '', + full_name: user.full_name || '', + role_ids: user.roles.map(r => { + const role = state.roles.find(role => role.code === r); + return role ? role.id : null; + }).filter(id => id !== null) + }; + } else { + state.userForm = { + username: '', + email: '', + password: '', + full_name: '', + role_ids: [] + }; + } + state.showUserModal = true; + }; + + const saveUser = async () => { + if (!state.userForm.username || !state.userForm.email) { + addNotification('请填写用户名和邮箱', 'error'); + return; + } + if (!state.editingUser && !state.userForm.password) { + addNotification('请填写密码', 'error'); + return; + } + try { - const updated = await apiRequest(`/api/auth/users/${user.id}/toggle-admin`, { method: 'PUT' }); - const index = state.users.findIndex(u => u.id === user.id); - if (index > -1) state.users[index] = updated; - addNotification(`用户 ${user.username} ${updated.is_superuser ? '已设为管理员' : '已取消管理员'}`, 'success'); + if (state.editingUser) { + await apiRequest(`/api/auth/users/${state.editingUser.id}`, { + method: 'PUT', + body: JSON.stringify({ + email: state.userForm.email, + full_name: state.userForm.full_name || null, + role_ids: state.userForm.role_ids + }) + }); + addNotification('用户更新成功', 'success'); + } else { + await apiRequest('/api/auth/users', { + method: 'POST', + body: JSON.stringify(state.userForm) + }); + addNotification('用户创建成功', 'success'); + } + state.showUserModal = false; + loadUsers(); } catch (e) { - handleApiError(e, '切换管理员权限'); + handleApiError(e, '保存用户'); } }; - onMounted(() => { + const deleteUser = async (user) => { + if (!confirm(`确定要删除用户 ${user.username} 吗?`)) return; + + try { + await apiRequest(`/api/auth/users/${user.id}`, { method: 'DELETE' }); + addNotification('用户已删除', 'success'); + loadUsers(); + } catch (e) { + handleApiError(e, '删除用户'); + } + }; + + const resetPassword = async (user) => { + const newPassword = prompt(`请输入 ${user.username} 的新密码:`); + if (!newPassword || newPassword.length < 6) { + addNotification('密码长度至少6位', 'error'); + return; + } + + try { + await apiRequest(`/api/auth/users/${user.id}/reset-password`, { + method: 'PUT', + body: JSON.stringify(newPassword) + }); + addNotification('密码已重置', 'success'); + } catch (e) { + handleApiError(e, '重置密码'); + } + }; + + onMounted(async () => { if (!appState.user?.is_superuser) { router.push('/'); return; } + await loadRoles(); loadUsers(); }); - return { state, appState, toggleActive, toggleAdmin, formatDateTime }; + return { state, appState, openUserModal, saveUser, deleteUser, resetPassword, formatDateTime }; }, template: `
@@ -647,33 +685,62 @@ const UsersView = { - - {{ user.is_superuser ? '管理员' : '普通用户' }} + + {{ role }} {{ formatDateTime(user.created_at) }}
- - + + +
+ +
` };