import uuid from datetime import datetime, timedelta import jwt from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession import bcrypt from database import get_db from models import User, UserRole, Role, RolePermission, Permission from schemas import LoginRequest, TokenResponse, UserOut, RoleOut from config import settings def hash_password(password: str) -> str: return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') router = APIRouter(prefix="/api/auth", tags=["auth"]) async def get_permission_codes(db: AsyncSession, role_ids: list[uuid.UUID]) -> list[str]: result = await db.execute( select(Permission.code) .join(RolePermission) .where(RolePermission.role_id.in_(role_ids)) ) return list(set(result.scalars().all())) async def get_user_roles(db: AsyncSession, user_id: uuid.UUID) -> list[RoleOut]: result = await db.execute( select(Role).join(UserRole).where(UserRole.user_id == user_id) ) roles = result.scalars().all() out = [] for role in roles: rp_result = await db.execute( select(Permission.code) .join(RolePermission) .where(RolePermission.role_id == role.id) ) perms = list(rp_result.scalars().all()) out.append(RoleOut( id=role.id, name=role.name, code=role.code, description=role.description, is_system=role.is_system, data_scope=role.data_scope, permissions=perms, )) return out @router.post("/login", response_model=TokenResponse) async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)): result = await db.execute(select(User).where(User.username == req.username)) user = result.scalar_one_or_none() if not user or not bcrypt.checkpw(req.password.encode('utf-8'), user.password_hash.encode('utf-8')): raise HTTPException(401, "用户名或密码错误") if user.status != "active": raise HTTPException(403, "账户已被禁用") roles = await get_user_roles(db, user.id) expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES) token = jwt.encode( {"sub": str(user.id), "username": user.username, "exp": expire}, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM, ) return TokenResponse( access_token=token, user=UserOut( id=user.id, username=user.username, display_name=user.display_name, email=user.email, phone=user.phone, wecom_user_id=user.wecom_user_id, department_id=user.department_id, position=user.position, manager_id=user.manager_id, status=user.status, roles=roles, created_at=user.created_at, ), ) @router.get("/me", response_model=UserOut) async def get_me(request: Request, db: AsyncSession = Depends(get_db)): user_ctx = request.state.user result = await db.execute(select(User).where(User.id == user_ctx["id"])) user = result.scalar_one_or_none() if not user: raise HTTPException(404, "用户不存在") roles = await get_user_roles(db, user.id) return UserOut( id=user.id, username=user.username, display_name=user.display_name, email=user.email, phone=user.phone, wecom_user_id=user.wecom_user_id, department_id=user.department_id, position=user.position, manager_id=user.manager_id, status=user.status, roles=roles, created_at=user.created_at, ) @router.get("/wecom/oauth-url") async def get_wecom_oauth_url(request: Request): corp_id = settings.WECOM_CORP_ID or "" if not corp_id: return {"code": 400, "message": "请先配置 WECOM_CORP_ID"} base_url = str(request.base_url).rstrip("/") redirect_uri = f"{base_url}/api/auth/wecom/callback" url = f"https://open.weixin.qq.com/connect/oauth2/authorize?appid={corp_id}&redirect_uri={redirect_uri}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect" return {"code": 200, "data": {"url": url}} @router.put("/me") async def update_me( request: Request, payload: dict, db: AsyncSession = Depends(get_db), ): user_ctx = request.state.user result = await db.execute(select(User).where(User.id == user_ctx["id"])) user = result.scalar_one_or_none() if not user: raise HTTPException(404, "用户不存在") if "display_name" in payload: user.display_name = payload["display_name"] if "email" in payload: user.email = payload["email"] if "phone" in payload: user.phone = payload["phone"] await db.commit() roles = await get_user_roles(db, user.id) return UserOut( id=user.id, username=user.username, display_name=user.display_name, email=user.email, phone=user.phone, wecom_user_id=user.wecom_user_id, department_id=user.department_id, position=user.position, manager_id=user.manager_id, status=user.status, roles=roles, created_at=user.created_at, ) @router.put("/password") async def change_password( request: Request, payload: dict, db: AsyncSession = Depends(get_db), ): user_ctx = request.state.user result = await db.execute(select(User).where(User.id == user_ctx["id"])) user = result.scalar_one_or_none() if not user: raise HTTPException(404, "用户不存在") old_pw = payload.get("old_password", "") new_pw = payload.get("new_password", "") if not bcrypt.checkpw(old_pw.encode('utf-8'), user.password_hash.encode('utf-8')): raise HTTPException(400, "当前密码错误") if len(new_pw) < 6: raise HTTPException(400, "新密码至少6位") user.password_hash = hash_password(new_pw) await db.commit() return {"code": 200, "message": "密码已修改"}