Compare commits
3 Commits
67260e9322
...
682213fdee
| Author | SHA1 | Date | |
|---|---|---|---|
| 682213fdee | |||
| 3d1586f025 | |||
| 64082ca877 |
14
.env.example
14
.env.example
@@ -24,7 +24,7 @@ DATABASE_URL=postgresql://orion_user:secure_password@localhost:5432/orion_db
|
||||
# =============================================================================
|
||||
# These are used by init_production.py to create the platform admin
|
||||
# ⚠️ CHANGE THESE IN PRODUCTION!
|
||||
ADMIN_EMAIL=admin@orion.lu
|
||||
ADMIN_EMAIL=admin@wizard.lu
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=change-me-in-production
|
||||
ADMIN_FIRST_NAME=Platform
|
||||
@@ -49,9 +49,9 @@ API_PORT=8000
|
||||
# Development
|
||||
DOCUMENTATION_URL=http://localhost:8001
|
||||
# Staging
|
||||
# DOCUMENTATION_URL=https://staging-docs.orion.lu
|
||||
# DOCUMENTATION_URL=https://staging-docs.wizard.lu
|
||||
# Production
|
||||
# DOCUMENTATION_URL=https://docs.orion.lu
|
||||
# DOCUMENTATION_URL=https://docs.wizard.lu
|
||||
|
||||
# =============================================================================
|
||||
# RATE LIMITING
|
||||
@@ -70,7 +70,7 @@ LOG_FILE=logs/app.log
|
||||
# PLATFORM DOMAIN CONFIGURATION
|
||||
# =============================================================================
|
||||
# Your main platform domain
|
||||
PLATFORM_DOMAIN=orion.lu
|
||||
PLATFORM_DOMAIN=wizard.lu
|
||||
|
||||
# Custom domain features
|
||||
# Enable/disable custom domains
|
||||
@@ -85,7 +85,7 @@ SSL_PROVIDER=letsencrypt
|
||||
AUTO_PROVISION_SSL=False
|
||||
|
||||
# DNS verification
|
||||
DNS_VERIFICATION_PREFIX=_orion-verify
|
||||
DNS_VERIFICATION_PREFIX=_wizard-verify
|
||||
DNS_VERIFICATION_TTL=3600
|
||||
|
||||
# =============================================================================
|
||||
@@ -103,8 +103,8 @@ STRIPE_TRIAL_DAYS=30
|
||||
# =============================================================================
|
||||
# Provider: smtp, sendgrid, mailgun, ses
|
||||
EMAIL_PROVIDER=smtp
|
||||
EMAIL_FROM_ADDRESS=noreply@orion.lu
|
||||
EMAIL_FROM_NAME=Orion
|
||||
EMAIL_FROM_ADDRESS=noreply@wizard.lu
|
||||
EMAIL_FROM_NAME=Wizard
|
||||
EMAIL_REPLY_TO=
|
||||
|
||||
# SMTP Settings (used when EMAIL_PROVIDER=smtp)
|
||||
|
||||
9
Makefile
9
Makefile
@@ -1,7 +1,7 @@
|
||||
# Orion Multi-Tenant E-Commerce Platform Makefile
|
||||
# Cross-platform compatible (Windows & Linux)
|
||||
|
||||
.PHONY: install install-dev install-docs install-all dev test test-coverage lint format check docker-build docker-up docker-down clean help tailwind-install tailwind-dev tailwind-build tailwind-watch arch-check arch-check-file arch-check-object test-db-up test-db-down test-db-reset test-db-status celery-worker celery-beat celery-dev flower celery-status celery-purge urls
|
||||
.PHONY: install install-dev install-docs install-all dev test test-coverage lint format check docker-build docker-up docker-down clean help tailwind-install tailwind-dev tailwind-build tailwind-watch arch-check arch-check-file arch-check-object test-db-up test-db-down test-db-reset test-db-status celery-worker celery-beat celery-dev flower celery-status celery-purge urls infra-check
|
||||
|
||||
# Detect OS
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@@ -44,7 +44,7 @@ setup: install-all migrate-up init-prod
|
||||
# =============================================================================
|
||||
|
||||
dev:
|
||||
$(PYTHON) -m uvicorn main:app --reload --host 0.0.0.0 --port 9999
|
||||
$(PYTHON) -m uvicorn main:app --reload --host 0.0.0.0 --port $(or $(API_PORT),8000)
|
||||
|
||||
# =============================================================================
|
||||
# DATABASE MIGRATIONS
|
||||
@@ -497,6 +497,10 @@ urls-prod:
|
||||
urls-check:
|
||||
@$(PYTHON) scripts/show_urls.py --check
|
||||
|
||||
infra-check:
|
||||
@echo "Running infrastructure verification..."
|
||||
bash scripts/verify-server.sh
|
||||
|
||||
check-env:
|
||||
@echo "Checking Python environment..."
|
||||
@echo "Detected OS: $(DETECTED_OS)"
|
||||
@@ -602,6 +606,7 @@ help:
|
||||
@echo " urls-dev - Show development URLs only"
|
||||
@echo " urls-prod - Show production URLs only"
|
||||
@echo " urls-check - Check dev URLs with curl (server must be running)"
|
||||
@echo " infra-check - Run infrastructure verification (verify-server.sh)"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " check-env - Check Python environment and OS"
|
||||
@echo ""
|
||||
|
||||
26
alembic/versions/a44f4956cfb1_merge_heads.py
Normal file
26
alembic/versions/a44f4956cfb1_merge_heads.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""merge heads
|
||||
|
||||
Revision ID: a44f4956cfb1
|
||||
Revises: z_store_domain_platform_id, tenancy_001
|
||||
Create Date: 2026-02-17 16:10:36.287976
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a44f4956cfb1'
|
||||
down_revision: Union[str, None] = ('z_store_domain_platform_id', 'tenancy_001')
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -113,10 +113,11 @@
|
||||
<script defer src="{{ url_for('static', path='shared/js/i18n.js') }}"></script>
|
||||
<script>
|
||||
// Initialize i18n with current language and preload modules
|
||||
(async function() {
|
||||
// Wrapped in DOMContentLoaded so deferred i18n.js has loaded
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const modules = {% block i18n_modules %}[]{% endblock %};
|
||||
await I18n.init('{{ current_language | default("en") }}', modules);
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 5. FIFTH: API Client (depends on Utils) -->
|
||||
|
||||
@@ -72,10 +72,11 @@
|
||||
<script defer src="{{ url_for('static', path='shared/js/i18n.js') }}"></script>
|
||||
<script>
|
||||
// Initialize i18n with dashboard language and preload modules
|
||||
(async function() {
|
||||
// Wrapped in DOMContentLoaded so deferred i18n.js has loaded
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const modules = {% block i18n_modules %}[]{% endblock %};
|
||||
await I18n.init('{{ dashboard_language | default("en") }}', modules);
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 5. FIFTH: API Client (depends on Utils) -->
|
||||
|
||||
@@ -330,10 +330,11 @@
|
||||
<script defer src="{{ url_for('static', path='shared/js/i18n.js') }}"></script>
|
||||
<script>
|
||||
// Initialize i18n with storefront language and preload modules
|
||||
(async function() {
|
||||
// Wrapped in DOMContentLoaded so deferred i18n.js has loaded
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const modules = {% block i18n_modules %}[]{% endblock %};
|
||||
await I18n.init('{{ storefront_language | default("en") }}', modules);
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
|
||||
{# 6. API Client #}
|
||||
|
||||
@@ -9,7 +9,6 @@ services:
|
||||
POSTGRES_PASSWORD: secure_password
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
mem_limit: 512m
|
||||
|
||||
@@ -132,13 +132,51 @@ def create_admin_user(db: Session, auth_manager: AuthManager) -> User:
|
||||
return admin
|
||||
|
||||
|
||||
def create_loyalty_admin(db: Session, auth_manager: AuthManager, loyalty_platform: Platform) -> User | None:
|
||||
"""Create a platform admin for the Loyalty+ platform."""
|
||||
from app.modules.tenancy.models.admin_platform import AdminPlatform
|
||||
|
||||
email = "admin@rewardflow.lu"
|
||||
existing = db.execute(select(User).where(User.email == email)).scalar_one_or_none()
|
||||
if existing:
|
||||
print_warning(f"Loyalty admin already exists: {email}")
|
||||
return existing
|
||||
|
||||
password = "admin123" # Dev default, change in production
|
||||
admin = User(
|
||||
username="loyalty_admin",
|
||||
email=email,
|
||||
hashed_password=auth_manager.hash_password(password),
|
||||
role="admin",
|
||||
is_super_admin=False,
|
||||
first_name="Loyalty",
|
||||
last_name="Administrator",
|
||||
is_active=True,
|
||||
is_email_verified=True,
|
||||
)
|
||||
db.add(admin)
|
||||
db.flush()
|
||||
|
||||
# Assign to loyalty platform
|
||||
assignment = AdminPlatform(
|
||||
user_id=admin.id,
|
||||
platform_id=loyalty_platform.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.flush()
|
||||
|
||||
print_success(f"Created loyalty admin: {email} (password: {password})")
|
||||
return admin
|
||||
|
||||
|
||||
def create_default_platforms(db: Session) -> list[Platform]:
|
||||
"""Create all default platforms (OMS, Main, Loyalty+)."""
|
||||
|
||||
platform_defs = [
|
||||
{
|
||||
"code": "oms",
|
||||
"name": "Orion OMS",
|
||||
"name": "OMS",
|
||||
"description": "Order Management System for multi-store e-commerce",
|
||||
"domain": "omsflow.lu",
|
||||
"path_prefix": "oms",
|
||||
@@ -149,7 +187,7 @@ def create_default_platforms(db: Session) -> list[Platform]:
|
||||
},
|
||||
{
|
||||
"code": "main",
|
||||
"name": "Orion",
|
||||
"name": "Wizard",
|
||||
"description": "Main marketing site showcasing all Orion platforms",
|
||||
"domain": "wizard.lu",
|
||||
"path_prefix": None,
|
||||
@@ -160,7 +198,7 @@ def create_default_platforms(db: Session) -> list[Platform]:
|
||||
},
|
||||
{
|
||||
"code": "loyalty",
|
||||
"name": "Loyalty+",
|
||||
"name": "Loyalty",
|
||||
"description": "Customer loyalty program platform for Luxembourg businesses",
|
||||
"domain": "rewardflow.lu",
|
||||
"path_prefix": "loyalty",
|
||||
@@ -450,17 +488,26 @@ def create_subscription_tiers(db: Session, platform: Platform) -> int:
|
||||
def create_platform_modules(db: Session, platforms: list[Platform]) -> int:
|
||||
"""Create PlatformModule records for all platforms.
|
||||
|
||||
Enables all discovered modules for each platform so the app works
|
||||
out of the box. Admins can disable optional modules later via the API.
|
||||
Core modules are enabled for every platform. Optional modules are
|
||||
selectively enabled per platform. All other modules are created but
|
||||
disabled (available to toggle on later via the admin API).
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
from app.modules.registry import MODULES, is_core_module
|
||||
|
||||
# Optional modules enabled per platform (core modules always enabled)
|
||||
PLATFORM_MODULES = {
|
||||
"oms": ["inventory", "catalog", "orders", "marketplace", "analytics", "cart", "checkout"],
|
||||
"main": ["analytics", "monitoring", "dev-tools"],
|
||||
"loyalty": ["loyalty"],
|
||||
}
|
||||
|
||||
now = datetime.now(UTC)
|
||||
records_created = 0
|
||||
|
||||
for platform in platforms:
|
||||
enabled_extras = set(PLATFORM_MODULES.get(platform.code, []))
|
||||
|
||||
for code in MODULES:
|
||||
# Check if record already exists
|
||||
existing = db.execute(
|
||||
select(PlatformModule).where(
|
||||
PlatformModule.platform_id == platform.id,
|
||||
@@ -471,11 +518,12 @@ def create_platform_modules(db: Session, platforms: list[Platform]) -> int:
|
||||
if existing:
|
||||
continue
|
||||
|
||||
enabled = is_core_module(code) or code in enabled_extras
|
||||
pm = PlatformModule(
|
||||
platform_id=platform.id,
|
||||
module_code=code,
|
||||
is_enabled=True,
|
||||
enabled_at=now,
|
||||
is_enabled=enabled,
|
||||
enabled_at=now if enabled else None,
|
||||
config={},
|
||||
)
|
||||
db.add(pm) # noqa: PERF006
|
||||
@@ -559,6 +607,14 @@ def initialize_production(db: Session, auth_manager: AuthManager):
|
||||
print_step(3, "Creating default platforms...")
|
||||
platforms = create_default_platforms(db)
|
||||
|
||||
# Step 3b: Create loyalty platform admin
|
||||
print_step("3b", "Creating loyalty platform admin...")
|
||||
loyalty_platform = next((p for p in platforms if p.code == "loyalty"), None)
|
||||
if loyalty_platform:
|
||||
create_loyalty_admin(db, auth_manager, loyalty_platform)
|
||||
else:
|
||||
print_warning("Loyalty platform not found, skipping loyalty admin creation")
|
||||
|
||||
# Step 4: Set up default role templates
|
||||
print_step(4, "Setting up role templates...")
|
||||
create_default_role_templates(db)
|
||||
@@ -603,12 +659,40 @@ def print_summary(db: Session):
|
||||
print(f" Admin settings: {setting_count}")
|
||||
print(f" Sub. tiers: {tier_count}")
|
||||
|
||||
# Show platforms
|
||||
platforms = db.query(Platform).order_by(Platform.code).all()
|
||||
enabled_counts = {}
|
||||
for pm in db.query(PlatformModule).filter(PlatformModule.is_enabled.is_(True)).all():
|
||||
enabled_counts[pm.platform_id] = enabled_counts.get(pm.platform_id, 0) + 1
|
||||
|
||||
port = settings.api_port
|
||||
print("\n" + "─" * 70)
|
||||
print("🌐 PLATFORMS")
|
||||
print("─" * 70)
|
||||
for p in platforms:
|
||||
n_enabled = enabled_counts.get(p.id, 0)
|
||||
if p.code == "main":
|
||||
dev_url = f"http://localhost:{port}/"
|
||||
else:
|
||||
dev_url = f"http://localhost:{port}/platforms/{p.code}/"
|
||||
print(f" {p.name} ({p.code})")
|
||||
print(f" Domain: {p.domain}")
|
||||
print(f" Dev URL: {dev_url}")
|
||||
print(f" Modules: {n_enabled} enabled")
|
||||
|
||||
print("\n" + "─" * 70)
|
||||
print("🔐 ADMIN CREDENTIALS")
|
||||
print("─" * 70)
|
||||
print(" URL: /admin/login")
|
||||
print(f" Username: {settings.admin_username}")
|
||||
print(f" Password: {settings.admin_password}") # noqa: SEC021
|
||||
admin_url = f"http://localhost:{port}/admin/login"
|
||||
print(" Super Admin (all platforms):")
|
||||
print(f" URL: {admin_url}")
|
||||
print(f" Username: {settings.admin_username}")
|
||||
print(f" Password: {settings.admin_password}") # noqa: SEC021
|
||||
print()
|
||||
print(" Loyalty Platform Admin (loyalty only):")
|
||||
print(f" URL: {admin_url}")
|
||||
print(" Username: loyalty_admin")
|
||||
print(" Password: admin123")
|
||||
print("─" * 70)
|
||||
|
||||
# Show security warnings if in production
|
||||
@@ -625,17 +709,16 @@ def print_summary(db: Session):
|
||||
print(" Change them in production via .env file")
|
||||
|
||||
print("\n🚀 NEXT STEPS:")
|
||||
print(" 1. Login to admin panel")
|
||||
if is_production():
|
||||
print(f" 1. Login to admin panel: {admin_url}")
|
||||
print(" 2. CHANGE DEFAULT PASSWORD IMMEDIATELY!") # noqa: SEC021
|
||||
print(" 3. Configure admin settings")
|
||||
print(" 4. Create first store")
|
||||
else:
|
||||
print(" 2. Create demo data: make seed-demo")
|
||||
print(" 3. Start development: make dev")
|
||||
|
||||
print("\n📝 FOR DEMO DATA (Development only):")
|
||||
print(" make seed-demo")
|
||||
print(" 1. Start development: make dev")
|
||||
print(f" 2. Admin panel: {admin_url}")
|
||||
print(f" 3. Merchant panel: http://localhost:{port}/merchants/login")
|
||||
print(" 4. Create demo data: make seed-demo")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -144,12 +144,12 @@ DEMO_COMPANIES = [
|
||||
DEMO_STORES = [
|
||||
{
|
||||
"merchant_index": 0, # WizaCorp
|
||||
"store_code": "ORION",
|
||||
"name": "Orion",
|
||||
"subdomain": "orion",
|
||||
"store_code": "WIZATECH",
|
||||
"name": "WizaTech",
|
||||
"subdomain": "wizatech",
|
||||
"description": "Premium electronics and gadgets marketplace",
|
||||
"theme_preset": "modern",
|
||||
"custom_domain": "orion.shop",
|
||||
"custom_domain": "wizatech.shop",
|
||||
},
|
||||
{
|
||||
"merchant_index": 0, # WizaCorp
|
||||
@@ -216,7 +216,7 @@ DEMO_TEAM_MEMBERS = [
|
||||
"password": "password123", # noqa: SEC001
|
||||
"first_name": "Alice",
|
||||
"last_name": "Manager",
|
||||
"store_codes": ["ORION", "WIZAGADGETS"], # manages two stores
|
||||
"store_codes": ["WIZATECH", "WIZAGADGETS"], # manages two stores
|
||||
"user_type": "member",
|
||||
},
|
||||
{
|
||||
@@ -287,20 +287,20 @@ THEME_PRESETS = {
|
||||
# Store content page overrides (demonstrates CMS store override feature)
|
||||
# Each store can override platform default pages with custom content
|
||||
STORE_CONTENT_PAGES = {
|
||||
"ORION": [
|
||||
"WIZATECH": [
|
||||
{
|
||||
"slug": "about",
|
||||
"title": "About Orion",
|
||||
"title": "About WizaTech",
|
||||
"content": """
|
||||
<div class="prose-content">
|
||||
<h2>Welcome to Orion</h2>
|
||||
<h2>Welcome to WizaTech</h2>
|
||||
<p>Your premier destination for cutting-edge electronics and innovative gadgets.</p>
|
||||
|
||||
<h3>Our Story</h3>
|
||||
<p>Founded by tech enthusiasts, Orion has been bringing the latest technology to customers since 2020.
|
||||
<p>Founded by tech enthusiasts, WizaTech has been bringing the latest technology to customers since 2020.
|
||||
We carefully curate our selection to ensure you get only the best products at competitive prices.</p>
|
||||
|
||||
<h3>Why Choose Orion?</h3>
|
||||
<h3>Why Choose WizaTech?</h3>
|
||||
<ul>
|
||||
<li><strong>Expert Selection:</strong> Our team tests and reviews every product</li>
|
||||
<li><strong>Best Prices:</strong> We negotiate directly with manufacturers</li>
|
||||
@@ -312,20 +312,20 @@ STORE_CONTENT_PAGES = {
|
||||
<p>123 Tech Street, Luxembourg City<br>Open Monday-Saturday, 9am-7pm</p>
|
||||
</div>
|
||||
""",
|
||||
"meta_description": "Orion - Your trusted source for premium electronics and gadgets in Luxembourg",
|
||||
"meta_description": "WizaTech - Your trusted source for premium electronics and gadgets in Luxembourg",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
},
|
||||
{
|
||||
"slug": "contact",
|
||||
"title": "Contact Orion",
|
||||
"title": "Contact WizaTech",
|
||||
"content": """
|
||||
<div class="prose-content">
|
||||
<h2>Get in Touch with Orion</h2>
|
||||
<h2>Get in Touch with WizaTech</h2>
|
||||
|
||||
<h3>Customer Support</h3>
|
||||
<ul>
|
||||
<li><strong>Email:</strong> support@orion.lu</li>
|
||||
<li><strong>Email:</strong> support@wizatech.lu</li>
|
||||
<li><strong>Phone:</strong> +352 123 456 789</li>
|
||||
<li><strong>WhatsApp:</strong> +352 123 456 789</li>
|
||||
<li><strong>Hours:</strong> Monday-Friday, 9am-6pm CET</li>
|
||||
@@ -334,7 +334,7 @@ STORE_CONTENT_PAGES = {
|
||||
<h3>Technical Support</h3>
|
||||
<p>Need help with your gadgets? Our tech experts are here to help!</p>
|
||||
<ul>
|
||||
<li><strong>Email:</strong> tech@orion.lu</li>
|
||||
<li><strong>Email:</strong> tech@wizatech.lu</li>
|
||||
<li><strong>Live Chat:</strong> Available on our website</li>
|
||||
</ul>
|
||||
|
||||
@@ -342,7 +342,7 @@ STORE_CONTENT_PAGES = {
|
||||
<p>123 Tech Street<br>Luxembourg City, L-1234<br>Luxembourg</p>
|
||||
</div>
|
||||
""",
|
||||
"meta_description": "Contact Orion customer support for electronics and gadget inquiries",
|
||||
"meta_description": "Contact WizaTech customer support for electronics and gadget inquiries",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
},
|
||||
@@ -837,6 +837,7 @@ def create_demo_customers(
|
||||
"""Create demo customers for a store."""
|
||||
|
||||
customers = []
|
||||
new_count = 0
|
||||
# Use a simple demo password for all customers
|
||||
demo_password = "customer123" # noqa: SEC001
|
||||
|
||||
@@ -869,10 +870,10 @@ def create_demo_customers(
|
||||
)
|
||||
db.add(customer) # noqa: PERF006
|
||||
customers.append(customer)
|
||||
new_count += 1
|
||||
|
||||
db.flush()
|
||||
|
||||
new_count = len([c for c in customers if c.id is None or db.is_modified(c)])
|
||||
if new_count > 0:
|
||||
print_success(f"Created {new_count} customers for {store.name}")
|
||||
else:
|
||||
@@ -885,6 +886,7 @@ def create_demo_products(db: Session, store: Store, count: int) -> list[Product]
|
||||
"""Create demo products for a store."""
|
||||
|
||||
products = []
|
||||
new_count = 0
|
||||
|
||||
for i in range(1, count + 1):
|
||||
marketplace_product_id = f"{store.store_code}-MP-{i:04d}"
|
||||
@@ -922,7 +924,7 @@ def create_demo_products(db: Session, store: Store, count: int) -> list[Product]
|
||||
availability="in stock",
|
||||
condition="new",
|
||||
google_product_category="Electronics > Computers > Laptops",
|
||||
marketplace="Orion",
|
||||
marketplace="OMS",
|
||||
store_name=store.name,
|
||||
currency="EUR",
|
||||
created_at=datetime.now(UTC),
|
||||
@@ -964,10 +966,10 @@ def create_demo_products(db: Session, store: Store, count: int) -> list[Product]
|
||||
)
|
||||
db.add(product) # noqa: PERF006
|
||||
products.append(product)
|
||||
new_count += 1
|
||||
|
||||
db.flush()
|
||||
|
||||
new_count = len([p for p in products if p.id is None or db.is_modified(p)])
|
||||
if new_count > 0:
|
||||
print_success(f"Created {new_count} products for {store.name}")
|
||||
else:
|
||||
@@ -1205,7 +1207,7 @@ def print_summary(db: Session):
|
||||
print(" All customers:")
|
||||
print(" Email: customer1@{subdomain}.example.com")
|
||||
print(" Password: customer123") # noqa: SEC021
|
||||
print(" (Replace {subdomain} with store subdomain, e.g., orion)")
|
||||
print(" (Replace {subdomain} with store subdomain, e.g., wizatech)")
|
||||
print()
|
||||
|
||||
print("\n🏪 Shop Access (Development):")
|
||||
@@ -1220,15 +1222,14 @@ def print_summary(db: Session):
|
||||
|
||||
print("⚠️ ALL DEMO CREDENTIALS ARE INSECURE - For development only!")
|
||||
|
||||
port = settings.api_port
|
||||
print("\n🚀 NEXT STEPS:")
|
||||
print(" 1. Start development: make dev")
|
||||
print(" 2. Login as store:")
|
||||
print(" • Path-based: http://localhost:8000/store/ORION/login")
|
||||
print(" • Subdomain: http://orion.localhost:8000/store/login") # noqa: SEC034
|
||||
print(" 3. Visit store shop: http://localhost:8000/stores/ORION/shop/")
|
||||
print(" 4. Admin panel: http://localhost:8000/admin/login")
|
||||
print(f" Username: {settings.admin_username}")
|
||||
print(f" Password: {settings.admin_password}") # noqa: SEC021
|
||||
print(f" 2. Admin panel: http://localhost:{port}/admin/login")
|
||||
print(f" 3. Merchant panel: http://localhost:{port}/merchants/login")
|
||||
print(f" 4. Store panel: http://localhost:{port}/store/WIZATECH/login")
|
||||
print(f" 5. Storefront: http://localhost:{port}/stores/WIZATECH/shop/")
|
||||
print(f" 6. Customer login: http://localhost:{port}/stores/WIZATECH/shop/account")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-server.sh — Check all Orion infrastructure is properly deployed
|
||||
# Run on the production server: bash scripts/verify-server.sh
|
||||
# verify-server.sh — Check Orion infrastructure health
|
||||
# Automatically detects dev vs production from .env DEBUG flag.
|
||||
# Override with: bash scripts/verify-server.sh --dev | --prod
|
||||
set -uo pipefail
|
||||
|
||||
PASS=0
|
||||
@@ -14,238 +15,403 @@ warn() { echo " [WARN] $1"; WARN=$((WARN + 1)); }
|
||||
section() { echo ""; echo "=== $1 ==="; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "1. fail2ban"
|
||||
# Detect environment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if systemctl is-active --quiet fail2ban; then
|
||||
pass "fail2ban service running"
|
||||
else
|
||||
fail "fail2ban service not running"
|
||||
fi
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
ENV_FILE="$PROJECT_DIR/.env"
|
||||
|
||||
if sudo fail2ban-client status sshd &>/dev/null; then
|
||||
pass "SSH jail active"
|
||||
else
|
||||
fail "SSH jail not active"
|
||||
fi
|
||||
|
||||
if sudo fail2ban-client status caddy-auth &>/dev/null; then
|
||||
pass "Caddy auth jail active"
|
||||
else
|
||||
fail "Caddy auth jail not active — deploy /etc/fail2ban/jail.d/caddy.conf"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "2. Unattended Upgrades"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if dpkg -l unattended-upgrades &>/dev/null; then
|
||||
pass "unattended-upgrades package installed"
|
||||
else
|
||||
fail "unattended-upgrades not installed"
|
||||
fi
|
||||
|
||||
if [ -f /etc/apt/apt.conf.d/20auto-upgrades ]; then
|
||||
if grep -q 'Unattended-Upgrade "1"' /etc/apt/apt.conf.d/20auto-upgrades; then
|
||||
pass "Automatic upgrades enabled"
|
||||
MODE=""
|
||||
if [ "${1:-}" = "--dev" ]; then
|
||||
MODE="dev"
|
||||
elif [ "${1:-}" = "--prod" ]; then
|
||||
MODE="prod"
|
||||
elif [ -f "$ENV_FILE" ]; then
|
||||
if grep -qE '^DEBUG=True' "$ENV_FILE" 2>/dev/null; then
|
||||
MODE="dev"
|
||||
else
|
||||
fail "Automatic upgrades not enabled in 20auto-upgrades"
|
||||
MODE="prod"
|
||||
fi
|
||||
else
|
||||
fail "/etc/apt/apt.conf.d/20auto-upgrades missing"
|
||||
# No .env found — assume production (server deployment)
|
||||
MODE="prod"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "3. Docker Containers"
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "==========================================="
|
||||
echo " Orion Infrastructure Check (${MODE})"
|
||||
echo "==========================================="
|
||||
|
||||
ORION_DIR="${ORION_DIR:-$HOME/apps/orion}"
|
||||
# Helper: read a value from .env
|
||||
env_val() {
|
||||
grep -E "^${1}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-
|
||||
}
|
||||
|
||||
EXPECTED_CONTAINERS="db redis api celery-worker celery-beat flower prometheus grafana node-exporter cadvisor alertmanager"
|
||||
for name in $EXPECTED_CONTAINERS; do
|
||||
container=$(docker compose --profile full -f "$ORION_DIR/docker-compose.yml" ps --format '{{.Name}}' 2>/dev/null | grep "$name" || true)
|
||||
if [ -n "$container" ]; then
|
||||
state=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null || echo "unknown")
|
||||
if [ "$state" = "running" ]; then
|
||||
pass "Container $name: running"
|
||||
# ===========================================================================
|
||||
# DEVELOPMENT CHECKS
|
||||
# ===========================================================================
|
||||
|
||||
if [ "$MODE" = "dev" ]; then
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "1. .env Configuration"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
pass ".env file exists"
|
||||
else
|
||||
fail ".env file not found — copy from .env.example"
|
||||
fi
|
||||
|
||||
REQUIRED_KEYS="DATABASE_URL REDIS_URL JWT_SECRET_KEY ADMIN_EMAIL PLATFORM_DOMAIN"
|
||||
for key in $REQUIRED_KEYS; do
|
||||
val=$(env_val "$key")
|
||||
if [ -n "$val" ]; then
|
||||
pass "$key is set"
|
||||
else
|
||||
fail "Container $name: $state (expected running)"
|
||||
fail "$key is missing or empty"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for stale wizamart references
|
||||
if grep -qiE 'wizamart' "$ENV_FILE" 2>/dev/null; then
|
||||
fail "Stale 'wizamart' references found in .env"
|
||||
else
|
||||
pass "No stale wizamart references"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "2. PostgreSQL"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
DB_URL=$(env_val "DATABASE_URL")
|
||||
if [ -n "$DB_URL" ]; then
|
||||
# Extract host and port from DATABASE_URL
|
||||
DB_HOST=$(echo "$DB_URL" | sed -E 's|.*@([^:/]+).*|\1|')
|
||||
DB_PORT=$(echo "$DB_URL" | sed -E 's|.*:([0-9]+)/.*|\1|')
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
|
||||
if command -v pg_isready &>/dev/null; then
|
||||
if pg_isready -h "$DB_HOST" -p "$DB_PORT" &>/dev/null; then
|
||||
pass "PostgreSQL reachable at $DB_HOST:$DB_PORT"
|
||||
else
|
||||
fail "PostgreSQL not reachable at $DB_HOST:$DB_PORT — start with: docker compose up -d db"
|
||||
fi
|
||||
elif (echo > /dev/tcp/"$DB_HOST"/"$DB_PORT") &>/dev/null; then
|
||||
pass "PostgreSQL port open at $DB_HOST:$DB_PORT"
|
||||
else
|
||||
fail "PostgreSQL not reachable at $DB_HOST:$DB_PORT — start with: docker compose up -d db"
|
||||
fi
|
||||
else
|
||||
fail "Container $name: not found"
|
||||
fail "DATABASE_URL not set"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for healthy status on containers with healthchecks
|
||||
for name in db redis api celery-worker; do
|
||||
container=$(docker compose --profile full -f "$ORION_DIR/docker-compose.yml" ps --format '{{.Name}}' 2>/dev/null | grep "$name" || true)
|
||||
if [ -n "$container" ]; then
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "none")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
pass "Container $name: healthy"
|
||||
elif [ "$health" = "none" ]; then
|
||||
warn "Container $name: no healthcheck configured"
|
||||
# -----------------------------------------------------------------------
|
||||
section "3. Redis"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
REDIS_URL=$(env_val "REDIS_URL")
|
||||
if [ -n "$REDIS_URL" ]; then
|
||||
# Extract host and port from redis://host:port/db
|
||||
REDIS_HOST=$(echo "$REDIS_URL" | sed -E 's|redis://([^:/]+).*|\1|')
|
||||
REDIS_PORT=$(echo "$REDIS_URL" | sed -E 's|redis://[^:]+:([0-9]+).*|\1|')
|
||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||
|
||||
if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ping &>/dev/null; then
|
||||
pass "Redis reachable at $REDIS_HOST:$REDIS_PORT"
|
||||
else
|
||||
fail "Container $name: $health (expected healthy)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "4. Caddy"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if systemctl is-active --quiet caddy; then
|
||||
pass "Caddy service running"
|
||||
else
|
||||
fail "Caddy service not running"
|
||||
fi
|
||||
|
||||
if [ -f /etc/caddy/Caddyfile ]; then
|
||||
pass "Caddyfile exists"
|
||||
else
|
||||
fail "Caddyfile not found"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "5. Backup Timer"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if systemctl is-active --quiet orion-backup.timer; then
|
||||
pass "Backup timer active"
|
||||
else
|
||||
fail "Backup timer not active — enable with: sudo systemctl enable --now orion-backup.timer"
|
||||
fi
|
||||
|
||||
LATEST_BACKUP=$(find "$HOME/backups/orion/daily/" -name "*.sql.gz" -mtime -2 2>/dev/null | head -1)
|
||||
if [ -n "$LATEST_BACKUP" ]; then
|
||||
pass "Recent backup found: $(basename "$LATEST_BACKUP")"
|
||||
else
|
||||
warn "No backup found from the last 2 days"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "6. Gitea Runner"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if systemctl is-active --quiet gitea-runner; then
|
||||
pass "Gitea runner service running"
|
||||
else
|
||||
fail "Gitea runner service not running"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "7. SSL Certificates"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DOMAINS="wizard.lu api.wizard.lu git.wizard.lu omsflow.lu rewardflow.lu"
|
||||
for domain in $DOMAINS; do
|
||||
expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$expiry" ]; then
|
||||
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || echo 0)
|
||||
now_epoch=$(date +%s)
|
||||
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||
if [ "$days_left" -gt 14 ]; then
|
||||
pass "SSL $domain: valid ($days_left days remaining)"
|
||||
elif [ "$days_left" -gt 0 ]; then
|
||||
warn "SSL $domain: expiring soon ($days_left days remaining)"
|
||||
else
|
||||
fail "SSL $domain: expired"
|
||||
fail "Redis not reachable at $REDIS_HOST:$REDIS_PORT — start with: docker compose up -d redis"
|
||||
fi
|
||||
else
|
||||
fail "SSL $domain: could not check certificate"
|
||||
fail "REDIS_URL not set"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "8. Flower Password"
|
||||
# ---------------------------------------------------------------------------
|
||||
# -----------------------------------------------------------------------
|
||||
section "4. Dev Server Health"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if [ -f "$ORION_DIR/.env" ]; then
|
||||
FLOWER_PW=$(grep -E '^FLOWER_PASSWORD=' "$ORION_DIR/.env" 2>/dev/null | cut -d= -f2- || echo "")
|
||||
if [ -z "$FLOWER_PW" ] || [ "$FLOWER_PW" = "changeme" ]; then
|
||||
fail "Flower password is default or empty — change FLOWER_PASSWORD in .env"
|
||||
# Dev server runs on port 9999 (make dev)
|
||||
DEV_PORT=9999
|
||||
HEALTH_URL="http://localhost:$DEV_PORT/health"
|
||||
READY_URL="http://localhost:$DEV_PORT/health/ready"
|
||||
|
||||
status=$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo "000")
|
||||
if [ "$status" = "200" ]; then
|
||||
pass "/health endpoint: HTTP 200 (port $DEV_PORT)"
|
||||
elif [ "$status" = "000" ]; then
|
||||
warn "Dev server not running on port $DEV_PORT — start with: make dev"
|
||||
else
|
||||
pass "Flower password changed from default"
|
||||
fail "/health endpoint: HTTP $status (port $DEV_PORT)"
|
||||
fi
|
||||
else
|
||||
warn ".env file not found at $ORION_DIR/.env"
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
ready_response=$(curl -s "$READY_URL" 2>/dev/null || echo "")
|
||||
if echo "$ready_response" | grep -q '"healthy"'; then
|
||||
pass "/health/ready: healthy"
|
||||
else
|
||||
fail "/health/ready: not healthy"
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "5. Migrations"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if command -v python3 &>/dev/null; then
|
||||
alembic_output=$(cd "$PROJECT_DIR" && python3 -m alembic current 2>&1 || echo "ERROR")
|
||||
if echo "$alembic_output" | grep -q "head"; then
|
||||
pass "Alembic migrations at head"
|
||||
elif echo "$alembic_output" | grep -q "ERROR"; then
|
||||
fail "Could not check migration status"
|
||||
else
|
||||
warn "Migrations may not be at head — run: make migrate-up"
|
||||
fi
|
||||
else
|
||||
warn "python3 not found, cannot check migrations"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "9. DNS Resolution"
|
||||
# ---------------------------------------------------------------------------
|
||||
# ===========================================================================
|
||||
# PRODUCTION CHECKS
|
||||
# ===========================================================================
|
||||
|
||||
EXPECTED_DOMAINS="wizard.lu api.wizard.lu git.wizard.lu grafana.wizard.lu flower.wizard.lu omsflow.lu rewardflow.lu"
|
||||
for domain in $EXPECTED_DOMAINS; do
|
||||
resolved=$(dig +short "$domain" A 2>/dev/null | head -1)
|
||||
if [ -n "$resolved" ]; then
|
||||
pass "DNS $domain: $resolved"
|
||||
if [ "$MODE" = "prod" ]; then
|
||||
|
||||
ORION_DIR="${ORION_DIR:-$HOME/apps/orion}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "1. fail2ban"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if systemctl is-active --quiet fail2ban; then
|
||||
pass "fail2ban service running"
|
||||
else
|
||||
fail "DNS $domain: no A record found"
|
||||
fail "fail2ban service not running"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "10. Health Endpoints"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HEALTH_URL="http://localhost:8001/health"
|
||||
READY_URL="http://localhost:8001/health/ready"
|
||||
|
||||
status=$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo "000")
|
||||
if [ "$status" = "200" ]; then
|
||||
pass "/health endpoint: HTTP 200"
|
||||
else
|
||||
fail "/health endpoint: HTTP $status"
|
||||
fi
|
||||
|
||||
ready_response=$(curl -s "$READY_URL" 2>/dev/null || echo "")
|
||||
if echo "$ready_response" | grep -q '"healthy"'; then
|
||||
pass "/health/ready: healthy"
|
||||
# Check individual checks
|
||||
if echo "$ready_response" | grep -q '"database"'; then
|
||||
pass "/health/ready: database check registered"
|
||||
if sudo fail2ban-client status sshd &>/dev/null; then
|
||||
pass "SSH jail active"
|
||||
else
|
||||
warn "/health/ready: database check not found"
|
||||
fail "SSH jail not active"
|
||||
fi
|
||||
if echo "$ready_response" | grep -q '"redis"'; then
|
||||
pass "/health/ready: redis check registered"
|
||||
|
||||
if sudo fail2ban-client status caddy-auth &>/dev/null; then
|
||||
pass "Caddy auth jail active"
|
||||
else
|
||||
warn "/health/ready: redis check not found"
|
||||
fail "Caddy auth jail not active — deploy /etc/fail2ban/jail.d/caddy.conf"
|
||||
fi
|
||||
else
|
||||
fail "/health/ready: not healthy — $ready_response"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "11. Prometheus Targets"
|
||||
# ---------------------------------------------------------------------------
|
||||
# -----------------------------------------------------------------------
|
||||
section "2. Unattended Upgrades"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
targets=$(curl -s http://localhost:9090/api/v1/targets 2>/dev/null || echo "")
|
||||
if [ -n "$targets" ]; then
|
||||
up_count=$(echo "$targets" | grep -o '"health":"up"' | wc -l)
|
||||
down_count=$(echo "$targets" | grep -o '"health":"down"' | wc -l)
|
||||
if [ "$down_count" -eq 0 ] && [ "$up_count" -gt 0 ]; then
|
||||
pass "Prometheus: all $up_count targets up"
|
||||
elif [ "$down_count" -gt 0 ]; then
|
||||
fail "Prometheus: $down_count target(s) down ($up_count up)"
|
||||
if dpkg -l unattended-upgrades &>/dev/null; then
|
||||
pass "unattended-upgrades package installed"
|
||||
else
|
||||
warn "Prometheus: no targets found"
|
||||
fail "unattended-upgrades not installed"
|
||||
fi
|
||||
else
|
||||
fail "Prometheus: could not reach API at localhost:9090"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "12. Grafana"
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ -f /etc/apt/apt.conf.d/20auto-upgrades ]; then
|
||||
if grep -q 'Unattended-Upgrade "1"' /etc/apt/apt.conf.d/20auto-upgrades; then
|
||||
pass "Automatic upgrades enabled"
|
||||
else
|
||||
fail "Automatic upgrades not enabled in 20auto-upgrades"
|
||||
fi
|
||||
else
|
||||
fail "/etc/apt/apt.conf.d/20auto-upgrades missing"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "3. Docker Containers"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
EXPECTED_CONTAINERS="db redis api celery-worker celery-beat flower prometheus grafana node-exporter cadvisor alertmanager"
|
||||
for name in $EXPECTED_CONTAINERS; do
|
||||
container=$(docker compose --profile full -f "$ORION_DIR/docker-compose.yml" ps --format '{{.Name}}' 2>/dev/null | grep "$name" || true)
|
||||
if [ -n "$container" ]; then
|
||||
state=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null || echo "unknown")
|
||||
if [ "$state" = "running" ]; then
|
||||
pass "Container $name: running"
|
||||
else
|
||||
fail "Container $name: $state (expected running)"
|
||||
fi
|
||||
else
|
||||
fail "Container $name: not found"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for healthy status on containers with healthchecks
|
||||
for name in db redis api celery-worker; do
|
||||
container=$(docker compose --profile full -f "$ORION_DIR/docker-compose.yml" ps --format '{{.Name}}' 2>/dev/null | grep "$name" || true)
|
||||
if [ -n "$container" ]; then
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "none")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
pass "Container $name: healthy"
|
||||
elif [ "$health" = "none" ]; then
|
||||
warn "Container $name: no healthcheck configured"
|
||||
else
|
||||
fail "Container $name: $health (expected healthy)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "4. Caddy"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if systemctl is-active --quiet caddy; then
|
||||
pass "Caddy service running"
|
||||
else
|
||||
fail "Caddy service not running"
|
||||
fi
|
||||
|
||||
if [ -f /etc/caddy/Caddyfile ]; then
|
||||
pass "Caddyfile exists"
|
||||
else
|
||||
fail "Caddyfile not found"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "5. Backup Timer"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if systemctl is-active --quiet orion-backup.timer; then
|
||||
pass "Backup timer active"
|
||||
else
|
||||
fail "Backup timer not active — enable with: sudo systemctl enable --now orion-backup.timer"
|
||||
fi
|
||||
|
||||
LATEST_BACKUP=$(find "$HOME/backups/orion/daily/" -name "*.sql.gz" -mtime -2 2>/dev/null | head -1)
|
||||
if [ -n "$LATEST_BACKUP" ]; then
|
||||
pass "Recent backup found: $(basename "$LATEST_BACKUP")"
|
||||
else
|
||||
warn "No backup found from the last 2 days"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "6. Gitea Runner"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if systemctl is-active --quiet gitea-runner; then
|
||||
pass "Gitea runner service running"
|
||||
else
|
||||
fail "Gitea runner service not running"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "7. SSL Certificates"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
DOMAINS="wizard.lu api.wizard.lu git.wizard.lu omsflow.lu rewardflow.lu"
|
||||
for domain in $DOMAINS; do
|
||||
expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$expiry" ]; then
|
||||
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || echo 0)
|
||||
now_epoch=$(date +%s)
|
||||
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||
if [ "$days_left" -gt 14 ]; then
|
||||
pass "SSL $domain: valid ($days_left days remaining)"
|
||||
elif [ "$days_left" -gt 0 ]; then
|
||||
warn "SSL $domain: expiring soon ($days_left days remaining)"
|
||||
else
|
||||
fail "SSL $domain: expired"
|
||||
fi
|
||||
else
|
||||
fail "SSL $domain: could not check certificate"
|
||||
fi
|
||||
done
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "8. Flower Password"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
if [ -f "$ORION_DIR/.env" ]; then
|
||||
FLOWER_PW=$(grep -E '^FLOWER_PASSWORD=' "$ORION_DIR/.env" 2>/dev/null | cut -d= -f2- || echo "")
|
||||
if [ -z "$FLOWER_PW" ] || [ "$FLOWER_PW" = "changeme" ]; then
|
||||
fail "Flower password is default or empty — change FLOWER_PASSWORD in .env"
|
||||
else
|
||||
pass "Flower password changed from default"
|
||||
fi
|
||||
else
|
||||
warn ".env file not found at $ORION_DIR/.env"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "9. DNS Resolution"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
EXPECTED_DOMAINS="wizard.lu api.wizard.lu git.wizard.lu grafana.wizard.lu flower.wizard.lu omsflow.lu rewardflow.lu"
|
||||
for domain in $EXPECTED_DOMAINS; do
|
||||
resolved=$(dig +short "$domain" A 2>/dev/null | head -1)
|
||||
if [ -n "$resolved" ]; then
|
||||
pass "DNS $domain: $resolved"
|
||||
else
|
||||
fail "DNS $domain: no A record found"
|
||||
fi
|
||||
done
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "10. Health Endpoints"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
HEALTH_URL="http://localhost:8001/health"
|
||||
READY_URL="http://localhost:8001/health/ready"
|
||||
|
||||
status=$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo "000")
|
||||
if [ "$status" = "200" ]; then
|
||||
pass "/health endpoint: HTTP 200"
|
||||
else
|
||||
fail "/health endpoint: HTTP $status"
|
||||
fi
|
||||
|
||||
ready_response=$(curl -s "$READY_URL" 2>/dev/null || echo "")
|
||||
if echo "$ready_response" | grep -q '"healthy"'; then
|
||||
pass "/health/ready: healthy"
|
||||
if echo "$ready_response" | grep -q '"database"'; then
|
||||
pass "/health/ready: database check registered"
|
||||
else
|
||||
warn "/health/ready: database check not found"
|
||||
fi
|
||||
if echo "$ready_response" | grep -q '"redis"'; then
|
||||
pass "/health/ready: redis check registered"
|
||||
else
|
||||
warn "/health/ready: redis check not found"
|
||||
fi
|
||||
else
|
||||
fail "/health/ready: not healthy — $ready_response"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "11. Prometheus Targets"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
targets=$(curl -s http://localhost:9090/api/v1/targets 2>/dev/null || echo "")
|
||||
if [ -n "$targets" ]; then
|
||||
up_count=$(echo "$targets" | grep -o '"health":"up"' | wc -l)
|
||||
down_count=$(echo "$targets" | grep -o '"health":"down"' | wc -l)
|
||||
if [ "$down_count" -eq 0 ] && [ "$up_count" -gt 0 ]; then
|
||||
pass "Prometheus: all $up_count targets up"
|
||||
elif [ "$down_count" -gt 0 ]; then
|
||||
fail "Prometheus: $down_count target(s) down ($up_count up)"
|
||||
else
|
||||
warn "Prometheus: no targets found"
|
||||
fi
|
||||
else
|
||||
fail "Prometheus: could not reach API at localhost:9090"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
section "12. Grafana"
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
grafana_status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3001/api/health 2>/dev/null || echo "000")
|
||||
if [ "$grafana_status" = "200" ]; then
|
||||
pass "Grafana: accessible (HTTP 200)"
|
||||
else
|
||||
fail "Grafana: HTTP $grafana_status (expected 200)"
|
||||
fi
|
||||
|
||||
grafana_status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3001/api/health 2>/dev/null || echo "000")
|
||||
if [ "$grafana_status" = "200" ]; then
|
||||
pass "Grafana: accessible (HTTP 200)"
|
||||
else
|
||||
fail "Grafana: HTTP $grafana_status (expected 200)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user