Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e0b8379
Your commit message
the-icemann May 21, 2026
012ca4d
feat: populate live crop images in seed data
the-icemann May 21, 2026
601a377
feat: seed listing images via internal URL endpoint, bypass Cloudinary
the-icemann May 21, 2026
97d9e09
fix: destroy_seed now cleans up notification_db
the-icemann May 21, 2026
e77b555
Trying to solve cloudFront header issue
the-icemann May 21, 2026
617efec
fix: replace session-based OAuth state with cookie-based state
the-icemann May 21, 2026
795aa31
fix: use HMAC-signed stateless OAuth state, drop cookie dependency
the-icemann May 21, 2026
2ed3c03
fix: correct user service URL in complete-profile endpoint
the-icemann May 22, 2026
b8eacd7
fix: route /auth/complete-profile to EC2 so POST reaches auth service
the-icemann May 22, 2026
741cd70
merge: pull colleague's fixes from origin/main
the-icemann May 23, 2026
32091c5
fix: redirect returning Google users to /auth/sign-in instead of /aut…
the-icemann May 23, 2026
a5bee10
fix: ensure soko-ml-bridge network exists before compose up, relax ga…
the-icemann May 23, 2026
b8beb23
fix: handle None quantity_kg in location-service route cache
the-icemann May 23, 2026
922251d
feat: developer alert email for unsupported crop requests
the-icemann May 23, 2026
f02b822
fix: remove hardcoded BOT_SECRET from auth_service environment
the-icemann May 24, 2026
eabc723
Merge remote-tracking branch 'upstream/main' into price_prediction_se…
the-icemann May 30, 2026
7781bb4
fix: inject Cloudinary credentials into produce and blog service envs
the-icemann May 30, 2026
d540faa
Merge branch 'ArielWandera:main' into price_prediction_service
the-icemann Jun 1, 2026
87d0b8a
Merge branch 'ArielWandera:main' into price_prediction_service
the-icemann Jun 1, 2026
a141843
fix(ml): location-service ordering, gateway port fallback, deploy hea…
the-icemann Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
bash scripts/fetch-secrets.sh
bash scripts/fetch-ml-secrets.sh

echo "=== Ensuring shared bridge network exists ==="
docker network create soko-ml-bridge 2>/dev/null || true

echo "=== Restarting core platform ==="
docker compose pull --ignore-pull-failures 2>/dev/null || true
docker compose up -d --build --remove-orphans
Expand All @@ -49,8 +52,8 @@ jobs:
docker image prune -f

