56d677e925
- CellNetwork.jsx CopyButton: use execCommand fallback when clipboard API is unavailable (HTTP non-localhost context) - Makefile reset-admin-password: run inside cell-api container via docker exec so bcrypt and all deps are available without host installation - docker-compose.yml: mount ./scripts:/app/scripts:ro in cell-api so the reset script is accessible inside the container - scripts/reset_admin_password.py: auto-detect API module path and data dir so the script works in both host (api/ sibling) and container (/app) layouts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Admin password management utility.
|
|
|
|
Usage:
|
|
reset_admin_password.py --generate # generate a random password and set it
|
|
reset_admin_password.py <new_password> # set a specific password
|
|
"""
|
|
import sys
|
|
import os
|
|
import secrets
|
|
import string
|
|
|
|
_here = os.path.dirname(os.path.abspath(__file__))
|
|
# Find auth_manager: host layout has api/ sibling, container has it one level up at /app
|
|
for _api_path in [os.path.join(_here, '..', 'api'), os.path.join(_here, '..'), '/app']:
|
|
if os.path.isfile(os.path.join(_api_path, 'auth_manager.py')):
|
|
sys.path.insert(0, os.path.normpath(_api_path))
|
|
break
|
|
|
|
ROOT = os.path.normpath(os.path.join(_here, '..'))
|
|
# data dir: host uses ROOT/data/api, container mounts data/api at ROOT/data
|
|
for _data in [os.path.join(ROOT, 'data', 'api'), os.path.join(ROOT, 'data'), '/app/data']:
|
|
if os.path.isdir(_data):
|
|
_DATA_DIR = _data
|
|
break
|
|
else:
|
|
_DATA_DIR = os.path.join(ROOT, 'data', 'api')
|
|
|
|
INIT_PW_FILE = os.path.join(_DATA_DIR, '.admin_initial_password')
|
|
TEST_PW_FILE = os.path.join(_DATA_DIR, '.test_admin_pass')
|
|
|
|
|
|
def _generate_password(length: int = 20) -> str:
|
|
alphabet = string.ascii_letters + string.digits + '!@#$%^&*'
|
|
while True:
|
|
pw = ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
# Ensure at least one of each character class required by the validator
|
|
if (any(c.isupper() for c in pw)
|
|
and any(c.islower() for c in pw)
|
|
and any(c.isdigit() for c in pw)
|
|
and any(c in '!@#$%^&*' for c in pw)):
|
|
return pw
|
|
|
|
|
|
def _set_password(new_password: str) -> None:
|
|
from auth_manager import AuthManager
|
|
data_dir = _DATA_DIR
|
|
os.makedirs(data_dir, exist_ok=True)
|
|
mgr = AuthManager(data_dir=data_dir, config_dir='/tmp')
|
|
if mgr.set_password_admin('admin', new_password):
|
|
print('[OK] Admin password updated in auth_users.json')
|
|
else:
|
|
print('[WARN] Admin user not found — creating it now')
|
|
mgr.create_user('admin', new_password, 'admin')
|
|
print('[OK] Admin user created')
|
|
|
|
|
|
def _print_banner(password: str) -> None:
|
|
border = '=' * 60
|
|
print(border)
|
|
print(' ADMIN PASSWORD')
|
|
print(border)
|
|
print(f' Username : admin')
|
|
print(f' Password : {password}')
|
|
print(border)
|
|
print(' Save this password — it will NOT be shown again.')
|
|
print(border)
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2:
|
|
print(__doc__, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
arg = sys.argv[1]
|
|
|
|
if arg == '--show':
|
|
# Show the initial password file if the API hasn't consumed it yet
|
|
if os.path.exists(INIT_PW_FILE):
|
|
pw = open(INIT_PW_FILE).read().strip()
|
|
_print_banner(pw)
|
|
print()
|
|
print(f' (file: {INIT_PW_FILE})')
|
|
print(' The API will delete this file on first start.')
|
|
else:
|
|
print('Initial password file not found.')
|
|
print('The API has already consumed it, or setup has not been run.')
|
|
print()
|
|
print('To set a new password run:')
|
|
print(' make reset-admin-password')
|
|
return
|
|
|
|
if arg == '--generate':
|
|
password = _generate_password()
|
|
else:
|
|
password = arg
|
|
|
|
if len(password) < 10:
|
|
print('Error: password must be at least 10 characters', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
_set_password(password)
|
|
|
|
# Write the initial password file (API reads it on first start, then deletes it)
|
|
os.makedirs(os.path.dirname(INIT_PW_FILE), exist_ok=True)
|
|
with open(INIT_PW_FILE, 'w') as f:
|
|
f.write(password)
|
|
|
|
# Write the persistent test password file (never deleted by the API)
|
|
with open(TEST_PW_FILE, 'w') as f:
|
|
f.write(password)
|
|
os.chmod(TEST_PW_FILE, 0o600)
|
|
|
|
_print_banner(password)
|
|
print(f'\n Also saved to: {INIT_PW_FILE}')
|
|
print(f' Test file: {TEST_PW_FILE} (persists across API restarts)')
|
|
print(' Restart the API container for the change to take effect:')
|
|
print(' docker restart cell-api')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|