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.*
|
.env.*
|
||||||
!.env.template
|
!.env.template
|
||||||
|
!.env.demo
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
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 asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from sqlalchemy import select, text
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import SessionLocal, engine
|
from app.core.database import SessionLocal, engine
|
||||||
from app.crud.user import user as user_crud
|
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 import User
|
||||||
|
from app.models.user_organization import UserOrganization
|
||||||
from app.schemas.users import UserCreate
|
from app.schemas.users import UserCreate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -26,7 +32,12 @@ async def init_db() -> User | None:
|
|||||||
"""
|
"""
|
||||||
# Use default values if not set in environment variables
|
# Use default values if not set in environment variables
|
||||||
superuser_email = settings.FIRST_SUPERUSER_EMAIL or "admin@example.com"
|
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:
|
if not settings.FIRST_SUPERUSER_EMAIL or not settings.FIRST_SUPERUSER_PASSWORD:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -58,25 +69,9 @@ async def init_db() -> User | None:
|
|||||||
|
|
||||||
logger.info(f"Created first superuser: {user.email}")
|
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:
|
if settings.DEMO_MODE:
|
||||||
demo_email = "demo@example.com"
|
await load_demo_data(session)
|
||||||
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}")
|
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@@ -86,6 +81,94 @@ async def init_db() -> User | None:
|
|||||||
raise
|
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():
|
async def main():
|
||||||
"""Main entry point for database initialization."""
|
"""Main entry point for database initialization."""
|
||||||
# Configure logging to show info logs
|
# 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 files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
.
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
Reference in New Issue
Block a user