diff --git a/backend/main.py b/backend/main.py index 5b4ad2f..c8e0935 100644 --- a/backend/main.py +++ b/backend/main.py @@ -89,14 +89,17 @@ return oauth.verify_identity(request.cookies.get(oauth.IDENTITY_COOKIE, "")) +@app.get("/login") +def login_page(): + """Explicit provider choice: X or Instagram (button disabled until configured).""" + return FileResponse(FRONTEND_DIR / "login.html") + + @app.get("/test") -def test_page(request: Request, anon: int = 0): - """Auth-before-test: no valid identity cookie -> redirect to X login. - ?anon=1 explicitly opts out (anonymous result, not bound to identity).""" - if not anon and _identity_from(request) is None: - url = oauth.x_authorize_url() - if url is not None: - return RedirectResponse(url[0], status_code=303) +def test_page(request: Request): + """Auth-before-test: no valid identity cookie -> explicit provider choice.""" + if _identity_from(request) is None: + return RedirectResponse("/login", status_code=303) return FileResponse(FRONTEND_DIR / "test.html") @@ -310,18 +313,19 @@ # --- identity binding (phase 2 scaffolding) ----------------------------------- -def _claim_error_page(result_id: str, provider: str) -> HTMLResponse: - """Standalone error page: claim failed, result untouched and still verifiable.""" +def _login_error_page(provider: str) -> HTMLResponse: + """Standalone error page: login failed, no identity cookie is set.""" body = ( '' - 'VerIQ - claim failed' + 'VerIQ - login failed' '' '
' - '

Claim failed

' - '

' + provider + ' did not return a usable profile. Your result is unchanged ' - 'and still verifiable. You can try again from the result page.

' - '

Back to home

' + '

Login failed

' + '

' + provider + ' did not return a usable profile. No result was ' + 'affected — you simply are not logged in. You can try again.

' + '

Back to login · ' + 'Back to home

' '
' ) return HTMLResponse(body, status_code=502) @@ -350,7 +354,7 @@ tok = oauth.x_exchange(code, flow["verifier"]) user = oauth.x_userinfo(tok.get("access_token")) if tok else None if not user or not user.get("username"): - return _claim_error_page("", "X") + return _login_error_page("X") token = oauth.sign_identity("x", str(user.get("id", "")), user["username"]) db.log_event("login", {"provider": "x", "username": user["username"]}) resp = RedirectResponse("/test", status_code=303) @@ -360,13 +364,14 @@ @app.get("/auth/ig/start") -def auth_ig_start(result_id: str): +def auth_ig_start(): + """Begin Instagram OAuth login (same identity-cookie flow as X).""" if not oauth.ig_configured(): raise HTTPException(503, "Instagram binding not configured yet") - url = oauth.ig_authorize_url(result_id) + url = oauth.ig_authorize_url("") if url is None: raise HTTPException(503, "Instagram binding not configured yet") - return JSONResponse({"authorize_url": url[0]}) + return RedirectResponse(url[0], status_code=303) @app.get("/auth/ig/callback") @@ -381,12 +386,13 @@ tok = oauth.ig_exchange(code) user = oauth.ig_userinfo(tok.get("access_token")) if tok else None if not user or not user.get("username"): - return _claim_error_page(flow["result_id"], "Instagram") - identity = "ig:@" + user["username"] - db.claim_result(flow["result_id"], identity) - db.log_event("claim", {"result_id": flow["result_id"], "provider": "ig", - "user_id": user.get("id"), "username": user["username"]}) - return RedirectResponse("/v/" + flow["result_id"], status_code=303) + return _login_error_page("Instagram") + token = oauth.sign_identity("ig", str(user.get("id", "")), user["username"]) + db.log_event("login", {"provider": "ig", "username": user["username"]}) + resp = RedirectResponse("/test", status_code=303) + resp.set_cookie(oauth.IDENTITY_COOKIE, token, max_age=oauth.IDENTITY_TTL_S, + httponly=True, samesite="lax", secure=True) + return resp # --- startup -------------------------------------------------------------------- diff --git a/frontend/assets/login.js b/frontend/assets/login.js new file mode 100644 index 0000000..4f41247 --- /dev/null +++ b/frontend/assets/login.js @@ -0,0 +1,16 @@ +/* VerIQ login page: show current identity, keep IG disabled until configured */ +(function () { + "use strict"; + + fetch("/api/me").then(function (r) { return r.json(); }).then(function (d) { + if (d && d.identity) { + document.getElementById("already").style.display = ""; + document.getElementById("alreadyName").textContent = d.identity; + } + }).catch(function () {}); + + var ig = document.getElementById("igBtn"); + if (ig) { + ig.addEventListener("click", function (e) { e.preventDefault(); }); + } +})(); \ No newline at end of file diff --git a/frontend/assets/verify.js b/frontend/assets/verify.js index 91edc68..f5285f7 100644 --- a/frontend/assets/verify.js +++ b/frontend/assets/verify.js @@ -51,7 +51,7 @@ $("kvId").textContent = d.result_id; $("kvDate").textContent = d.completed_at; - $("kvIdentity").textContent = d.identity || "not claimed (anonymous)"; + $("kvIdentity").textContent = d.identity || "not bound (anonymous)"; $("kvBank").textContent = "v" + d.bank_version; $("kvSig").textContent = d.signature; $("kvSig").style.color = d.signature_valid ? "var(--good)" : "var(--bad)"; diff --git a/frontend/how.html b/frontend/how.html index f74cea7..516d0e4 100644 --- a/frontend/how.html +++ b/frontend/how.html @@ -63,9 +63,9 @@

Honest limits: a signature proves the result was issued by this service and unaltered. It does not prove who sat behind the - keyboard — unless the result is bound to an X account before - the test starts (identity is part of the signed payload), and even - then it proves the account owner took it, not that they didn't + keyboard — unless the result is bound to an X or Instagram account + before the test starts (identity is part of the signed payload), + and even then it proves the account owner took it, not that they didn't get help. Scores are preliminary-calibrated until 500 completions. We say this on every result page. Tests that don't state their calibration status are selling confidence, not measurement. @@ -93,7 +93,7 @@

Try it

-

Get your own signed result — free, no registration.

+

Get your own signed result — free, one login, one active result.

Start the test →
diff --git a/frontend/index.html b/frontend/index.html index 0944fc7..895c616 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -23,13 +23,14 @@

Every IQ test on the internet gives you an image anyone can fake. VerIQ signs your result with a cryptographic key and binds it to your - X account before the test starts — so nobody can cherry-pick - their best attempt or claim someone else's score. The public link - anyone can verify. + X or Instagram account before the test starts — so nobody can + cherry-pick their best attempt or claim someone else's score. The + public link anyone can verify.

- Start the test → + Start the test →

- or take it anonymously — no login + Login (X or Instagram) binds the result to your account — one active + result per account.

40 tasks · ~25 minutes · free

diff --git a/frontend/login.html b/frontend/login.html new file mode 100644 index 0000000..8edebf1 --- /dev/null +++ b/frontend/login.html @@ -0,0 +1,50 @@ + + + + + +VerIQ — log in to take the test + + + + +
+
+ + one step before the test +
+ +
+

Log in to take the test.

+

+ Your result will be bound to the account you pick before the test + starts — that's what makes it verifiable. One active result per account: + a new attempt replaces the old one, so nobody can cherry-pick. +

+
+ +
+

Choose how to log in

+
+ Continue with X → + +
+ +

+ We only read your public profile (ID, @username, avatar) — read-only, + we never post. The @username is shown on your public result page. +

+
+ + +
+ + + \ No newline at end of file