feat: add trial tenant configuration and management to support auto-deletion
This commit is contained in:
+61
-1
@@ -82,7 +82,7 @@ from Web.modules.inventarsystem.data_protection import (
|
|||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
from Web.modules.database.settings import MongoClient
|
from Web.modules.database.settings import MongoClient
|
||||||
from tenant import get_tenant_context
|
from tenant import get_tenant_context, get_tenant_trial_status, purge_expired_trial_tenants
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
||||||
@@ -354,6 +354,54 @@ def _enforce_active_session_user():
|
|||||||
flash('Ihre Sitzung ist nicht mehr gültig. Bitte erneut anmelden.', 'error')
|
flash('Ihre Sitzung ist nicht mehr gültig. Bitte erneut anmelden.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def _enforce_trial_tenant_expiry():
|
||||||
|
endpoint = request.endpoint or ''
|
||||||
|
if endpoint == 'static' or endpoint.startswith('static'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if endpoint in {'login', 'logout', 'impressum', 'license'}:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx = get_tenant_context()
|
||||||
|
tenant_id = ctx.tenant_id if ctx else session.get('tenant_id')
|
||||||
|
if not tenant_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
trial_status = get_tenant_trial_status(tenant_id)
|
||||||
|
if not trial_status.get('enabled'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
session['trial_status'] = {
|
||||||
|
'enabled': True,
|
||||||
|
'expired': bool(trial_status.get('expired')),
|
||||||
|
'days_left': trial_status.get('days_left'),
|
||||||
|
'expires_at': trial_status.get('expires_at').isoformat() if trial_status.get('expires_at') else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not trial_status.get('expired'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if request.path.startswith('/api/') or request.is_json:
|
||||||
|
return jsonify({
|
||||||
|
'ok': False,
|
||||||
|
'message': 'Diese Testinstanz ist abgelaufen und wurde deaktiviert.',
|
||||||
|
'expired': True,
|
||||||
|
}), 410
|
||||||
|
|
||||||
|
session.pop('username', None)
|
||||||
|
session.pop('admin', None)
|
||||||
|
session.pop('is_admin', None)
|
||||||
|
session.pop('favorites', None)
|
||||||
|
session.pop('favorites_owner', None)
|
||||||
|
flash('Diese Testinstanz ist abgelaufen und wurde deaktiviert.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
except Exception as exc:
|
||||||
|
app.logger.warning(f'Trial expiry check failed: {exc}')
|
||||||
|
return None
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def _enforce_module_access():
|
def _enforce_module_access():
|
||||||
endpoint = request.endpoint or ''
|
endpoint = request.endpoint or ''
|
||||||
@@ -1108,6 +1156,7 @@ def inject_version():
|
|||||||
'permission_action_options': PERMISSION_ACTION_OPTIONS,
|
'permission_action_options': PERMISSION_ACTION_OPTIONS,
|
||||||
'permission_page_options': PERMISSION_PAGE_OPTIONS,
|
'permission_page_options': PERMISSION_PAGE_OPTIONS,
|
||||||
'permission_presets': us.get_permission_preset_definitions(),
|
'permission_presets': us.get_permission_preset_definitions(),
|
||||||
|
'trial_status': session.get('trial_status', {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1124,6 +1173,16 @@ def create_daily_backup():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Daily backup creation failed: {e}")
|
app.logger.error(f"Daily backup creation failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_expired_trial_tenants():
|
||||||
|
"""Remove expired trial tenants from config and MongoDB."""
|
||||||
|
try:
|
||||||
|
purged_tenants = purge_expired_trial_tenants()
|
||||||
|
if purged_tenants:
|
||||||
|
app.logger.warning(f"Purged expired trial tenants: {', '.join(purged_tenants)}")
|
||||||
|
except Exception as exc:
|
||||||
|
app.logger.error(f"Trial tenant cleanup failed: {exc}")
|
||||||
|
|
||||||
def update_appointment_statuses():
|
def update_appointment_statuses():
|
||||||
"""
|
"""
|
||||||
Aktualisiert automatisch die Status aller Terminplaner-Einträge.
|
Aktualisiert automatisch die Status aller Terminplaner-Einträge.
|
||||||
@@ -1319,6 +1378,7 @@ def _initialize_scheduler():
|
|||||||
scheduler.add_job(func=create_daily_backup, trigger="interval", hours=cfg.BACKUP_INTERVAL_HOURS)
|
scheduler.add_job(func=create_daily_backup, trigger="interval", hours=cfg.BACKUP_INTERVAL_HOURS)
|
||||||
scheduler.add_job(func=update_appointment_statuses, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
|
scheduler.add_job(func=update_appointment_statuses, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
|
||||||
scheduler.add_job(func=create_return_reminders, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
|
scheduler.add_job(func=create_return_reminders, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
|
||||||
|
scheduler.add_job(func=cleanup_expired_trial_tenants, trigger="interval", hours=1)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
_scheduler_initialized = True
|
_scheduler_initialized = True
|
||||||
app.logger.info(f"Scheduler started successfully (interval={cfg.SCHEDULER_INTERVAL_MIN} min)")
|
app.logger.info(f"Scheduler started successfully (interval={cfg.SCHEDULER_INTERVAL_MIN} min)")
|
||||||
|
|||||||
+222
-3
@@ -9,6 +9,8 @@ Each tenant can support up to 20+ users with isolated data and resource pools.
|
|||||||
|
|
||||||
from flask import request, g, session, has_request_context
|
from flask import request, g, session, has_request_context
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -18,10 +20,48 @@ from Web.modules.database.settings import MongoClient
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_TENANT_REGISTRY_MTIME = None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_tenant_registry_from_config():
|
||||||
|
registry = {}
|
||||||
|
config_path = getattr(cfg, 'CONFIG_PATH', None)
|
||||||
|
if config_path and os.path.isfile(config_path):
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as handle:
|
||||||
|
config = json.load(handle)
|
||||||
|
tenants = config.get('tenants', {})
|
||||||
|
if isinstance(tenants, dict):
|
||||||
|
registry.update(tenants)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to load tenant registry from config: %s", exc)
|
||||||
|
elif isinstance(getattr(cfg, 'TENANT_CONFIGS', None), dict):
|
||||||
|
registry.update(cfg.TENANT_CONFIGS)
|
||||||
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_tenant_registry():
|
||||||
|
global TENANT_REGISTRY, _TENANT_REGISTRY_MTIME
|
||||||
|
config_path = getattr(cfg, 'CONFIG_PATH', None)
|
||||||
|
|
||||||
|
current_mtime = None
|
||||||
|
if config_path and os.path.isfile(config_path):
|
||||||
|
try:
|
||||||
|
current_mtime = os.path.getmtime(config_path)
|
||||||
|
except OSError:
|
||||||
|
current_mtime = None
|
||||||
|
|
||||||
|
if current_mtime == _TENANT_REGISTRY_MTIME:
|
||||||
|
return TENANT_REGISTRY
|
||||||
|
|
||||||
|
TENANT_REGISTRY.clear()
|
||||||
|
TENANT_REGISTRY.update(_load_tenant_registry_from_config())
|
||||||
|
_TENANT_REGISTRY_MTIME = current_mtime
|
||||||
|
return TENANT_REGISTRY
|
||||||
|
|
||||||
|
|
||||||
# Tenant registry: maps subdomain/tenant_id to database name
|
# Tenant registry: maps subdomain/tenant_id to database name
|
||||||
TENANT_REGISTRY = {}
|
TENANT_REGISTRY = _load_tenant_registry_from_config()
|
||||||
if isinstance(getattr(cfg, 'TENANT_CONFIGS', None), dict):
|
|
||||||
TENANT_REGISTRY.update(cfg.TENANT_CONFIGS)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_nested_value(source, path, default=None):
|
def _get_nested_value(source, path, default=None):
|
||||||
@@ -139,6 +179,8 @@ def _is_ip_host(hostname):
|
|||||||
|
|
||||||
def get_tenant_config(tenant_id=None):
|
def get_tenant_config(tenant_id=None):
|
||||||
"""Return the registered config for a tenant, falling back to default."""
|
"""Return the registered config for a tenant, falling back to default."""
|
||||||
|
_refresh_tenant_registry()
|
||||||
|
|
||||||
if tenant_id is None:
|
if tenant_id is None:
|
||||||
ctx = get_tenant_context()
|
ctx = get_tenant_context()
|
||||||
tenant_id = ctx.tenant_id if ctx and ctx.tenant_id else 'default'
|
tenant_id = ctx.tenant_id if ctx and ctx.tenant_id else 'default'
|
||||||
@@ -232,6 +274,183 @@ def is_tenant_module_enabled(module_name, tenant_id=None, default=False):
|
|||||||
return bool(default)
|
return bool(default)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_datetime_value(value):
|
||||||
|
if value is None or value == '':
|
||||||
|
return None
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
parsed = value
|
||||||
|
else:
|
||||||
|
text = str(value).strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
parsed = datetime.datetime.fromisoformat(text.replace('Z', '+00:00'))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if parsed.tzinfo is not None:
|
||||||
|
parsed = parsed.astimezone(datetime.timezone.utc).replace(tzinfo=None)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def get_tenant_trial_config(tenant_id=None):
|
||||||
|
"""Return the optional trial/demo lifecycle settings for a tenant."""
|
||||||
|
config = get_tenant_config(tenant_id)
|
||||||
|
trial_config = _get_nested_value(config, ['trial'], {})
|
||||||
|
if not isinstance(trial_config, dict):
|
||||||
|
trial_config = {}
|
||||||
|
|
||||||
|
demo_config = _get_nested_value(config, ['demo'], {})
|
||||||
|
if isinstance(demo_config, dict):
|
||||||
|
merged = dict(demo_config)
|
||||||
|
merged.update(trial_config)
|
||||||
|
trial_config = merged
|
||||||
|
|
||||||
|
return trial_config
|
||||||
|
|
||||||
|
|
||||||
|
def get_tenant_trial_status(tenant_id=None, now=None):
|
||||||
|
"""Compute the current trial status for a tenant.
|
||||||
|
|
||||||
|
The config may define either an absolute "expires_at" timestamp or a
|
||||||
|
relative lifetime via "started_at"/"created_at" plus one of
|
||||||
|
"expires_after_days", "ttl_days", or "days".
|
||||||
|
"""
|
||||||
|
trial_config = get_tenant_trial_config(tenant_id)
|
||||||
|
now = now or datetime.datetime.now()
|
||||||
|
|
||||||
|
enabled = bool(trial_config.get('enabled') or trial_config.get('active'))
|
||||||
|
if not enabled:
|
||||||
|
return {
|
||||||
|
'enabled': False,
|
||||||
|
'expired': False,
|
||||||
|
'auto_delete': bool(trial_config.get('auto_delete', False)),
|
||||||
|
'started_at': None,
|
||||||
|
'expires_at': None,
|
||||||
|
'days_left': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
started_at = _parse_datetime_value(
|
||||||
|
trial_config.get('started_at')
|
||||||
|
or trial_config.get('created_at')
|
||||||
|
or trial_config.get('activated_at')
|
||||||
|
)
|
||||||
|
expires_at = _parse_datetime_value(trial_config.get('expires_at'))
|
||||||
|
|
||||||
|
if expires_at is None:
|
||||||
|
duration_days = trial_config.get('expires_after_days')
|
||||||
|
if duration_days is None:
|
||||||
|
duration_days = trial_config.get('ttl_days')
|
||||||
|
if duration_days is None:
|
||||||
|
duration_days = trial_config.get('days')
|
||||||
|
try:
|
||||||
|
duration_days = int(duration_days) if duration_days is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
duration_days = None
|
||||||
|
|
||||||
|
if duration_days is not None:
|
||||||
|
base_time = started_at or now
|
||||||
|
expires_at = base_time + datetime.timedelta(days=max(0, duration_days))
|
||||||
|
|
||||||
|
expired = bool(expires_at and now >= expires_at)
|
||||||
|
days_left = None
|
||||||
|
if expires_at:
|
||||||
|
remaining = expires_at - now
|
||||||
|
days_left = max(0, int(remaining.total_seconds() // 86400))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'enabled': True,
|
||||||
|
'expired': expired,
|
||||||
|
'auto_delete': bool(trial_config.get('auto_delete', False)),
|
||||||
|
'started_at': started_at,
|
||||||
|
'expires_at': expires_at,
|
||||||
|
'days_left': days_left,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_db_name_from_config(tenant_id):
|
||||||
|
config = get_tenant_config(tenant_id)
|
||||||
|
explicit_db = None
|
||||||
|
if isinstance(config, dict):
|
||||||
|
explicit_db = config.get('db') or config.get('db_name')
|
||||||
|
|
||||||
|
if explicit_db:
|
||||||
|
return _normalize_db_name(explicit_db)
|
||||||
|
|
||||||
|
sanitized = ''.join(c if c.isalnum() or c == '_' else '' for c in str(tenant_id).lower())
|
||||||
|
return f'inventar_{sanitized}' if sanitized else cfg.MONGODB_DB
|
||||||
|
|
||||||
|
|
||||||
|
def delete_tenant(tenant_id, *, drop_database=True, remove_from_config=True):
|
||||||
|
"""Delete a tenant's runtime data and optionally remove its config entry."""
|
||||||
|
tenant_id = str(tenant_id or '').strip()
|
||||||
|
if not tenant_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
db_name = _get_tenant_db_name_from_config(tenant_id)
|
||||||
|
|
||||||
|
if drop_database:
|
||||||
|
try:
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
try:
|
||||||
|
client.drop_database(db_name)
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to drop tenant database %s for %s: %s", db_name, tenant_id, exc)
|
||||||
|
|
||||||
|
if remove_from_config:
|
||||||
|
try:
|
||||||
|
config_path = getattr(cfg, 'CONFIG_PATH', None)
|
||||||
|
if config_path and os.path.isfile(config_path):
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as handle:
|
||||||
|
config = json.load(handle)
|
||||||
|
|
||||||
|
tenants = config.get('tenants', {})
|
||||||
|
if not isinstance(tenants, dict):
|
||||||
|
tenants = {}
|
||||||
|
|
||||||
|
aliases = set(_tenant_db_aliases(tenant_id))
|
||||||
|
aliases.add(tenant_id)
|
||||||
|
lowered = tenant_id.lower()
|
||||||
|
if lowered.startswith('schule'):
|
||||||
|
aliases.add('school' + lowered[len('schule'):])
|
||||||
|
elif lowered.startswith('school'):
|
||||||
|
aliases.add('schule' + lowered[len('school'):])
|
||||||
|
|
||||||
|
removed_any = False
|
||||||
|
for alias in aliases:
|
||||||
|
if alias in tenants:
|
||||||
|
tenants.pop(alias, None)
|
||||||
|
removed_any = True
|
||||||
|
|
||||||
|
if removed_any:
|
||||||
|
config['tenants'] = tenants
|
||||||
|
with open(config_path, 'w', encoding='utf-8') as handle:
|
||||||
|
json.dump(config, handle, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
for alias in [tenant_id, *_tenant_db_aliases(tenant_id)]:
|
||||||
|
TENANT_REGISTRY.pop(alias, None)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to remove tenant config for %s: %s", tenant_id, exc)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def purge_expired_trial_tenants(now=None):
|
||||||
|
"""Delete expired trial tenants that opted into auto-delete."""
|
||||||
|
now = now or datetime.datetime.now()
|
||||||
|
purged_tenants = []
|
||||||
|
|
||||||
|
for tenant_id in list(TENANT_REGISTRY.keys()):
|
||||||
|
status = get_tenant_trial_status(tenant_id, now=now)
|
||||||
|
if status.get('enabled') and status.get('expired') and status.get('auto_delete'):
|
||||||
|
if delete_tenant(tenant_id):
|
||||||
|
purged_tenants.append(tenant_id)
|
||||||
|
|
||||||
|
return purged_tenants
|
||||||
|
|
||||||
|
|
||||||
class TenantContext:
|
class TenantContext:
|
||||||
"""
|
"""
|
||||||
Manages current tenant context for request lifecycle.
|
Manages current tenant context for request lifecycle.
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ show_help() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Commands:"
|
echo "Commands:"
|
||||||
echo " add <tenant_id> [port] Add a new tenant (initializes database)"
|
echo " add <tenant_id> [port] Add a new tenant (initializes database)"
|
||||||
|
echo " trial <tenant_id> [port] [days] Create a 7-day demo tenant that auto-deletes after expiry"
|
||||||
echo " remove <tenant_id> Remove a tenant completely (deletes data!)"
|
echo " remove <tenant_id> Remove a tenant completely (deletes data!)"
|
||||||
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
|
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
|
||||||
echo " restart-all Restart all application containers (zero-downtime reload)"
|
echo " restart-all Restart all application containers (zero-downtime reload)"
|
||||||
@@ -65,6 +66,7 @@ show_help() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " ./manage-tenant.sh add school_a 10001"
|
echo " ./manage-tenant.sh add school_a 10001"
|
||||||
|
echo " ./manage-tenant.sh trial school_demo 10002 7"
|
||||||
echo " ./manage-tenant.sh remove test_tenant"
|
echo " ./manage-tenant.sh remove test_tenant"
|
||||||
echo " ./manage-tenant.sh module school_a inventory=off library=on"
|
echo " ./manage-tenant.sh module school_a inventory=off library=on"
|
||||||
echo " ./manage-tenant.sh restart-all"
|
echo " ./manage-tenant.sh restart-all"
|
||||||
@@ -137,6 +139,176 @@ PY
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
write_trial_tenant_config() {
|
||||||
|
local tenant_id="$1"
|
||||||
|
local port="$2"
|
||||||
|
local trial_days="$3"
|
||||||
|
|
||||||
|
if python3 - <<'PY' "$CONFIG_FILE" "$tenant_id" "$port" "$trial_days"
|
||||||
|
import json, sys, os, datetime
|
||||||
|
|
||||||
|
path, tenant_id, port_str, trial_days_str = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
print(f"Error: config file not found: {path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
|
||||||
|
tenants = cfg.get('tenants')
|
||||||
|
if tenants is None or not isinstance(tenants, dict):
|
||||||
|
tenants = {}
|
||||||
|
|
||||||
|
aliases = {tenant_id}
|
||||||
|
normalized = tenant_id.lower()
|
||||||
|
if normalized.startswith('schule'):
|
||||||
|
aliases.add('school' + normalized[len('schule'):])
|
||||||
|
elif normalized.startswith('school'):
|
||||||
|
aliases.add('schule' + normalized[len('school'):])
|
||||||
|
|
||||||
|
for tid, conf in tenants.items():
|
||||||
|
if port_str and isinstance(conf, dict) and str(conf.get('port')) == port_str and tid not in aliases:
|
||||||
|
print(f"Error: port {port_str} is already mapped to tenant {tid}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
trial_days = max(1, int(trial_days_str))
|
||||||
|
except ValueError:
|
||||||
|
print(f"Error: trial days must be numeric, got {trial_days_str!r}", file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
|
trial_config = {
|
||||||
|
'enabled': True,
|
||||||
|
'auto_delete': True,
|
||||||
|
'days': trial_days,
|
||||||
|
'ttl_days': trial_days,
|
||||||
|
'expires_after_days': trial_days,
|
||||||
|
'started_at': now,
|
||||||
|
'created_at': now,
|
||||||
|
}
|
||||||
|
|
||||||
|
existing = {}
|
||||||
|
for alias in aliases:
|
||||||
|
alias_cfg = tenants.get(alias)
|
||||||
|
if isinstance(alias_cfg, dict):
|
||||||
|
existing = alias_cfg
|
||||||
|
break
|
||||||
|
|
||||||
|
if port_str:
|
||||||
|
existing['port'] = int(port_str)
|
||||||
|
existing['modules'] = {
|
||||||
|
'inventory': {'enabled': True},
|
||||||
|
'library': {'enabled': True},
|
||||||
|
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
|
||||||
|
}
|
||||||
|
existing['trial'] = trial_config
|
||||||
|
|
||||||
|
for alias in aliases:
|
||||||
|
tenants[alias] = dict(existing)
|
||||||
|
|
||||||
|
cfg['tenants'] = tenants
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(cfg, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"Configured trial tenant {tenant_id} with {trial_days} day(s) and auto-delete enabled")
|
||||||
|
PY
|
||||||
|
then
|
||||||
|
echo "Trial tenant $tenant_id configured in config.json"
|
||||||
|
else
|
||||||
|
echo "Failed to configure trial tenant $tenant_id"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize_tenant_database() {
|
||||||
|
local tenant_id="$1"
|
||||||
|
local mode="$2"
|
||||||
|
|
||||||
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||||
|
if [ -z "$APP_CONTAINER" ]; then
|
||||||
|
echo "Warning: Application container is not running. Please start the multi-tenant system first."
|
||||||
|
echo "Data will be initialized upon first access by the tenant."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker exec "$APP_CONTAINER" python3 -c '
|
||||||
|
import sys, re, datetime, hashlib
|
||||||
|
sys.path.insert(0, "/app")
|
||||||
|
sys.path.insert(0, "/app/Web")
|
||||||
|
from Web.modules.database import settings
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
tenant_id = sys.argv[1].lower()
|
||||||
|
mode = sys.argv[2]
|
||||||
|
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
|
||||||
|
db_name = f"inventar_{sanitized}"
|
||||||
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||||
|
db = client[db_name]
|
||||||
|
hashed_pw = hashlib.sha512("admin123".encode()).hexdigest()
|
||||||
|
|
||||||
|
action_permissions = {
|
||||||
|
"can_borrow": True,
|
||||||
|
"can_insert": True,
|
||||||
|
"can_edit": True,
|
||||||
|
"can_delete": True,
|
||||||
|
"can_manage_users": True,
|
||||||
|
"can_manage_settings": True,
|
||||||
|
"can_view_logs": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
page_permissions = {
|
||||||
|
"home": True,
|
||||||
|
"tutorial_page": True,
|
||||||
|
"my_borrowed_items": True,
|
||||||
|
"notifications_view": True,
|
||||||
|
"impressum": True,
|
||||||
|
"license": True,
|
||||||
|
"library_view": True,
|
||||||
|
"terminplan": True,
|
||||||
|
"home_admin": True,
|
||||||
|
"upload_admin": True,
|
||||||
|
"library_admin": True,
|
||||||
|
"admin_borrowings": True,
|
||||||
|
"library_loans_admin": True,
|
||||||
|
"admin_damaged_items": True,
|
||||||
|
"admin_audit_dashboard": True,
|
||||||
|
"logs": True,
|
||||||
|
"manage_filters": True,
|
||||||
|
"manage_locations": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
if db.users.count_documents({"Username": "admin"}) == 0:
|
||||||
|
db.users.insert_one({
|
||||||
|
"Username": "admin",
|
||||||
|
"Password": hashed_pw,
|
||||||
|
"Admin": True,
|
||||||
|
"active_ausleihung": None,
|
||||||
|
"name": "Admin",
|
||||||
|
"last_name": "User",
|
||||||
|
"IsStudent": False,
|
||||||
|
"PermissionPreset": "full_access",
|
||||||
|
"ActionPermissions": action_permissions,
|
||||||
|
"PagePermissions": page_permissions,
|
||||||
|
})
|
||||||
|
|
||||||
|
if mode == "trial":
|
||||||
|
db.settings.update_one(
|
||||||
|
{"setting_type": "tenant_trial"},
|
||||||
|
{"$set": {
|
||||||
|
"setting_type": "tenant_trial",
|
||||||
|
"enabled": True,
|
||||||
|
"auto_delete": True,
|
||||||
|
"days": 7,
|
||||||
|
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
|
}},
|
||||||
|
upsert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123")
|
||||||
|
' "$tenant_id" "$mode"
|
||||||
|
}
|
||||||
|
|
||||||
update_runtime_ports() {
|
update_runtime_ports() {
|
||||||
local new_port="$1"
|
local new_port="$1"
|
||||||
local env_file="$PWD/.docker-build.env"
|
local env_file="$PWD/.docker-build.env"
|
||||||
@@ -471,6 +643,36 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
|||||||
echo "Data will be initialized upon first access by the tenant."
|
echo "Data will be initialized upon first access by the tenant."
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
trial)
|
||||||
|
if [ -z "$TENANT_ID" ]; then
|
||||||
|
echo "Error: Please provide a tenant_id."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PORT_ARG="${3:-}"
|
||||||
|
DAYS_ARG="${4:-7}"
|
||||||
|
|
||||||
|
if [ -n "$PORT_ARG" ]; then
|
||||||
|
if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
|
||||||
|
echo "Error: Port must be a numeric value."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
register_tenant_port "$TENANT_ID" "$PORT_ARG"
|
||||||
|
update_runtime_ports "$PORT_ARG"
|
||||||
|
sync_tenant_port_map
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_trial_tenant_config "$TENANT_ID" "$PORT_ARG" "$DAYS_ARG"
|
||||||
|
|
||||||
|
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||||
|
restart_app_container
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Initializing trial database for $TENANT_ID..."
|
||||||
|
initialize_tenant_database "$TENANT_ID" "trial"
|
||||||
|
echo "Trial tenant '$TENANT_ID' successfully configured. It will expire after $DAYS_ARG day(s) and self-delete."
|
||||||
|
;;
|
||||||
|
|
||||||
remove)
|
remove)
|
||||||
if [ -z "$TENANT_ID" ]; then
|
if [ -z "$TENANT_ID" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user