echo "=== Verifying health ==="
sleep 15
for port in 8001 8002 8003 8004 8005 8007 8008 8009; do
sleep 90
for port in 8001 8002 8003 8004 8005 8007 8008 8009 8080; do
status=$(curl -sf --max-time 5 http://localhost:${port}/health | jq -r '.status' 2>/dev/null || echo "unreachable")
echo "Service :${port} -> ${status}"
done
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ services:
DATABASE_URL: postgresql://auth_user:${AUTH_DB_PASS:-auth_pass}@auth_db:5432/auth_db
USER_SERVICE_URL: http://user_service:8002
REDIS_URL: redis://redis:6379/2
BOT_SECRET: soko-bot-secret-change-in-prod
depends_on:
auth_db:
condition: service_healthy
Expand Down
16 changes: 10 additions & 6 deletions infrastructure/cloudfront.tf
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,23 @@ resource "aws_cloudfront_distribution" "frontend" {
max_ttl = 0
}

# complete-profile: forward query_string so the ?access_token= param (set by
# the backend after Google OAuth for returning users) reaches the SPA.
# complete-profile: must reach EC2 for POST (form submit).
# GET returns 404 from the auth service, which CloudFront's custom_error_response
# catches and serves index.html so the SPA still loads.
ordered_cache_behavior {
path_pattern = "/auth/complete-profile"
target_origin_id = "s3-frontend"
target_origin_id = "ec2-api"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "PATCH", "POST", "DELETE"]
cached_methods = ["GET", "HEAD"]
compress = true
compress = false

forwarded_values {
query_string = true
cookies { forward = "none" }
headers = ["Authorization", "Content-Type", "Accept", "Origin", "X-User-Id", "X-User-Role"]
cookies { forward = "all" }
}

min_ttl = 0
default_ttl = 0
max_ttl = 0
Expand Down
7 changes: 4 additions & 3 deletions nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,10 @@ http {
limit_req zone=api_limit burst=20 nodelay;
set $auth_svc "auth_service:8001";
proxy_pass http://$auth_svc$auth_upstream_path;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}

location /oauth/ {
Expand Down
70 changes: 35 additions & 35 deletions scripts/.seed_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,42 +119,42 @@
}
],
"listing_ids": [
"a1881336-12c1-4253-b7e7-6f601b9e0a08",
"e75af1bf-f6d4-4f4b-a387-1f54e8676ed1",
"0e776356-9934-4eba-b911-33370174032d",
"44a36d19-11f9-44c1-b6eb-1475b38fb667",
"b1443480-2920-4d64-a3d8-a9a61dc89596",
"75dbfa7a-6650-4938-9809-9c92ee4afd68",
"7ebc20bb-2f7d-4aca-a530-7a684c6e1bf6",
"b19c1a23-ce80-4666-92b0-2ef8859bfde8",
"b604e46b-2060-4b00-82d6-187f18d79173",
"65ab4997-c4a3-4e34-bccd-532820a9440f",
"f8e7ef91-222d-437e-a40d-ecd91ba8d97c",
"32945110-6c5c-4d8c-9e4a-e8bbc7ba7444",
"7ac3b02a-116c-43e8-a25d-f1968d169a8c",
"7fc508ca-4e1d-4132-9dcf-db149996cd0a",
"ebf680f4-c76b-4c1a-a977-bb1ca7664f8f",
"99e36ee6-9515-475d-8ca8-1731eedff989",
"cbbc017c-38c0-4f01-a77e-92d5aaf7fef5",
"463c30f7-f1e1-426e-bb84-2cecfa429fb8",
"cc260dec-2cfc-487a-a697-d148c4042ba2",
"8dbe2177-a4ed-4e15-9693-bc03cea8a596",
"f8b79aac-2386-474c-9b65-cddd71a2c4fa",
"b870c505-f284-43a6-9f53-46010ab1cb32",
"5fbbf2ac-4fda-4b2c-8eb5-4ef1942a3bc5",
"503950c9-94a4-47c4-91c2-e11dc85c8339",
"68db090c-b472-4de2-8fa5-f849d746bec6"
"416fa083-87e5-4102-ada2-605513dbffed",
"c0830a23-0798-43c4-9ce8-241ac3745a18",
"cece82fb-10fa-4ab0-85c9-1d391a32abdf",
"54b34337-c1dc-463d-9a90-2b520b53ed29",
"5427d0c2-3fc3-4db4-acd1-06addf4e5509",
"9c4ac485-522d-4da6-b81b-79d14950011a",
"33d25a41-075e-439a-b137-b3f6e0402538",
"191a56d7-6cdc-4ba9-ae1e-4315837031de",
"a2077d7e-bcd2-45d0-b738-58eba1c9f370",
"448a7a7a-c82a-4318-b107-7d4d17115f40",
"898644da-4fc3-4aff-a0b6-aa93eec9533b",
"f5a82f6e-40bd-4d95-97f7-234050aa3d83",
"404963f5-b92f-485f-b4bf-a37ec033e7e8",
"535e15d2-eeff-4cd2-8af6-a3ac5ad8debe",
"fa3c3dce-72d4-486a-8ebc-8bb731cf0a7c",
"b340091a-ae19-43a1-bafe-5392c7d20ecc",
"989e681c-9640-4b1c-95ce-a707c500d6b1",
"3b16d11a-7664-4161-a1ad-7ec4a8caf816",
"7a34155b-37a1-47e9-ac95-e071c6401538",
"e5c97328-3619-4a7e-b769-beda5bc1af60",
"0422aeff-9eda-42f1-ade9-314f7537ef36",
"89453ace-b691-4694-9da1-7f833081e8df",
"efc79519-57e7-4c63-8a48-17bd527e3a05",
"ca5b7fe0-cee0-4358-b858-903c34037cbb",
"71f698d4-6ddb-4c2a-b8cd-0e57fe55ca71"
],
"order_ids": [
"73a5edda-740c-40b3-8a0a-183cc3b43b6e",
"6a6b74ea-2735-41fc-9abc-a7cd546b1009",
"bff8c355-fb1a-4a20-a3f6-7a97c01d6d34",
"c9967bd1-2185-4a45-b240-fb2dc2420089",
"37b40b06-9586-4d94-aa86-954fc8cea3f6",
"e218e66c-ae80-4c6b-8303-60fcd289e5fc",
"263e19c2-22c5-4cf7-9eb6-42ceb734caa4",
"5b0b77bb-d634-45ac-9c07-99df5ee06428",
"adffbd85-5e0a-4380-b4fb-fac5891d83ba",
"38ef29b6-a314-48a7-b078-77054b5fe7c6"
"c82dd2ee-1824-4c1e-9fff-d61de1cfb0eb",
"58f88d80-5966-429f-b3ea-7943507a0725",
"bef494c8-1b92-419f-a148-cedc6e1ba323",
"08bf66bf-2860-41d0-a747-0cb760124002",
"5084cec4-6b82-45b1-93c1-ddde5d2b0509",
"f83dc042-2976-4822-80a8-b29a25aba348",
"41f3ef35-5c5b-4eed-8577-a45d3453cc45",
"e1f30f4f-9a4c-49ba-845c-c31e5f7ddc0f",
"fb903949-4a57-4c71-9739-46a0e037f251",
"096b8798-52e7-4f9e-bc30-adcb26f058b6"
]
}
28 changes: 28 additions & 0 deletions scripts/destroy_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,38 @@ def destroy_user_profiles(all_user_ids: list[str]) -> None:
f"DELETE FROM user_profiles WHERE id IN ({lit});")


