feat: Update CORS origins to allow Chrome extensions and improve logging in migration tool
This commit is contained in:
@@ -23,7 +23,10 @@ class Settings(BaseSettings):
|
|||||||
BACKEND_URL: str = "http://localhost:8000" # Backend base URL
|
BACKEND_URL: str = "http://localhost:8000" # Backend base URL
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
CORS_ORIGINS: list[str] = ["http://localhost:8001"] # Allowed origins for CORS
|
CORS_ORIGINS: list[str] = [
|
||||||
|
"http://localhost:8001", # Frontend development
|
||||||
|
"chrome-extension://*", # Chrome extensions
|
||||||
|
]
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
DATABASE_URL: str = "sqlite+aiosqlite:///data/soundboard.db"
|
DATABASE_URL: str = "sqlite+aiosqlite:///data/soundboard.db"
|
||||||
@@ -37,7 +40,9 @@ class Settings(BaseSettings):
|
|||||||
LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration
|
||||||
JWT_SECRET_KEY: str = "your-secret-key-change-in-production" # noqa: S105 default value if none set in .env
|
JWT_SECRET_KEY: str = (
|
||||||
|
"your-secret-key-change-in-production" # noqa: S105 default value if none set in .env
|
||||||
|
)
|
||||||
JWT_ALGORITHM: str = "HS256"
|
JWT_ALGORITHM: str = "HS256"
|
||||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
|
||||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from collections.abc import AsyncGenerator, Callable
|
from collections.abc import AsyncGenerator, Callable
|
||||||
|
|
||||||
|
from alembic.config import Config
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||||
from sqlmodel import SQLModel
|
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
# Import all models to ensure SQLModel metadata discovery
|
# Import all models to ensure SQLModel metadata discovery
|
||||||
import app.models # noqa: F401
|
import app.models # noqa: F401
|
||||||
|
from alembic import command
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.logging import get_logger
|
from app.core.logging import get_logger
|
||||||
|
|
||||||
@@ -44,8 +45,6 @@ async def init_db() -> None:
|
|||||||
try:
|
try:
|
||||||
logger.info("Running database migrations")
|
logger.info("Running database migrations")
|
||||||
# Run Alembic migrations programmatically
|
# Run Alembic migrations programmatically
|
||||||
from alembic import command
|
|
||||||
from alembic.config import Config
|
|
||||||
|
|
||||||
# Get the alembic config
|
# Get the alembic config
|
||||||
alembic_cfg = Config("alembic.ini")
|
alembic_cfg = Config("alembic.ini")
|
||||||
|
|||||||
@@ -598,7 +598,9 @@ class CreditService:
|
|||||||
current_credits = user.credits
|
current_credits = user.credits
|
||||||
plan_credits = user.plan.credits
|
plan_credits = user.plan.credits
|
||||||
max_credits = user.plan.max_credits
|
max_credits = user.plan.max_credits
|
||||||
target_credits = min(current_credits + plan_credits, max_credits)
|
target_credits = min(
|
||||||
|
current_credits + plan_credits, max_credits,
|
||||||
|
)
|
||||||
credits_added = target_credits - current_credits
|
credits_added = target_credits - current_credits
|
||||||
stats["total_credits_added"] += credits_added
|
stats["total_credits_added"] += credits_added
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -348,8 +348,12 @@ class SchedulerService:
|
|||||||
# Check if task is still active and pending
|
# Check if task is still active and pending
|
||||||
if not task.is_active or task.status != TaskStatus.PENDING:
|
if not task.is_active or task.status != TaskStatus.PENDING:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Task %s execution skipped - is_active: %s, status: %s (should be %s)",
|
"Task %s execution skipped - is_active: %s, status: %s "
|
||||||
task_id, task.is_active, task.status, TaskStatus.PENDING,
|
"(should be %s)",
|
||||||
|
task_id,
|
||||||
|
task.is_active,
|
||||||
|
task.status,
|
||||||
|
TaskStatus.PENDING,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -364,7 +368,9 @@ class SchedulerService:
|
|||||||
|
|
||||||
# Mark task as running
|
# Mark task as running
|
||||||
logger.info(
|
logger.info(
|
||||||
"Task %s starting execution (type: %s)", task_id, task.recurrence_type,
|
"Task %s starting execution (type: %s)",
|
||||||
|
task_id,
|
||||||
|
task.recurrence_type,
|
||||||
)
|
)
|
||||||
await repo.mark_as_running(task)
|
await repo.mark_as_running(task)
|
||||||
|
|
||||||
@@ -383,7 +389,8 @@ class SchedulerService:
|
|||||||
# For CRON tasks, update execution metadata but keep PENDING
|
# For CRON tasks, update execution metadata but keep PENDING
|
||||||
# APScheduler handles the recurring schedule automatically
|
# APScheduler handles the recurring schedule automatically
|
||||||
logger.info(
|
logger.info(
|
||||||
"Task %s (CRON) executed successfully, updating metadata", task_id,
|
"Task %s (CRON) executed successfully, updating metadata",
|
||||||
|
task_id,
|
||||||
)
|
)
|
||||||
task.last_executed_at = datetime.now(tz=UTC)
|
task.last_executed_at = datetime.now(tz=UTC)
|
||||||
task.executions_count += 1
|
task.executions_count += 1
|
||||||
@@ -392,8 +399,11 @@ class SchedulerService:
|
|||||||
session.add(task)
|
session.add(task)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Task %s (CRON) metadata updated, status: %s, executions: %s",
|
"Task %s (CRON) metadata updated, status: %s, "
|
||||||
task_id, task.status, task.executions_count,
|
"executions: %s",
|
||||||
|
task_id,
|
||||||
|
task.status,
|
||||||
|
task.executions_count,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# For non-CRON recurring tasks, calculate next execution
|
# For non-CRON recurring tasks, calculate next execution
|
||||||
|
|||||||
@@ -80,11 +80,19 @@ class TaskHandlerRegistry:
|
|||||||
msg = f"Invalid user_id format: {user_id}"
|
msg = f"Invalid user_id format: {user_id}"
|
||||||
raise TaskExecutionError(msg) from e
|
raise TaskExecutionError(msg) from e
|
||||||
|
|
||||||
transaction = await self.credit_service.recharge_user_credits_auto(user_id_int)
|
transaction = await self.credit_service.recharge_user_credits_auto(
|
||||||
|
user_id_int,
|
||||||
|
)
|
||||||
if transaction:
|
if transaction:
|
||||||
logger.info("Recharged credits for user %s: %s credits added", user_id, transaction.amount)
|
logger.info(
|
||||||
|
"Recharged credits for user %s: %s credits added",
|
||||||
|
user_id,
|
||||||
|
transaction.amount,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("No credits added for user %s (already at maximum)", user_id)
|
logger.info(
|
||||||
|
"No credits added for user %s (already at maximum)", user_id,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Recharge all users (system task)
|
# Recharge all users (system task)
|
||||||
stats = await self.credit_service.recharge_all_users_credits()
|
stats = await self.credit_service.recharge_all_users_credits()
|
||||||
|
|||||||
42
migrate.py
42
migrate.py
@@ -2,25 +2,33 @@
|
|||||||
"""Database migration CLI tool."""
|
"""Database migration CLI tool."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from alembic import command
|
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
|
|
||||||
|
from alembic import command
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Main CLI function for database migrations."""
|
"""Run database migration CLI tool."""
|
||||||
parser = argparse.ArgumentParser(description="Database migration tool")
|
parser = argparse.ArgumentParser(description="Database migration tool")
|
||||||
subparsers = parser.add_subparsers(dest="command", help="Migration commands")
|
subparsers = parser.add_subparsers(dest="command", help="Migration commands")
|
||||||
|
|
||||||
# Upgrade command
|
# Upgrade command
|
||||||
upgrade_parser = subparsers.add_parser("upgrade", help="Upgrade database to latest revision")
|
upgrade_parser = subparsers.add_parser(
|
||||||
|
"upgrade", help="Upgrade database to latest revision",
|
||||||
|
)
|
||||||
upgrade_parser.add_argument(
|
upgrade_parser.add_argument(
|
||||||
"revision",
|
"revision",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
default="head",
|
default="head",
|
||||||
help="Target revision (default: head)"
|
help="Target revision (default: head)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Downgrade command
|
# Downgrade command
|
||||||
@@ -35,8 +43,12 @@ def main() -> None:
|
|||||||
|
|
||||||
# Generate migration command
|
# Generate migration command
|
||||||
revision_parser = subparsers.add_parser("revision", help="Create new migration")
|
revision_parser = subparsers.add_parser("revision", help="Create new migration")
|
||||||
revision_parser.add_argument("-m", "--message", required=True, help="Migration message")
|
revision_parser.add_argument(
|
||||||
revision_parser.add_argument("--autogenerate", action="store_true", help="Auto-generate migration")
|
"-m", "--message", required=True, help="Migration message",
|
||||||
|
)
|
||||||
|
revision_parser.add_argument(
|
||||||
|
"--autogenerate", action="store_true", help="Auto-generate migration",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -47,7 +59,7 @@ def main() -> None:
|
|||||||
# Get the alembic config
|
# Get the alembic config
|
||||||
config_path = Path("alembic.ini")
|
config_path = Path("alembic.ini")
|
||||||
if not config_path.exists():
|
if not config_path.exists():
|
||||||
print("Error: alembic.ini not found. Run from the backend directory.")
|
logger.error("Error: alembic.ini not found. Run from the backend directory.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
alembic_cfg = Config(str(config_path))
|
alembic_cfg = Config(str(config_path))
|
||||||
@@ -55,11 +67,15 @@ def main() -> None:
|
|||||||
try:
|
try:
|
||||||
if args.command == "upgrade":
|
if args.command == "upgrade":
|
||||||
command.upgrade(alembic_cfg, args.revision)
|
command.upgrade(alembic_cfg, args.revision)
|
||||||
print(f"Successfully upgraded database to revision: {args.revision}")
|
logger.info(
|
||||||
|
"Successfully upgraded database to revision: %s", args.revision,
|
||||||
|
)
|
||||||
|
|
||||||
elif args.command == "downgrade":
|
elif args.command == "downgrade":
|
||||||
command.downgrade(alembic_cfg, args.revision)
|
command.downgrade(alembic_cfg, args.revision)
|
||||||
print(f"Successfully downgraded database to revision: {args.revision}")
|
logger.info(
|
||||||
|
"Successfully downgraded database to revision: %s", args.revision,
|
||||||
|
)
|
||||||
|
|
||||||
elif args.command == "current":
|
elif args.command == "current":
|
||||||
command.current(alembic_cfg)
|
command.current(alembic_cfg)
|
||||||
@@ -70,13 +86,13 @@ def main() -> None:
|
|||||||
elif args.command == "revision":
|
elif args.command == "revision":
|
||||||
if args.autogenerate:
|
if args.autogenerate:
|
||||||
command.revision(alembic_cfg, message=args.message, autogenerate=True)
|
command.revision(alembic_cfg, message=args.message, autogenerate=True)
|
||||||
print(f"Created new auto-generated migration: {args.message}")
|
logger.info("Created new auto-generated migration: %s", args.message)
|
||||||
else:
|
else:
|
||||||
command.revision(alembic_cfg, message=args.message)
|
command.revision(alembic_cfg, message=args.message)
|
||||||
print(f"Created new empty migration: {args.message}")
|
logger.info("Created new empty migration: %s", args.message)
|
||||||
|
|
||||||
except Exception as e:
|
except (OSError, RuntimeError):
|
||||||
print(f"Error: {e}")
|
logger.exception("Error occurred during migration")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class TestApiTokenDependencies:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test flexible authentication falls back to JWT when no API token."""
|
"""Test flexible authentication falls back to JWT when no API token."""
|
||||||
# Mock the get_current_user function (normally imported)
|
# Mock the get_current_user function (normally imported)
|
||||||
with pytest.raises(Exception, match="Database error|Could not validate"):
|
with pytest.raises(Exception, match=r"Database error|Could not validate"):
|
||||||
# This will fail because we can't easily mock the get_current_user import
|
# This will fail because we can't easily mock the get_current_user import
|
||||||
# In a real test, you'd mock the import or use dependency injection
|
# In a real test, you'd mock the import or use dependency injection
|
||||||
await get_current_user_flexible(mock_auth_service, "jwt_token", None)
|
await get_current_user_flexible(mock_auth_service, "jwt_token", None)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class TestUserRepository:
|
|||||||
test_session: AsyncSession,
|
test_session: AsyncSession,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test creating a new user."""
|
"""Test creating a new user."""
|
||||||
free_plan, pro_plan = ensure_plans
|
free_plan, _pro_plan = ensure_plans
|
||||||
plan_id = free_plan.id
|
plan_id = free_plan.id
|
||||||
plan_credits = free_plan.credits
|
plan_credits = free_plan.credits
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class TestAuthService:
|
|||||||
assert response.user.role == "admin" # First user gets admin role
|
assert response.user.role == "admin" # First user gets admin role
|
||||||
assert response.user.is_active is True
|
assert response.user.is_active is True
|
||||||
# First user gets pro plan
|
# First user gets pro plan
|
||||||
free_plan, pro_plan = ensure_plans
|
_free_plan, pro_plan = ensure_plans
|
||||||
assert response.user.credits == pro_plan.credits
|
assert response.user.credits == pro_plan.credits
|
||||||
assert response.user.plan["code"] == pro_plan.code
|
assert response.user.plan["code"] == pro_plan.code
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user