Adds the full Next.js app (app/, components/, lib/, scripts/, assets/, docs/, Dockerfile, compose, configs) to version control. Security: - CIPC BizPortal customer-code logins (Login 1 + Login 3) now read CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE / CIPC_LOGIN3_PASSWORD from .env.local instead of source. - BizPortal personal login in apply-bizportal route reads BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD. - Added .env.example as the committed template. .gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright storage_state.json (live CIPC cookies), scratch dirs, client PDFs and debug screenshots.
150 lines
5 KiB
Python
150 lines
5 KiB
Python
# ─────────────────────────────────────────────────────────────────
|
|
# otp_fill.py — fill an SMS OTP into a running SARS login Chromium
|
|
# session via Chrome DevTools Protocol.
|
|
#
|
|
# Used by the parent agent when `SARS login Company spesific.py`
|
|
# detects a SARS OTP prompt and pauses. The login script writes its
|
|
# CDP port + PID to `scripts/.sars_cdp.json`; we read it from there
|
|
# (or accept the port as an argument) and connect.
|
|
#
|
|
# Usage:
|
|
# .venv\Scripts\python otp_fill.py <cdp_port> <otp>
|
|
# .venv\Scripts\python otp_fill.py --from-marker <otp>
|
|
#
|
|
# Returns exit code 0 on success, non-zero on failure (with a clear
|
|
# stderr message so the agent can debug).
|
|
# ─────────────────────────────────────────────────────────────────
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
CDP_INFO_PATH = SCRIPTS_DIR / ".sars_cdp.json"
|
|
|
|
# Selectors that match SARS's OTP input. Kept in sync with
|
|
# SARS login Company spesific.py::OTP_SELECTORS.
|
|
OTP_SELECTORS = [
|
|
"input[aria-label*='OTP' i]",
|
|
"input[aria-label*='One' i]", # "One Time Pin"
|
|
"input[aria-label*='verification' i]",
|
|
"input[placeholder*='OTP' i]",
|
|
"input[placeholder*='PIN' i]",
|
|
"input[name*='otp' i]",
|
|
"input[id*='otp' i]",
|
|
"input[type='tel']",
|
|
]
|
|
|
|
# Button names SARS uses to submit the OTP. First match wins.
|
|
SUBMIT_BUTTON_NAMES = ["Submit", "Verify", "Continue", "Login", "Confirm", "Next"]
|
|
|
|
|
|
def _fill_otp(page, otp: str) -> str:
|
|
"""Find the OTP input and fill it. Returns the selector that matched."""
|
|
for sel in OTP_SELECTORS:
|
|
try:
|
|
loc = page.locator(sel).first
|
|
if loc.count() > 0:
|
|
loc.fill(otp)
|
|
return sel
|
|
except Exception:
|
|
continue
|
|
raise RuntimeError(
|
|
f"No OTP field found matching any of {OTP_SELECTORS} "
|
|
f"(current URL: {page.url})"
|
|
)
|
|
|
|
|
|
def _click_submit(page) -> str:
|
|
"""Click the submit button. Returns the button name that matched."""
|
|
for name in SUBMIT_BUTTON_NAMES:
|
|
try:
|
|
btn = page.get_by_role("button", name=name).first
|
|
if btn.count() > 0:
|
|
btn.click(timeout=3_000)
|
|
return name
|
|
except Exception:
|
|
continue
|
|
raise RuntimeError(
|
|
f"No submit button found matching any of {SUBMIT_BUTTON_NAMES}"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(
|
|
description=(
|
|
"Inject an SMS OTP into a running SARS login Chromium session "
|
|
"via Chrome DevTools Protocol."
|
|
)
|
|
)
|
|
ap.add_argument("port_or_marker", help="CDP port number, or --from-marker flag value")
|
|
ap.add_argument("otp", help="OTP/PIN value to type into the browser")
|
|
ap.add_argument(
|
|
"--from-marker",
|
|
action="store_true",
|
|
help=(
|
|
"Read CDP port + PID from scripts/.sars_cdp.json "
|
|
"(written by the SARS login script)."
|
|
),
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
if args.from_marker:
|
|
try:
|
|
with open(CDP_INFO_PATH) as f:
|
|
port = json.load(f)["cdp_port"]
|
|
except Exception as e:
|
|
print(
|
|
f"ERROR: Could not read {CDP_INFO_PATH}: {e}",
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
print(f"Read CDP port {port} from {CDP_INFO_PATH}")
|
|
else:
|
|
port = int(args.port_or_marker)
|
|
|
|
otp = args.otp
|
|
|
|
try:
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{port}")
|
|
if not browser.contexts:
|
|
print("ERROR: Browser has no contexts", file=sys.stderr)
|
|
return 3
|
|
ctx = browser.contexts[0]
|
|
if not ctx.pages:
|
|
print("ERROR: Context has no pages", file=sys.stderr)
|
|
return 4
|
|
page = ctx.pages[0]
|
|
print(f"Connected to port {port}. URL: {page.url}")
|
|
|
|
matched_sel = _fill_otp(page, otp)
|
|
print(f"Filled OTP into field matched by: {matched_sel}")
|
|
|
|
try:
|
|
btn_name = _click_submit(page)
|
|
print(f"Clicked button: {btn_name}")
|
|
except RuntimeError as e:
|
|
print(f"WARNING: {e}", file=sys.stderr)
|
|
print("OTP filled but not submitted. Typing Enter as fallback.")
|
|
try:
|
|
page.keyboard.press("Enter")
|
|
except Exception:
|
|
pass
|
|
|
|
time.sleep(2)
|
|
print(f"After submit, URL: {page.url}")
|
|
except Exception as e:
|
|
print(f"ERROR: {type(e).__name__}: {e}", file=sys.stderr)
|
|
return 5
|
|
|
|
print("OK")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|