def destroy_notifications(all_user_ids: list[str]) -> None:
if not all_user_ids:
return
print(" notifications ...")
lit = ids_literal(all_user_ids)
psql("notification_db", "notification_user", "notification_db",
f"DELETE FROM notifications WHERE user_id IN ({lit});")


def destroy_auth_credentials() -> None:
print(" auth credentials (@sokodev.ug accounts) ...")
psql("auth_db", "auth_user", "auth_db",
"DELETE FROM auth_credentials WHERE email LIKE '%@sokodev.ug';")


def flush_api_caches() -> None:
"""Flush Redis caches for produce (db 0) and blog (db 1) services.
Both services cache API responses in Redis; without this flush, deleted
seed data keeps appearing until TTL expiry (up to 10 min).
"""
print(" Redis API caches (produce db0, blog db1) ...")
for db in (0, 1):
cmd = [
"docker", "compose", "-f", "docker-compose.yml",
"exec", "-T", "redis",
"redis-cli", "-n", str(db), "FLUSHDB",
]
result = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
if result.returncode != 0 and result.stderr.strip():
print(f" WARN (redis db{db}): {result.stderr.strip()[:120]}")


def reset_ml_feature_store() -> None:
print(" ML feature store (user_profiles, price_observations, interactions, coverage_gaps) ...")
ml_compose = str(ROOT / "services" / "soko-ml" / "docker-compose.yml")
Expand Down Expand Up @@ -193,10 +219,12 @@ def main() -> None:
destroy_product_reviews(buyer_ids)
destroy_listings(farmer_ids)
destroy_messages(all_ids)
destroy_notifications(all_ids)
destroy_payments(order_ids)
destroy_orders(buyer_ids)
destroy_user_profiles(all_ids)
destroy_auth_credentials()
flush_api_caches()
reset_ml_feature_store()

MANIFEST.unlink(missing_ok=True)
Expand Down
2 changes: 2 additions & 0 deletions scripts/fetch-secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ INTERNAL_SECRET=$(s INTERNAL_SECRET)
USER_SERVICE_URL=http://user_service:8002
REDIS_URL=redis://redis:6379/2
BOT_SECRET=$(s BOT_SECRET)
SENDGRID_API_KEY=$(s SENDGRID_API_KEY)
SENDGRID_FROM_EMAIL=$(s SENDGRID_FROM_EMAIL)
EOF
chmod 600 "$REPO_DIR/services/auth/.env"

Expand Down
76 changes: 74 additions & 2 deletions scripts/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,40 @@
(9, 24), # Tumwesige Paul → Waiswa / Yellow Beans Bam 1
]

# ── Crop image URLs (Unsplash CDN, publicly accessible) ──────────────────────
# Matched by lowercase keyword in the listing name.
# Used by seed_listing_images() to download → upload via Cloudinary.
CROP_IMAGES = {
"maize": "https://images.unsplash.com/photo-1601593346740-925612772716?w=800&q=80",
"sorghum": "https://images.unsplash.com/photo-1603048588665-791ca8aea617?w=800&q=80",
"millet": "https://images.unsplash.com/photo-1574323347407-f5e1ad6d020b?w=800&q=80",
"bean": "https://images.unsplash.com/photo-1553682538-a32cf88c62c7?w=800&q=80",
"potato": "https://images.unsplash.com/photo-1518977676601-b53f82aba655?w=800&q=80",
"tomato": "https://images.unsplash.com/photo-1592924357228-91a4daadcfea?w=800&q=80",
"matoke": "https://images.unsplash.com/photo-1571771894821-ce9b6c11b08e?w=800&q=80",
"banana": "https://images.unsplash.com/photo-1571771894821-ce9b6c11b08e?w=800&q=80",
"cassava": "https://images.unsplash.com/photo-1614668701670-d23b2c5aac01?w=800&q=80",
}
CROP_IMAGE_DEFAULT = "https://images.unsplash.com/photo-1500937386664-56d1dfef3854?w=800&q=80"

