Refactor golden image handling in backup upload process</message>
<message>Update the _set_golden_from_path function to improve the handling of existing golden image files. Replace the existing unlink logic with a more robust method that safely removes files or broken symlinks using the missing_ok parameter. This change enhances the reliability of the backup upload process by ensuring that stale references are properly cleared before setting a new golden image path.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
API routes for GNSS Guard Server
|
||||
"""
|
||||
|
||||
488
backup-from-device/gnss-guard/tm-gnss-guard/server/routes/api.py
Normal file
488
backup-from-device/gnss-guard/tm-gnss-guard/server/routes/api.py
Normal file
@@ -0,0 +1,488 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
REST API endpoints for GNSS Guard Server
|
||||
Handles validation data submission and retrieval
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
|
||||
from database import get_db
|
||||
from models import (
|
||||
Asset, ValidationHistory, AssetNotificationState,
|
||||
ValidationSubmission, ValidationBatchSubmission,
|
||||
ValidationResponse, AssetStatus, AssetResponse, AssetCreate, AssetWithToken,
|
||||
AssetImport, AssetBatchImport
|
||||
)
|
||||
from routes.auth import get_current_user
|
||||
from services.telegram_service import get_telegram_service
|
||||
|
||||
logger = logging.getLogger("gnss_guard.server.api")
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["api"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Asset Token Authentication Dependency
|
||||
# =============================================================================
|
||||
|
||||
async def get_current_asset(
|
||||
authorization: str = Header(..., description="Bearer token for asset authentication"),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Asset:
|
||||
"""
|
||||
Dependency to authenticate asset using Bearer token.
|
||||
Returns the authenticated asset or raises 401.
|
||||
"""
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Invalid authorization header format")
|
||||
|
||||
token = authorization[7:] # Remove "Bearer " prefix
|
||||
token_hash = Asset.hash_token(token)
|
||||
|
||||
asset = db.query(Asset).filter(
|
||||
Asset.token_hash == token_hash,
|
||||
Asset.is_active == True
|
||||
).first()
|
||||
|
||||
if not asset:
|
||||
raise HTTPException(status_code=401, detail="Invalid or inactive token")
|
||||
|
||||
return asset
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Validation Endpoints (Asset Authentication Required)
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/validation", status_code=201)
|
||||
async def submit_validation(
|
||||
data: ValidationSubmission,
|
||||
asset: Asset = Depends(get_current_asset),
|
||||
db: Session = Depends(get_db)
|
||||
) -> dict:
|
||||
"""
|
||||
Submit a single validation record from an asset.
|
||||
Also triggers Telegram notifications if state changed.
|
||||
"""
|
||||
try:
|
||||
validation = ValidationHistory(
|
||||
asset_id=asset.id,
|
||||
validation_timestamp=data.validation_timestamp,
|
||||
validation_timestamp_unix=data.validation_timestamp_unix,
|
||||
is_valid=data.is_valid,
|
||||
sources_missing=json.dumps(data.sources_missing),
|
||||
sources_stale=json.dumps(data.sources_stale),
|
||||
coordinate_differences=json.dumps(data.coordinate_differences),
|
||||
source_coordinates=json.dumps(data.source_coordinates),
|
||||
validation_details=json.dumps(data.validation_details),
|
||||
)
|
||||
|
||||
db.add(validation)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Validation received from asset '{asset.name}' at {data.validation_timestamp}")
|
||||
|
||||
# Process Telegram notification (will only send if state changed)
|
||||
try:
|
||||
telegram_service = get_telegram_service()
|
||||
validation_data = {
|
||||
"sources_missing": data.sources_missing,
|
||||
"sources_stale": data.sources_stale,
|
||||
"validation_details": data.validation_details,
|
||||
"source_coordinates": data.source_coordinates,
|
||||
}
|
||||
telegram_service.process_validation(db, asset, validation_data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Telegram notification error for {asset.name}: {e}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Validation record saved",
|
||||
"id": validation.id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving validation from {asset.name}: {e}")
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/validation/batch", status_code=201)
|
||||
async def submit_validation_batch(
|
||||
data: ValidationBatchSubmission,
|
||||
asset: Asset = Depends(get_current_asset),
|
||||
db: Session = Depends(get_db)
|
||||
) -> dict:
|
||||
"""
|
||||
Submit multiple validation records (for catching up after offline period).
|
||||
Only sends Telegram notification for the most recent record to avoid spam.
|
||||
"""
|
||||
try:
|
||||
saved_count = 0
|
||||
skipped_count = 0
|
||||
latest_record = None
|
||||
latest_timestamp = 0
|
||||
|
||||
for record in data.records:
|
||||
# Check if this timestamp already exists for this asset
|
||||
existing = db.query(ValidationHistory).filter(
|
||||
ValidationHistory.asset_id == asset.id,
|
||||
ValidationHistory.validation_timestamp_unix == record.validation_timestamp_unix
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
validation = ValidationHistory(
|
||||
asset_id=asset.id,
|
||||
validation_timestamp=record.validation_timestamp,
|
||||
validation_timestamp_unix=record.validation_timestamp_unix,
|
||||
is_valid=record.is_valid,
|
||||
sources_missing=json.dumps(record.sources_missing),
|
||||
sources_stale=json.dumps(record.sources_stale),
|
||||
coordinate_differences=json.dumps(record.coordinate_differences),
|
||||
source_coordinates=json.dumps(record.source_coordinates),
|
||||
validation_details=json.dumps(record.validation_details),
|
||||
)
|
||||
db.add(validation)
|
||||
saved_count += 1
|
||||
|
||||
# Track the most recent record for notification
|
||||
if record.validation_timestamp_unix > latest_timestamp:
|
||||
latest_timestamp = record.validation_timestamp_unix
|
||||
latest_record = record
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Batch validation from '{asset.name}': {saved_count} saved, {skipped_count} skipped")
|
||||
|
||||
# Process Telegram notification for the most recent record only
|
||||
if latest_record:
|
||||
try:
|
||||
telegram_service = get_telegram_service()
|
||||
validation_data = {
|
||||
"sources_missing": latest_record.sources_missing,
|
||||
"sources_stale": latest_record.sources_stale,
|
||||
"validation_details": latest_record.validation_details,
|
||||
"source_coordinates": latest_record.source_coordinates,
|
||||
}
|
||||
telegram_service.process_validation(db, asset, validation_data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Telegram notification error for {asset.name}: {e}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"saved": saved_count,
|
||||
"skipped": skipped_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving batch validation from {asset.name}: {e}")
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Read Endpoints (Session Authentication Required)
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/assets", response_model=List[AssetResponse])
|
||||
async def list_assets(
|
||||
user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> List[AssetResponse]:
|
||||
"""
|
||||
List all registered assets.
|
||||
Requires user session authentication.
|
||||
"""
|
||||
assets = db.query(Asset).filter(Asset.is_active == True).all()
|
||||
return assets
|
||||
|
||||
|
||||
@router.get("/assets/{asset_name}/status")
|
||||
async def get_asset_status(
|
||||
asset_name: str,
|
||||
user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> AssetStatus:
|
||||
"""
|
||||
Get current status of an asset (latest validation).
|
||||
Requires user session authentication.
|
||||
"""
|
||||
asset = db.query(Asset).filter(
|
||||
Asset.name == asset_name,
|
||||
Asset.is_active == True
|
||||
).first()
|
||||
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail=f"Asset '{asset_name}' not found")
|
||||
|
||||
# Get latest validation
|
||||
latest = db.query(ValidationHistory).filter(
|
||||
ValidationHistory.asset_id == asset.id
|
||||
).order_by(desc(ValidationHistory.validation_timestamp_unix)).first()
|
||||
|
||||
# Get online status from notification state (consistent with Telegram alerts)
|
||||
notification_state = db.query(AssetNotificationState).filter(
|
||||
AssetNotificationState.asset_id == asset.id
|
||||
).first()
|
||||
|
||||
is_online = notification_state.is_online if notification_state else False
|
||||
last_seen = notification_state.last_validation_at if notification_state else None
|
||||
|
||||
# Fall back to validation timestamp if no notification state
|
||||
if not last_seen and latest and latest.received_at:
|
||||
last_seen = latest.received_at
|
||||
|
||||
latest_validation = None
|
||||
if latest:
|
||||
latest_validation = ValidationResponse(
|
||||
id=latest.id,
|
||||
asset_name=asset.name,
|
||||
validation_timestamp=latest.validation_timestamp,
|
||||
validation_timestamp_unix=latest.validation_timestamp_unix,
|
||||
is_valid=latest.is_valid,
|
||||
sources_missing=json.loads(latest.sources_missing or "[]"),
|
||||
sources_stale=json.loads(latest.sources_stale or "[]"),
|
||||
coordinate_differences=json.loads(latest.coordinate_differences or "{}"),
|
||||
source_coordinates=json.loads(latest.source_coordinates or "{}"),
|
||||
validation_details=json.loads(latest.validation_details or "{}"),
|
||||
received_at=latest.received_at
|
||||
)
|
||||
|
||||
return AssetStatus(
|
||||
asset_name=asset.name,
|
||||
is_online=is_online,
|
||||
last_seen=last_seen,
|
||||
latest_validation=latest_validation
|
||||
)
|
||||
|
||||
|
||||
@router.get("/assets/{asset_name}/history")
|
||||
async def get_asset_history(
|
||||
asset_name: str,
|
||||
hours: int = Query(default=72, ge=1, le=168, description="Hours of history (max 168 = 7 days)"),
|
||||
user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> List[ValidationResponse]:
|
||||
"""
|
||||
Get validation history for an asset (default: 72 hours).
|
||||
Requires user session authentication.
|
||||
"""
|
||||
asset = db.query(Asset).filter(
|
||||
Asset.name == asset_name,
|
||||
Asset.is_active == True
|
||||
).first()
|
||||
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail=f"Asset '{asset_name}' not found")
|
||||
|
||||
# Calculate cutoff timestamp
|
||||
cutoff = datetime.utcnow() - timedelta(hours=hours)
|
||||
cutoff_unix = cutoff.timestamp()
|
||||
|
||||
# Get validation history
|
||||
validations = db.query(ValidationHistory).filter(
|
||||
ValidationHistory.asset_id == asset.id,
|
||||
ValidationHistory.validation_timestamp_unix >= cutoff_unix
|
||||
).order_by(desc(ValidationHistory.validation_timestamp_unix)).all()
|
||||
|
||||
return [
|
||||
ValidationResponse(
|
||||
id=v.id,
|
||||
asset_name=asset.name,
|
||||
validation_timestamp=v.validation_timestamp,
|
||||
validation_timestamp_unix=v.validation_timestamp_unix,
|
||||
is_valid=v.is_valid,
|
||||
sources_missing=json.loads(v.sources_missing or "[]"),
|
||||
sources_stale=json.loads(v.sources_stale or "[]"),
|
||||
coordinate_differences=json.loads(v.coordinate_differences or "{}"),
|
||||
source_coordinates=json.loads(v.source_coordinates or "{}"),
|
||||
validation_details=json.loads(v.validation_details or "{}"),
|
||||
received_at=v.received_at
|
||||
)
|
||||
for v in validations
|
||||
]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Admin Endpoints (Session Authentication Required)
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/admin/assets", response_model=AssetWithToken, status_code=201)
|
||||
async def create_asset(
|
||||
data: AssetCreate,
|
||||
user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> AssetWithToken:
|
||||
"""
|
||||
Create a new asset and return its token.
|
||||
Requires user session authentication.
|
||||
"""
|
||||
# Check if asset already exists
|
||||
existing = db.query(Asset).filter(Asset.name == data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail=f"Asset '{data.name}' already exists")
|
||||
|
||||
# Generate token
|
||||
token = Asset.generate_token()
|
||||
token_hash = Asset.hash_token(token)
|
||||
|
||||
asset = Asset(
|
||||
name=data.name,
|
||||
token_hash=token_hash,
|
||||
description=data.description,
|
||||
telegram_chat_id=data.telegram_chat_id,
|
||||
telegram_enabled=data.telegram_enabled
|
||||
)
|
||||
|
||||
db.add(asset)
|
||||
db.commit()
|
||||
db.refresh(asset)
|
||||
|
||||
logger.info(f"Created new asset: {data.name}")
|
||||
|
||||
# Return asset with the unhashed token (only shown once!)
|
||||
return AssetWithToken(
|
||||
id=asset.id,
|
||||
name=asset.name,
|
||||
is_active=asset.is_active,
|
||||
created_at=asset.created_at,
|
||||
description=asset.description,
|
||||
telegram_chat_id=asset.telegram_chat_id,
|
||||
telegram_enabled=asset.telegram_enabled,
|
||||
token=token
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/admin/assets/{asset_name}")
|
||||
async def deactivate_asset(
|
||||
asset_name: str,
|
||||
user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> dict:
|
||||
"""
|
||||
Deactivate an asset (soft delete).
|
||||
Requires user session authentication.
|
||||
"""
|
||||
asset = db.query(Asset).filter(Asset.name == asset_name).first()
|
||||
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail=f"Asset '{asset_name}' not found")
|
||||
|
||||
asset.is_active = False
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Deactivated asset: {asset_name}")
|
||||
|
||||
return {"status": "success", "message": f"Asset '{asset_name}' deactivated"}
|
||||
|
||||
|
||||
@router.post("/admin/assets/import", response_model=AssetResponse, status_code=201)
|
||||
async def import_asset(
|
||||
data: AssetImport,
|
||||
user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> AssetResponse:
|
||||
"""
|
||||
Import an asset with a specific token.
|
||||
If asset exists, updates its token. If not, creates it.
|
||||
Requires user session authentication.
|
||||
"""
|
||||
# Hash the provided token
|
||||
token_hash = Asset.hash_token(data.token)
|
||||
|
||||
# Check if asset already exists
|
||||
existing = db.query(Asset).filter(Asset.name == data.name).first()
|
||||
|
||||
if existing:
|
||||
# Update existing asset's token
|
||||
existing.token_hash = token_hash
|
||||
existing.is_active = True
|
||||
if data.description:
|
||||
existing.description = data.description
|
||||
if data.telegram_chat_id is not None:
|
||||
existing.telegram_chat_id = data.telegram_chat_id
|
||||
existing.telegram_enabled = data.telegram_enabled
|
||||
db.commit()
|
||||
db.refresh(existing)
|
||||
logger.info(f"Updated token for existing asset: {data.name}")
|
||||
return existing
|
||||
else:
|
||||
# Create new asset with provided token
|
||||
asset = Asset(
|
||||
name=data.name,
|
||||
token_hash=token_hash,
|
||||
description=data.description,
|
||||
telegram_chat_id=data.telegram_chat_id,
|
||||
telegram_enabled=data.telegram_enabled
|
||||
)
|
||||
db.add(asset)
|
||||
db.commit()
|
||||
db.refresh(asset)
|
||||
logger.info(f"Imported new asset: {data.name}")
|
||||
return asset
|
||||
|
||||
|
||||
@router.post("/admin/assets/import/batch")
|
||||
async def import_assets_batch(
|
||||
data: AssetBatchImport,
|
||||
user: str = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> dict:
|
||||
"""
|
||||
Batch import assets with specific tokens.
|
||||
Creates new assets or updates existing ones.
|
||||
Requires user session authentication.
|
||||
"""
|
||||
created = 0
|
||||
updated = 0
|
||||
errors = []
|
||||
|
||||
for asset_data in data.assets:
|
||||
try:
|
||||
token_hash = Asset.hash_token(asset_data.token)
|
||||
existing = db.query(Asset).filter(Asset.name == asset_data.name).first()
|
||||
|
||||
if existing:
|
||||
existing.token_hash = token_hash
|
||||
existing.is_active = True
|
||||
if asset_data.description:
|
||||
existing.description = asset_data.description
|
||||
if asset_data.telegram_chat_id is not None:
|
||||
existing.telegram_chat_id = asset_data.telegram_chat_id
|
||||
existing.telegram_enabled = asset_data.telegram_enabled
|
||||
updated += 1
|
||||
logger.info(f"Updated token for asset: {asset_data.name}")
|
||||
else:
|
||||
asset = Asset(
|
||||
name=asset_data.name,
|
||||
token_hash=token_hash,
|
||||
description=asset_data.description,
|
||||
telegram_chat_id=asset_data.telegram_chat_id,
|
||||
telegram_enabled=asset_data.telegram_enabled
|
||||
)
|
||||
db.add(asset)
|
||||
created += 1
|
||||
logger.info(f"Created asset: {asset_data.name}")
|
||||
except Exception as e:
|
||||
errors.append({"name": asset_data.name, "error": str(e)})
|
||||
logger.error(f"Failed to import asset {asset_data.name}: {e}")
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"errors": errors
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Authentication routes for GNSS Guard Server
|
||||
Handles user session authentication for the web UI
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from pydantic import BaseModel
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
from config import get_config
|
||||
|
||||
logger = logging.getLogger("gnss_guard.server.auth")
|
||||
|
||||
router = APIRouter(tags=["auth"])
|
||||
|
||||
# Rate limiter instance (uses app.state.limiter set in main.py)
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
# Simple in-memory session storage (for single-user scenario)
|
||||
# In production with multiple servers, use Redis or database
|
||||
_sessions: dict = {}
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
def create_session(username: str) -> str:
|
||||
"""Create a new session and return session ID"""
|
||||
import secrets
|
||||
session_id = secrets.token_urlsafe(32)
|
||||
config = get_config()
|
||||
|
||||
_sessions[session_id] = {
|
||||
"username": username,
|
||||
"created_at": datetime.utcnow(),
|
||||
"expires_at": datetime.utcnow() + timedelta(minutes=config.session_expire_minutes)
|
||||
}
|
||||
|
||||
return session_id
|
||||
|
||||
|
||||
def validate_session(session_id: str) -> Optional[str]:
|
||||
"""Validate session and return username if valid"""
|
||||
if not session_id or session_id not in _sessions:
|
||||
return None
|
||||
|
||||
session = _sessions[session_id]
|
||||
if datetime.utcnow() > session["expires_at"]:
|
||||
del _sessions[session_id]
|
||||
return None
|
||||
|
||||
return session["username"]
|
||||
|
||||
|
||||
def get_current_user(request: Request) -> str:
|
||||
"""
|
||||
Dependency to get current authenticated user.
|
||||
Raises 401 if not authenticated.
|
||||
"""
|
||||
session_id = request.cookies.get("session_id")
|
||||
username = validate_session(session_id)
|
||||
|
||||
if not username:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
||||
return username
|
||||
|
||||
|
||||
def get_optional_user(request: Request) -> Optional[str]:
|
||||
"""
|
||||
Dependency to get current user if authenticated, None otherwise.
|
||||
"""
|
||||
session_id = request.cookies.get("session_id")
|
||||
return validate_session(session_id)
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
@limiter.limit("5/minute") # Rate limit: 5 login attempts per minute per IP
|
||||
async def login(request: Request, data: LoginRequest, response: Response):
|
||||
"""
|
||||
Login endpoint - validates credentials and sets session cookie.
|
||||
Rate limited to prevent brute force attacks.
|
||||
"""
|
||||
config = get_config()
|
||||
|
||||
# Verify credentials against hardcoded user
|
||||
if data.username != config.web_username or data.password != config.web_password:
|
||||
logger.warning(f"Failed login attempt for user: {data.username} from IP: {request.client.host}")
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
# Create session
|
||||
session_id = create_session(data.username)
|
||||
|
||||
# Set session cookie
|
||||
# secure=True ensures cookie only sent over HTTPS
|
||||
response.set_cookie(
|
||||
key="session_id",
|
||||
value=session_id,
|
||||
httponly=True,
|
||||
secure=True, # Only send over HTTPS
|
||||
samesite="lax",
|
||||
max_age=config.session_expire_minutes * 60
|
||||
)
|
||||
|
||||
logger.info(f"User logged in: {data.username}")
|
||||
|
||||
return {"message": "Login successful", "username": data.username}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request, response: Response):
|
||||
"""
|
||||
Logout endpoint - clears session.
|
||||
"""
|
||||
session_id = request.cookies.get("session_id")
|
||||
|
||||
if session_id and session_id in _sessions:
|
||||
del _sessions[session_id]
|
||||
|
||||
response.delete_cookie("session_id")
|
||||
|
||||
return {"message": "Logged out successfully"}
|
||||
|
||||
|
||||
@router.get("/auth/check")
|
||||
async def check_auth(request: Request):
|
||||
"""
|
||||
Check if current session is authenticated.
|
||||
"""
|
||||
session_id = request.cookies.get("session_id")
|
||||
username = validate_session(session_id)
|
||||
|
||||
if username:
|
||||
return {"authenticated": True, "username": username}
|
||||
else:
|
||||
return {"authenticated": False}
|
||||
|
||||
Reference in New Issue
Block a user