Adding message sending. Profile and message get seem to work.

This commit is contained in:
Michael Woods
2025-12-20 22:29:27 -05:00
parent ae52188bcc
commit 0e88677dad
9 changed files with 262 additions and 158 deletions

View File

@@ -1,19 +1,12 @@
# packetserver/http/server.py
from fastapi import FastAPI, Depends, HTTPException, status, Query
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.responses import HTMLResponse
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
from typing import Optional
from datetime import datetime
from persistent.mapping import PersistentMapping
import persistent.list
from pathlib import Path
# No database module needed get_db_connection is provided by the runner
# get_http_user logic inlined directly in the dependency (simple and avoids module)
from .routers import public, profile, messages, send
from .auth import HttpUser
BASE_DIR = Path(__file__).parent.resolve()
app = FastAPI(
title="PacketServer HTTP API",
@@ -21,151 +14,12 @@ app = FastAPI(
version="0.1.0",
)
# New static (relative to this file's location always works)
from pathlib import Path
BASE_DIR = Path(__file__).parent.resolve()
# Static and templates
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
templates = Jinja2Templates(directory=BASE_DIR / "templates")
security = HTTPBasic()
async def get_current_http_user(credentials: HTTPBasicCredentials = Depends(security)):
# get_db_connection is injected by the standalone runner
conn = get_db_connection()
root = conn.root()
# Inline httpUsers mapping access (replaces get_http_user from nonexistent database.py)
http_users = root.get("httpUsers")
if http_users is None:
# No users yet auth fails
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
headers={"WWW-Authenticate": "Basic"},
)
user: HttpUser | None = http_users.get(credentials.username.upper())
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
headers={"WWW-Authenticate": "Basic"},
)
if not user.enabled:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="HTTP access disabled for this user",
)
if not user.verify_password(credentials.password):
user.record_login_failure()
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
headers={"WWW-Authenticate": "Basic"},
)
user.record_login_success()
return user
# ------------------------------------------------------------------
# Public routes (no auth)
# ------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
return templates.TemplateResponse(
"index.html",
{"request": request, "message": "Welcome to PacketServer HTTP Interface"}
)
@app.get("/health")
async def health():
return {"status": "ok", "service": "packetserver-http"}
# ------------------------------------------------------------------
# Protected routes (require auth)
# ------------------------------------------------------------------
@app.get("/api/v1/profile")
async def profile(current_user: HttpUser = Depends(get_current_http_user)):
return {
"username": current_user.username,
"enabled": current_user.enabled,
"rf_enabled": current_user.rf_enabled,
"created_at": current_user.created_at,
"last_login": current_user.last_login,
}
@app.get("/api/v1/messages")
async def get_messages(
current_user: HttpUser = Depends(get_current_http_user),
type: str = Query("received", description="received, sent, or all"),
limit: Optional[int] = Query(20, le=100, description="Max messages to return (default 20, max 100)"),
since: Optional[str] = Query(None, description="ISO UTC timestamp (e.g. 2025-12-01T00:00:00Z) to filter newer messages")
):
"""
List messages for the authenticated user.
- received: incoming (default)
- sent: outgoing (stored in sender's mailbox)
- all: both incoming and outgoing
"""
if limit is None or limit < 1:
limit = 20
username = current_user.username # already uppercase
conn = get_db_connection()
root = conn.root()
# Ensure mailbox exists (safe, matches server logic)
if 'messages' not in root:
root['messages'] = PersistentMapping()
if username not in root['messages']:
root['messages'][username] = persistent.list.PersistentList()
mailbox = root['messages'][username]
# Parse 'since' if provided
since_dt = None
if since:
try:
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
except ValueError:
raise HTTPException(status_code=400, detail="Invalid 'since' format use ISO UTC (e.g. 2025-12-01T00:00:00Z)")
messages = []
for msg in mailbox:
# Filter by type
if type == "received" and msg.msg_from == username:
continue # skip own sent messages
if type == "sent" and msg.msg_from != username:
continue # skip received
# 'all' includes everything
# Filter by since
if since_dt and msg.sent_at < since_dt:
continue
# Basic dict (no attachments data yet)
messages.append({
"id": str(msg.msg_id),
"from": msg.msg_from,
"to": list(msg.msg_to) if isinstance(msg.msg_to, tuple) else [msg.msg_to],
"sent_at": msg.sent_at.isoformat() + "Z",
"text": msg.text,
"has_attachments": len(msg.attachments) > 0,
"retrieved": msg.retrieved # expose if marked read on RF
})
# Sort newest first
messages.sort(key=lambda m: m["sent_at"], reverse=True)
# Apply limit
return {"messages": messages[:limit], "total_returned": len(messages[:limit])}
# Include routers
app.include_router(public.router)
app.include_router(profile.router)
app.include_router(messages.router)
app.include_router(send.router)