Skip to content

Commit a87c350

Browse files
authored
Merge pull request #180 from Tinna23/fix/stellar-wave-issues-120-121-122-123
fix: resolve issues #120, #121, #122, #123 (Stellar Wave)
2 parents 69a4822 + 460be56 commit a87c350

8 files changed

Lines changed: 63 additions & 28 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Required in all environments. Must be at least 32 characters.
22
QR_SIGNING_KEY=replace_with_a_minimum_32_character_secret_key
3+
4+
# API keys — required, no defaults. Must be at least 32 characters.
5+
# Never use the placeholder values below in production.
6+
# SERVICE_API_KEY=<generate_a_secure_random_32plus_char_secret>
7+
# ADMIN_API_KEY=<generate_a_secure_random_32plus_char_secret>
38
DATABASE_URL=postgresql://veritix:veritix@localhost:5432/veritix
49
NEST_API_BASE_URL=https://api.example.com
510
NEST_API_TOKEN=your_nest_api_token

src/analytics/service.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@ def _update_analytics_stats(self, event_id: str,
599599
increment_transfer: bool = False, is_successful: bool = True,
600600
increment_invalid: bool = False):
601601
"""Internal method to update analytics stats."""
602+
session = None
602603
try:
603604
session = get_session()
604605

@@ -645,9 +646,11 @@ def _update_analytics_stats(self, event_id: str,
645646
"event_id": event_id,
646647
"error": str(e)
647648
})
648-
session.rollback()
649+
if session:
650+
session.rollback()
649651
finally:
650-
session.close()
652+
if session:
653+
session.close()
651654

652655

653656
# Global instance

src/auth/dependencies.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from fastapi import Depends, HTTPException, status
44
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
55

6-
from src.config import settings
6+
from src.config import get_settings
77

88
# auto_error=False allows us to manually raise 401 when header is absent.
99
bearer_scheme = HTTPBearer(auto_error=False)
@@ -19,7 +19,7 @@ def require_service_key(
1919
detail="Missing or invalid authentication token",
2020
)
2121

