Add comprehensive demo data loading logic and .env.demo configuration
- Implemented `load_demo_data` to populate organizations, users, and relationships from `demo_data.json`. - Refactored database initialization to handle demo-specific passwords and multi-entity creation in demo mode. - Added `demo_data.json` with sample organizations and users for better demo showcase. - Introduced `.env.demo` to simplify environment setup for demo scenarios. - Updated `.gitignore` to include `.env.demo` while keeping other `.env` files excluded.
This commit is contained in:
31
.env.demo
Normal file
31
.env.demo
Normal file
@@ -0,0 +1,31 @@
|
||||
# Common settings
|
||||
PROJECT_NAME=App
|
||||
VERSION=1.0.0
|
||||
|
||||
# Database settings
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=app
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_URL=postgresql://postgres:postgres@db:5432/app
|
||||
|
||||
# Backend settings
|
||||
BACKEND_PORT=8000
|
||||
# CRITICAL: Generate a secure SECRET_KEY for production!
|
||||
# Generate with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
|
||||
# Must be at least 32 characters
|
||||
SECRET_KEY=demo_secret_key_for_testing_only_do_not_use_in_prod
|
||||
ENVIRONMENT=development
|
||||
DEMO_MODE=true
|
||||
DEBUG=true
|
||||
BACKEND_CORS_ORIGINS=["http://localhost:3000"]
|
||||
FIRST_SUPERUSER_EMAIL=admin@example.com
|
||||
# IMPORTANT: Use a strong password (min 12 chars, mixed case, digits)
|
||||
# Default weak passwords like 'Admin123' are rejected
|
||||
FIRST_SUPERUSER_PASSWORD=Admin123!
|
||||
|
||||
# Frontend settings
|
||||
FRONTEND_PORT=3000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NODE_ENV=development
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -268,6 +268,7 @@ celerybeat.pid
|
||||
.env
|
||||
.env.*
|
||||
!.env.template
|
||||
!.env.demo
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
|
||||
103
backend/app/core/demo_data.json
Normal file
103
backend/app/core/demo_data.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"organizations": [
|
||||
{
|
||||
"name": "Acme Corp",
|
||||
"slug": "acme-corp",
|
||||
"description": "A leading provider of coyote-catching equipment."
|
||||
},
|
||||
{
|
||||
"name": "Globex Corporation",
|
||||
"slug": "globex",
|
||||
"description": "We own the East Coast."
|
||||
},
|
||||
{
|
||||
"name": "Soylent Corp",
|
||||
"slug": "soylent",
|
||||
"description": "Making food for the future."
|
||||
},
|
||||
{
|
||||
"name": "Initech",
|
||||
"slug": "initech",
|
||||
"description": "Software for the soul."
|
||||
},
|
||||
{
|
||||
"name": "Umbrella Corporation",
|
||||
"slug": "umbrella",
|
||||
"description": "Our business is life itself."
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"email": "alice@acme.com",
|
||||
"password": "Demo123!",
|
||||
"first_name": "Alice",
|
||||
"last_name": "Smith",
|
||||
"is_superuser": false,
|
||||
"organization_slug": "acme-corp",
|
||||
"role": "admin"
|
||||
},
|
||||
{
|
||||
"email": "bob@acme.com",
|
||||
"password": "Demo123!",
|
||||
"first_name": "Bob",
|
||||
"last_name": "Jones",
|
||||
"is_superuser": false,
|
||||
"organization_slug": "acme-corp",
|
||||
"role": "member"
|
||||
},
|
||||
{
|
||||
"email": "carol@globex.com",
|
||||
"password": "Demo123!",
|
||||
"first_name": "Carol",
|
||||
"last_name": "Williams",
|
||||
"is_superuser": false,
|
||||
"organization_slug": "globex",
|
||||
"role": "owner"
|
||||
},
|
||||
{
|
||||
"email": "dave@soylent.com",
|
||||
"password": "Demo123!",
|
||||
"first_name": "Dave",
|
||||
"last_name": "Brown",
|
||||
"is_superuser": false,
|
||||
"organization_slug": "soylent",
|
||||
"role": "member"
|
||||
},
|
||||
{
|
||||
"email": "eve@initech.com",
|
||||
"password": "Demo123!",
|
||||
"first_name": "Eve",
|
||||
"last_name": "Davis",
|
||||
"is_superuser": false,
|
||||
"organization_slug": "initech",
|
||||
"role": "admin"
|
||||
},
|
||||
{
|
||||
"email": "frank@umbrella.com",
|
||||
"password": "Demo123!",
|
||||
"first_name": "Frank",
|
||||
"last_name": "Miller",
|
||||
"is_superuser": false,
|
||||
"organization_slug": "umbrella",
|
||||
"role": "member"
|
||||
},
|
||||
{
|
||||
"email": "grace@example.com",
|
||||
"password": "Demo123!",
|
||||
"first_name": "Grace",
|
||||
"last_name": "Hopper",
|
||||
"is_superuser": false,
|
||||
"organization_slug": null,
|
||||
"role": null
|
||||
},
|
||||
{
|
||||
"email": "heidi@example.com",
|
||||
"password": "Demo123!",
|
||||
"first_name": "Heidi",
|
||||
"last_name": "Klum",
|
||||
"is_superuser": false,
|
||||
"organization_slug": null,
|
||||
"role": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,12 +6,18 @@ Creates the first superuser if configured and doesn't already exist.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import SessionLocal, engine
|
||||
from app.crud.user import user as user_crud
|
||||
from app.models.organization import Organization
|
||||
from app.models.user import User
|
||||
from app.models.user_organization import UserOrganization
|
||||
from app.schemas.users import UserCreate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,7 +32,12 @@ async def init_db() -> User | None:
|
||||
"""
|
||||
# Use default values if not set in environment variables
|
||||
superuser_email = settings.FIRST_SUPERUSER_EMAIL or "admin@example.com"
|
||||
superuser_password = settings.FIRST_SUPERUSER_PASSWORD or "AdminPassword123!"
|
||||
|
||||
default_password = "AdminPassword123!"
|
||||
if settings.DEMO_MODE:
|
||||
default_password = "Admin123!"
|
||||
|
||||
superuser_password = settings.FIRST_SUPERUSER_PASSWORD or default_password
|
||||
|
||||
if not settings.FIRST_SUPERUSER_EMAIL or not settings.FIRST_SUPERUSER_PASSWORD:
|
||||
logger.warning(
|
||||
@@ -58,25 +69,9 @@ async def init_db() -> User | None:
|
||||
|
||||
logger.info(f"Created first superuser: {user.email}")
|
||||
|
||||
# Create demo user if in demo mode
|
||||
# Create demo data if in demo mode
|
||||
if settings.DEMO_MODE:
|
||||
demo_email = "demo@example.com"
|
||||
demo_password = "Demo123!"
|
||||
|
||||
existing_demo_user = await user_crud.get_by_email(session, email=demo_email)
|
||||
if not existing_demo_user:
|
||||
demo_user_in = UserCreate(
|
||||
email=demo_email,
|
||||
password=demo_password,
|
||||
first_name="Demo",
|
||||
last_name="User",
|
||||
is_superuser=False,
|
||||
)
|
||||
demo_user = await user_crud.create(session, obj_in=demo_user_in)
|
||||
await session.commit()
|
||||
logger.info(f"Created demo user: {demo_user.email}")
|
||||
else:
|
||||
logger.info(f"Demo user already exists: {existing_demo_user.email}")
|
||||
await load_demo_data(session)
|
||||
|
||||
return user
|
||||
|
||||
@@ -86,6 +81,94 @@ async def init_db() -> User | None:
|
||||
raise
|
||||
|
||||
|
||||
def _load_json_file(path: Path):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
async def load_demo_data(session):
|
||||
"""Load demo data from JSON file."""
|
||||
demo_data_path = Path(__file__).parent / "core" / "demo_data.json"
|
||||
if not demo_data_path.exists():
|
||||
logger.warning(f"Demo data file not found: {demo_data_path}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Use asyncio.to_thread to avoid blocking the event loop
|
||||
data = await asyncio.to_thread(_load_json_file, demo_data_path)
|
||||
|
||||
# Create Organizations
|
||||
org_map = {}
|
||||
for org_data in data.get("organizations", []):
|
||||
# Check if org exists
|
||||
result = await session.execute(
|
||||
text("SELECT * FROM organizations WHERE slug = :slug"),
|
||||
{"slug": org_data["slug"]},
|
||||
)
|
||||
existing_org = result.first()
|
||||
|
||||
if not existing_org:
|
||||
org = Organization(
|
||||
name=org_data["name"],
|
||||
slug=org_data["slug"],
|
||||
description=org_data.get("description"),
|
||||
is_active=True,
|
||||
)
|
||||
session.add(org)
|
||||
await session.flush() # Flush to get ID
|
||||
org_map[org.slug] = org
|
||||
logger.info(f"Created demo organization: {org.name}")
|
||||
else:
|
||||
# We can't easily get the ORM object from raw SQL result for map without querying again or mapping
|
||||
# So let's just query it properly if we need it for relationships
|
||||
# But for simplicity in this script, let's just assume we created it or it exists.
|
||||
# To properly map for users, we need the ID.
|
||||
# Let's use a simpler approach: just try to create, if slug conflict, skip.
|
||||
pass
|
||||
|
||||
# Re-query all orgs to build map for users
|
||||
result = await session.execute(select(Organization))
|
||||
orgs = result.scalars().all()
|
||||
org_map = {org.slug: org for org in orgs}
|
||||
|
||||
# Create Users
|
||||
for user_data in data.get("users", []):
|
||||
existing_user = await user_crud.get_by_email(
|
||||
session, email=user_data["email"]
|
||||
)
|
||||
if not existing_user:
|
||||
user_in = UserCreate(
|
||||
email=user_data["email"],
|
||||
password=user_data["password"],
|
||||
first_name=user_data["first_name"],
|
||||
last_name=user_data["last_name"],
|
||||
is_superuser=user_data["is_superuser"],
|
||||
)
|
||||
user = await user_crud.create(session, obj_in=user_in)
|
||||
logger.info(f"Created demo user: {user.email}")
|
||||
|
||||
# Add to organization if specified
|
||||
org_slug = user_data.get("organization_slug")
|
||||
role = user_data.get("role")
|
||||
if org_slug and org_slug in org_map and role:
|
||||
org = org_map[org_slug]
|
||||
# Check if membership exists (it shouldn't for new user)
|
||||
member = UserOrganization(
|
||||
user_id=user.id, organization_id=org.id, role=role
|
||||
)
|
||||
session.add(member)
|
||||
logger.info(f"Added {user.email} to {org.name} as {role}")
|
||||
else:
|
||||
logger.info(f"Demo user already exists: {existing_user.email}")
|
||||
|
||||
await session.commit()
|
||||
logger.info("Demo data loaded successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading demo data: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for database initialization."""
|
||||
# Configure logging to show info logs
|
||||
|
||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -33,6 +33,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
.
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
Reference in New Issue
Block a user