Files
ServerKit/backend/cli.py
T
Juan DenisandClaude Opus 4.5 4ba0afee97 fix(2): load .env file in CLI before app initialization
CLI commands failed with "unable to open database file" because
environment variables weren't loaded from .env before create_app().

Now checks multiple .env locations:
- Same directory as cli.py
- Parent directory
- /opt/serverkit/.env (production)
- /opt/serverkit/backend/.env

Add --debug flag to show which env file was loaded.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 01:57:39 -05:00

228 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""ServerKit CLI - Administrative commands for ServerKit."""
import os
import click
import secrets
import sys
from pathlib import Path
# Load .env file before importing app
# Check multiple locations for the .env file
def load_env():
"""Load environment variables from .env file."""
try:
from dotenv import load_dotenv
# Try multiple locations
env_locations = [
Path(__file__).parent / '.env', # Same directory as cli.py
Path(__file__).parent.parent / '.env', # Parent directory
Path('/opt/serverkit/.env'), # Production location
Path('/opt/serverkit/backend/.env'), # Alternative production
]
for env_path in env_locations:
if env_path.exists():
load_dotenv(env_path)
return str(env_path)
# Also check if DATABASE_URL is already set
if os.environ.get('DATABASE_URL'):
return 'environment'
return None
except ImportError:
# python-dotenv not installed
return None
# Load env before any other imports that might use config
_env_loaded = load_env()
from werkzeug.security import generate_password_hash
from app import create_app, db
from app.models import User
@click.group()
@click.option('--debug', is_flag=True, help='Show debug information')
@click.pass_context
def cli(ctx, debug):
"""ServerKit administrative CLI."""
ctx.ensure_object(dict)
ctx.obj['debug'] = debug
if debug and _env_loaded:
click.echo(f"Loaded environment from: {_env_loaded}")
@cli.command()
@click.option('--email', prompt=True, help='Admin email address')
@click.option('--username', prompt=True, help='Admin username')
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='Admin password')
def create_admin(email, username, password):
"""Create a new admin user."""
app = create_app()
with app.app_context():
# Check if user already exists
if User.query.filter((User.email == email) | (User.username == username)).first():
click.echo(click.style('Error: User with this email or username already exists.', fg='red'))
sys.exit(1)
user = User(
email=email,
username=username,
role='admin',
is_active=True
)
user.set_password(password)
db.session.add(user)
db.session.commit()
click.echo(click.style(f'Admin user "{username}" created successfully!', fg='green'))
@cli.command()
@click.option('--email', prompt=True, help='User email address')
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='New password')
def reset_password(email, password):
"""Reset a user's password."""
app = create_app()
with app.app_context():
user = User.query.filter_by(email=email).first()
if not user:
click.echo(click.style(f'Error: User with email "{email}" not found.', fg='red'))
sys.exit(1)
user.set_password(password)
user.failed_login_count = 0
user.locked_until = None
db.session.commit()
click.echo(click.style(f'Password reset successfully for "{user.username}"!', fg='green'))
@cli.command()
@click.option('--email', prompt=True, help='User email address')
def unlock_user(email):
"""Unlock a locked user account."""
app = create_app()
with app.app_context():
user = User.query.filter_by(email=email).first()
if not user:
click.echo(click.style(f'Error: User with email "{email}" not found.', fg='red'))
sys.exit(1)
user.failed_login_count = 0
user.locked_until = None
db.session.commit()
click.echo(click.style(f'User "{user.username}" unlocked successfully!', fg='green'))
@cli.command()
@click.option('--email', prompt=True, help='User email address')
def make_admin(email):
"""Promote a user to admin role."""
app = create_app()
with app.app_context():
user = User.query.filter_by(email=email).first()
if not user:
click.echo(click.style(f'Error: User with email "{email}" not found.', fg='red'))
sys.exit(1)
user.role = 'admin'
db.session.commit()
click.echo(click.style(f'User "{user.username}" is now an admin!', fg='green'))
@cli.command()
@click.option('--email', prompt=True, help='User email address')
def deactivate_user(email):
"""Deactivate a user account."""
app = create_app()
with app.app_context():
user = User.query.filter_by(email=email).first()
if not user:
click.echo(click.style(f'Error: User with email "{email}" not found.', fg='red'))
sys.exit(1)
user.is_active = False
db.session.commit()
click.echo(click.style(f'User "{user.username}" has been deactivated.', fg='yellow'))
@cli.command()
@click.option('--email', prompt=True, help='User email address')
def activate_user(email):
"""Activate a user account."""
app = create_app()
with app.app_context():
user = User.query.filter_by(email=email).first()
if not user:
click.echo(click.style(f'Error: User with email "{email}" not found.', fg='red'))
sys.exit(1)
user.is_active = True
db.session.commit()
click.echo(click.style(f'User "{user.username}" has been activated.', fg='green'))
@cli.command()
def list_users():
"""List all users."""
app = create_app()
with app.app_context():
users = User.query.all()
if not users:
click.echo('No users found.')
return
click.echo(f"\n{'ID':<5} {'Username':<20} {'Email':<30} {'Role':<10} {'Active':<8} {'Locked':<8}")
click.echo('-' * 85)
for user in users:
locked = 'Yes' if user.is_locked else 'No'
active = 'Yes' if user.is_active else 'No'
click.echo(f"{user.id:<5} {user.username:<20} {user.email:<30} {user.role:<10} {active:<8} {locked:<8}")
click.echo(f"\nTotal: {len(users)} user(s)")
@cli.command()
def generate_keys():
"""Generate secure SECRET_KEY and JWT_SECRET_KEY."""
secret_key = secrets.token_hex(32)
jwt_secret_key = secrets.token_hex(32)
click.echo("\nAdd these to your .env file:\n")
click.echo(f"SECRET_KEY={secret_key}")
click.echo(f"JWT_SECRET_KEY={jwt_secret_key}")
click.echo()
@cli.command()
def init_db():
"""Initialize the database."""
app = create_app()
with app.app_context():
db.create_all()
click.echo(click.style('Database initialized successfully!', fg='green'))
@cli.command()
@click.confirmation_option(prompt='Are you sure you want to drop all tables?')
def drop_db():
"""Drop all database tables."""
app = create_app()
with app.app_context():
db.drop_all()
click.echo(click.style('All tables dropped!', fg='yellow'))
if __name__ == '__main__':
cli()