Commit 6b9a6730 authored by Mahmoud Aglan's avatar Mahmoud Aglan

topc

parent da697713
"""
Superadmin routes: user management, stats, oversight.
"""
from pydantic import BaseModel
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func
from backend.database import get_db
from backend.models import User, Chat, Message, KnowledgeBase
from backend.auth import require_superadmin, hash_password
router = APIRouter()
class UpdateUserBody(BaseModel):
email: Optional[str] = None
role: Optional[str] = None
is_active: Optional[bool] = None
quota_tokens_monthly: Optional[int] = None
password: Optional[str] = None
class CreateUserBody(BaseModel):
username: str
email: str
password: str
role: str = "user"
quota_tokens_monthly: int = 2_000_000
@router.get("/stats")
def get_stats(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
return {
"total_users": db.query(User).count(),
"active_users": db.query(User).filter(User.is_active == True).count(),
"total_chats": db.query(Chat).count(),
"total_messages": db.query(Message).count(),
"total_tokens_used": db.query(func.sum(User.tokens_used_this_month)).scalar() or 0,
"total_knowledge_bases": db.query(KnowledgeBase).count(),
}
@router.get("/users")
def list_users(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
users = db.query(User).order_by(User.created_at.desc()).all()
result = []
for u in users:
chat_count = db.query(Chat).filter(Chat.user_id == u.id).count()
result.append({
"id": u.id,
"username": u.username,
"email": u.email,
"role": u.role,
"is_active": u.is_active,
"quota_tokens_monthly": u.quota_tokens_monthly,
"tokens_used_this_month": u.tokens_used_this_month,
"chat_count": chat_count,
"created_at": str(u.created_at),
})
return result
@router.post("/users")
def create_user(body: CreateUserBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
if db.query(User).filter(
(User.username == body.username) | (User.email == body.email)
).first():
raise HTTPException(409, "Username or email taken")
user = User(
username=body.username,
email=body.email,
password_hash=hash_password(body.password),
role=body.role,
quota_tokens_monthly=body.quota_tokens_monthly,
)
db.add(user)
db.commit()
return {"id": user.id, "username": user.username}
@router.put("/users/{user_id}")
def update_user(user_id: str, body: UpdateUserBody, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(404)
if body.email is not None:
user.email = body.email
if body.role is not None:
user.role = body.role
if body.is_active is not None:
user.is_active = body.is_active
if body.quota_tokens_monthly is not None:
user.quota_tokens_monthly = body.quota_tokens_monthly
if body.password:
user.password_hash = hash_password(body.password)
db.commit()
return {"ok": True}
@router.delete("/users/{user_id}")
def delete_user(user_id: str, admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(404)
if user.role == "superadmin":
raise HTTPException(400, "Cannot delete superadmin")
db.delete(user)
db.commit()
return {"ok": True}
@router.get("/chats")
def list_all_chats(admin: User = Depends(require_superadmin), db: Session = Depends(get_db)):
chats = db.query(Chat).order_by(Chat.updated_at.desc()).limit(200).all()
result = []
for c in chats:
user = db.query(User).filter(User.id == c.user_id).first()
msg_count = db.query(Message).filter(Message.chat_id == c.id).count()
result.append({
"id": c.id,
"title": c.title,
"username": user.username if user else "?",
"message_count": msg_count,
"updated_at": str(c.updated_at),
})
return result
\ No newline at end of file
"""
Chat attachment upload, serve, and delete routes.
"""
import os
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from backend.database import get_db
from backend.models import User, Chat, ChatAttachment
from backend.auth import get_current_user, decode_token
from backend.services import attachment_service
from backend.config import MAX_ATTACHMENT_BYTES
router = APIRouter()
@router.post("/chats/{chat_id}/attachments")
async def upload_attachments(
chat_id: str,
files: list[UploadFile] = File(...),
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Upload one or more files as chat attachments. Returns attachment metadata."""
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat:
raise HTTPException(404, "Chat not found")
results = []
for file in files:
filename = file.filename or "file"
try:
content = await file.read()
if len(content) > MAX_ATTACHMENT_BYTES:
results.append({
"error": f"File too large: {filename} ({len(content) // 1024 // 1024}MB). Max {MAX_ATTACHMENT_BYTES // 1024 // 1024}MB.",
})
continue
meta = attachment_service.save_attachment(
chat_id=chat_id,
filename=filename,
content=content,
content_type=file.content_type,
)
att = ChatAttachment(
id=meta["id"],
chat_id=chat_id,
filename=meta["filename"],
original_filename=meta["original_filename"],
mime_type=meta["mime_type"],
file_type=meta["file_type"],
file_size=meta["file_size"],
storage_path=meta["storage_path"],
text_extract=meta.get("text_extract"),
)
db.add(att)
db.commit()
db.refresh(att)
results.append(_att_dict(att))
except Exception as e:
results.append({"error": f"Failed to upload {filename}: {str(e)}"})
return {"attachments": results}
@router.get("/attachments/{attachment_id}/file")
def serve_attachment(
attachment_id: str,
token: Optional[str] = Query(None),
user: Optional[User] = Depends(_optional_current_user),
db: Session = Depends(get_db),
):
"""
Serve an attachment file.
Supports both Bearer header auth and ?token= query param
(needed for <img> tags that can't send headers).
"""
# Try query param auth if header auth didn't work
if user is None and token:
try:
payload = decode_token(token)
user = db.query(User).filter(User.id == payload["sub"]).first()
except Exception:
pass
if user is None:
raise HTTPException(401, "Authentication required")
att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
if not att:
raise HTTPException(404, "Attachment not found")
chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
if not chat or (chat.user_id != user.id and user.role != "superadmin"):
raise HTTPException(403, "Access denied")
if not os.path.exists(att.storage_path):
raise HTTPException(404, "File not found on disk")
return FileResponse(
att.storage_path,
media_type=att.mime_type,
filename=att.original_filename,
)
@router.delete("/attachments/{attachment_id}")
def delete_attachment(
attachment_id: str,
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Delete a single attachment."""
att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
if not att:
raise HTTPException(404)
chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
if not chat or (chat.user_id != user.id and user.role != "superadmin"):
raise HTTPException(403)
attachment_service.delete_attachment_file(att.storage_path)
db.delete(att)
db.commit()
return {"ok": True}
def _optional_current_user(
db: Session = Depends(get_db),
):
"""
A dependency that tries to get current user but returns None on failure.
This allows the endpoint to also accept ?token= query param.
"""
# This is a placeholder — the actual auth is handled in the route
# by checking both header and query param
return None
def _att_dict(att: ChatAttachment) -> dict:
return {
"id": att.id,
"chat_id": att.chat_id,
"message_id": att.message_id,
"filename": att.filename,
"original_filename": att.original_filename,
"mime_type": att.mime_type,
"file_type": att.file_type,
"file_size": att.file_size,
"created_at": str(att.created_at),
}
\ No newline at end of file
"""
Chat attachment upload, serve, and delete routes.
"""
import os
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Request
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from backend.database import get_db
from backend.models import User, Chat, ChatAttachment
from backend.auth import get_current_user, decode_token
from backend.services import attachment_service
from backend.config import MAX_ATTACHMENT_BYTES
router = APIRouter()
def _get_user_from_request(request: Request, db: Session, token_param: Optional[str] = None) -> User:
"""
Resolve user from either:
1. Authorization: Bearer <token> header
2. ?token=<token> query parameter (for img/video tags)
"""
raw_token = None
# Try header first
auth_header = request.headers.get("authorization", "")
if auth_header.startswith("Bearer "):
raw_token = auth_header[7:]
# Fall back to query param
if not raw_token and token_param:
raw_token = token_param
if not raw_token:
raise HTTPException(401, "Authentication required")
payload = decode_token(raw_token)
user = db.query(User).filter(User.id == payload["sub"]).first()
if not user or not user.is_active:
raise HTTPException(401, "User not found or inactive")
return user
@router.post("/chats/{chat_id}/attachments")
async def upload_attachments(
chat_id: str,
files: list[UploadFile] = File(...),
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Upload one or more files as chat attachments. Returns attachment metadata."""
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat:
raise HTTPException(404, "Chat not found")
results = []
for file in files:
filename = file.filename or "file"
try:
content = await file.read()
if len(content) > MAX_ATTACHMENT_BYTES:
results.append({
"error": f"File too large: {filename} ({len(content) // 1024 // 1024}MB). Max {MAX_ATTACHMENT_BYTES // 1024 // 1024}MB.",
})
continue
meta = attachment_service.save_attachment(
chat_id=chat_id,
filename=filename,
content=content,
content_type=file.content_type,
)
att = ChatAttachment(
id=meta["id"],
chat_id=chat_id,
filename=meta["filename"],
original_filename=meta["original_filename"],
mime_type=meta["mime_type"],
file_type=meta["file_type"],
file_size=meta["file_size"],
storage_path=meta["storage_path"],
text_extract=meta.get("text_extract"),
)
db.add(att)
db.commit()
db.refresh(att)
results.append(_att_dict(att))
except Exception as e:
results.append({"error": f"Failed to upload {filename}: {str(e)}"})
return {"attachments": results}
@router.get("/attachments/{attachment_id}/file")
def serve_attachment(
attachment_id: str,
request: Request,
token: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""
Serve an attachment file.
Supports both Bearer header auth and ?token= query param
(needed for <img> tags that can't send headers).
"""
user = _get_user_from_request(request, db, token)
att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
if not att:
raise HTTPException(404, "Attachment not found")
chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
if not chat or (chat.user_id != user.id and user.role != "superadmin"):
raise HTTPException(403, "Access denied")
if not os.path.exists(att.storage_path):
raise HTTPException(404, "File not found on disk")
return FileResponse(
att.storage_path,
media_type=att.mime_type,
filename=att.original_filename,
)
@router.delete("/attachments/{attachment_id}")
def delete_attachment(
attachment_id: str,
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Delete a single attachment."""
att = db.query(ChatAttachment).filter(ChatAttachment.id == attachment_id).first()
if not att:
raise HTTPException(404)
chat = db.query(Chat).filter(Chat.id == att.chat_id).first()
if not chat or (chat.user_id != user.id and user.role != "superadmin"):
raise HTTPException(403)
attachment_service.delete_attachment_file(att.storage_path)
db.delete(att)
db.commit()
return {"ok": True}
def _att_dict(att: ChatAttachment) -> dict:
return {
"id": att.id,
"chat_id": att.chat_id,
"message_id": att.message_id,
"filename": att.filename,
"original_filename": att.original_filename,
"mime_type": att.mime_type,
"file_type": att.file_type,
"file_size": att.file_size,
"created_at": str(att.created_at),
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment