Spaces:
Runtime error
Runtime error
ScamDetect Bot commited on
Commit ·
396563b
1
Parent(s): 3d253e5
Auto-sync backend and fix configuration
Browse files- backend/api/routes/auth.py +101 -0
- backend/api/routes/history.py +19 -5
- backend/api/routes/scanner.py +50 -11
- backend/main.py +2 -0
backend/api/routes/auth.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timedelta
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 4 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 5 |
+
from jose import JWTError, jwt
|
| 6 |
+
from passlib.context import CryptContext
|
| 7 |
+
from sqlalchemy.orm import Session
|
| 8 |
+
|
| 9 |
+
from database import get_db
|
| 10 |
+
import models.db_models as db_models
|
| 11 |
+
import models.schemas as schemas
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
# Configuration
|
| 15 |
+
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
|
| 16 |
+
ALGORITHM = "HS256"
|
| 17 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 1 week
|
| 18 |
+
|
| 19 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 20 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
|
| 21 |
+
|
| 22 |
+
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
| 23 |
+
|
| 24 |
+
def verify_password(plain_password, hashed_password):
|
| 25 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 26 |
+
|
| 27 |
+
def get_password_hash(password):
|
| 28 |
+
return pwd_context.hash(password)
|
| 29 |
+
|
| 30 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
| 31 |
+
to_encode = data.copy()
|
| 32 |
+
if expires_delta:
|
| 33 |
+
expire = datetime.utcnow() + expires_delta
|
| 34 |
+
else:
|
| 35 |
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
| 36 |
+
to_encode.update({"exp": expire})
|
| 37 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 38 |
+
return encoded_jwt
|
| 39 |
+
|
| 40 |
+
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
| 41 |
+
if not token:
|
| 42 |
+
return None
|
| 43 |
+
|
| 44 |
+
credentials_exception = HTTPException(
|
| 45 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 46 |
+
detail="Could not validate credentials",
|
| 47 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 48 |
+
)
|
| 49 |
+
try:
|
| 50 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 51 |
+
username: str = payload.get("sub")
|
| 52 |
+
if username is None:
|
| 53 |
+
raise credentials_exception
|
| 54 |
+
token_data = schemas.TokenData(username=username)
|
| 55 |
+
except JWTError:
|
| 56 |
+
raise credentials_exception
|
| 57 |
+
|
| 58 |
+
user = db.query(db_models.User).filter(db_models.User.username == token_data.username).first()
|
| 59 |
+
if user is None:
|
| 60 |
+
raise credentials_exception
|
| 61 |
+
return user
|
| 62 |
+
|
| 63 |
+
@router.post("/register", response_model=schemas.UserResponse)
|
| 64 |
+
def register_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
| 65 |
+
db_user = db.query(db_models.User).filter(
|
| 66 |
+
(db_models.User.username == user.username) | (db_models.User.email == user.email)
|
| 67 |
+
).first()
|
| 68 |
+
if db_user:
|
| 69 |
+
raise HTTPException(status_code=400, detail="Username or email already registered")
|
| 70 |
+
|
| 71 |
+
hashed_password = get_password_hash(user.password)
|
| 72 |
+
db_user = db_models.User(
|
| 73 |
+
username=user.username,
|
| 74 |
+
email=user.email,
|
| 75 |
+
hashed_password=hashed_password
|
| 76 |
+
)
|
| 77 |
+
db.add(db_user)
|
| 78 |
+
db.commit()
|
| 79 |
+
db.refresh(db_user)
|
| 80 |
+
return db_user
|
| 81 |
+
|
| 82 |
+
@router.post("/login", response_model=schemas.Token)
|
| 83 |
+
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
| 84 |
+
user = db.query(db_models.User).filter(db_models.User.username == form_data.username).first()
|
| 85 |
+
if not user or not verify_password(form_data.password, user.hashed_password):
|
| 86 |
+
raise HTTPException(
|
| 87 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 88 |
+
detail="Incorrect username or password",
|
| 89 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 90 |
+
)
|
| 91 |
+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 92 |
+
access_token = create_access_token(
|
| 93 |
+
data={"sub": user.username}, expires_delta=access_token_expires
|
| 94 |
+
)
|
| 95 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
| 96 |
+
|
| 97 |
+
@router.get("/me", response_model=schemas.UserResponse)
|
| 98 |
+
def read_users_me(current_user: db_models.User = Depends(get_current_user)):
|
| 99 |
+
if not current_user:
|
| 100 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
| 101 |
+
return current_user
|
backend/api/routes/history.py
CHANGED
|
@@ -1,17 +1,29 @@
|
|
| 1 |
-
from fastapi import APIRouter, Depends
|
| 2 |
from sqlalchemy.orm import Session
|
| 3 |
from database import get_db
|
| 4 |
import models.db_models as db_models
|
| 5 |
import json
|
|
|
|
|
|
|
| 6 |
|
| 7 |
router = APIRouter(prefix="/history", tags=["History"])
|
| 8 |
|
| 9 |
@router.get("")
|
| 10 |
-
async def get_scan_history(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"""Retrieve the most recent scan records from the database."""
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
results = []
|
| 17 |
for record in records:
|
|
@@ -24,6 +36,8 @@ async def get_scan_history(limit: int = 50, db: Session = Depends(get_db)):
|
|
| 24 |
"threat_categories": json.loads(record.threat_categories) if record.threat_categories else [],
|
| 25 |
"raw_text_extracted": record.raw_text_extracted,
|
| 26 |
"behavioral_profile": json.loads(record.behavioral_profile) if getattr(record, "behavioral_profile", None) else None,
|
|
|
|
|
|
|
| 27 |
"explanations": [
|
| 28 |
{
|
| 29 |
"feature": exp.feature,
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, Query
|
| 2 |
from sqlalchemy.orm import Session
|
| 3 |
from database import get_db
|
| 4 |
import models.db_models as db_models
|
| 5 |
import json
|
| 6 |
+
from typing import Optional
|
| 7 |
+
from api.routes.auth import get_current_user
|
| 8 |
|
| 9 |
router = APIRouter(prefix="/history", tags=["History"])
|
| 10 |
|
| 11 |
@router.get("")
|
| 12 |
+
async def get_scan_history(
|
| 13 |
+
limit: int = 50,
|
| 14 |
+
scope: str = Query("global", description="Scope of history: 'global' or 'my'"),
|
| 15 |
+
db: Session = Depends(get_db),
|
| 16 |
+
current_user: Optional[db_models.User] = Depends(get_current_user)
|
| 17 |
+
):
|
| 18 |
"""Retrieve the most recent scan records from the database."""
|
| 19 |
+
query = db.query(db_models.ScanRecord)
|
| 20 |
+
|
| 21 |
+
if scope == "my":
|
| 22 |
+
if not current_user:
|
| 23 |
+
return [] # Or raise 401
|
| 24 |
+
query = query.filter(db_models.ScanRecord.user_id == current_user.id)
|
| 25 |
+
|
| 26 |
+
records = query.order_by(db_models.ScanRecord.timestamp.desc()).limit(limit).all()
|
| 27 |
|
| 28 |
results = []
|
| 29 |
for record in records:
|
|
|
|
| 36 |
"threat_categories": json.loads(record.threat_categories) if record.threat_categories else [],
|
| 37 |
"raw_text_extracted": record.raw_text_extracted,
|
| 38 |
"behavioral_profile": json.loads(record.behavioral_profile) if getattr(record, "behavioral_profile", None) else None,
|
| 39 |
+
"source": getattr(record, "source", None),
|
| 40 |
+
"user_id": getattr(record, "user_id", None),
|
| 41 |
"explanations": [
|
| 42 |
{
|
| 43 |
"feature": exp.feature,
|
backend/api/routes/scanner.py
CHANGED
|
@@ -5,13 +5,15 @@ from services.scam_detection import process_image, process_video, process_url, p
|
|
| 5 |
from sqlalchemy.orm import Session
|
| 6 |
from database import get_db
|
| 7 |
import models.db_models as db_models
|
|
|
|
|
|
|
| 8 |
import json
|
| 9 |
import asyncio
|
| 10 |
from api.ws_manager import manager
|
| 11 |
|
| 12 |
router = APIRouter(prefix="/scan", tags=["Scanner"])
|
| 13 |
|
| 14 |
-
def save_scan_to_db(db: Session, result: ScanningResult):
|
| 15 |
"""Persist a ScanningResult into the SQLite database."""
|
| 16 |
db_record = db_models.ScanRecord(
|
| 17 |
id=result.id,
|
|
@@ -21,7 +23,9 @@ def save_scan_to_db(db: Session, result: ScanningResult):
|
|
| 21 |
risk_level=result.risk_level,
|
| 22 |
threat_categories=json.dumps(result.threat_categories),
|
| 23 |
raw_text_extracted=result.raw_text_extracted,
|
| 24 |
-
behavioral_profile=json.dumps(result.behavioral_profile) if result.behavioral_profile else None
|
|
|
|
|
|
|
| 25 |
)
|
| 26 |
|
| 27 |
for exp in result.explanations:
|
|
@@ -37,36 +41,68 @@ def save_scan_to_db(db: Session, result: ScanningResult):
|
|
| 37 |
db.commit()
|
| 38 |
|
| 39 |
@router.post("/image", response_model=ScanningResult)
|
| 40 |
-
async def scan_image(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
contents = await file.read()
|
| 42 |
result = process_image(contents)
|
|
|
|
|
|
|
| 43 |
if ephemeral.lower() != "true":
|
| 44 |
-
save_scan_to_db(db, result)
|
| 45 |
broadcast_scan_result(result)
|
| 46 |
return result
|
| 47 |
|
| 48 |
@router.post("/video", response_model=ScanningResult)
|
| 49 |
-
async def scan_video(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
contents = await file.read()
|
| 51 |
result = process_video(contents)
|
|
|
|
|
|
|
| 52 |
if ephemeral.lower() != "true":
|
| 53 |
-
save_scan_to_db(db, result)
|
| 54 |
broadcast_scan_result(result)
|
| 55 |
return result
|
| 56 |
|
| 57 |
@router.post("/url", response_model=ScanningResult)
|
| 58 |
-
async def scan_url(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
result = process_url(url)
|
|
|
|
|
|
|
| 60 |
if ephemeral.lower() != "true":
|
| 61 |
-
save_scan_to_db(db, result)
|
| 62 |
broadcast_scan_result(result)
|
| 63 |
return result
|
| 64 |
|
| 65 |
@router.post("/text", response_model=ScanningResult)
|
| 66 |
-
async def scan_text(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
result = process_text(text)
|
|
|
|
|
|
|
| 68 |
if ephemeral.lower() != "true":
|
| 69 |
-
save_scan_to_db(db, result)
|
| 70 |
broadcast_scan_result(result)
|
| 71 |
return result
|
| 72 |
|
|
@@ -77,6 +113,9 @@ def broadcast_scan_result(result: ScanningResult):
|
|
| 77 |
"risk_score": result.risk_score,
|
| 78 |
"risk_level": result.risk_level,
|
| 79 |
"threat_categories": result.threat_categories,
|
| 80 |
-
"timestamp": result.timestamp
|
|
|
|
| 81 |
}
|
|
|
|
|
|
|
| 82 |
asyncio.create_task(manager.broadcast(minimal_payload))
|
|
|
|
| 5 |
from sqlalchemy.orm import Session
|
| 6 |
from database import get_db
|
| 7 |
import models.db_models as db_models
|
| 8 |
+
from typing import Optional
|
| 9 |
+
from api.routes.auth import get_current_user
|
| 10 |
import json
|
| 11 |
import asyncio
|
| 12 |
from api.ws_manager import manager
|
| 13 |
|
| 14 |
router = APIRouter(prefix="/scan", tags=["Scanner"])
|
| 15 |
|
| 16 |
+
def save_scan_to_db(db: Session, result: ScanningResult, source: Optional[str] = None, user_id: Optional[int] = None):
|
| 17 |
"""Persist a ScanningResult into the SQLite database."""
|
| 18 |
db_record = db_models.ScanRecord(
|
| 19 |
id=result.id,
|
|
|
|
| 23 |
risk_level=result.risk_level,
|
| 24 |
threat_categories=json.dumps(result.threat_categories),
|
| 25 |
raw_text_extracted=result.raw_text_extracted,
|
| 26 |
+
behavioral_profile=json.dumps(result.behavioral_profile) if result.behavioral_profile else None,
|
| 27 |
+
source=source,
|
| 28 |
+
user_id=user_id
|
| 29 |
)
|
| 30 |
|
| 31 |
for exp in result.explanations:
|
|
|
|
| 41 |
db.commit()
|
| 42 |
|
| 43 |
@router.post("/image", response_model=ScanningResult)
|
| 44 |
+
async def scan_image(
|
| 45 |
+
file: UploadFile = File(...),
|
| 46 |
+
ephemeral: str = Form("false"),
|
| 47 |
+
source: Optional[str] = Form(None),
|
| 48 |
+
db: Session = Depends(get_db),
|
| 49 |
+
current_user: Optional[db_models.User] = Depends(get_current_user)
|
| 50 |
+
):
|
| 51 |
contents = await file.read()
|
| 52 |
result = process_image(contents)
|
| 53 |
+
result.source = source
|
| 54 |
+
result.user_id = current_user.id if current_user else None
|
| 55 |
if ephemeral.lower() != "true":
|
| 56 |
+
save_scan_to_db(db, result, source, current_user.id if current_user else None)
|
| 57 |
broadcast_scan_result(result)
|
| 58 |
return result
|
| 59 |
|
| 60 |
@router.post("/video", response_model=ScanningResult)
|
| 61 |
+
async def scan_video(
|
| 62 |
+
file: UploadFile = File(...),
|
| 63 |
+
ephemeral: str = Form("false"),
|
| 64 |
+
source: Optional[str] = Form(None),
|
| 65 |
+
db: Session = Depends(get_db),
|
| 66 |
+
current_user: Optional[db_models.User] = Depends(get_current_user)
|
| 67 |
+
):
|
| 68 |
contents = await file.read()
|
| 69 |
result = process_video(contents)
|
| 70 |
+
result.source = source
|
| 71 |
+
result.user_id = current_user.id if current_user else None
|
| 72 |
if ephemeral.lower() != "true":
|
| 73 |
+
save_scan_to_db(db, result, source, current_user.id if current_user else None)
|
| 74 |
broadcast_scan_result(result)
|
| 75 |
return result
|
| 76 |
|
| 77 |
@router.post("/url", response_model=ScanningResult)
|
| 78 |
+
async def scan_url(
|
| 79 |
+
url: str = Form(...),
|
| 80 |
+
ephemeral: str = Form("false"),
|
| 81 |
+
source: Optional[str] = Form(None),
|
| 82 |
+
db: Session = Depends(get_db),
|
| 83 |
+
current_user: Optional[db_models.User] = Depends(get_current_user)
|
| 84 |
+
):
|
| 85 |
result = process_url(url)
|
| 86 |
+
result.source = source
|
| 87 |
+
result.user_id = current_user.id if current_user else None
|
| 88 |
if ephemeral.lower() != "true":
|
| 89 |
+
save_scan_to_db(db, result, source, current_user.id if current_user else None)
|
| 90 |
broadcast_scan_result(result)
|
| 91 |
return result
|
| 92 |
|
| 93 |
@router.post("/text", response_model=ScanningResult)
|
| 94 |
+
async def scan_text(
|
| 95 |
+
text: str = Form(...),
|
| 96 |
+
ephemeral: str = Form("false"),
|
| 97 |
+
source: Optional[str] = Form(None),
|
| 98 |
+
db: Session = Depends(get_db),
|
| 99 |
+
current_user: Optional[db_models.User] = Depends(get_current_user)
|
| 100 |
+
):
|
| 101 |
result = process_text(text)
|
| 102 |
+
result.source = source
|
| 103 |
+
result.user_id = current_user.id if current_user else None
|
| 104 |
if ephemeral.lower() != "true":
|
| 105 |
+
save_scan_to_db(db, result, source, current_user.id if current_user else None)
|
| 106 |
broadcast_scan_result(result)
|
| 107 |
return result
|
| 108 |
|
|
|
|
| 113 |
"risk_score": result.risk_score,
|
| 114 |
"risk_level": result.risk_level,
|
| 115 |
"threat_categories": result.threat_categories,
|
| 116 |
+
"timestamp": result.timestamp,
|
| 117 |
+
"source": result.source
|
| 118 |
}
|
| 119 |
+
|
| 120 |
+
# Broadcast to all clients for real-time alerting
|
| 121 |
asyncio.create_task(manager.broadcast(minimal_payload))
|
backend/main.py
CHANGED
|
@@ -4,6 +4,7 @@ from api.routes import scanner
|
|
| 4 |
from api.routes import history
|
| 5 |
from api.routes import stats
|
| 6 |
from api.routes import export
|
|
|
|
| 7 |
from api.ws_manager import manager
|
| 8 |
import os
|
| 9 |
|
|
@@ -41,6 +42,7 @@ app.include_router(scanner.router, prefix="/api")
|
|
| 41 |
app.include_router(history.router, prefix="/api")
|
| 42 |
app.include_router(stats.router, prefix="/api")
|
| 43 |
app.include_router(export.router, prefix="/api")
|
|
|
|
| 44 |
|
| 45 |
# WebSocket endpoint — mounted directly on app to bypass CORS middleware
|
| 46 |
@app.websocket("/ws/notifications")
|
|
|
|
| 4 |
from api.routes import history
|
| 5 |
from api.routes import stats
|
| 6 |
from api.routes import export
|
| 7 |
+
from api.routes import auth
|
| 8 |
from api.ws_manager import manager
|
| 9 |
import os
|
| 10 |
|
|
|
|
| 42 |
app.include_router(history.router, prefix="/api")
|
| 43 |
app.include_router(stats.router, prefix="/api")
|
| 44 |
app.include_router(export.router, prefix="/api")
|
| 45 |
+
app.include_router(auth.router, prefix="/api")
|
| 46 |
|
| 47 |
# WebSocket endpoint — mounted directly on app to bypass CORS middleware
|
| 48 |
@app.websocket("/ws/notifications")
|