Skip to content

Commit ae281d5

Browse files
committed
Added download audit model and endpoint
1 parent 6c704e1 commit ae281d5

File tree

3 files changed

+90
-1
lines changed

3 files changed

+90
-1
lines changed

odp/api/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import FastAPI, Request, Response
22
from fastapi.middleware.cors import CORSMiddleware
33

4-
from odp.api.routers import catalog, client, collection, provider, record, role, schema, scope, status, tag, token, user, vocabulary
4+
from odp.api.routers import catalog, client, collection, provider, record, role, schema, scope, status, tag, token, user, vocabulary, download
55
from odp.config import config
66
from odp.db import Session
77
from odp.version import VERSION
@@ -28,6 +28,7 @@
2828
app.include_router(token.router, prefix='/token', tags=['Token'])
2929
app.include_router(user.router, prefix='/user', tags=['User'])
3030
app.include_router(vocabulary.router, prefix='/vocabulary', tags=['Vocabulary'])
31+
app.include_router(download.router, prefix='/download', tags=['Download'])
3132

3233
app.add_middleware(
3334
CORSMiddleware,

odp/api/routers/download.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from datetime import datetime, timezone
2+
3+
from fastapi import APIRouter, Depends, HTTPException, Request
4+
from starlette.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST
5+
from sqlalchemy import insert
6+
7+
from odp.db import Session
8+
from odp.db.models import DownloadAudit
9+
from odp.api.lib.auth import Authorize, Authorized # only if you want auth; otherwise omit
10+
11+
router = APIRouter()
12+
13+
@router.post('/audit', status_code=HTTP_201_CREATED)
14+
async def create_download_audit(request: Request):
15+
"""
16+
Accept JSON payload to record a download audit and return success.
17+
"""
18+
payload = await request.json()
19+
if not isinstance(payload, dict):
20+
raise HTTPException(HTTP_400_BAD_REQUEST, 'Invalid JSON payload')
21+
22+
# derive client_id/user_id from request context (if you have that info).
23+
# Use placeholders if not available
24+
client_id = getattr(request.state, 'client_id', None) or payload.get('client_id') or 'unknown'
25+
user_id = getattr(request.state, 'user_id', None) or payload.get('user_id')
26+
27+
download_url = payload.get('download_url')
28+
file_size = payload.get('file_size')
29+
success = bool(payload.get('success', True))
30+
meta = payload.get('meta', {}) or {}
31+
32+
# copy optional form fields into meta for storage
33+
for k in ('name', 'email', 'organisation'):
34+
if payload.get(k) is not None:
35+
meta[k] = payload.get(k)
36+
37+
# capture IP and UA
38+
ip_address = request.client.host if request.client else None
39+
user_agent = request.headers.get('user-agent')
40+
41+
# persist using Session
42+
#
43+
# --- THIS IS THE FIX ---
44+
# We use `Session.begin()` to manage the transaction,
45+
# and call methods on the imported `Session` object itself.
46+
#
47+
with Session.begin():
48+
audit = DownloadAudit(
49+
client_id=client_id,
50+
user_id=user_id,
51+
download_url=download_url,
52+
ip_address=ip_address,
53+
user_agent=user_agent,
54+
file_size=file_size,
55+
success=success,
56+
timestamp=datetime.now(timezone.utc),
57+
meta=meta,
58+
)
59+
Session.add(audit)
60+
Session.flush()
61+
audit_id = audit.id
62+
# --- END OF FIX ---
63+
64+
return {"status": "ok", "audit_id": audit_id}

odp/db/models/download.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from datetime import datetime, timezone
2+
3+
from sqlalchemy import Column, Integer, String, TIMESTAMP, Boolean, BigInteger, Text
4+
from sqlalchemy.dialects.postgresql import JSONB
5+
from sqlalchemy.orm import relationship
6+
7+
from odp.db import Base
8+
9+
10+
class DownloadAudit(Base):
11+
"""Download audit log."""
12+
13+
__tablename__ = 'download_audit'
14+
15+
id = Column(Integer, primary_key=True, autoincrement=True)
16+
client_id = Column(String, nullable=False)
17+
user_id = Column(String, nullable=True)
18+
download_url = Column(String, nullable=True)
19+
ip_address = Column(String, nullable=True)
20+
user_agent = Column(Text, nullable=True)
21+
file_size = Column(BigInteger, nullable=True)
22+
success = Column(Boolean, nullable=False)
23+
timestamp = Column(TIMESTAMP(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
24+
meta = Column(JSONB, nullable=True)

0 commit comments

Comments
 (0)