import uuid import secrets 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 _oauth_states: dict[str, float] = {} _OAUTH_STATE_TTL = 600 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" state = secrets.token_urlsafe(32) import time _oauth_states[state] = time.time() expired = [k for k, v in _oauth_states.items() if time.time() - v > _OAUTH_STATE_TTL] for k in expired: del _oauth_states[k] 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, "state": state}} @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": "密码已修改"}