Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 19 additions & 24 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -356,37 +356,35 @@ jobs:
run: |
BUILDTAG=${{ needs.Generate-Tag.outputs.tag_name }}

# Determine environment and database
# Note: Using Aurora Serverless v2 endpoints (cypher.*.mor.org)
# Aurora has replaced the old RDS instances (db.*.mor.org)
# Determine environment from branch
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
ENV="dev"
DB_HOST="cypher.dev.mor.org"
elif [ "${{ github.ref_name }}" == "stg" ]; then
ENV="stg"
DB_HOST="cypher.stg.mor.org"
elif [ "${{ github.ref_name }}" == "main" ]; then
ENV="prd"
DB_HOST="cypher.mor.org"
else
echo "❌ Unsupported branch for deployment: ${{ github.ref_name }}"
exit 1
fi

echo "🗄️ Running database migrations for environment: $ENV"
echo "📍 Database host: $DB_HOST"

# Get database credentials from AWS Secrets Manager
# Get database connection details from the dedicated DB-creds secret
# (contains only POSTGRES_USER/PASSWORD/DB/HOST/PORT — no app secrets)
SECRET_VALUE=$(aws secretsmanager get-secret-value \
--secret-id "${ENV}-morpheus-api" \
--secret-id "${ENV}-morpheus-api-rds-proxy-credentials" \
--query SecretString --output text)

DB_USER=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_USER')
DB_PASSWORD=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PASSWORD')
DB_NAME=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_DB')
DB_HOST=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_HOST')
DB_PORT=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PORT')

echo "🗄️ Running database migrations for environment: $ENV"
echo "📍 Database host: $DB_HOST"

# Set database URL for migrations
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}"
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
export ENVIRONMENT="$ENV"

# Create backup point (get current revision before migration)
Expand Down Expand Up @@ -793,36 +791,33 @@ jobs:
exit 0
fi

# Determine environment and database
# Note: Using Aurora Serverless v2 endpoints (cypher.*.mor.org)
# Aurora has replaced the old RDS instances (db.*.mor.org)
# Determine environment from branch
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
ENV="dev"
DB_HOST="cypher.dev.mor.org"
elif [ "${{ github.ref_name }}" == "stg" ]; then
ENV="stg"
DB_HOST="cypher.stg.mor.org"
elif [ "${{ github.ref_name }}" == "main" ]; then
ENV="prd"
DB_HOST="cypher.mor.org"
else
echo "❌ Unknown environment for rollback"
exit 1
fi

echo "📍 Rolling back database in environment: $ENV"
echo "🔄 Target rollback revision: ${{ env.PRE_MIGRATION_REVISION }}"

# Get database credentials from AWS Secrets Manager
# Get database connection details from the dedicated DB-creds secret
SECRET_VALUE=$(aws secretsmanager get-secret-value \
--secret-id "${ENV}-morpheus-api" \
--secret-id "${ENV}-morpheus-api-rds-proxy-credentials" \
--query SecretString --output text)

DB_USER=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_USER')
DB_PASSWORD=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PASSWORD')
DB_NAME=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_DB')
DB_HOST=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_HOST')
DB_PORT=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PORT')

echo "📍 Rolling back database in environment: $ENV"
echo "🔄 Target rollback revision: ${{ env.PRE_MIGRATION_REVISION }}"

export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}"
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"

# Check current revision before rollback
CURRENT_REV=$(poetry run alembic current --verbose 2>/dev/null | grep "Current revision" | awk '{print $NF}' || echo "none")
Expand Down
32 changes: 32 additions & 0 deletions alembic/versions/2026_03_05_0001_drop_email_and_name_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Drop email and name columns from users table

PII (email, name) is managed exclusively in Cognito. The API resolves email
on-demand via the user's access token when needed (e.g. GET /me). The database
only stores cognito_user_id as the identity key.

Revision ID: drop_email_name_2026
Revises: e1f2a3b4c5d6
Create Date: 2026-03-05 00:01:00.000000
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = 'drop_email_name_2026'
down_revision: Union[str, None] = 'e1f2a3b4c5d6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.drop_index('ix_users_email_nonunique', table_name='users')
op.drop_column('users', 'email')
op.drop_column('users', 'name')


def downgrade() -> None:
op.add_column('users', sa.Column('name', sa.String(), nullable=True))
op.add_column('users', sa.Column('email', sa.String(), nullable=True))
op.create_index('ix_users_email_nonunique', 'users', ['email'], unique=False)
56 changes: 27 additions & 29 deletions src/api/v1/auth/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime, timezone

from fastapi import APIRouter, HTTPException, status, Depends, Body, Request, Response, Query
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession

from ....crud import user as user_crud
Expand All @@ -20,43 +21,40 @@

router = APIRouter(tags=["Auth"])

