#!/usr/bin/env python3 """ Box JWT service-account admin CLI. One-off script for setting up + inspecting the Box V2 webhooks that drive the QC pipeline. Run on the dev/prod server once the Box admin has invited the service account to each client folder, with the JWT config JSON in place at backend/config/box_jwt_config.json (or BOX_JWT_CONFIG_PATH). Subcommands: list-webhooks Show every webhook the service account can see. list-folder List the items in a Box folder. Sanity-check the service account can actually read the folder (otherwise it isn't a collaborator yet). list-clients Print which clients in client_config.py have a box_folder_id set. create-webhook
Register a FILE.UPLOADED V2 webhook on a folder. Address is the public URL of /api/box/webhook (e.g. https://optical-dev.oliver.solutions/ai_qc/api/box/webhook). delete-webhook Remove a single webhook by id. register-all-clients
For every client with a box_folder_id, ensure a FILE.UPLOADED webhook pointing at
exists on that folder. Idempotent — already-present webhooks are left alone. After registering, set the signing keys (Box Developer Console → Custom App → Webhooks Settings) in the env file as BOX_WEBHOOK_PRIMARY_KEY and BOX_WEBHOOK_SECONDARY_KEY, then restart ai-qc.service. """ from __future__ import annotations import argparse import json import os import sys THIS_DIR = os.path.dirname(os.path.abspath(__file__)) BACKEND_DIR = os.path.dirname(THIS_DIR) sys.path.insert(0, BACKEND_DIR) import box_jwt_client from client_config import get_clients_with_box_folder WEBHOOK_TRIGGERS_DEFAULT = ['FILE.UPLOADED'] def cmd_list_webhooks(_args): webhooks = box_jwt_client.list_webhooks() if not webhooks: print('No webhooks visible to this service account.') return 0 for wh in webhooks: target = wh.get('target') or {} print( f" id={wh.get('id')} target={target.get('type')}/{target.get('id')}" f" address={wh.get('address')} triggers={wh.get('triggers')}" ) return 0 def cmd_list_folder(args): items = box_jwt_client.list_folder_items(args.folder_id, fields=['id', 'name', 'type', 'size']) print(f'Folder {args.folder_id} contains {len(items)} items:') for it in items: size = f" ({it.get('size')} bytes)" if it.get('size') is not None else '' print(f" {it.get('type')}/{it.get('id')} {it.get('name')}{size}") return 0 def cmd_list_clients(_args): configured = get_clients_with_box_folder() if not configured: print('No clients have box_folder_id set in client_config.py.') return 0 for cid, cfg in configured: print( f" {cid}: source_folder={cfg.get('box_folder_id')} " f"reports_folder={cfg.get('box_reports_folder_id') or '(falls back to source)'} " f"default_profile={cfg.get('default_profile') or '(first profile in list)'}" ) return 0 def cmd_create_webhook(args): wh = box_jwt_client.create_webhook( target_type='folder', target_id=args.folder_id, address=args.address, triggers=WEBHOOK_TRIGGERS_DEFAULT, ) print('Created webhook:') print(json.dumps(wh, indent=2)) return 0 def cmd_delete_webhook(args): box_jwt_client.delete_webhook(args.webhook_id) print(f'Deleted webhook {args.webhook_id}.') return 0 def cmd_register_all_clients(args): """Idempotent: skip folders that already have a webhook pointing at this address.""" existing = box_jwt_client.list_webhooks() existing_by_folder = {} for wh in existing: target = wh.get('target') or {} if target.get('type') == 'folder': existing_by_folder.setdefault(str(target.get('id')), []).append(wh) configured = get_clients_with_box_folder() if not configured: print('No clients have box_folder_id set. Nothing to register.') return 0 created = 0 skipped = 0 errors = 0 for cid, cfg in configured: folder_id = str(cfg['box_folder_id']) already = existing_by_folder.get(folder_id, []) matching = [w for w in already if w.get('address') == args.address] if matching: print(f" {cid} ({folder_id}): SKIP — webhook already exists (id={matching[0].get('id')})") skipped += 1 continue try: wh = box_jwt_client.create_webhook( target_type='folder', target_id=folder_id, address=args.address, triggers=WEBHOOK_TRIGGERS_DEFAULT, ) print(f" {cid} ({folder_id}): CREATED webhook id={wh.get('id')}") created += 1 except Exception as exc: print(f" {cid} ({folder_id}): ERROR — {exc}") errors += 1 print() print(f'Summary: {created} created, {skipped} already present, {errors} errored.') if errors: print('Common causes for errors: service account not invited as collaborator on the folder yet,') print('or the folder_id in client_config.py is wrong.') return 0 if errors == 0 else 1 def main(): parser = argparse.ArgumentParser(description='Box JWT admin CLI') sub = parser.add_subparsers(dest='cmd', required=True) sub.add_parser('list-webhooks').set_defaults(func=cmd_list_webhooks) p_lf = sub.add_parser('list-folder', help='List items in a Box folder') p_lf.add_argument('folder_id') p_lf.set_defaults(func=cmd_list_folder) sub.add_parser('list-clients').set_defaults(func=cmd_list_clients) p_cw = sub.add_parser('create-webhook', help='Create a single FILE.UPLOADED webhook') p_cw.add_argument('folder_id') p_cw.add_argument('address', help='Public URL of /api/box/webhook') p_cw.set_defaults(func=cmd_create_webhook) p_dw = sub.add_parser('delete-webhook', help='Delete a webhook by id') p_dw.add_argument('webhook_id') p_dw.set_defaults(func=cmd_delete_webhook) p_ra = sub.add_parser('register-all-clients', help='Create FILE.UPLOADED webhooks for every client with a box_folder_id') p_ra.add_argument('address', help='Public URL of /api/box/webhook') p_ra.set_defaults(func=cmd_register_all_clients) args = parser.parse_args() if not box_jwt_client.is_configured(): print(f'ERROR: Box JWT config not found. Drop the JSON from Box at ' f'{os.path.join(BACKEND_DIR, "config", "box_jwt_config.json")} or set BOX_JWT_CONFIG_PATH.', file=sys.stderr) return 2 try: return args.func(args) or 0 except box_jwt_client.BoxJWTError as exc: print(f'Box API error: {exc}', file=sys.stderr) return 1 if __name__ == '__main__': sys.exit(main())