Compare commits
3
Commits
f9c8bda6e0
...
245484a0c5
| 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 fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from .config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
security_optional = HTTPBearer(auto_error=False)
|
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:
|
def hash_password(password: str) -> str:
|
||||||
"""使用 bcrypt 直接生成密码哈希"""
|
"""使用 bcrypt 直接生成密码哈希"""
|
||||||
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
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
|
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()
|
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))
|
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
|
||||||
to_encode.update({"exp": expire})
|
to_encode.update({"exp": expire})
|
||||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class User(Base):
|
|||||||
phone = Column(String(20), default=None)
|
phone = Column(String(20), default=None)
|
||||||
password_hash = Column(String(255), nullable=False)
|
password_hash = Column(String(255), nullable=False)
|
||||||
avatar = Column(String(500), default=None)
|
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
|
status = Column(String(10), nullable=False, default="active") # active / banned
|
||||||
real_name_status = Column(String(20), nullable=False, default="unverified")
|
real_name_status = Column(String(20), nullable=False, default="unverified")
|
||||||
real_name = Column(String(50), default=None)
|
real_name = Column(String(50), default=None)
|
||||||
@@ -40,6 +40,7 @@ class User(Base):
|
|||||||
membership_plan_id = Column(String(36), default=None)
|
membership_plan_id = Column(String(36), default=None)
|
||||||
membership_level = Column(Integer, default=0)
|
membership_level = Column(Integer, default=0)
|
||||||
membership_expire_at = Column(DateTime, default=None)
|
membership_expire_at = Column(DateTime, default=None)
|
||||||
|
tenant_id = Column(String(36), default=None)
|
||||||
auto_renew = Column(Boolean, default=False)
|
auto_renew = Column(Boolean, default=False)
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
@@ -76,6 +77,47 @@ class ApiKey(Base):
|
|||||||
# AI聊天与频道
|
# 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):
|
class Conversation(Base):
|
||||||
__tablename__ = "conversations"
|
__tablename__ = "conversations"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,30 @@ from contextlib import asynccontextmanager
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
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
|
from datetime import datetime
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
@@ -206,6 +230,9 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 前台 API
|
# 前台 API
|
||||||
|
|
||||||
|
# Phase0: Register RLS middleware
|
||||||
|
app.add_middleware(TenantRLSMiddleware)
|
||||||
app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"])
|
app.include_router(auth.router, prefix="/app/api/auth", tags=["认证"])
|
||||||
app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"])
|
app.include_router(chat.router, prefix="/app/api/chat", tags=["AI聊天"])
|
||||||
app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"])
|
app.include_router(knowledge.router, prefix="/app/api/knowledge", tags=["知识库"])
|
||||||
|
|||||||
Reference in new issue
Block a user