Update backend configuration and dependencies
This commit is contained in:
parent
4d29a097a3
commit
77d1277cd8
9 changed files with 300 additions and 81 deletions
|
|
@ -26,19 +26,24 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy Python packages from builder
|
# Create non-root user for security
|
||||||
COPY --from=builder /root/.local /root/.local
|
RUN useradd -m -u 1000 appuser
|
||||||
|
|
||||||
|
# Copy Python packages from builder to appuser's home
|
||||||
|
COPY --from=builder /root/.local /home/appuser/.local
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
RUN chown -R appuser:appuser /app /home/appuser/.local
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV PATH=/root/.local/bin:$PATH
|
ENV PATH=/home/appuser/.local/bin:$PATH
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
# Copy application code
|
# Copy application code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN chown -R appuser:appuser /app
|
||||||
|
|
||||||
# Create non-root user for security
|
|
||||||
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Job ID: MTAD-IMPL-2025-11-18-CL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from app.api.v1 import auth, blog, forum, merch, podcast, profiles, resources, tribute, health
|
from app.api.v1 import auth, blog, forum, merch, podcast, profiles, resources, tribute, health, sso
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -21,5 +21,6 @@ api_router.include_router(profiles.router, prefix="/profiles", tags=["Profiles"]
|
||||||
api_router.include_router(resources.router, prefix="/resources", tags=["Resources"])
|
api_router.include_router(resources.router, prefix="/resources", tags=["Resources"])
|
||||||
api_router.include_router(tribute.router, prefix="/tribute", tags=["Tribute"])
|
api_router.include_router(tribute.router, prefix="/tribute", tags=["Tribute"])
|
||||||
api_router.include_router(health.router, tags=["Health"])
|
api_router.include_router(health.router, tags=["Health"])
|
||||||
|
api_router.include_router(sso.router, prefix="/sso", tags=["SSO"])
|
||||||
|
|
||||||
__all__ = ["api_router"]
|
__all__ = ["api_router"]
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,69 @@
|
||||||
"""
|
"""
|
||||||
Blog MVP API endpoints.
|
Blog API endpoints.
|
||||||
|
|
||||||
Job ID: MTAD-IMPL-2025-11-18-CL
|
Job ID: MTAD-IMPL-2025-11-18-CL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
import feedparser
|
||||||
from app.database import get_db
|
import httpx
|
||||||
from app.models import BlogPost
|
from datetime import datetime
|
||||||
|
from time import mktime
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# TODO: Get this from settings/env
|
||||||
|
RSS_FEED_URL = "https://morethanadiagnosis.org/feed/" # Placeholder, need actual URL
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/rss")
|
||||||
async def list_blog_posts(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
|
async def get_blog_rss():
|
||||||
"""List all published blog posts."""
|
"""Fetch and parse blog posts from WordPress RSS feed."""
|
||||||
posts = db.query(BlogPost).filter(BlogPost.published_at.isnot(None)).offset(skip).limit(limit).all()
|
async with httpx.AsyncClient() as client:
|
||||||
return {"items": posts, "count": len(posts)}
|
try:
|
||||||
|
response = await client.get(RSS_FEED_URL)
|
||||||
|
response.raise_for_status()
|
||||||
|
except httpx.RequestError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=f"Error fetching blog feed: {exc}"
|
||||||
|
)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=exc.response.status_code,
|
||||||
|
detail=f"Error fetching blog feed: {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
feed = feedparser.parse(response.text)
|
||||||
|
|
||||||
|
posts = []
|
||||||
|
for entry in feed.entries:
|
||||||
|
# Parse published date
|
||||||
|
published_at = None
|
||||||
|
if hasattr(entry, 'published_parsed'):
|
||||||
|
published_at = datetime.fromtimestamp(mktime(entry.published_parsed))
|
||||||
|
|
||||||
|
# Extract image if available (WordPress often puts it in content or media:content)
|
||||||
|
image_url = None
|
||||||
|
if 'media_content' in entry:
|
||||||
|
image_url = entry.media_content[0]['url']
|
||||||
|
elif 'links' in entry:
|
||||||
|
for link in entry.links:
|
||||||
|
if link.get('rel') == 'enclosure' and link.get('type', '').startswith('image/'):
|
||||||
|
image_url = link['href']
|
||||||
|
break
|
||||||
|
|
||||||
@router.get("/{post_id}")
|
posts.append({
|
||||||
async def get_blog_post(post_id: str, db: Session = Depends(get_db)):
|
"id": entry.get("id", entry.get("link")),
|
||||||
"""Get a specific blog post by ID."""
|
"title": entry.get("title"),
|
||||||
post = db.query(BlogPost).filter(BlogPost.id == post_id).first()
|
"link": entry.get("link"),
|
||||||
if not post:
|
"published_at": published_at,
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blog post not found")
|
"summary": entry.get("summary"),
|
||||||
return post
|
"content": entry.get("content", [{"value": ""}])[0]["value"],
|
||||||
|
"image_url": image_url,
|
||||||
|
"author": entry.get("author")
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
@router.post("/")
|
"title": feed.feed.get("title", "Blog"),
|
||||||
async def create_blog_post(title: str, slug: str, content: str, db: Session = Depends(get_db)):
|
"description": feed.feed.get("description", ""),
|
||||||
"""Create a new blog post (requires authentication)."""
|
"items": posts
|
||||||
# TODO: Implement with proper auth and validation
|
}
|
||||||
return {"message": "Blog post creation not yet implemented"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{post_id}")
|
|
||||||
async def update_blog_post(post_id: str, title: str, content: str, db: Session = Depends(get_db)):
|
|
||||||
"""Update a blog post (requires authentication and ownership)."""
|
|
||||||
# TODO: Implement with proper auth and validation
|
|
||||||
return {"message": "Blog post update not yet implemented"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{post_id}")
|
|
||||||
async def delete_blog_post(post_id: str, db: Session = Depends(get_db)):
|
|
||||||
"""Delete a blog post (requires authentication and ownership)."""
|
|
||||||
# TODO: Implement with proper auth and validation
|
|
||||||
return {"message": "Blog post deletion not yet implemented"}
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models import ForumCategory, ForumThread, ForumPost
|
from app.models import ForumCategory, ForumThread, ForumPost
|
||||||
|
import uuid
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -27,14 +28,52 @@ async def list_posts(thread_id: str, skip: int = 0, limit: int = 20, db: Session
|
||||||
@router.post("/categories")
|
@router.post("/categories")
|
||||||
async def create_category(name: str, db: Session = Depends(get_db)):
|
async def create_category(name: str, db: Session = Depends(get_db)):
|
||||||
"""Create forum category (admin only)."""
|
"""Create forum category (admin only)."""
|
||||||
return {"message": "Category creation not yet implemented"}
|
# Mock implementation for MVP
|
||||||
|
category = ForumCategory(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
name=name,
|
||||||
|
slug=name.lower().replace(" ", "-"),
|
||||||
|
description=f"Discussions about {name}",
|
||||||
|
order=0
|
||||||
|
)
|
||||||
|
db.add(category)
|
||||||
|
db.commit()
|
||||||
|
return category
|
||||||
|
|
||||||
@router.post("/threads")
|
@router.post("/threads")
|
||||||
async def create_thread(category_id: str, title: str, db: Session = Depends(get_db)):
|
async def create_thread(category_id: str, title: str, content: str, user_id: str = "test-user", db: Session = Depends(get_db)):
|
||||||
"""Create forum thread (requires authentication)."""
|
"""Create forum thread."""
|
||||||
return {"message": "Thread creation not yet implemented"}
|
# Mock user for MVP
|
||||||
|
thread = ForumThread(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
category_id=category_id,
|
||||||
|
author_id=user_id,
|
||||||
|
title=title,
|
||||||
|
slug=title.lower().replace(" ", "-"),
|
||||||
|
)
|
||||||
|
db.add(thread)
|
||||||
|
|
||||||
|
# Create initial post
|
||||||
|
post = ForumPost(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
thread_id=thread.id,
|
||||||
|
author_id=user_id,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
db.add(post)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return thread
|
||||||
|
|
||||||
@router.post("/posts")
|
@router.post("/posts")
|
||||||
async def create_post(thread_id: str, content: str, db: Session = Depends(get_db)):
|
async def create_post(thread_id: str, content: str, user_id: str = "test-user", db: Session = Depends(get_db)):
|
||||||
"""Create forum post (requires authentication)."""
|
"""Create forum post."""
|
||||||
return {"message": "Post creation not yet implemented"}
|
post = ForumPost(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
thread_id=thread_id,
|
||||||
|
author_id=user_id,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
db.add(post)
|
||||||
|
db.commit()
|
||||||
|
return post
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,84 @@
|
||||||
"""Podcast MVP API endpoints. Job ID: MTAD-IMPL-2025-11-18-CL"""
|
from fastapi import APIRouter, HTTPException
|
||||||
from fastapi import APIRouter, Depends
|
import feedparser
|
||||||
from sqlalchemy.orm import Session
|
import httpx
|
||||||
from app.database import get_db
|
from pydantic import BaseModel
|
||||||
from app.models import PodcastEpisode
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/episodes")
|
RSS_FEED_URL = "https://rss.libsyn.com/shows/563180/destinations/4867845.xml"
|
||||||
async def list_episodes(skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
|
|
||||||
"""List all podcast episodes."""
|
|
||||||
episodes = db.query(PodcastEpisode).filter(PodcastEpisode.published_at.isnot(None)).order_by(PodcastEpisode.published_at.desc()).offset(skip).limit(limit).all()
|
|
||||||
return {"items": episodes}
|
|
||||||
|
|
||||||
@router.get("/episodes/{episode_id}")
|
class PodcastEpisode(BaseModel):
|
||||||
async def get_episode(episode_id: str, db: Session = Depends(get_db)):
|
id: str
|
||||||
"""Get a specific episode."""
|
title: str
|
||||||
episode = db.query(PodcastEpisode).filter(PodcastEpisode.id == episode_id).first()
|
description: str
|
||||||
if not episode:
|
audio_url: str
|
||||||
from fastapi import HTTPException, status
|
published_at: str
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Episode not found")
|
duration: Optional[str] = None
|
||||||
return episode
|
image_url: Optional[str] = None
|
||||||
|
|
||||||
@router.post("/episodes")
|
class PodcastFeed(BaseModel):
|
||||||
async def create_episode(title: str, audio_url: str, db: Session = Depends(get_db)):
|
title: str
|
||||||
"""Create podcast episode (admin only)."""
|
description: str
|
||||||
return {"message": "Episode creation not yet implemented"}
|
image_url: Optional[str] = None
|
||||||
|
episodes: List[PodcastEpisode]
|
||||||
|
|
||||||
|
@router.get("/", response_model=PodcastFeed)
|
||||||
|
async def get_podcasts():
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(RSS_FEED_URL)
|
||||||
|
response.raise_for_status()
|
||||||
|
rss_content = response.text
|
||||||
|
|
||||||
|
feed = feedparser.parse(rss_content)
|
||||||
|
|
||||||
|
if feed.bozo:
|
||||||
|
# Log warning but try to proceed if possible, or raise error
|
||||||
|
print(f"Warning: Feed parsing error: {feed.bozo_exception}")
|
||||||
|
|
||||||
|
episodes = []
|
||||||
|
for entry in feed.entries:
|
||||||
|
# Extract audio URL
|
||||||
|
audio_url = ""
|
||||||
|
for link in entry.links:
|
||||||
|
if link.type == "audio/mpeg":
|
||||||
|
audio_url = link.href
|
||||||
|
break
|
||||||
|
|
||||||
|
if not audio_url:
|
||||||
|
continue # Skip episodes without audio
|
||||||
|
|
||||||
|
# Extract image
|
||||||
|
image_url = None
|
||||||
|
if 'image' in entry:
|
||||||
|
image_url = entry.image.href
|
||||||
|
elif 'itunes_image' in entry:
|
||||||
|
image_url = entry.itunes_image.href
|
||||||
|
|
||||||
|
episodes.append(PodcastEpisode(
|
||||||
|
id=entry.id,
|
||||||
|
title=entry.title,
|
||||||
|
description=entry.summary,
|
||||||
|
audio_url=audio_url,
|
||||||
|
published_at=entry.published,
|
||||||
|
duration=entry.get('itunes_duration'),
|
||||||
|
image_url=image_url
|
||||||
|
))
|
||||||
|
|
||||||
|
feed_image = None
|
||||||
|
if 'image' in feed.feed:
|
||||||
|
feed_image = feed.feed.image.href
|
||||||
|
elif 'itunes_image' in feed.feed:
|
||||||
|
feed_image = feed.feed.itunes_image.href
|
||||||
|
|
||||||
|
return PodcastFeed(
|
||||||
|
title=feed.feed.title,
|
||||||
|
description=feed.feed.description,
|
||||||
|
image_url=feed_image,
|
||||||
|
episodes=episodes
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to fetch podcasts: {str(e)}")
|
||||||
|
|
|
||||||
90
backend/app/api/v1/sso.py
Normal file
90
backend/app/api/v1/sso.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
"""
|
||||||
|
Discourse SSO (Connect) integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import urllib.parse
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models import User
|
||||||
|
from app.api.v1.auth import get_current_user
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/discourse")
|
||||||
|
async def discourse_sso(
|
||||||
|
sso: str,
|
||||||
|
sig: str,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Handle Discourse SSO login request.
|
||||||
|
|
||||||
|
1. Validate the signature of the incoming payload.
|
||||||
|
2. Decode the payload (nonce).
|
||||||
|
3. Construct a new payload with user data.
|
||||||
|
4. Sign the new payload.
|
||||||
|
5. Redirect back to Discourse.
|
||||||
|
"""
|
||||||
|
if not settings.discourse_sso_secret:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Discourse SSO secret not configured"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. Validate Signature
|
||||||
|
secret = settings.discourse_sso_secret.encode("utf-8")
|
||||||
|
expected_sig = hmac.new(secret, sso.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
if sig != expected_sig:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid SSO signature"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Decode Payload
|
||||||
|
try:
|
||||||
|
decoded_sso = base64.b64decode(sso).decode("utf-8")
|
||||||
|
params = urllib.parse.parse_qs(decoded_sso)
|
||||||
|
nonce = params.get("nonce", [None])[0]
|
||||||
|
|
||||||
|
if not nonce:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Missing nonce in SSO payload"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid SSO payload"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Construct User Payload
|
||||||
|
# Discourse expects: nonce, email, external_id, username, name, etc.
|
||||||
|
user_params = {
|
||||||
|
"nonce": nonce,
|
||||||
|
"email": current_user.email,
|
||||||
|
"external_id": current_user.id,
|
||||||
|
"username": current_user.profile.display_name.replace(" ", "_") if current_user.profile and current_user.profile.display_name else f"user_{current_user.id[:8]}",
|
||||||
|
"name": current_user.profile.display_name if current_user.profile else "",
|
||||||
|
"require_activation": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Encode and Sign
|
||||||
|
encoded_params = urllib.parse.urlencode(user_params)
|
||||||
|
base64_params = base64.b64encode(encoded_params.encode("utf-8")).decode("utf-8")
|
||||||
|
new_sig = hmac.new(secret, base64_params.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
# 5. Redirect
|
||||||
|
# Redirect to Discourse's return URL
|
||||||
|
# Usually: {DISCOURSE_URL}/session/sso_login
|
||||||
|
return_url = f"{settings.discourse_url}/session/sso_login"
|
||||||
|
redirect_url = f"{return_url}?sso={base64_params}&sig={new_sig}"
|
||||||
|
|
||||||
|
return RedirectResponse(url=redirect_url)
|
||||||
|
|
@ -77,6 +77,10 @@ class Settings(BaseSettings):
|
||||||
feature_mfa_enabled: bool = True
|
feature_mfa_enabled: bool = True
|
||||||
feature_social_login_enabled: bool = False
|
feature_social_login_enabled: bool = False
|
||||||
feature_pseudonym_enabled: bool = True
|
feature_pseudonym_enabled: bool = True
|
||||||
|
|
||||||
|
# Discourse
|
||||||
|
discourse_sso_secret: str = "change-me-in-production"
|
||||||
|
discourse_url: str = "https://forum.morethanadiagnosis.org"
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
# Gemini Parallel Deployment
|
# Gemini Parallel Deployment
|
||||||
# Ports shifted to avoid conflict with production
|
# Ports shifted to avoid conflict with production
|
||||||
|
|
@ -12,7 +12,6 @@ services:
|
||||||
POSTGRES_USER: admin
|
POSTGRES_USER: admin
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-gemini-test-password}
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-gemini-test-password}
|
||||||
POSTGRES_DB: morethanadiagnosis
|
POSTGRES_DB: morethanadiagnosis
|
||||||
POSTGRES_INITDB_ARGS: "-c shared_preload_libraries=pg_stat_statements"
|
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data_gemini:/var/lib/postgresql/data
|
- postgres_data_gemini:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -20,7 +19,7 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- mtad-network-gemini
|
- mtad-network-gemini
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U admin -d morethanadiagnosis"]
|
test: [ "CMD-SHELL", "pg_isready -U admin -d morethanadiagnosis" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
@ -37,7 +36,7 @@ services:
|
||||||
- mtad-network-gemini
|
- mtad-network-gemini
|
||||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-gemini-test-password}
|
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-gemini-test-password}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: [ "CMD", "redis-cli", "ping" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
@ -54,7 +53,7 @@ services:
|
||||||
DATABASE_URL: postgresql://admin:${DB_PASSWORD:-gemini-test-password}@postgres:5432/morethanadiagnosis
|
DATABASE_URL: postgresql://admin:${DB_PASSWORD:-gemini-test-password}@postgres:5432/morethanadiagnosis
|
||||||
REDIS_URL: redis://:${REDIS_PASSWORD:-gemini-test-password}@redis:6379/0
|
REDIS_URL: redis://:${REDIS_PASSWORD:-gemini-test-password}@redis:6379/0
|
||||||
SECRET_KEY: ${SECRET_KEY:-gemini-test-secret}
|
SECRET_KEY: ${SECRET_KEY:-gemini-test-secret}
|
||||||
CORS_ORIGINS: '["http://localhost:3001", "http://localhost:8081"]'
|
CORS_ORIGINS: '["http://localhost:3001", "http://localhost:8081", "http://216.158.230.94:8081"]'
|
||||||
LOG_LEVEL: DEBUG
|
LOG_LEVEL: DEBUG
|
||||||
ports:
|
ports:
|
||||||
- "8001:8000"
|
- "8001:8000"
|
||||||
|
|
@ -68,7 +67,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./app:/app/app:ro
|
- ./app:/app/app:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
test: [ "CMD", "curl", "-f", "http://localhost:8000/health" ]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
@ -78,6 +77,8 @@ services:
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../web
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
NEXT_PUBLIC_API_BASE_URL: http://216.158.230.94:8001/api/v1
|
||||||
container_name: mtad-web-gemini
|
container_name: mtad-web-gemini
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
expose:
|
expose:
|
||||||
|
|
@ -87,7 +88,7 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- api
|
||||||
environment:
|
environment:
|
||||||
NEXT_PUBLIC_API_BASE_URL: http://api:8000/api/v1
|
NEXT_PUBLIC_API_BASE_URL: http://216.158.230.94:8001/api/v1
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
|
|
@ -105,7 +106,7 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- api
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
|
test: [ "CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health" ]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,10 @@ opentelemetry-exporter-jaeger==1.21.0
|
||||||
opentelemetry-instrumentation-fastapi==0.42b0
|
opentelemetry-instrumentation-fastapi==0.42b0
|
||||||
opentelemetry-instrumentation-sqlalchemy==0.42b0
|
opentelemetry-instrumentation-sqlalchemy==0.42b0
|
||||||
|
|
||||||
|
# Data Validation & Transformation
|
||||||
# Data Validation & Transformation
|
# Data Validation & Transformation
|
||||||
marshmallow==3.20.1
|
marshmallow==3.20.1
|
||||||
|
feedparser>=6.0.10
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
pytest==7.4.3
|
pytest==7.4.3
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue