#!/opt/homebrew/bin/python3.11 """ Sync users from SFTPGo to Momentry users table. Usage: python3 scripts/sync_users_from_sftpgo.py python3 scripts/sync_users_from_sftpgo.py --sftpgo-url http://localhost:8080 python3 scripts/sync_users_from_sftpgo.py --db "dbname=momentry user=accusys" Environment: SFTPGO_BASE_URL Default: http://localhost:8080 DATABASE_URL Default: dbname=momentry user=accusys host=localhost This script does NOT copy passwords. It creates user records with placeholder password hashes. The real password will be captured on the user's first login through Momentry (which verifies against SFTPGo and caches the hash). """ import argparse import json import os import sys from typing import Any import psycopg2 import psycopg2.extras import requests def get_sftpgo_users(sftpgo_url: str, admin_user: str, admin_pass: str) -> list[dict[str, Any]]: """Get all users from SFTPGo.""" # Get admin token (SFTPGo uses GET, not POST) token_url = f"{sftpgo_url}/api/v2/token" resp = requests.get(token_url, auth=(admin_user, admin_pass), timeout=10) resp.raise_for_status() token = resp.json().get("access_token") if not token: print("ERROR: Failed to get SFTPGo admin token", file=sys.stderr) sys.exit(1) # List users users_url = f"{sftpgo_url}/api/v2/users" headers = {"Authorization": f"Bearer {token}"} resp = requests.get(users_url, headers=headers, timeout=10) resp.raise_for_status() return resp.json() def main(): parser = argparse.ArgumentParser(description="Sync SFTPGo users to Momentry") parser.add_argument("--sftpgo-url", default=os.getenv("SFTPGO_BASE_URL", "http://localhost:8080")) parser.add_argument("--db", default=os.getenv("DATABASE_URL", "dbname=momentry user=accusys host=localhost")) parser.add_argument("--admin-user", default="admin") parser.add_argument("--admin-pass", default=os.getenv("SFTPGO_ADMIN_PASSWORD", "Test3200Test3200")) parser.add_argument("--dry-run", action="store_true", help="Print what would be done without executing") args = parser.parse_args() # Fetch users from SFTPGo print(f"[SFTPGo] Connecting to {args.sftpgo_url}...") try: sftpgo_users = get_sftpgo_users(args.sftpgo_url, args.admin_user, args.admin_pass) except Exception as e: print(f"ERROR: Failed to fetch SFTPGo users: {e}", file=sys.stderr) sys.exit(1) print(f"[SFTPGo] Found {len(sftpgo_users)} users") # Connect to Momentry DB and set schema conn = psycopg2.connect(args.db) cur = conn.cursor() cur.execute("SET search_path TO dev") synced = 0 skipped = 0 for user in sftpgo_users: username = user.get("username") status = user.get("status", 0) if not username or status != 1: skipped += 1 continue role = "admin" if username == "admin" else "user" # Placeholder hash — will be updated on first login via SFTPGo fallback placeholder_hash = "$placeholder$synced_from_sftpgo" if args.dry_run: print(f" Would insert: {username} (role={role})") synced += 1 continue try: cur.execute( "INSERT INTO users (username, password_hash, role) VALUES (%s, %s, %s) " "ON CONFLICT (username) DO NOTHING", (username, placeholder_hash, role), ) if cur.rowcount > 0: print(f" ✅ {username} (role={role})") synced += 1 else: print(f" ⏭️ {username} already exists, skipped") skipped += 1 except Exception as e: print(f" ❌ {username}: {e}", file=sys.stderr) skipped += 1 conn.commit() cur.close() conn.close() print(f"\nDone: {synced} synced, {skipped} skipped/errors") print("Note: Password hashes are placeholders. First login via Momentry will cache the real hash.") if __name__ == "__main__": main()