This commit is contained in:
2026-03-04 01:08:00 +08:00
parent 006a30ea0c
commit 24c799f08e
6 changed files with 960 additions and 214 deletions
+7 -1
View File
@@ -69,7 +69,13 @@ class Settings:
# JWT配置 # JWT配置
self.SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production') self.SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production')
self.ALGORITHM = os.getenv('ALGORITHM', 'HS256') 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 @property
def DATABASE_URL(self) -> str: def DATABASE_URL(self) -> str:
+426 -69
View File
@@ -1,40 +1,33 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, EmailStr from pydantic import BaseModel
from typing import Optional from typing import Optional, List
from datetime import timedelta from datetime import timedelta
from sqlalchemy import select
from database.database import get_db_session from database.database import get_db_session
from services.auth_service import ( from services.auth_service import (
authenticate_user, authenticate_user,
create_access_token, create_access_token,
create_user,
get_user_by_username,
get_user_by_email,
get_current_active_user, 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 config.settings import settings
from utils.logger import get_logger
logger = get_logger(__name__)
router = APIRouter(prefix="/api/auth", tags=["认证"]) router = APIRouter(prefix="/api/auth", tags=["认证"])
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
full_name: Optional[str] = None
class UserResponse(BaseModel): class UserResponse(BaseModel):
id: int id: int
username: str username: str
email: str email: str
full_name: Optional[str] full_name: Optional[str]
is_active: bool is_active: bool
is_superuser: bool roles: List[str]
class Config: class Config:
from_attributes = True from_attributes = True
@@ -51,6 +44,63 @@ class LoginRequest(BaseModel):
password: str 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) @router.post("/login", response_model=Token)
async def login( async def login(
form_data: OAuth2PasswordRequestForm = Depends(), form_data: OAuth2PasswordRequestForm = Depends(),
@@ -72,7 +122,14 @@ async def login(
return Token( return Token(
access_token=access_token, access_token=access_token,
token_type="bearer", 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( return Token(
access_token=access_token, access_token=access_token,
token_type="bearer", 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) @router.get("/me", response_model=UserResponse)
async def get_current_user_info( async def get_current_user_info(
current_user: User = Depends(get_current_active_user) 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") @router.post("/logout")
@@ -136,58 +183,368 @@ async def logout():
return {"message": "已登出"} return {"message": "已登出"}
@router.get("/users", response_model=list[UserResponse]) @router.get("/users", response_model=List[UserResponse])
async def list_users( async def list_users(
db_session: AsyncSession = Depends(get_db_session), 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)) result = await db_session.execute(
select(User).options(
select(User).options()
)
)
users = result.scalars().all() 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) @router.post("/users", response_model=UserResponse, status_code=201)
async def toggle_user_active( async def create_user(
user_id: int, user_data: UserCreate,
db_session: AsyncSession = Depends(get_db_session), 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: existing = await db_session.execute(
raise HTTPException(status_code=404, detail="用户不存在") 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: existing_email = await db_session.execute(
raise HTTPException(status_code=400, detail="不能禁用自己的账户") 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.commit()
await db_session.refresh(user) 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) @router.put("/users/{user_id}", response_model=UserResponse)
async def toggle_user_admin( async def update_user(
user_id: int, user_id: int,
user_data: UserUpdate,
db_session: AsyncSession = Depends(get_db_session), 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)) result = await db_session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
if not user: if not user:
raise HTTPException(status_code=404, detail="用户不存在") raise HTTPException(status_code=404, detail="用户不存在")
if user.id == admin_user.id: if user_data.email is not None:
raise HTTPException(status_code=400, detail="不能修改自己的管理员权限") 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.commit()
await db_session.refresh(user) 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": "权限已删除"}
+123 -34
View File
@@ -1,42 +1,108 @@
# database/init_db.py
import asyncio import asyncio
from database.database import db_manager
from utils.logger import get_logger
from sqlalchemy import select 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 services.auth_service import get_password_hash
from config.settings import settings
from utils.logger import get_logger
logger = get_logger(__name__) 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(): 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"]},
try: {"code": "user", "name": "普通用户", "description": "普通用户,可使用模具分析和查看库存", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers"]},
async with db_manager.session() as session: {"code": "viewer", "name": "只读用户", "description": "只读用户,只能查看数据", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers"]},
result = await session.execute( ]
select(User).where(User.username == "admin")
)
existing_admin = result.scalar_one_or_none() async def init_permissions(session):
"""初始化权限"""
if existing_admin: result = await session.execute(select(Permission))
logger.info("管理员账户已存在") existing_perms = result.scalars().all()
return
if existing_perms:
admin = User( logger.info("权限已初始化")
username="admin", return
email="admin@gemold.com",
hashed_password=get_password_hash("admin123"), perm_map = {}
full_name="系统管理员", for perm_data in DEFAULT_PERMISSIONS:
is_active=True, perm = Permission(**perm_data)
is_superuser=True session.add(perm)
) await session.flush()
perm_map[perm.code] = perm.id
session.add(admin)
await session.commit() logger.info(f"创建了 {len(DEFAULT_PERMISSIONS)} 个权限")
logger.info("默认管理员账户创建成功 (用户名: admin, 密码: admin123)") return perm_map
except Exception as e:
logger.error(f"创建默认管理员失败: {e}") 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(): async def init_database():
@@ -44,13 +110,36 @@ async def init_database():
try: try:
await db_manager.connect() await db_manager.connect()
await db_manager.create_tables() 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("数据库初始化完成") 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 return True
except Exception as e: except Exception as e:
logger.error(f"数据库初始化失败: {e}") logger.error(f"数据库初始化失败: {e}")
print(f"数据库初始化失败: {e}")
return False return False
finally:
await db_manager.disconnect()
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(init_database()) asyncio.run(init_database())
+90 -3
View File
@@ -17,15 +17,102 @@ class User(Base):
hashed_password = Column(String(255), nullable=False) hashed_password = Column(String(255), nullable=False)
full_name = Column(String(100)) full_name = Column(String(100))
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime, default=func.now())
last_login = Column(DateTime, nullable=True) last_login = Column(DateTime, nullable=True)
# 关联关系
stp_files = relationship("STPFile", back_populates="user") 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): def __repr__(self):
return f"<User(id={self.id}, username='{self.username}', email='{self.email}')>" return f"<User(id={self.id}, username='{self.username}')>"
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"<Role(code='{self.code}', name='{self.name}')>"
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"<Permission(code='{self.code}', name='{self.name}')>"
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"<UserRole(user_id={self.user_id}, role_id={self.role_id})>"
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"<RolePermission(role_id={self.role_id}, permission_id={self.permission_id})>"
class STPFile(Base): class STPFile(Base):
"""STP源文件元数据表""" """STP源文件元数据表"""
+140
View File
@@ -870,6 +870,141 @@ a:hover {
color: var(--text-secondary); 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); color: var(--primary-700);
} }
.auth-tip {
color: var(--text-tertiary);
font-size: var(--text-sm);
}
.page-container { .page-container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
+174 -107
View File
@@ -271,11 +271,8 @@ const LoginView = {
setup() { setup() {
const router = useRouter(); const router = useRouter();
const state = reactive({ const state = reactive({
isLogin: true,
username: '', username: '',
password: '', password: '',
email: '',
full_name: '',
loading: false, loading: false,
error: '' error: ''
}); });
@@ -296,51 +293,24 @@ const LoginView = {
state.error = ''; state.error = '';
try { try {
if (state.isLogin) { const res = await fetch('/api/auth/login/json', {
const res = await fetch('/api/auth/login/json', { method: 'POST',
method: 'POST', headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
body: JSON.stringify({ username: state.username,
username: state.username, password: state.password
password: state.password })
}) });
});
if (!res.ok) {
if (!res.ok) { const error = await res.json();
const error = await res.json(); throw new Error(error.detail || '登录失败');
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 data = await res.json();
saveAuth(data.access_token, data.user);
addNotification('登录成功', 'success');
router.push('/');
} catch (e) { } catch (e) {
state.error = e.message; state.error = e.message;
addNotification(e.message, 'error'); addNotification(e.message, 'error');
@@ -356,8 +326,8 @@ const LoginView = {
<div class="auth-card"> <div class="auth-card">
<div class="auth-header"> <div class="auth-header">
<div class="auth-logo">◆</div> <div class="auth-logo">◆</div>
<h1>{{ state.isLogin ? '登录' : '注册' }}</h1> <h1>登录</h1>
<p>{{ state.isLogin ? '登录到 Gemold 系统' : '创建新账户' }}</p> <p>登录到 Gemold 系统</p>
</div> </div>
<form @submit.prevent="handleSubmit" class="auth-form"> <form @submit.prevent="handleSubmit" class="auth-form">
@@ -371,47 +341,25 @@ const LoginView = {
/> />
</div> </div>
<div class="form-group" v-if="!state.isLogin">
<label>邮箱</label>
<input
v-model="state.email"
type="email"
placeholder="请输入邮箱"
autocomplete="email"
/>
</div>
<div class="form-group" v-if="!state.isLogin">
<label>姓名</label>
<input
v-model="state.full_name"
type="text"
placeholder="请输入姓名(可选)"
/>
</div>
<div class="form-group"> <div class="form-group">
<label>密码</label> <label>密码</label>
<input <input
v-model="state.password" v-model="state.password"
type="password" type="password"
placeholder="请输入密码" placeholder="请输入密码"
:autocomplete="state.isLogin ? 'current-password' : 'new-password'" autocomplete="current-password"
/> />
</div> </div>
<div v-if="state.error" class="error-message">{{ state.error }}</div> <div v-if="state.error" class="error-message">{{ state.error }}</div>
<button type="submit" class="btn-primary btn-full" :disabled="state.loading"> <button type="submit" class="btn-primary btn-full" :disabled="state.loading">
{{ state.loading ? '处理中...' : (state.isLogin ? '登录' : '注册') }} {{ state.loading ? '登录中...' : '登录' }}
</button> </button>
</form> </form>
<div class="auth-footer"> <div class="auth-footer">
<span>{{ state.isLogin ? '没有账户?' : '已有账户?' }}</span> <p class="auth-tip">如需开通账号,请联系管理员</p>
<a @click="state.isLogin = !state.isLogin; state.error = ''">
{{ state.isLogin ? '立即注册' : '立即登录' }}
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -566,7 +514,17 @@ const UsersView = {
const router = useRouter(); const router = useRouter();
const state = reactive({ const state = reactive({
users: [], users: [],
loading: true roles: [],
loading: true,
showUserModal: false,
editingUser: null,
userForm: {
username: '',
email: '',
password: '',
full_name: '',
role_ids: []
}
}); });
const loadUsers = async () => { const loadUsers = async () => {
@@ -579,43 +537,123 @@ const UsersView = {
} }
}; };
const toggleActive = async (user) => { const loadRoles = async () => {
try { try {
const updated = await apiRequest(`/api/auth/users/${user.id}/toggle-active`, { method: 'PUT' }); state.roles = await apiRequest('/api/auth/roles');
const index = state.users.findIndex(u => u.id === user.id);
if (index > -1) state.users[index] = updated;
addNotification(`用户 ${user.username} 已${updated.is_active ? '启用' : '禁用'}`, 'success');
} catch (e) { } 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 { try {
const updated = await apiRequest(`/api/auth/users/${user.id}/toggle-admin`, { method: 'PUT' }); if (state.editingUser) {
const index = state.users.findIndex(u => u.id === user.id); await apiRequest(`/api/auth/users/${state.editingUser.id}`, {
if (index > -1) state.users[index] = updated; method: 'PUT',
addNotification(`用户 ${user.username} ${updated.is_superuser ? '已设为管理员' : '已取消管理员'}`, 'success'); 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) { } 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) { if (!appState.user?.is_superuser) {
router.push('/'); router.push('/');
return; return;
} }
await loadRoles();
loadUsers(); loadUsers();
}); });
return { state, appState, toggleActive, toggleAdmin, formatDateTime }; return { state, appState, openUserModal, saveUser, deleteUser, resetPassword, formatDateTime };
}, },
template: ` template: `
<div class="page-container"> <div class="page-container">
<div class="page-header"> <div class="page-header">
<h1>用户管理</h1> <div>
<p>管理系统用户和权限</p> <h1>用户管理</h1>
<p>管理系统用户和权限</p>
</div>
<button class="btn-primary" @click="openUserModal()">+ 添加用户</button>
</div> </div>
<div v-if="state.loading" class="loading-state"> <div v-if="state.loading" class="loading-state">
@@ -647,33 +685,62 @@ const UsersView = {
</span> </span>
</td> </td>
<td> <td>
<span :class="['badge', user.is_superuser ? 'badge-primary' : 'badge-neutral']"> <span v-for="role in user.roles" :key="role" class="badge badge-info" style="margin-right: 4px;">
{{ user.is_superuser ? '管理员' : '普通用户' }} {{ role }}
</span> </span>
</td> </td>
<td>{{ formatDateTime(user.created_at) }}</td> <td>{{ formatDateTime(user.created_at) }}</td>
<td> <td>
<div class="action-buttons"> <div class="action-buttons">
<button <button class="btn-sm btn-primary" @click="openUserModal(user)">编辑</button>
v-if="user.id !== appState.user?.id" <button class="btn-sm btn-warning" @click="resetPassword(user)">重置密码</button>
:class="['btn-sm', user.is_active ? 'btn-warning' : 'btn-success']" <button v-if="user.id !== appState.user?.id" class="btn-sm btn-error" @click="deleteUser(user)">删除</button>
@click="toggleActive(user)"
>
{{ user.is_active ? '禁用' : '启用' }}
</button>
<button
v-if="user.id !== appState.user?.id"
:class="['btn-sm', user.is_superuser ? 'btn-warning' : 'btn-primary']"
@click="toggleAdmin(user)"
>
{{ user.is_superuser ? '取消管理员' : '设为管理员' }}
</button>
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div v-if="state.showUserModal" class="modal-overlay" @click.self="state.showUserModal = false">
<div class="modal-content">
<div class="modal-header">
<h2>{{ state.editingUser ? '编辑用户' : '添加用户' }}</h2>
<button class="modal-close" @click="state.showUserModal = false">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>用户名</label>
<input v-model="state.userForm.username" type="text" :disabled="!!state.editingUser" />
</div>
<div class="form-group">
<label>邮箱</label>
<input v-model="state.userForm.email" type="email" />
</div>
<div class="form-group" v-if="!state.editingUser">
<label>密码</label>
<input v-model="state.userForm.password" type="password" />
</div>
<div class="form-group">
<label>姓名</label>
<input v-model="state.userForm.full_name" type="text" />
</div>
<div class="form-group">
<label>角色</label>
<div class="checkbox-group">
<label v-for="role in state.roles" :key="role.id" class="checkbox-label">
<input type="checkbox" :value="role.id" v-model="state.userForm.role_ids" />
{{ role.name }}
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="state.showUserModal = false">取消</button>
<button class="btn-primary" @click="saveUser">保存</button>
</div>
</div>
</div>
</div> </div>
` `
}; };