mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
feat: add support for login with ChatGPT
This commit is contained in:
8
codex-rs/Cargo.lock
generated
8
codex-rs/Cargo.lock
generated
@@ -704,6 +704,14 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-login"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-mcp-client"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"exec",
|
||||
"execpolicy",
|
||||
"linux-sandbox",
|
||||
"login",
|
||||
"mcp-client",
|
||||
"mcp-server",
|
||||
"mcp-types",
|
||||
|
||||
17
codex-rs/login/Cargo.toml
Normal file
17
codex-rs/login/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "codex-login"
|
||||
version = { workspace = true }
|
||||
edition = "2024"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
46
codex-rs/login/src/lib.rs
Normal file
46
codex-rs/login/src/lib.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
|
||||
const SOURCE_FOR_PYTHON_SERVER: &str = include_str!("./login_with_chatgpt.py");
|
||||
|
||||
const JSON_PATH_FOR_API_KEY: &str = "OPENAI_API_KEY";
|
||||
|
||||
/// Run `python3 -c {{SOURCE_FOR_PYTHON_SERVER}}` with the CODEX_HOME
|
||||
/// environment variable set to the provided `codex_home` path. If the
|
||||
/// subprocess exits 0, read the OPENAI_API_KEY property out of
|
||||
/// CODEX_HOME/auth.json and return Ok(OPENAI_API_KEY). Otherwise, return Err
|
||||
/// with any information from the subprocess.
|
||||
pub async fn login_with_chatgpt(codex_home: &Path) -> std::io::Result<String> {
|
||||
let child = Command::new("python3")
|
||||
.arg("-c")
|
||||
.arg(SOURCE_FOR_PYTHON_SERVER)
|
||||
.env("CODEX_HOME", codex_home)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let output = child.wait_with_output().await?;
|
||||
if output.status.success() {
|
||||
let auth_path = codex_home.join("auth.json");
|
||||
let mut file = fs::File::open(&auth_path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let v: serde_json::Value = serde_json::from_str(&contents)?;
|
||||
if let Some(api_key) = v.get(JSON_PATH_FOR_API_KEY).and_then(|t| t.as_str()) {
|
||||
Ok(api_key.to_string())
|
||||
} else {
|
||||
Err(std::io::Error::other(format!(
|
||||
"{auth_path:?} missing {JSON_PATH_FOR_API_KEY} field"
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(std::io::Error::other(format!(
|
||||
"login_with_chatgpt subprocess failed: {stderr}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
504
codex-rs/login/src/login_with_chatgpt.py
Normal file
504
codex-rs/login/src/login_with_chatgpt.py
Normal file
@@ -0,0 +1,504 @@
|
||||
"""Script that spawns a local webserver for retrieving an OpenAI API key.
|
||||
|
||||
- Listens on 127.0.0.1:1455
|
||||
- Opens http://localhost:1455/auth/callback in the browser
|
||||
- If the user successfully navigates the auth flow,
|
||||
$CODEX_HOME/auth.json will be written with the API key.
|
||||
- User will be redirected to http://localhost:1455/success upon success.
|
||||
|
||||
The script should exit with a non-zero code if the user fails to navigate the
|
||||
auth flow.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import datetime
|
||||
import errno
|
||||
import hashlib
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
# Required port for OAuth client.
|
||||
REQUIRED_PORT = 1455
|
||||
DEFAULT_ISSUER = "https://auth.openai.com"
|
||||
DEFAULT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
|
||||
EXIT_CODE_WHEN_ADDRESS_ALREADY_IN_USE = 13
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Retrieve API key via local HTTP flow")
|
||||
parser.add_argument(
|
||||
"--no-browser",
|
||||
action="store_true",
|
||||
help="Do not automatically open the browser",
|
||||
)
|
||||
parser.add_argument("--verbose", action="store_true", help="Enable request logging")
|
||||
args = parser.parse_args()
|
||||
|
||||
codex_home = os.environ.get("CODEX_HOME")
|
||||
if not codex_home:
|
||||
eprint("ERROR: CODEX_HOME environment variable is not set")
|
||||
sys.exit(1)
|
||||
|
||||
# Spawn server.
|
||||
try:
|
||||
httpd = _ApiKeyHTTPServer(
|
||||
("127.0.0.1", REQUIRED_PORT),
|
||||
_ApiKeyHTTPHandler,
|
||||
codex_home=codex_home,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
except OSError as e:
|
||||
eprint(f"ERROR: {e}")
|
||||
if e.errno == errno.EADDRINUSE:
|
||||
# Caller might want to handle this case specially.
|
||||
sys.exit(EXIT_CODE_WHEN_ADDRESS_ALREADY_IN_USE)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
auth_url = httpd.auth_url()
|
||||
|
||||
with httpd:
|
||||
eprint(f"Starting local login server on http://localhost:{REQUIRED_PORT}/")
|
||||
if not args.no_browser:
|
||||
try:
|
||||
webbrowser.open(auth_url, new=1, autoraise=True)
|
||||
except Exception as e:
|
||||
eprint(f"Failed to open browser: {e}")
|
||||
|
||||
eprint(
|
||||
f"If your browser did not open, navigate to this URL to authenticate:\n {auth_url}\n"
|
||||
)
|
||||
|
||||
# Run the server in the main thread until `shutdown()` is called by the
|
||||
# request handler.
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
eprint("\nKeyboard interrupt received, exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class _ApiKeyHTTPHandler(http.server.BaseHTTPRequestHandler):
|
||||
"""A minimal request handler that captures an *api key* from query/post."""
|
||||
|
||||
# We store the result in the server instance itself.
|
||||
server: "_ApiKeyHTTPServer" # type: ignore[override] - helpful annotation
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802 – required by BaseHTTPRequestHandler
|
||||
path = urllib.parse.urlparse(self.path).path
|
||||
|
||||
if path == "/success":
|
||||
self._send_html(LOGIN_SUCCESS_HTML)
|
||||
sys.exit(self.server.exit_code)
|
||||
elif path == "/auth/callback":
|
||||
query = urllib.parse.urlparse(self.path).query
|
||||
params = urllib.parse.parse_qs(query)
|
||||
|
||||
# Validate state -------------------------------------------------
|
||||
if params.get("state", [None])[0] != self.server.state:
|
||||
self.send_error(400, "State parameter mismatch")
|
||||
return
|
||||
|
||||
# Standard OAuth flow -----------------------------------------
|
||||
code = params.get("code", [None])[0]
|
||||
if not code:
|
||||
self.send_error(400, "Missing authorization code")
|
||||
return
|
||||
|
||||
try:
|
||||
api_key = self._exchange_code_for_api_key(code)
|
||||
except Exception as exc: # noqa: BLE001 – propagate to client
|
||||
self.send_error(500, f"Token exchange failed: {exc}")
|
||||
return
|
||||
|
||||
if _write_auth_file(api_key, self.server.codex_home):
|
||||
self.server.exit_code = 0
|
||||
self._send_redirect("/success")
|
||||
else:
|
||||
self.send_error(500, "Unable to persist auth file")
|
||||
else:
|
||||
self.send_error(404, "Endpoint not supported")
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802 – required by BaseHTTPRequestHandler
|
||||
self.send_error(404, "Endpoint not supported")
|
||||
|
||||
def send_error(self, code, message=None, explain=None):
|
||||
super().send_error(code, message, explain)
|
||||
sys.exit(self.server.exit_code)
|
||||
|
||||
def _send_redirect(self, url: str) -> None:
|
||||
self.send_response(302)
|
||||
self.send_header("Location", url)
|
||||
self.end_headers()
|
||||
|
||||
def _send_html(self, body: str) -> None:
|
||||
encoded = body.encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(encoded)))
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
|
||||
# Silence logging for cleanliness unless --verbose flag is used.
|
||||
def log_message(self, fmt: str, *args): # type: ignore[override]
|
||||
if getattr(self.server, "verbose", False): # type: ignore[attr-defined]
|
||||
super().log_message(fmt, *args)
|
||||
|
||||
def _exchange_code_for_api_key(self, code: str) -> str:
|
||||
"""Perform token + token-exchange to obtain an OpenAI API key."""
|
||||
|
||||
token_endpoint = f"{self.server.issuer}/oauth/token"
|
||||
|
||||
# 1. Authorization-code -> (id_token, access_token, refresh_token)
|
||||
data = urllib.parse.urlencode(
|
||||
{
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": self.server.redirect_uri,
|
||||
"client_id": self.server.client_id,
|
||||
"code_verifier": self.server.pkce.code_verifier,
|
||||
}
|
||||
).encode()
|
||||
|
||||
id_token: str
|
||||
refresh_token: str
|
||||
|
||||
with urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
token_endpoint,
|
||||
data=data,
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
) as resp:
|
||||
payload = json.loads(resp.read().decode())
|
||||
id_token = payload["id_token"]
|
||||
refresh_token = payload.get("refresh_token", "")
|
||||
|
||||
# 2. Token exchange to obtain API key
|
||||
exchange_data = urllib.parse.urlencode(
|
||||
{
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
"client_id": self.server.client_id,
|
||||
"requested_token": "openai-api-key",
|
||||
"subject_token": id_token,
|
||||
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
|
||||
"name": "Codex CLI [auto-generated]",
|
||||
}
|
||||
).encode()
|
||||
|
||||
with urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
token_endpoint,
|
||||
data=exchange_data,
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
) as resp:
|
||||
exchange_payload = json.loads(resp.read().decode())
|
||||
api_key = exchange_payload["access_token"]
|
||||
|
||||
# Persist refresh_token/id_token for future use (redeem credits etc.)
|
||||
# Not strictly necessary here, but included for parity with TS.
|
||||
auth_extra = {
|
||||
"tokens": {
|
||||
"id_token": id_token,
|
||||
"refresh_token": refresh_token,
|
||||
},
|
||||
"last_refresh": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
}
|
||||
# Merge into existing auth file once the key is written.
|
||||
self._auth_extra = auth_extra # type: ignore[attr-defined]
|
||||
|
||||
return api_key
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _write_auth_file(api_key: str, codex_home: str) -> bool:
|
||||
"""Persist *api_key* to $CODEX_HOME/auth.json.
|
||||
|
||||
Returns True on success, False otherwise. Any error is printed to
|
||||
*stderr* so that the Rust layer can surface the problem.
|
||||
"""
|
||||
if not os.path.isdir(codex_home):
|
||||
try:
|
||||
os.makedirs(codex_home, exist_ok=True)
|
||||
except Exception as exc: # pragma: no cover – unlikely
|
||||
eprint(f"ERROR: unable to create CODEX_HOME directory: {exc}")
|
||||
return False
|
||||
|
||||
auth_path = os.path.join(codex_home, "auth.json")
|
||||
try:
|
||||
with open(auth_path, "w", encoding="utf-8") as fp:
|
||||
json.dump({"OPENAI_API_KEY": api_key}, fp)
|
||||
os.chmod(auth_path, 0o600)
|
||||
except Exception as exc: # pragma: no cover – permissions/filesystem
|
||||
eprint(f"ERROR: unable to write auth file: {exc}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class PkceCodes:
|
||||
code_verifier: str
|
||||
code_challenge: str
|
||||
|
||||
|
||||
class _ApiKeyHTTPServer(http.server.HTTPServer):
|
||||
"""HTTPServer with shutdown helper & self-contained OAuth configuration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server_address: tuple[str, int],
|
||||
request_handler_class: type[http.server.BaseHTTPRequestHandler],
|
||||
*,
|
||||
codex_home: str,
|
||||
verbose: bool = False,
|
||||
) -> None:
|
||||
super().__init__(server_address, request_handler_class, bind_and_activate=True)
|
||||
|
||||
self.exit_code = 1
|
||||
self.codex_home = codex_home
|
||||
self.verbose: bool = verbose
|
||||
|
||||
self.issuer: str = DEFAULT_ISSUER
|
||||
self.client_id: str = DEFAULT_CLIENT_ID
|
||||
port = server_address[1]
|
||||
self.redirect_uri: str = f"http://localhost:{port}/auth/callback"
|
||||
self.pkce: PkceCodes = _generate_pkce()
|
||||
self.state: str = secrets.token_hex(32)
|
||||
|
||||
def auth_url(self) -> str:
|
||||
"""Return fully-formed OpenID authorization URL."""
|
||||
params = {
|
||||
"response_type": "code",
|
||||
"client_id": self.client_id,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"scope": "openid profile email offline_access",
|
||||
"code_challenge": self.pkce.code_challenge,
|
||||
"code_challenge_method": "S256",
|
||||
"id_token_add_organizations": "true",
|
||||
"state": self.state,
|
||||
}
|
||||
return f"{self.issuer}/oauth/authorize?" + urllib.parse.urlencode(params)
|
||||
|
||||
|
||||
def _generate_pkce() -> PkceCodes:
|
||||
"""Generate PKCE *code_verifier* and *code_challenge* (S256)."""
|
||||
code_verifier = secrets.token_hex(64)
|
||||
digest = hashlib.sha256(code_verifier.encode()).digest()
|
||||
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
|
||||
return PkceCodes(code_verifier, code_challenge)
|
||||
|
||||
|
||||
def eprint(*args, **kwargs) -> None:
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
LOGIN_SUCCESS_HTML = """const LOGIN_SUCCESS_HTML = String.raw`
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Sign into Codex CLI</title>
|
||||
<link rel="icon" href='data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"%3E%3Cpath stroke="%23000" stroke-linecap="round" stroke-width="2.484" d="M22.356 19.797H17.17M9.662 12.29l1.979 3.576a.511.511 0 0 1-.005.504l-1.974 3.409M30.758 16c0 8.15-6.607 14.758-14.758 14.758-8.15 0-14.758-6.607-14.758-14.758C1.242 7.85 7.85 1.242 16 1.242c8.15 0 14.758 6.608 14.758 14.758Z"/%3E%3C/svg%3E' type="image/svg+xml">
|
||||
<style>
|
||||
.container {
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: white;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
.inner-container {
|
||||
width: 400px;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
display: inline-flex;
|
||||
}
|
||||
.content {
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
display: flex;
|
||||
}
|
||||
.svg-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
color: var(--text-primary, #0D0D0D);
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
line-height: 36.40px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.setup-box {
|
||||
width: 600px;
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-primary, white);
|
||||
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 16px;
|
||||
outline: 1px var(--border-default, rgba(13, 13, 13, 0.10)) solid;
|
||||
outline-offset: -1px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
display: inline-flex;
|
||||
}
|
||||
.setup-content {
|
||||
flex: 1 1 0;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
}
|
||||
.setup-text {
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
display: inline-flex;
|
||||
}
|
||||
.setup-title {
|
||||
align-self: stretch;
|
||||
color: var(--text-primary, #0D0D0D);
|
||||
font-size: 14px;
|
||||
font-weight: 510;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.setup-description {
|
||||
align-self: stretch;
|
||||
color: var(--text-secondary, #5D5D5D);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.redirect-box {
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
}
|
||||
.close-button,
|
||||
.redirect-button {
|
||||
height: 28px;
|
||||
padding: 8px 16px;
|
||||
background: var(--interactive-bg-primary-default, #0D0D0D);
|
||||
border-radius: 999px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
}
|
||||
.close-button,
|
||||
.redirect-text {
|
||||
color: var(--interactive-label-primary-default, white);
|
||||
font-size: 14px;
|
||||
font-weight: 510;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="inner-container">
|
||||
<div class="content">
|
||||
<div data-svg-wrapper class="svg-wrapper">
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.6665 28.0003C4.6665 15.1137 15.1132 4.66699 27.9998 4.66699C40.8865 4.66699 51.3332 15.1137 51.3332 28.0003C51.3332 40.887 40.8865 51.3337 27.9998 51.3337C15.1132 51.3337 4.6665 40.887 4.6665 28.0003ZM37.5093 18.5088C36.4554 17.7672 34.9999 18.0203 34.2583 19.0742L24.8508 32.4427L20.9764 28.1808C20.1095 27.2272 18.6338 27.1569 17.6803 28.0238C16.7267 28.8906 16.6565 30.3664 17.5233 31.3199L23.3566 37.7366C23.833 38.2606 24.5216 38.5399 25.2284 38.4958C25.9353 38.4517 26.5838 38.089 26.9914 37.5098L38.0747 21.7598C38.8163 20.7059 38.5632 19.2504 37.5093 18.5088Z" fill="var(--green-400, #04B84C)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="title">Signed in to Codex CLI</div>
|
||||
</div>
|
||||
<div class="close-box" style="display: none;">
|
||||
<div class="setup-description">You may now close this page</div>
|
||||
</div>
|
||||
<div class="setup-box" style="display: none;">
|
||||
<div class="setup-content">
|
||||
<div class="setup-text">
|
||||
<div class="setup-title">Finish setting up your API organization</div>
|
||||
<div class="setup-description">Add a payment method to use your organization.</div>
|
||||
</div>
|
||||
<div class="redirect-box">
|
||||
<div data-hasendicon="false" data-hasstarticon="false" data-ishovered="false" data-isinactive="false" data-ispressed="false" data-size="large" data-type="primary" class="redirect-button">
|
||||
<div class="redirect-text">Redirecting in 3s...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const needsSetup = params.get('needs_setup') === 'true';
|
||||
const platformUrl = params.get('platform_url') || 'https://platform.openai.com';
|
||||
const orgId = params.get('org_id');
|
||||
const projectId = params.get('project_id');
|
||||
const planType = params.get('plan_type');
|
||||
const idToken = params.get('id_token');
|
||||
// Show different message and optional redirect when setup is required
|
||||
if (needsSetup) {
|
||||
const setupBox = document.querySelector('.setup-box');
|
||||
setupBox.style.display = 'flex';
|
||||
const redirectUrlObj = new URL('/org-setup', platformUrl);
|
||||
redirectUrlObj.searchParams.set('p', planType);
|
||||
redirectUrlObj.searchParams.set('t', idToken);
|
||||
redirectUrlObj.searchParams.set('with_org', orgId);
|
||||
redirectUrlObj.searchParams.set('project_id', projectId);
|
||||
const redirectUrl = redirectUrlObj.toString();
|
||||
const message = document.querySelector('.redirect-text');
|
||||
let countdown = 3;
|
||||
function tick() {
|
||||
message.textContent =
|
||||
'Redirecting in ' + countdown + 's…';
|
||||
if (countdown === 0) {
|
||||
window.location.replace(redirectUrl);
|
||||
} else {
|
||||
countdown -= 1;
|
||||
setTimeout(tick, 1000);
|
||||
}
|
||||
}
|
||||
tick();
|
||||
} else {
|
||||
const closeBox = document.querySelector('.close-box');
|
||||
closeBox.style.display = 'flex';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# Unconditionally call `main()` instead of gating it behind
|
||||
# `if __name__ == "__main__"` because this script is either:
|
||||
#
|
||||
# - invoked as a string passed to `python3 -c`
|
||||
# - run via `python3 login_with_chatgpt.py` for testing as part of local
|
||||
# development
|
||||
main()
|
||||
Reference in New Issue
Block a user