feat: implement serve command with dev mode support

- CLI serve command now runs uvicorn with migrations
- Add dev_mode setting to bypass auth with default admin user
- Add bin/animaltrack wrapper for Nix environment
- Add bin/serve-dev for quick local development
- Update flake.nix shellHook for PYTHONPATH and bin PATH

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-29 21:00:00 +00:00
parent 352fa387af
commit 85b5e81e35
6 changed files with 48 additions and 3 deletions

2
bin/animaltrack Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
exec python -m animaltrack.cli "$@"

2
bin/serve-dev Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
exec env CSRF_SECRET="dev-secret" DEV_MODE=true python -m animaltrack.cli serve "$@"

View File

@@ -80,6 +80,8 @@
]; ];
shellHook = '' shellHook = ''
export PYTHONPATH="$PWD/src:$PYTHONPATH"
export PATH="$PWD/bin:$PATH"
echo "AnimalTrack development environment ready!" echo "AnimalTrack development environment ready!"
echo "Run 'animaltrack serve' to start the app" echo "Run 'animaltrack serve' to start the app"
''; '';

View File

@@ -26,7 +26,7 @@ def main():
# serve command # serve command
serve_parser = subparsers.add_parser("serve", help="Start the web server") serve_parser = subparsers.add_parser("serve", help="Start the web server")
serve_parser.add_argument("--port", type=int, default=5000, help="Port to listen on") serve_parser.add_argument("--port", type=int, default=3366, help="Port to listen on")
serve_parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind to") serve_parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind to")
args = parser.parse_args() args = parser.parse_args()
@@ -86,9 +86,33 @@ def main():
run_seeds(db) run_seeds(db)
print("Seed data loaded successfully") print("Seed data loaded successfully")
elif args.command == "serve": elif args.command == "serve":
import uvicorn
from animaltrack.config import Settings
from animaltrack.db import get_db
from animaltrack.migrations import run_migrations
from animaltrack.web.app import create_app
settings = Settings()
# Run migrations first
print("Running migrations...")
success = run_migrations(
db_path=settings.db_path,
migrations_dir="migrations",
verbose=False,
)
if not success:
print("Migration failed", file=sys.stderr)
sys.exit(1)
# Create app
db = get_db(settings.db_path)
app, _ = create_app(settings=settings, db=db)
# Start server
print(f"Starting server on {args.host}:{args.port}...") print(f"Starting server on {args.host}:{args.port}...")
# TODO: Implement server uvicorn.run(app, host=args.host, port=args.port)
print("Server not yet implemented")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -37,6 +37,7 @@ class Settings(BaseSettings):
seed_on_start: bool = False seed_on_start: bool = False
log_level: str = "INFO" log_level: str = "INFO"
metrics_enabled: bool = True metrics_enabled: bool = True
dev_mode: bool = False # Bypasses auth, sets default user
@cached_property @cached_property
def trusted_proxy_ips(self) -> list[str]: def trusted_proxy_ips(self) -> list[str]:

View File

@@ -10,6 +10,7 @@ from starlette.responses import PlainTextResponse, Response
from animaltrack.config import Settings from animaltrack.config import Settings
from animaltrack.id_gen import generate_id from animaltrack.id_gen import generate_id
from animaltrack.models.reference import User, UserRole
from animaltrack.repositories.users import UserRepository from animaltrack.repositories.users import UserRepository
# Safe HTTP methods that don't require CSRF protection # Safe HTTP methods that don't require CSRF protection
@@ -170,6 +171,8 @@ def auth_before(req: Request, settings: Settings, db) -> Response | None:
- Auth header is present - Auth header is present
- User exists and is active in database - User exists and is active in database
In dev_mode, bypasses all checks and uses a default admin user.
Args: Args:
req: The Starlette request object. req: The Starlette request object.
settings: Application settings. settings: Application settings.
@@ -178,6 +181,17 @@ def auth_before(req: Request, settings: Settings, db) -> Response | None:
Returns: Returns:
None to continue processing, or Response to short-circuit. None to continue processing, or Response to short-circuit.
""" """
# Dev mode: bypass auth entirely
if settings.dev_mode:
req.scope["auth"] = User(
username="dev",
role=UserRole.ADMIN,
active=True,
created_at_utc=0,
updated_at_utc=0,
)
return None
# Check trusted proxy # Check trusted proxy
if not is_trusted_proxy(req, settings): if not is_trusted_proxy(req, settings):
return PlainTextResponse("Forbidden: Request not from trusted proxy", status_code=403) return PlainTextResponse("Forbidden: Request not from trusted proxy", status_code=403)