# Note: Authentication is now handled by Cognito
# Users authenticate via Cognito OAuth2 flow and receive JWT tokens
# The frontend should redirect to Cognito for login/registration

# OAuth2 callback is handled by the /docs/oauth2-redirect endpoint
_bearer = HTTPBearer(auto_error=False)

@router.get("/me", response_model=dict)
async def get_current_user_info(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db_session)
token: Optional[HTTPAuthorizationCredentials] = Depends(_bearer),
):
"""
Get current user information.
Requires JWT Bearer authentication with Cognito token.
User data is automatically kept up-to-date during authentication.

Email and name are fetched live from Cognito (not stored in DB).
Uses the caller's own access token — same pattern as the frontend.
"""
user = current_user
data_source = "database_auto_updated"

# Return user data focusing on email and cognito_id
response_data = {
"id": user.id,
"cognito_user_id": user.cognito_user_id,
"email": user.email,
"name": user.name,
"is_active": user.is_active,
"age_verified": user.age_verified,
"age_verified_at": user.age_verified_at,
"created_at": user.created_at,
"updated_at": user.updated_at,
"data_source": data_source
email = None
name = None

if token:
cognito_info = await cognito_service.get_user_by_token(token.credentials)
if cognito_info:
email = cognito_info.get('email')
name = cognito_info.get('name')

return {
"id": current_user.id,
"cognito_user_id": current_user.cognito_user_id,
"email": email,
"name": name,
"is_active": current_user.is_active,
"age_verified": current_user.age_verified,
"age_verified_at": current_user.age_verified_at,
"created_at": current_user.created_at,
"updated_at": current_user.updated_at,
"data_source": "cognito_live",
}

# Email should now be automatically updated during authentication

return response_data

@router.post("/verify-age", response_model=dict)
async def verify_age(
Expand Down Expand Up @@ -383,7 +381,7 @@ async def get_default_api_key_decrypted(
"name": api_key_obj.name,
"created_at": api_key_obj.created_at
},
"suggestion": "Try refreshing your user data with GET /api/v1/auth/me?refresh_from_cognito=true, or create a new API key"
"suggestion": "Try creating a new API key using POST /api/v1/auth/keys"
}

# Success case
Expand Down
5 changes: 1 addition & 4 deletions src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,8 @@ def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str
PROXY_ROUTER_CHAT_TIMEOUT: float = Field(default=float(os.getenv("PROXY_ROUTER_CHAT_TIMEOUT", "300.0")))
PROXY_ROUTER_STREAM_TIMEOUT: float = Field(default=float(os.getenv("PROXY_ROUTER_STREAM_TIMEOUT", "300.0")))

# AWS settings
# AWS settings (credentials come from ECS task role; no explicit keys needed)
AWS_REGION: str = os.getenv("AWS_REGION", "us-east-2")
AWS_ACCESS_KEY_ID: str | None = Field(default=os.getenv("AWS_ACCESS_KEY_ID"))
AWS_SECRET_ACCESS_KEY: str | None = Field(default=os.getenv("AWS_SECRET_ACCESS_KEY"))
AWS_SESSION_TOKEN: str | None = Field(default=os.getenv("AWS_SESSION_TOKEN"))

# AWS Cognito Settings
COGNITO_USER_POOL_ID: str = Field(default=os.getenv("COGNITO_USER_POOL_ID", "us-east-2_tqCTHoSST"))
Expand Down
11 changes: 2 additions & 9 deletions src/core/local_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,9 @@ async def get_or_create_test_user(db: AsyncSession) -> User:
test_user = await user_crud.get_user_by_cognito_id(db, "local-test-user")

if not test_user:
# Create test user
user_data = {
'cognito_user_id': 'local-test-user',
'email': 'test@local.dev',
'name': 'Local Test User'
}
test_user = await user_crud.create_user_from_cognito(db, user_data)
test_user = await user_crud.create_user_from_cognito(db, 'local-test-user')
logger.info("Created test user for local development",
test_user_id=test_user.id,
test_email=user_data['email'],
event_type="test_user_created")

return test_user
Expand All @@ -52,7 +45,7 @@ def log_local_testing_status():
if is_local_testing_mode():
logger.warning("LOCAL TESTING MODE ACTIVE",
bypass_cognito=True,
test_user_email="test@local.dev",
test_cognito_id="local-test-user",
production_safe=False,
event_type="local_testing_active")
logger.warning("Cognito authentication BYPASSED - NOT FOR PRODUCTION USE",
Expand Down
1 change: 0 additions & 1 deletion src/crud/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,6 @@ async def get_decrypted_api_key(db: AsyncSession, api_key_id: int, user_id: int)
api_key_id=api_key_id,
user_id=user_id,
cognito_user_id=api_key.user.cognito_user_id[:8] + "...",
user_email=api_key.user.email,
encrypted_key_length=len(api_key.encrypted_key),
event_type="api_key_found")

Expand Down
Loading