Bunch of fixes for new database model.
This commit is contained in:
@@ -8,7 +8,7 @@ import time
|
|||||||
from persistent.mapping import PersistentMapping
|
from persistent.mapping import PersistentMapping
|
||||||
from persistent.list import PersistentList
|
from persistent.list import PersistentList
|
||||||
from packetserver.common.util import is_valid_ax25_callsign
|
from packetserver.common.util import is_valid_ax25_callsign
|
||||||
from .database import ConnectionDependency
|
from .database import DbDependency
|
||||||
|
|
||||||
ph = PasswordHasher()
|
ph = PasswordHasher()
|
||||||
|
|
||||||
@@ -52,43 +52,44 @@ class HttpUser(Persistent):
|
|||||||
# rf enabled checks..
|
# rf enabled checks..
|
||||||
#
|
#
|
||||||
|
|
||||||
def is_rf_enabled(self, conn: ConnectionDependency) -> bool:
|
def is_rf_enabled(self, db: DbDependency) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if RF gateway is enabled (i.e., callsign NOT in global blacklist).
|
Check if RF gateway is enabled (i.e., callsign NOT in global blacklist).
|
||||||
Requires an open ZODB connection.
|
Requires an open ZODB connection.
|
||||||
"""
|
"""
|
||||||
root = conn.root()
|
with db.transaction() as conn:
|
||||||
blacklist = root.get('config', {}).get('blacklist', [])
|
root = conn.root()
|
||||||
return self.username not in blacklist
|
blacklist = root.get('config', {}).get('blacklist', [])
|
||||||
|
return self.username not in blacklist
|
||||||
|
|
||||||
def set_rf_enabled(self, conn: ConnectionDependency, allow: bool):
|
def set_rf_enabled(self, db: DbDependency, allow: bool):
|
||||||
"""
|
"""
|
||||||
Enable/disable RF gateway by adding/removing from global blacklist.
|
Enable/disable RF gateway by adding/removing from global blacklist.
|
||||||
Requires an open ZODB connection (inside a transaction).
|
Requires an open ZODB connection (inside a transaction).
|
||||||
Only allows enabling if the username is a valid AX.25 callsign.
|
Only allows enabling if the username is a valid AX.25 callsign.
|
||||||
"""
|
"""
|
||||||
from packetserver.common.util import is_valid_ax25_callsign # our validator
|
from packetserver.common.util import is_valid_ax25_callsign # our validator
|
||||||
|
with db.transaction() as conn:
|
||||||
|
root = conn.root()
|
||||||
|
config = root.setdefault('config', PersistentMapping())
|
||||||
|
blacklist = config.setdefault('blacklist', PersistentList())
|
||||||
|
|
||||||
root = conn.root()
|
upper_name = self.username
|
||||||
config = root.setdefault('config', PersistentMapping())
|
|
||||||
blacklist = config.setdefault('blacklist', PersistentList())
|
|
||||||
|
|
||||||
upper_name = self.username
|
if allow:
|
||||||
|
if not is_valid_ax25_callsign(upper_name):
|
||||||
|
raise ValueError(f"{upper_name} is not a valid AX.25 callsign – cannot enable RF access")
|
||||||
|
if upper_name in blacklist:
|
||||||
|
blacklist.remove(upper_name)
|
||||||
|
blacklist._p_changed = True
|
||||||
|
else:
|
||||||
|
if upper_name not in blacklist:
|
||||||
|
blacklist.append(upper_name)
|
||||||
|
blacklist._p_changed = True
|
||||||
|
|
||||||
if allow:
|
config._p_changed = True
|
||||||
if not is_valid_ax25_callsign(upper_name):
|
root._p_changed = True
|
||||||
raise ValueError(f"{upper_name} is not a valid AX.25 callsign – cannot enable RF access")
|
transaction.commit()
|
||||||
if upper_name in blacklist:
|
|
||||||
blacklist.remove(upper_name)
|
|
||||||
blacklist._p_changed = True
|
|
||||||
else:
|
|
||||||
if upper_name not in blacklist:
|
|
||||||
blacklist.append(upper_name)
|
|
||||||
blacklist._p_changed = True
|
|
||||||
|
|
||||||
config._p_changed = True
|
|
||||||
root._p_changed = True
|
|
||||||
transaction.commit()
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Password handling (unchanged)
|
# Password handling (unchanged)
|
||||||
|
|||||||
@@ -46,16 +46,16 @@ def get_db() -> ZODB.DB:
|
|||||||
raise RuntimeError("Database not initialized – call init_db() on startup")
|
raise RuntimeError("Database not initialized – call init_db() on startup")
|
||||||
return _db
|
return _db
|
||||||
|
|
||||||
def get_connection() -> Generator[Connection, None, None]:
|
#def get_connection() -> Generator[Connection, None, None]:
|
||||||
"""Per-request dependency: yields an open Connection, closes on exit."""
|
# """Per-request dependency: yields an open Connection, closes on exit."""
|
||||||
db = get_db()
|
# db = get_db()
|
||||||
conn = db.open()
|
# conn = db.open()
|
||||||
try:
|
# try:
|
||||||
yield conn
|
# yield conn
|
||||||
finally:
|
# finally:
|
||||||
#print("not closing connection")
|
# #print("not closing connection")
|
||||||
#conn.close()
|
# #conn.close()
|
||||||
pass
|
# pass
|
||||||
|
|
||||||
# Optional: per-request transaction (if you want automatic commit/abort)
|
# Optional: per-request transaction (if you want automatic commit/abort)
|
||||||
def get_transaction_manager():
|
def get_transaction_manager():
|
||||||
@@ -63,4 +63,4 @@ def get_transaction_manager():
|
|||||||
|
|
||||||
# Annotated dependencies for routers
|
# Annotated dependencies for routers
|
||||||
DbDependency = Annotated[ZODB.DB, Depends(get_db)]
|
DbDependency = Annotated[ZODB.DB, Depends(get_db)]
|
||||||
ConnectionDependency = Annotated[Connection, Depends(get_connection)]
|
#ConnectionDependency = Annotated[Connection, Depends(get_connection)]
|
||||||
@@ -3,49 +3,49 @@ from fastapi import Depends, HTTPException, status
|
|||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
|
|
||||||
from .auth import HttpUser
|
from .auth import HttpUser
|
||||||
from .database import ConnectionDependency
|
from .database import DbDependency
|
||||||
|
|
||||||
security = HTTPBasic()
|
security = HTTPBasic()
|
||||||
|
|
||||||
|
|
||||||
async def get_current_http_user(conn: ConnectionDependency, credentials: HTTPBasicCredentials = Depends(security)):
|
async def get_current_http_user(db: DbDependency, credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
"""
|
"""
|
||||||
Authenticate via Basic Auth using HttpUser from ZODB.
|
Authenticate via Basic Auth using HttpUser from ZODB.
|
||||||
Injected by the standalone runner (get_db_connection available).
|
Injected by the standalone runner (get_db_connection available).
|
||||||
"""
|
"""
|
||||||
|
with db.transaction() as conn:
|
||||||
|
root = conn.root()
|
||||||
|
|
||||||
root = conn.root()
|
http_users = root.get("httpUsers")
|
||||||
|
if http_users is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid username or password",
|
||||||
|
headers={"WWW-Authenticate": "Basic"},
|
||||||
|
)
|
||||||
|
|
||||||
http_users = root.get("httpUsers")
|
user: HttpUser | None = http_users.get(credentials.username.upper())
|
||||||
if http_users is None:
|
|
||||||
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:
|
if not user.http_enabled:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid username or password",
|
detail="HTTP access disabled for this user",
|
||||||
headers={"WWW-Authenticate": "Basic"},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if not user.http_enabled:
|
if not user.verify_password(credentials.password):
|
||||||
raise HTTPException(
|
user.record_login_failure()
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
raise HTTPException(
|
||||||
detail="HTTP access disabled for this user",
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
)
|
detail="Invalid username or password",
|
||||||
|
headers={"WWW-Authenticate": "Basic"},
|
||||||
|
)
|
||||||
|
|
||||||
if not user.verify_password(credentials.password):
|
user.record_login_success()
|
||||||
user.record_login_failure()
|
return user
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Invalid username or password",
|
|
||||||
headers={"WWW-Authenticate": "Basic"},
|
|
||||||
)
|
|
||||||
|
|
||||||
user.record_login_success()
|
|
||||||
return user
|
|
||||||
@@ -7,7 +7,7 @@ import transaction
|
|||||||
from persistent.list import PersistentList
|
from persistent.list import PersistentList
|
||||||
from ZODB.Connection import Connection
|
from ZODB.Connection import Connection
|
||||||
|
|
||||||
from packetserver.http.database import DbDependency, ConnectionDependency, get_db
|
from packetserver.http.database import DbDependency
|
||||||
from ..dependencies import get_current_http_user
|
from ..dependencies import get_current_http_user
|
||||||
from ..auth import HttpUser
|
from ..auth import HttpUser
|
||||||
from ..server import templates
|
from ..server import templates
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from fastapi.responses import HTMLResponse
|
|||||||
from packetserver.http.dependencies import get_current_http_user
|
from packetserver.http.dependencies import get_current_http_user
|
||||||
from packetserver.http.auth import HttpUser
|
from packetserver.http.auth import HttpUser
|
||||||
from packetserver.http.server import templates
|
from packetserver.http.server import templates
|
||||||
from packetserver.http.database import ConnectionDependency
|
from packetserver.http.database import DbDependency
|
||||||
|
|
||||||
router = APIRouter(tags=["dashboard"])
|
router = APIRouter(tags=["dashboard"])
|
||||||
|
|
||||||
@@ -16,41 +16,43 @@ from .bulletins import list_bulletins
|
|||||||
|
|
||||||
@router.get("/dashboard", response_class=HTMLResponse)
|
@router.get("/dashboard", response_class=HTMLResponse)
|
||||||
async def dashboard(
|
async def dashboard(
|
||||||
conn: ConnectionDependency,
|
db: DbDependency,
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: HttpUser = Depends(get_current_http_user)
|
current_user: HttpUser = Depends(get_current_http_user)
|
||||||
):
|
):
|
||||||
# Internal call – pass explicit defaults to avoid Query object injection
|
|
||||||
messages_resp = await api_get_messages(
|
messages_resp = await api_get_messages(
|
||||||
conn,
|
db,
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
type="all",
|
type="all",
|
||||||
limit=100,
|
limit=100,
|
||||||
since=None # prevents Query wrapper
|
since=None # prevents Query wrapper
|
||||||
)
|
)
|
||||||
messages = messages_resp["messages"]
|
with db.transaction() as conn:
|
||||||
|
# Internal call – pass explicit defaults to avoid Query object injection
|
||||||
|
|
||||||
bulletins_resp = await list_bulletins(conn, limit=10, since=None)
|
messages = messages_resp["messages"]
|
||||||
recent_bulletins = bulletins_resp["bulletins"]
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
bulletins_resp = await list_bulletins(conn, limit=10, since=None)
|
||||||
"dashboard.html",
|
recent_bulletins = bulletins_resp["bulletins"]
|
||||||
{
|
|
||||||
"request": request,
|
return templates.TemplateResponse(
|
||||||
"current_user": current_user.username,
|
"dashboard.html",
|
||||||
"messages": messages,
|
{
|
||||||
"bulletins": recent_bulletins
|
"request": request,
|
||||||
}
|
"current_user": current_user.username,
|
||||||
)
|
"messages": messages,
|
||||||
|
"bulletins": recent_bulletins
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@router.get("/dashboard/profile", response_class=HTMLResponse)
|
@router.get("/dashboard/profile", response_class=HTMLResponse)
|
||||||
async def profile_page(
|
async def profile_page(
|
||||||
conn: ConnectionDependency,
|
db: DbDependency,
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: HttpUser = Depends(get_current_http_user)
|
current_user: HttpUser = Depends(get_current_http_user)
|
||||||
):
|
):
|
||||||
from packetserver.http.routers.profile import profile as api_profile
|
from packetserver.http.routers.profile import profile as api_profile
|
||||||
profile_data = await api_profile(conn, current_user=current_user)
|
profile_data = await api_profile(db, current_user=current_user)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"profile.html",
|
"profile.html",
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ from fastapi.responses import HTMLResponse
|
|||||||
from packetserver.http.dependencies import get_current_http_user
|
from packetserver.http.dependencies import get_current_http_user
|
||||||
from packetserver.http.auth import HttpUser
|
from packetserver.http.auth import HttpUser
|
||||||
from packetserver.http.server import templates
|
from packetserver.http.server import templates
|
||||||
|
from packetserver.http.database import DbDependency
|
||||||
|
|
||||||
router = APIRouter(tags=["message-detail"])
|
router = APIRouter(tags=["message-detail"])
|
||||||
|
|
||||||
@router.get("/dashboard/message/{msg_id}", response_class=HTMLResponse)
|
@router.get("/dashboard/message/{msg_id}", response_class=HTMLResponse)
|
||||||
async def message_detail_page(
|
async def message_detail_page(
|
||||||
|
db: DbDependency,
|
||||||
request: Request,
|
request: Request,
|
||||||
msg_id: str = Path(..., description="Message UUID as string"),
|
msg_id: str = Path(..., description="Message UUID as string"),
|
||||||
current_user: HttpUser = Depends(get_current_http_user)
|
current_user: HttpUser = Depends(get_current_http_user)
|
||||||
@@ -18,6 +20,7 @@ async def message_detail_page(
|
|||||||
|
|
||||||
# Call with mark_retrieved=True to auto-mark as read on view (optional—remove if you prefer manual)
|
# Call with mark_retrieved=True to auto-mark as read on view (optional—remove if you prefer manual)
|
||||||
message_data = await api_get_message(
|
message_data = await api_get_message(
|
||||||
|
db,
|
||||||
msg_id=msg_id,
|
msg_id=msg_id,
|
||||||
mark_retrieved=True,
|
mark_retrieved=True,
|
||||||
current_user=current_user
|
current_user=current_user
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field, validator
|
|||||||
|
|
||||||
from packetserver.http.dependencies import get_current_http_user
|
from packetserver.http.dependencies import get_current_http_user
|
||||||
from packetserver.http.auth import HttpUser
|
from packetserver.http.auth import HttpUser
|
||||||
from packetserver.http.database import ConnectionDependency
|
from packetserver.http.database import DbDependency
|
||||||
|
|
||||||
|
|
||||||
html_router = APIRouter(tags=["messages-html"])
|
html_router = APIRouter(tags=["messages-html"])
|
||||||
@@ -29,7 +29,7 @@ class MarkRetrievedRequest(BaseModel):
|
|||||||
|
|
||||||
@router.get("/messages")
|
@router.get("/messages")
|
||||||
async def get_messages(
|
async def get_messages(
|
||||||
conn: ConnectionDependency,
|
db: DbDependency,
|
||||||
current_user: HttpUser = Depends(get_current_http_user),
|
current_user: HttpUser = Depends(get_current_http_user),
|
||||||
type: str = Query("received", description="received, sent, or all"),
|
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)"),
|
limit: Optional[int] = Query(20, le=100, description="Max messages to return (default 20, max 100)"),
|
||||||
@@ -40,128 +40,131 @@ async def get_messages(
|
|||||||
limit = 20
|
limit = 20
|
||||||
|
|
||||||
username = current_user.username
|
username = current_user.username
|
||||||
root = conn.root()
|
with db.transaction() as conn:
|
||||||
|
root = conn.root()
|
||||||
|
|
||||||
if 'messages' not in root:
|
if 'messages' not in root:
|
||||||
root['messages'] = PersistentMapping()
|
root['messages'] = PersistentMapping()
|
||||||
if username not in root['messages']:
|
if username not in root['messages']:
|
||||||
root['messages'][username] = persistent.list.PersistentList()
|
root['messages'][username] = persistent.list.PersistentList()
|
||||||
|
|
||||||
mailbox = root['messages'][username]
|
mailbox = root['messages'][username]
|
||||||
|
|
||||||
since_dt = None
|
since_dt = None
|
||||||
if since:
|
if since:
|
||||||
try:
|
try:
|
||||||
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid 'since' format")
|
raise HTTPException(status_code=400, detail="Invalid 'since' format")
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
for msg in mailbox:
|
for msg in mailbox:
|
||||||
if type == "received" and msg.msg_from == username:
|
if type == "received" and msg.msg_from == username:
|
||||||
continue
|
continue
|
||||||
if type == "sent" and msg.msg_from != username:
|
if type == "sent" and msg.msg_from != username:
|
||||||
continue
|
continue
|
||||||
if since_dt and msg.sent_at < since_dt:
|
if since_dt and msg.sent_at < since_dt:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
messages.append({
|
messages.append({
|
||||||
"id": str(msg.msg_id),
|
"id": str(msg.msg_id),
|
||||||
"from": msg.msg_from,
|
"from": msg.msg_from,
|
||||||
"to": list(msg.msg_to) if isinstance(msg.msg_to, tuple) else [msg.msg_to],
|
"to": list(msg.msg_to) if isinstance(msg.msg_to, tuple) else [msg.msg_to],
|
||||||
"sent_at": msg.sent_at.isoformat() + "Z",
|
"sent_at": msg.sent_at.isoformat() + "Z",
|
||||||
"text": msg.text,
|
"text": msg.text,
|
||||||
"has_attachments": len(msg.attachments) > 0,
|
"has_attachments": len(msg.attachments) > 0,
|
||||||
"retrieved": msg.retrieved,
|
"retrieved": msg.retrieved,
|
||||||
})
|
})
|
||||||
|
|
||||||
messages.sort(key=lambda m: m["sent_at"], reverse=True)
|
messages.sort(key=lambda m: m["sent_at"], reverse=True)
|
||||||
|
|
||||||
return {"messages": messages[:limit], "total_returned": len(messages[:limit])}
|
return {"messages": messages[:limit], "total_returned": len(messages[:limit])}
|
||||||
|
|
||||||
@router.get("/messages/{msg_id}")
|
@router.get("/messages/{msg_id}")
|
||||||
async def get_message(
|
async def get_message(
|
||||||
conn: ConnectionDependency,
|
db: DbDependency,
|
||||||
msg_id: str = Path(..., description="UUID of the message (as string)"),
|
msg_id: str = Path(..., description="UUID of the message (as string)"),
|
||||||
mark_retrieved: bool = Query(False, description="If true, mark message as retrieved/read"),
|
mark_retrieved: bool = Query(False, description="If true, mark message as retrieved/read"),
|
||||||
current_user: HttpUser = Depends(get_current_http_user)
|
current_user: HttpUser = Depends(get_current_http_user)
|
||||||
):
|
):
|
||||||
root = conn.root()
|
with db.transaction() as conn:
|
||||||
|
root = conn.root()
|
||||||
|
|
||||||
username = current_user.username
|
username = current_user.username
|
||||||
|
|
||||||
messages_root = root.get('messages', {})
|
messages_root = root.get('messages', {})
|
||||||
mailbox = messages_root.get(username)
|
mailbox = messages_root.get(username)
|
||||||
if not mailbox:
|
if not mailbox:
|
||||||
raise HTTPException(status_code=404, detail="Mailbox not found")
|
raise HTTPException(status_code=404, detail="Mailbox not found")
|
||||||
|
|
||||||
# Find message by ID
|
# Find message by ID
|
||||||
target_msg = None
|
target_msg = None
|
||||||
for msg in mailbox:
|
for msg in mailbox:
|
||||||
if str(msg.msg_id) == msg_id:
|
if str(msg.msg_id) == msg_id:
|
||||||
target_msg = msg
|
target_msg = msg
|
||||||
break
|
break
|
||||||
|
|
||||||
if not target_msg:
|
if not target_msg:
|
||||||
raise HTTPException(status_code=404, detail="Message not found")
|
raise HTTPException(status_code=404, detail="Message not found")
|
||||||
|
|
||||||
# Optionally mark as retrieved
|
# Optionally mark as retrieved
|
||||||
if mark_retrieved and not target_msg.retrieved:
|
if mark_retrieved and not target_msg.retrieved:
|
||||||
target_msg.retrieved = True
|
target_msg.retrieved = True
|
||||||
target_msg._p_changed = True
|
target_msg._p_changed = True
|
||||||
mailbox._p_changed = True
|
mailbox._p_changed = True
|
||||||
# Explicit transaction for the write
|
# Explicit transaction for the write
|
||||||
transaction.get().commit()
|
transaction.get().commit()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": str(target_msg.msg_id),
|
"id": str(target_msg.msg_id),
|
||||||
"from": target_msg.msg_from or "UNKNOWN",
|
"from": target_msg.msg_from or "UNKNOWN",
|
||||||
"to": list(target_msg.msg_to),
|
"to": list(target_msg.msg_to),
|
||||||
"sent_at": target_msg.sent_at.isoformat() + "Z",
|
"sent_at": target_msg.sent_at.isoformat() + "Z",
|
||||||
"text": target_msg.text,
|
"text": target_msg.text,
|
||||||
"retrieved": target_msg.retrieved,
|
"retrieved": target_msg.retrieved,
|
||||||
"has_attachments": len(target_msg.attachments) > 0,
|
"has_attachments": len(target_msg.attachments) > 0,
|
||||||
# Future: "attachments": [...] metadata
|
# Future: "attachments": [...] metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@router.patch("/messages/{msg_id}")
|
@router.patch("/messages/{msg_id}")
|
||||||
async def mark_message_retrieved(
|
async def mark_message_retrieved(
|
||||||
conn: ConnectionDependency,
|
db: DbDependency,
|
||||||
msg_id: str = Path(..., description="Message UUID as string"),
|
msg_id: str = Path(..., description="Message UUID as string"),
|
||||||
payload: MarkRetrievedRequest = None,
|
payload: MarkRetrievedRequest = None,
|
||||||
current_user: HttpUser = Depends(get_current_http_user)
|
current_user: HttpUser = Depends(get_current_http_user)
|
||||||
):
|
):
|
||||||
root = conn.root()
|
with db.transaction() as conn:
|
||||||
|
root = conn.root()
|
||||||
|
|
||||||
username = current_user.username
|
username = current_user.username
|
||||||
mailbox = root.get('messages', {}).get(username)
|
mailbox = root.get('messages', {}).get(username)
|
||||||
|
|
||||||
if not mailbox:
|
if not mailbox:
|
||||||
raise HTTPException(status_code=404, detail="Mailbox not found")
|
raise HTTPException(status_code=404, detail="Mailbox not found")
|
||||||
|
|
||||||
target_msg = None
|
target_msg = None
|
||||||
for msg in mailbox:
|
for msg in mailbox:
|
||||||
if str(msg.msg_id) == msg_id:
|
if str(msg.msg_id) == msg_id:
|
||||||
target_msg = msg
|
target_msg = msg
|
||||||
break
|
break
|
||||||
|
|
||||||
if not target_msg:
|
if not target_msg:
|
||||||
raise HTTPException(status_code=404, detail="Message not found")
|
raise HTTPException(status_code=404, detail="Message not found")
|
||||||
|
|
||||||
if target_msg.retrieved:
|
if target_msg.retrieved:
|
||||||
# Already marked – idempotent success
|
# Already marked – idempotent success
|
||||||
return {"status": "already_retrieved", "id": msg_id}
|
return {"status": "already_retrieved", "id": msg_id}
|
||||||
|
|
||||||
target_msg.retrieved = True
|
target_msg.retrieved = True
|
||||||
target_msg._p_changed = True
|
target_msg._p_changed = True
|
||||||
mailbox._p_changed = True
|
mailbox._p_changed = True
|
||||||
transaction.get().commit()
|
transaction.get().commit()
|
||||||
|
|
||||||
return {"status": "marked_retrieved", "id": msg_id}
|
return {"status": "marked_retrieved", "id": msg_id}
|
||||||
|
|
||||||
@html_router.get("/messages", response_class=HTMLResponse)
|
@html_router.get("/messages", response_class=HTMLResponse)
|
||||||
async def message_list_page(
|
async def message_list_page(
|
||||||
conn: ConnectionDependency,
|
db: DbDependency,
|
||||||
request: Request,
|
request: Request,
|
||||||
type: str = Query("received", alias="msg_type"), # matches your filter links
|
type: str = Query("received", alias="msg_type"), # matches your filter links
|
||||||
limit: Optional[int] = Query(50, le=100),
|
limit: Optional[int] = Query(50, le=100),
|
||||||
@@ -169,7 +172,7 @@ async def message_list_page(
|
|||||||
):
|
):
|
||||||
from packetserver.http.server import templates
|
from packetserver.http.server import templates
|
||||||
# Directly call the existing API endpoint function
|
# Directly call the existing API endpoint function
|
||||||
api_resp = await get_messages(conn, current_user=current_user, type=type, limit=limit, since=None)
|
api_resp = await get_messages(db, current_user=current_user, type=type, limit=limit, since=None)
|
||||||
messages = api_resp["messages"]
|
messages = api_resp["messages"]
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import mimetypes
|
|||||||
|
|
||||||
from packetserver.http.dependencies import get_current_http_user
|
from packetserver.http.dependencies import get_current_http_user
|
||||||
from packetserver.http.auth import HttpUser
|
from packetserver.http.auth import HttpUser
|
||||||
from packetserver.http.database import DbDependency, ConnectionDependency
|
from packetserver.http.database import DbDependency
|
||||||
from packetserver.server.objects import Object
|
from packetserver.server.objects import Object
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|||||||
@@ -3,26 +3,28 @@ from fastapi import APIRouter, Depends
|
|||||||
|
|
||||||
from packetserver.http.dependencies import get_current_http_user
|
from packetserver.http.dependencies import get_current_http_user
|
||||||
from packetserver.http.auth import HttpUser
|
from packetserver.http.auth import HttpUser
|
||||||
from packetserver.http.database import ConnectionDependency
|
from packetserver.http.database import DbDependency
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1", tags=["auth"])
|
router = APIRouter(prefix="/api/v1", tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/profile")
|
@router.get("/profile")
|
||||||
async def profile(conn: ConnectionDependency,current_user: HttpUser = Depends(get_current_http_user)):
|
async def profile(db: DbDependency, current_user: HttpUser = Depends(get_current_http_user)):
|
||||||
username = current_user.username
|
username = current_user.username
|
||||||
root = conn.root()
|
rf_enabled = current_user.is_rf_enabled(db)
|
||||||
|
|
||||||
# Get main BBS User and safe dict
|
# Get main BBS User and safe dict
|
||||||
main_users = root.get('users', {})
|
with db.transaction() as conn:
|
||||||
bbs_user = main_users.get(username)
|
root = conn.root()
|
||||||
safe_profile = bbs_user.to_safe_dict() if bbs_user else {}
|
main_users = root.get('users', {})
|
||||||
rf_enabled = current_user.is_rf_enabled(conn)
|
bbs_user = main_users.get(username)
|
||||||
|
safe_profile = bbs_user.to_safe_dict() if bbs_user else {}
|
||||||
|
|
||||||
return {
|
|
||||||
**safe_profile,
|
return {
|
||||||
"http_enabled": current_user.http_enabled,
|
**safe_profile,
|
||||||
"rf_enabled": rf_enabled,
|
"http_enabled": current_user.http_enabled,
|
||||||
"http_created_at": current_user.created_at,
|
"rf_enabled": rf_enabled,
|
||||||
"http_last_login": current_user.last_login,
|
"http_created_at": current_user.created_at,
|
||||||
}
|
"http_last_login": current_user.last_login,
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ from packetserver.http.dependencies import get_current_http_user
|
|||||||
from packetserver.http.auth import HttpUser
|
from packetserver.http.auth import HttpUser
|
||||||
from packetserver.server.messages import Message
|
from packetserver.server.messages import Message
|
||||||
from packetserver.common.util import is_valid_ax25_callsign
|
from packetserver.common.util import is_valid_ax25_callsign
|
||||||
from packetserver.http.database import ConnectionDependency
|
from packetserver.http.database import DbDependency
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1", tags=["messages"])
|
router = APIRouter(prefix="/api/v1", tags=["messages"])
|
||||||
|
|
||||||
@@ -40,55 +40,57 @@ class SendMessageRequest(BaseModel):
|
|||||||
|
|
||||||
@router.post("/messages")
|
@router.post("/messages")
|
||||||
async def send_message(
|
async def send_message(
|
||||||
conn: ConnectionDependency,
|
db: DbDependency,
|
||||||
payload: SendMessageRequest,
|
payload: SendMessageRequest,
|
||||||
current_user: HttpUser = Depends(get_current_http_user)
|
current_user: HttpUser = Depends(get_current_http_user)
|
||||||
):
|
):
|
||||||
root = conn.root()
|
is_rf_enabled = current_user.is_rf_enabled(db)
|
||||||
|
with db.transaction() as conn:
|
||||||
|
root = conn.root()
|
||||||
|
|
||||||
if not current_user.is_rf_enabled(conn):
|
if not is_rf_enabled:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="RF gateway access required to send messages"
|
detail="RF gateway access required to send messages"
|
||||||
|
)
|
||||||
|
|
||||||
|
username = current_user.username
|
||||||
|
|
||||||
|
# Prepare recipients
|
||||||
|
to_list = payload.to
|
||||||
|
to_tuple = tuple(to_list)
|
||||||
|
if "ALL" in to_list:
|
||||||
|
to_tuple = ("ALL",)
|
||||||
|
|
||||||
|
is_bulletin = "ALL" in to_list
|
||||||
|
recipients = to_list if not is_bulletin else list(root.get('users', {}).keys())
|
||||||
|
|
||||||
|
# Create message using only supported core params
|
||||||
|
new_msg = Message(
|
||||||
|
text=payload.text,
|
||||||
|
msg_from=username,
|
||||||
|
msg_to=to_tuple,
|
||||||
|
attachments=()
|
||||||
)
|
)
|
||||||
|
|
||||||
username = current_user.username
|
# Deliver to recipients + always sender (sent folder)
|
||||||
|
messages_root = root.setdefault('messages', PersistentMapping())
|
||||||
|
delivered_to = set()
|
||||||
|
|
||||||
# Prepare recipients
|
for recip in set(recipients) | {username}:
|
||||||
to_list = payload.to
|
mailbox = messages_root.setdefault(recip, PersistentList())
|
||||||
to_tuple = tuple(to_list)
|
mailbox.append(new_msg)
|
||||||
if "ALL" in to_list:
|
mailbox._p_changed = True
|
||||||
to_tuple = ("ALL",)
|
delivered_to.add(recip)
|
||||||
|
|
||||||
is_bulletin = "ALL" in to_list
|
messages_root._p_changed = True
|
||||||
recipients = to_list if not is_bulletin else list(root.get('users', {}).keys())
|
transaction.commit()
|
||||||
|
|
||||||
# Create message using only supported core params
|
return {
|
||||||
new_msg = Message(
|
"status": "sent",
|
||||||
text=payload.text,
|
"message_id": str(new_msg.msg_id),
|
||||||
msg_from=username,
|
"from": username,
|
||||||
msg_to=to_tuple,
|
"to": list(to_tuple),
|
||||||
attachments=()
|
"sent_at": new_msg.sent_at.isoformat() + "Z",
|
||||||
)
|
"recipients_delivered": len(delivered_to)
|
||||||
|
}
|
||||||
# Deliver to recipients + always sender (sent folder)
|
|
||||||
messages_root = root.setdefault('messages', PersistentMapping())
|
|
||||||
delivered_to = set()
|
|
||||||
|
|
||||||
for recip in set(recipients) | {username}:
|
|
||||||
mailbox = messages_root.setdefault(recip, PersistentList())
|
|
||||||
mailbox.append(new_msg)
|
|
||||||
mailbox._p_changed = True
|
|
||||||
delivered_to.add(recip)
|
|
||||||
|
|
||||||
messages_root._p_changed = True
|
|
||||||
transaction.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "sent",
|
|
||||||
"message_id": str(new_msg.msg_id),
|
|
||||||
"from": username,
|
|
||||||
"to": list(to_tuple),
|
|
||||||
"sent_at": new_msg.sent_at.isoformat() + "Z",
|
|
||||||
"recipients_delivered": len(delivered_to)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user