# Per-post cover images indexed to match BLOG_POSTS order.
# These are stored directly as URLs in the blog DB (no Cloudinary).
BLOG_POST_IMAGES = [
"https://images.unsplash.com/photo-1601593346740-925612772716?w=800&q=80", # maize/solar dryer
"https://images.unsplash.com/photo-1518977676601-b53f82aba655?w=800&q=80", # potatoes
"https://images.unsplash.com/photo-1553682538-a32cf88c62c7?w=800&q=80", # K132 yellow beans
"https://images.unsplash.com/photo-1592924357228-91a4daadcfea?w=800&q=80", # tomatoes / drip
]


def pick_crop_image(name: str) -> str:
n = name.lower()
for keyword, url in CROP_IMAGES.items():
if keyword in n:
return url
return CROP_IMAGE_DEFAULT


# ── Blog posts authored by seeded farmers ────────────────────────────────────
BLOG_POSTS = [
{
Expand Down Expand Up @@ -785,6 +819,38 @@ def create_listings(farmers: list) -> list:
return all_listings


# ── Phase 3b: Seed listing images ────────────────────────────────────────────

def seed_listing_images(listings: list) -> None:
"""
Registers a crop-appropriate Unsplash URL directly on each listing via the
internal /images/url endpoint — no Cloudinary upload, no interference with
how real users upload images. Skips gracefully if the endpoint is unavailable.
"""
import os
internal_secret = os.getenv("INTERNAL_SECRET", "")
if not internal_secret:
print("\n── Phase 3b: Skipping images (INTERNAL_SECRET not set) ─────────")
return

print("\n── Phase 3b: Seeding listing images (direct URL, no Cloudinary) ─")
for listing in listings:
img_url = pick_crop_image(listing["name"])
try:
resp = requests.post(
f"{PRODUCE}/listings/{listing['id']}/images/url",
json={"url": img_url},
headers={"X-Internal-Secret": internal_secret},
timeout=10,
)
if resp.ok:
print(f" ✓ Image: {listing['name']}")
else:
print(f" ~ Image failed ({resp.status_code}): {listing['name']}")
except Exception as e:
print(f" ~ Skipped {listing['name']}: {e}")


# ── Phase 4: Place orders ────────────────────────────────────────────────────

def place_orders(buyers: list, listings: list) -> list:
Expand Down Expand Up @@ -898,17 +964,22 @@ def create_blog_posts(farmers: list) -> None:
print("\n── Phase 6: Publishing farmer blog posts ────────────────────────")

all_farmers = farmers # same order as FARMERS + EXTRA_FARMERS
for post_def in BLOG_POSTS:
for post_idx, post_def in enumerate(BLOG_POSTS):
idx = post_def["farmer_idx"]
if idx >= len(all_farmers):
print(f" SKIP blog post (farmer_idx {idx} out of range)")
continue

f = all_farmers[idx]
cover = (
BLOG_POST_IMAGES[post_idx]
if post_idx < len(BLOG_POST_IMAGES)
else BLOG_POST_IMAGES[-1]
)
payload = {
"title": post_def["title"],
"excerpt": post_def["excerpt"],
"image": "https://images.unsplash.com/photo-1628352081506-83c43123a6b9?w=800",
"image": cover,
"category": post_def["category"],
"tags": post_def["tags"],
"body": post_def["body"],
Expand Down Expand Up @@ -1061,6 +1132,7 @@ def print_summary(farmers: list, buyers: list, listings: list, order_ids: list):
farmers, buyers = register_users()
update_profiles(farmers)
listings = create_listings(farmers)
seed_listing_images(listings)
order_ids = place_orders(buyers, listings)
create_conversations(buyers, listings)
create_blog_posts(farmers)
Expand Down
2 changes: 2 additions & 0 deletions services/auth/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class Settings(BaseSettings):
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
REDIS_URL: str = "redis://redis:6379/2"
BOT_SECRET: str = ""
SENDGRID_API_KEY: str = ""
SENDGRID_FROM_EMAIL: str = ""

class Config:
env_file = ".env"
Expand Down
7 changes: 4 additions & 3 deletions services/auth/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from starlette.middleware.sessions import SessionMiddleware
from app.db.session import Base, engine
from app.core.config import settings
from app.routers import auth, oauth, bot_auth
from app.routers import auth, oauth, bot_auth, alert

@asynccontextmanager
async def lifespan(app: FastAPI):
Expand All @@ -18,8 +18,9 @@ async def lifespan(app: FastAPI):
root_path="/auth",
)

app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY) # ← must be here
app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY)

app.include_router(auth.router)
app.include_router(oauth.router)
app.include_router(bot_auth.router)
app.include_router(bot_auth.router)
app.include_router(alert.router)
Loading
Loading