22-
if not secrets.compare_digest(credentials.credentials, settings.SERVICE_API_KEY):
22+
if not secrets.compare_digest(credentials.credentials, get_settings().SERVICE_API_KEY):
2323
raise HTTPException(
2424
status_code=status.HTTP_403_FORBIDDEN,
2525
detail="Invalid service token",
@@ -38,7 +38,7 @@ def require_admin_key(
3838
detail="Missing or invalid authentication token",
3939
)
4040

41-
if not secrets.compare_digest(credentials.credentials, settings.ADMIN_API_KEY):
41+
if not secrets.compare_digest(credentials.credentials, get_settings().ADMIN_API_KEY):
4242
raise HTTPException(
4343
status_code=status.HTTP_403_FORBIDDEN,
4444
detail="Invalid admin token",

src/config.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from functools import lru_cache
22
from typing import Optional
33

4-
from pydantic import Field
4+
from pydantic import Field, field_validator
55
from pydantic_settings import BaseSettings, SettingsConfigDict
66

77
class Settings(BaseSettings):
@@ -35,16 +35,23 @@ class Settings(BaseSettings):
3535
REPORT_CACHE_MINUTES: int = 60
3636
SHUTDOWN_TIMEOUT_SECONDS: int = 30
3737

38-
SERVICE_API_KEY: str = "default_service_secret_change_me"
39-
ADMIN_API_KEY: str = "default_admin_secret_change_me"
38+
SERVICE_API_KEY: str = Field(...)
39+
ADMIN_API_KEY: str = Field(...)
40+
41+
@field_validator("SERVICE_API_KEY", "ADMIN_API_KEY")
42+
@classmethod
43+
def validate_api_keys(cls, v: str, info) -> str:
44+
forbidden = {"default_service_secret_change_me", "default_admin_secret_change_me"}
45+
if v in forbidden or len(v) < 32:
46+
raise ValueError(
47+
f"{info.field_name} must be at least 32 characters and must not use a default value"
48+
)
49+
return v
4050
KNOWN_LOCATIONS: str = (
4151
"lagos,abuja,port harcourt,kano,ibadan,benin,kaduna,jos,enugu,calabar,"
4252
"owerri,warri,uyo,akure,ilorin,sokoto,zaria,maiduguri,asaba,nnewi"
4353
)
4454

45-
settings = Settings()
46-
47-
4855
@lru_cache
4956
def get_settings() -> Settings:
5057
return Settings()

src/report_service.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,19 +276,39 @@ def _query_event_names() -> Dict[str, str]:
276276

277277

278278
def _query_transfer_stats(target_date: Optional[date] = None) -> Dict[str, int]:
279-
"""Query transfer statistics.
279+
engine = _pg_engine()
280+
if engine is None:
281+
logger.warning("DATABASE_URL not set; cannot query transfer stats")
282+
return {"total_transfers": 0}
280283

281-
Returns a placeholder dict; a real implementation would query a transfers table.
282-
"""
283-
return {"total_transfers": 0}
284+
if target_date is None:
285+
target_date = date.today()
286+
287+
with engine.connect() as conn:
288+
result = conn.execute(
289+
text("SELECT COUNT(*) FROM ticket_transfers WHERE transfer_timestamp::date = :target_date"),
290+
{"target_date": target_date},
291+
)
292+
row = result.fetchone()
293+
return {"total_transfers": int(row[0]) if row else 0}
284294

285295

286296
def _query_invalid_scans(target_date: Optional[date] = None) -> Dict[str, int]:
287-
"""Query invalid scan statistics.
297+
engine = _pg_engine()
298+
if engine is None:
299+
logger.warning("DATABASE_URL not set; cannot query invalid scan stats")
300+
return {"invalid_scans": 0}
288301

289-
Returns a placeholder dict; a real implementation would query a scans table.
290-
"""
291-
return {"invalid_scans": 0}
302+
if target_date is None:
303+
target_date = date.today()
304+
305+
with engine.connect() as conn:
306+
result = conn.execute(
307+
text("SELECT COUNT(*) FROM invalid_attempts WHERE attempt_timestamp::date = :target_date"),
308+
{"target_date": target_date},
309+
)
310+
row = result.fetchone()
311+
return {"invalid_scans": int(row[0]) if row else 0}
292312

293313

294314
def generate_daily_report_csv(

src/tests/test_auth.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from fastapi import FastAPI, Depends
33
from fastapi.testclient import TestClient
44
from src.auth.dependencies import require_service_key, require_admin_key
5-
from src.config import settings
5+
from src.config import get_settings
66

77
# Setup a dummy FastAPI app for isolated auth testing
88
app = FastAPI()
@@ -41,21 +41,21 @@ def test_invalid_token():
4141
assert response.json()["detail"] == "Invalid admin token"
4242

4343
def test_valid_service_token():
44-
headers = {"Authorization": f"Bearer {settings.SERVICE_API_KEY}"}
44+
headers = {"Authorization": f"Bearer {get_settings().SERVICE_API_KEY}"}
4545
response = client.get("/service-protected", headers=headers)
4646
assert response.status_code == 200
4747
assert response.json()["msg"] == "service success"
4848

4949
def test_valid_admin_token():
50-
headers = {"Authorization": f"Bearer {settings.ADMIN_API_KEY}"}
50+
headers = {"Authorization": f"Bearer {get_settings().ADMIN_API_KEY}"}
5151
response = client.get("/admin-protected", headers=headers)
5252
assert response.status_code == 200
5353
assert response.json()["msg"] == "admin success"
5454

5555
def test_cross_auth_fails():
5656
"""Ensure a valid service token cannot access an admin route and vice-versa."""
57-
service_headers = {"Authorization": f"Bearer {settings.SERVICE_API_KEY}"}
58-
admin_headers = {"Authorization": f"Bearer {settings.ADMIN_API_KEY}"}
57+
service_headers = {"Authorization": f"Bearer {get_settings().SERVICE_API_KEY}"}
58+
admin_headers = {"Authorization": f"Bearer {get_settings().ADMIN_API_KEY}"}
5959

6060
response1 = client.get("/admin-protected", headers=service_headers)
6161
assert response1.status_code == 403

tests/test_heatmap.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
from fastapi.testclient import TestClient
77

88
from src.analytics.service import AnalyticsService
9-
from src.config import settings
9+
from src.config import get_settings
1010
from src.main import app
1111

1212
client = TestClient(app)
1313

14-
SERVICE_HEADERS = {"Authorization": f"Bearer {settings.SERVICE_API_KEY}"}
14+
SERVICE_HEADERS = {"Authorization": f"Bearer {get_settings().SERVICE_API_KEY}"}
1515

1616

1717
# ---------------------------------------------------------------------------

tests/test_reports_list.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
import pytest
1111
from fastapi.testclient import TestClient
1212

13-
from src.config import settings
13+
from src.config import get_settings
1414
from src.main import app
1515
from src.report_service import REPORTS_DIR
1616

1717
client = TestClient(app)
1818

19-
ADMIN_HEADERS = {"Authorization": f"Bearer {settings.ADMIN_API_KEY}"}
19+
ADMIN_HEADERS = {"Authorization": f"Bearer {get_settings().ADMIN_API_KEY}"}
2020

2121
# ---------------------------------------------------------------------------
2222
# Helpers / fixtures

0 commit comments

Comments
 (0)