Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
245484a0c5 | ||
|
|
2068421d41 | ||
|
|
7b1941fdaf |
No files matched your search
@@ -12,12 +12,35 @@ from fastapi import Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from .config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
|
||||
|
||||
# Phase0: 5-level RBAC
|
||||
ROLE_HIERARCHY = {
|
||||
"super_admin": 100,
|
||||
"admin": 80,
|
||||
"developer": 50,
|
||||
"user": 30,
|
||||
"guest": 10,
|
||||
}
|
||||
|
||||
def has_role(user_role: str, required: str) -> bool:
|
||||
return ROLE_HIERARCHY.get(user_role, 0) >= ROLE_HIERARCHY.get(required, 0)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
security = HTTPBearer()
|
||||
security_optional = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
async def get_current_tenant(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
):
|
||||
try:
|
||||
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
return payload.get("tenant_id")
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""使用 bcrypt 直接生成密码哈希"""
|
||||
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
@@ -35,8 +58,10 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None, tenant_id: str = None) -> str:
|
||||
to_encode = data.copy()
|
||||
if tenant_id:
|
||||
to_encode["tenant_id"] = tenant_id
|
||||
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
@@ -32,7 +32,7 @@ class User(Base):
|
||||
phone = Column(String(20), default=None)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
avatar = Column(String(500), default=None)
|
||||
role = Column(String(10), nullable=False, default="user") # user / admin
|
||||
role = Column(String(20), nullable=False, default="user") # super_admin/admin/developer/user/guest
|
||||
status = Column(String(10), nullable=False, default="active") # active / banned
|
||||
real_name_status = Column(String(20), nullable=False, default="unverified")
|
||||
real_name = Column(String(50), default=None)
|
||||
@@ -40,6 +40,7 @@ class User(Base):
|
||||
membership_plan_id = Column(String(36), default=None)
|
||||
membership_level = Column(Integer, default=0)
|
||||
membership_expire_at = Column(DateTime, default=None)
|
||||
tenant_id = Column(String(36), default=None)
|
||||
auto_renew = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
@@ -76,6 +77,47 @@ class ApiKey(Base):
|
||||
# AI聊天与频道
|
||||
# ============================================================
|
||||
|
||||
|
||||
|
||||
class UserGroup(Base):
|
||||
__tablename__ = "user_groups"
|
||||
id = Column(String(36), primary_key=True, default=gen_uuid)
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
description = Column(String(500), default=None)
|
||||
owner_id = Column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
tenant_id = Column(String(36), default=None)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
owner = relationship("User", foreign_keys=[owner_id])
|
||||
|
||||
class UserGroupMember(Base):
|
||||
__tablename__ = "user_group_members"
|
||||
id = Column(String(36), primary_key=True, default=gen_uuid)
|
||||
group_id = Column(String(36), ForeignKey("user_groups.id"), nullable=False)
|
||||
user_id = Column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
joined_at = Column(DateTime, default=datetime.now)
|
||||
group = relationship("UserGroup")
|
||||
user = relationship("User")
|
||||
|
||||
class GroupModelQuota(Base):
|
||||
__tablename__ = "group_model_quotas"
|
||||
id = Column(String(36), primary_key=True, default=gen_uuid)
|
||||
group_id = Column(String(36), ForeignKey("user_groups.id"), nullable=False)
|
||||
model_id = Column(String(36), ForeignKey("models.id"), nullable=False)
|
||||
daily_limit = Column(Integer, default=1000)
|
||||
rpm_limit = Column(Integer, default=60)
|
||||
group = relationship("UserGroup")
|
||||
model = relationship("AIModel")
|
||||
|
||||
class Tenant(Base):
|
||||
__tablename__ = "tenants"
|
||||
id = Column(String(36), primary_key=True, default=gen_uuid)
|
||||
name = Column(String(100), nullable=False)
|
||||
slug = Column(String(50), unique=True, nullable=False)
|
||||
status = Column(String(20), default="active")
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Conversation(Base):
|
||||
__tablename__ = "conversations"
|
||||
|
||||
|
||||
@@ -11,6 +11,30 @@ from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
|
||||
# Phase0: Tenant RLS Middleware
|
||||
class TenantRLSMiddleware(BaseHTTPMiddleware):
|
||||
"""Inject tenant_id from JWT into PostgreSQL for RLS policies"""
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
try:
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
from jose import jwt
|
||||
from app.auth import SECRET_KEY, ALGORITHM
|
||||
token = auth_header[7:]
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
tenant_id = payload.get("tenant_id")
|
||||
if tenant_id:
|
||||
from app.database import SessionLocal
|
||||
db = SessionLocal()
|
||||
try:
|
||||
db.execute(f"SET app.current_tenant = '{tenant_id}'")
|
||||
finally:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
return await call_next(request)
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
@@ -206,6 +230,9 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
# 前台 API
|
||||
|
||||
# Phase0: Register RLS middleware
|
||||
app.add_middleware(TenantRLSMiddleware)
|
||||
app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"])
|
||||
app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"])
|
||||
app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"])
|
||||
|
||||
Reference in new issue
Block a user