ohmyapi Claude Opus 4.6 (1M context) commited on
Commit
2563fc7
·
1 Parent(s): 6343462

fix: robust CI auto-import script, fix admin auth token verification

Browse files

- Replace fragile inline bash with register/auto_import.py module
- Fix _verify_admin: accept both raw password and hashed token as Bearer
- Previous bug: login returns SHA256 hash but Bearer check only matched raw password

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

.github/workflows/register-outlook.yml CHANGED
@@ -71,61 +71,4 @@ jobs:
71
  env:
72
  OUTLOOK2API_URL: ${{ secrets.OUTLOOK2API_URL }}
73
  ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
74
- run: |
75
- if [ -z "$OUTLOOK2API_URL" ] || [ -z "$ADMIN_PASSWORD" ]; then
76
- echo "Skipping auto-import (OUTLOOK2API_URL or ADMIN_PASSWORD not set)"
77
- exit 0
78
- fi
79
- # Login and get token
80
- TOKEN=$(curl -sf -X POST "$OUTLOOK2API_URL/admin/api/login" \
81
- -H 'Content-Type: application/json' \
82
- -d "{\"password\": \"$ADMIN_PASSWORD\"}" | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])" 2>/dev/null)
83
- if [ -z "$TOKEN" ]; then
84
- echo "Admin login failed, skipping import"
85
- exit 0
86
- fi
87
- # Collect accounts from staging files
88
- ACCOUNTS="[]"
89
- if [ -d "output/.staging_outlook" ]; then
90
- ACCOUNTS=$(python3 -c "
91
- import os, json, glob
92
- accs = []
93
- for f in glob.glob('output/.staging_outlook/outlook_*.json'):
94
- try:
95
- d = json.load(open(f))
96
- if d.get('email') and d.get('password'):
97
- accs.append(d['email'] + ':' + d['password'])
98
- except: pass
99
- print(json.dumps(accs))
100
- ")
101
- fi
102
- # Also try the zip file
103
- for z in output/*Outlook.zip; do
104
- [ -f "$z" ] || continue
105
- python3 -c "
106
- import zipfile, json, sys
107
- with zipfile.ZipFile('$z') as zf:
108
- for name in zf.namelist():
109
- if name.endswith('.txt'):
110
- content = zf.read(name).decode('utf-8', errors='replace')
111
- existing = json.loads(sys.argv[1]) if sys.argv[1] != '[]' else []
112
- for line in content.strip().splitlines():
113
- line = line.strip()
114
- if ':' in line and line not in existing:
115
- existing.append(line)
116
- print(json.dumps(existing))
117
- sys.exit(0)
118
- print(sys.argv[1])
119
- " "$ACCOUNTS" > /tmp/accs.json && ACCOUNTS=$(cat /tmp/accs.json)
120
- done
121
- COUNT=$(echo "$ACCOUNTS" | python3 -c "import sys,json;print(len(json.load(sys.stdin)))")
122
- if [ "$COUNT" = "0" ]; then
123
- echo "No accounts to import"
124
- exit 0
125
- fi
126
- echo "Importing $COUNT accounts..."
127
- RESULT=$(curl -sf -X POST "$OUTLOOK2API_URL/admin/api/accounts/bulk" \
128
- -H "Authorization: Bearer $TOKEN" \
129
- -H 'Content-Type: application/json' \
130
- -d "{\"accounts\": $ACCOUNTS, \"source\": \"ci\"}")
131
- echo "Import result: $RESULT"
 
71
  env:
72
  OUTLOOK2API_URL: ${{ secrets.OUTLOOK2API_URL }}
73
  ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
74
+ run: python -m register.auto_import
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
outlook2api/admin_routes.py CHANGED
@@ -21,14 +21,17 @@ def _verify_admin(request: Request) -> None:
21
  """Check admin password from cookie or Authorization header."""
22
  cfg = get_config()
23
  expected = cfg["admin_password"]
 
24
  # Cookie auth
25
  token = request.cookies.get("admin_token", "")
26
- if token and token == hashlib.sha256(expected.encode()).hexdigest():
27
  return
28
- # Header auth
29
  auth = request.headers.get("Authorization", "")
30
- if auth.startswith("Bearer ") and auth[7:].strip() == expected:
31
- return
 
 
32
  raise HTTPException(status_code=401, detail="Unauthorized")
33
 
34
 
 
21
  """Check admin password from cookie or Authorization header."""
22
  cfg = get_config()
23
  expected = cfg["admin_password"]
24
+ expected_hash = hashlib.sha256(expected.encode()).hexdigest()
25
  # Cookie auth
26
  token = request.cookies.get("admin_token", "")
27
+ if token and token == expected_hash:
28
  return
29
+ # Header auth: accept both raw password and hashed token
30
  auth = request.headers.get("Authorization", "")
31
+ if auth.startswith("Bearer "):
32
+ bearer = auth[7:].strip()
33
+ if bearer == expected or bearer == expected_hash:
34
+ return
35
  raise HTTPException(status_code=401, detail="Unauthorized")
36
 
37
 
register/auto_import.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Auto-import registered accounts to the admin panel.
3
+
4
+ Reads accounts from output/*Outlook.zip or output/.staging_outlook/*.json
5
+ and POSTs them to the admin bulk import endpoint.
6
+
7
+ Usage:
8
+ python -m register.auto_import
9
+
10
+ Environment variables:
11
+ OUTLOOK2API_URL - Admin panel URL (e.g. https://ohmyapi-outlook2api.hf.space)
12
+ ADMIN_PASSWORD - Admin panel password
13
+ """
14
+ import glob
15
+ import json
16
+ import os
17
+ import sys
18
+ import zipfile
19
+
20
+ import requests
21
+
22
+
23
+ def collect_accounts() -> list[str]:
24
+ """Collect email:password lines from zip files and staging dir."""
25
+ accounts = []
26
+ seen = set()
27
+
28
+ # From zip files
29
+ for zpath in sorted(glob.glob("output/*Outlook.zip")):
30
+ try:
31
+ with zipfile.ZipFile(zpath) as zf:
32
+ for name in zf.namelist():
33
+ if name.endswith(".txt"):
34
+ content = zf.read(name).decode("utf-8", errors="replace")
35
+ for line in content.strip().splitlines():
36
+ line = line.strip()
37
+ if ":" in line and line not in seen:
38
+ seen.add(line)
39
+ accounts.append(line)
40
+ except Exception as e:
41
+ print(f"[Import] Error reading {zpath}: {e}")
42
+
43
+ # From staging dir
44
+ for fpath in sorted(glob.glob("output/.staging_outlook/outlook_*.json")):
45
+ try:
46
+ with open(fpath) as f:
47
+ d = json.load(f)
48
+ email = d.get("email", "").strip()
49
+ password = d.get("password", "").strip()
50
+ if email and password:
51
+ line = f"{email}:{password}"
52
+ if line not in seen:
53
+ seen.add(line)
54
+ accounts.append(line)
55
+ except Exception:
56
+ pass
57
+
58
+ return accounts
59
+
60
+
61
+ def main():
62
+ url = os.environ.get("OUTLOOK2API_URL", "").rstrip("/")
63
+ password = os.environ.get("ADMIN_PASSWORD", "")
64
+
65
+ if not url or not password:
66
+ print("[Import] Skipping: OUTLOOK2API_URL or ADMIN_PASSWORD not set")
67
+ return
68
+
69
+ # Login
70
+ try:
71
+ r = requests.post(f"{url}/admin/api/login",
72
+ json={"password": password}, timeout=15)
73
+ r.raise_for_status()
74
+ token = r.json()["token"]
75
+ print(f"[Import] Logged in to {url}")
76
+ except Exception as e:
77
+ print(f"[Import] Login failed: {e}")
78
+ return
79
+
80
+ # Collect accounts
81
+ accounts = collect_accounts()
82
+ if not accounts:
83
+ print("[Import] No accounts to import")
84
+ return
85
+
86
+ print(f"[Import] Found {len(accounts)} accounts")
87
+
88
+ # Bulk import
89
+ try:
90
+ r = requests.post(
91
+ f"{url}/admin/api/accounts/bulk",
92
+ headers={"Authorization": f"Bearer {token}"},
93
+ json={"accounts": accounts, "source": "ci"},
94
+ timeout=30,
95
+ )
96
+ r.raise_for_status()
97
+ result = r.json()
98
+ print(f"[Import] Result: imported={result.get('imported')}, skipped={result.get('skipped')}")
99
+ except Exception as e:
100
+ print(f"[Import] Bulk import failed: {e}")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()