Jun 28 - Update tenant self-service signup, trial period enforcement

This commit is contained in:
2026-06-28 18:51:04 -04:00
parent e6b5be8c4d
commit 54fa5b8c87
7 changed files with 415 additions and 30 deletions
+34 -13
View File
@@ -156,19 +156,31 @@ def drop_mysql_db_and_user(db_name, db_user, user_host='%'):
# ── admin seeding (writes into the tenant DB) ────────────────────────────────
def seed_admin(db_uri, email, username=None, full_name=None, expires_hours=72):
"""Insert the first admin into a tenant DB with a set-password token.
def seed_admin(db_uri, email, username=None, full_name=None, expires_hours=72,
password_hash=None):
"""Insert the first admin into a tenant DB.
Returns (username, token). The account is created with password_set=0 and an
unusable placeholder hash; the admin completes setup via the returned link.
When `password_hash` is provided the account is created ready-to-use
(password_set=1, no token). Otherwise a set-password token is generated and
returned so the admin completes setup via a one-time link.
Returns (username, token). token is None when password_hash is supplied.
"""
username = username or re.sub(r'[^a-zA-Z0-9_.-]', '', email.split('@')[0]) or 'admin'
full_name = full_name or username
token = secrets.token_hex(32)
placeholder = generate_password_hash(secrets.token_urlsafe(32))
now = now_eastern()
from datetime import timedelta
expires = now + timedelta(hours=expires_hours)
if password_hash:
ph = password_hash
token = None
password_set = 1
expires = None
else:
token = secrets.token_hex(32)
ph = generate_password_hash(secrets.token_urlsafe(32))
password_set = 0
expires = now + timedelta(hours=expires_hours)
eng = create_engine(db_uri, future=True)
try:
@@ -180,9 +192,9 @@ def seed_admin(db_uri, email, username=None, full_name=None, expires_hours=72):
set_password_token, set_password_token_expires)
VALUES
(:u, :fn, :em, :ph, 'admin',
:ca, 1, 0, :tok, :exp)
"""), {'u': username, 'fn': full_name, 'em': email, 'ph': placeholder,
'ca': now, 'tok': token, 'exp': expires})
:ca, 1, :ps, :tok, :exp)
"""), {'u': username, 'fn': full_name, 'em': email, 'ph': ph,
'ca': now, 'ps': password_set, 'tok': token, 'exp': expires})
finally:
eng.dispose()
return username, token
@@ -219,7 +231,7 @@ def _add_domains(session, tenant_id, slug, base_domain, custom_domain=None):
def create_tenant(slug, name, plan_code, admin_email, admin_username=None,
admin_full_name=None, custom_domain=None, base_domain=None,
db_host=None, user_host='%'):
db_host=None, user_host='%', admin_password=None, trial_days=14):
if not _SLUG_RE.match(slug or ''):
raise ValueError(f"Invalid slug '{slug}' (must be a DNS label).")
base = _base_domain(base_domain)
@@ -238,6 +250,7 @@ def create_tenant(slug, name, plan_code, admin_email, admin_username=None,
job_id = None
tenant_id = None
trial_ends_at = None
db_created = False
try:
with control_session() as s:
@@ -249,13 +262,18 @@ def create_tenant(slug, name, plan_code, admin_email, admin_username=None,
db_created = True
with control_session() as s:
from datetime import timedelta
t = Tenant(slug=slug, name=name, plan_id=plan_id, status='provisioning',
db_host=host, db_port=3306, db_name=dbname, db_user=dbuser,
created_at=now_eastern())
if trial_days:
t.subscription_status = 'trial'
t.trial_ends_at = now_eastern() + timedelta(days=trial_days)
t.set_db_password(password)
s.add(t); s.flush()
tenant_id = t.id
db_uri = t.db_uri
trial_ends_at = t.trial_ends_at
j = s.get(ProvisioningJob, job_id)
if j:
j.tenant_id = tenant_id
@@ -267,7 +285,9 @@ def create_tenant(slug, name, plan_code, admin_email, admin_username=None,
bootstrap_tenant(ref)
# first admin + setup link
username, token = seed_admin(db_uri, admin_email, admin_username, admin_full_name)
pw_hash = generate_password_hash(admin_password) if admin_password else None
username, token = seed_admin(db_uri, admin_email, admin_username, admin_full_name,
password_hash=pw_hash)
with control_session() as s:
primary = _add_domains(s, tenant_id, slug, base, custom_domain)
@@ -282,7 +302,8 @@ def create_tenant(slug, name, plan_code, admin_email, admin_username=None,
'tenant_id': tenant_id, 'slug': slug, 'db_name': dbname,
'db_user': dbuser, 'primary_domain': primary,
'custom_domain': custom_domain, 'admin_username': username,
'setup_link': setup_link(primary, token),
'setup_link': setup_link(primary, token) if token else None,
'trial_ends_at': trial_ends_at,
}
except Exception as e: