One-off script used to register/inspect Box V2 webhooks against the
service account. Subcommands: list-webhooks, list-folder, list-clients,
create-webhook, delete-webhook, register-all-clients.
Typical bootstrap flow on a fresh deploy:
1. Drop box_jwt_config.json on the server (gitignored, scp'd in).
2. Verify the service account can read each client folder:
`python backend/scripts/box_setup.py list-folder <folder_id>`
3. Once a client's box_folder_id is set in client_config.py, register
its webhook idempotently:
`python backend/scripts/box_setup.py register-all-clients \
https://optical-dev.oliver.solutions/ai_qc/api/box/webhook`
4. Copy the signing keys from the Box Developer Console (Custom App →
Webhooks) into BOX_WEBHOOK_PRIMARY_KEY / BOX_WEBHOOK_SECONDARY_KEY
in the env file, then restart ai-qc.service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
193 lines
6.7 KiB
Python
Executable file
193 lines
6.7 KiB
Python
Executable file
#!/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 <folder_id>
|
|
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 <folder_id> <address>
|
|
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 <webhook_id>
|
|
Remove a single webhook by id.
|
|
register-all-clients <address>
|
|
For every client with a box_folder_id, ensure a FILE.UPLOADED webhook
|
|
pointing at <address> 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())
|