mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Update Codex login success page UX (#20136)
## Summary update the local login success page to match the Codex desktop auth UX use theme-aware colors and an inline 20px Codex mark keep the actual localhost success page aligned with the browser auth UX PR ## Tests <img width="1728" height="1117" alt="Screenshot 2026-04-29 at 12 00 34 PM" src="https://github.com/user-attachments/assets/76a40c3f-07c3-452c-97da-e7c43717cd2c" />
This commit is contained in:
committed by
GitHub
Unverified
parent
74f06dcdfb
commit
98f67b15d3
@@ -1586,6 +1586,9 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"chatgpt"
|
||||
|
||||
+3
@@ -10075,6 +10075,9 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"chatgpt"
|
||||
|
||||
+3
@@ -6729,6 +6729,9 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"chatgpt"
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"chatgpt"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt" } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens",
|
||||
export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens",
|
||||
/**
|
||||
* Access token (JWT) supplied by the client.
|
||||
* This token is used for backend API requests and email extraction.
|
||||
|
||||
@@ -2227,7 +2227,9 @@ mod tests {
|
||||
fn serialize_account_login_chatgpt() -> Result<()> {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::LoginAccountParams::Chatgpt,
|
||||
params: v2::LoginAccountParams::Chatgpt {
|
||||
codex_streamlined_login: false,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
json!({
|
||||
@@ -2242,6 +2244,28 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_account_login_chatgpt_streamlined() -> Result<()> {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::LoginAccountParams::Chatgpt {
|
||||
codex_streamlined_login: true,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "account/login/start",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"type": "chatgpt",
|
||||
"codexStreamlinedLogin": true
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_account_login_chatgpt_device_code() -> Result<()> {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
|
||||
@@ -2174,9 +2174,12 @@ pub enum LoginAccountParams {
|
||||
#[ts(rename = "apiKey")]
|
||||
api_key: String,
|
||||
},
|
||||
#[serde(rename = "chatgpt")]
|
||||
#[ts(rename = "chatgpt")]
|
||||
Chatgpt,
|
||||
#[serde(rename = "chatgpt", rename_all = "camelCase")]
|
||||
#[ts(rename = "chatgpt", rename_all = "camelCase")]
|
||||
Chatgpt {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
codex_streamlined_login: bool,
|
||||
},
|
||||
#[serde(rename = "chatgptDeviceCode")]
|
||||
#[ts(rename = "chatgptDeviceCode")]
|
||||
ChatgptDeviceCode,
|
||||
|
||||
@@ -1607,7 +1607,9 @@ impl CodexClient {
|
||||
let request_id = self.request_id();
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: request_id.clone(),
|
||||
params: codex_app_server_protocol::LoginAccountParams::Chatgpt,
|
||||
params: codex_app_server_protocol::LoginAccountParams::Chatgpt {
|
||||
codex_streamlined_login: false,
|
||||
},
|
||||
};
|
||||
|
||||
self.send_request(request, request_id, "account/login/start")
|
||||
|
||||
@@ -1338,8 +1338,11 @@ impl CodexMessageProcessor {
|
||||
self.login_api_key_v2(request_id, LoginApiKeyParams { api_key })
|
||||
.await;
|
||||
}
|
||||
LoginAccountParams::Chatgpt => {
|
||||
self.login_chatgpt_v2(request_id).await;
|
||||
LoginAccountParams::Chatgpt {
|
||||
codex_streamlined_login,
|
||||
} => {
|
||||
self.login_chatgpt_v2(request_id, codex_streamlined_login)
|
||||
.await;
|
||||
}
|
||||
LoginAccountParams::ChatgptDeviceCode => {
|
||||
self.login_chatgpt_device_code_v2(request_id).await;
|
||||
@@ -1441,6 +1444,7 @@ impl CodexMessageProcessor {
|
||||
// Build options for a ChatGPT login attempt; performs validation.
|
||||
async fn login_chatgpt_common(
|
||||
&self,
|
||||
codex_streamlined_login: bool,
|
||||
) -> std::result::Result<LoginServerOptions, JSONRPCErrorError> {
|
||||
let config = self.config.as_ref();
|
||||
|
||||
@@ -1458,6 +1462,7 @@ impl CodexMessageProcessor {
|
||||
|
||||
let opts = LoginServerOptions {
|
||||
open_browser: false,
|
||||
codex_streamlined_login,
|
||||
..LoginServerOptions::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
CLIENT_ID.to_string(),
|
||||
@@ -1496,13 +1501,20 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_chatgpt_v2(&self, request_id: ConnectionRequestId) {
|
||||
let result = self.login_chatgpt_response().await;
|
||||
async fn login_chatgpt_v2(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
codex_streamlined_login: bool,
|
||||
) {
|
||||
let result = self.login_chatgpt_response(codex_streamlined_login).await;
|
||||
self.outgoing.send_result(request_id, result).await;
|
||||
}
|
||||
|
||||
async fn login_chatgpt_response(&self) -> Result<LoginAccountResponse, JSONRPCErrorError> {
|
||||
let opts = self.login_chatgpt_common().await?;
|
||||
async fn login_chatgpt_response(
|
||||
&self,
|
||||
codex_streamlined_login: bool,
|
||||
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
|
||||
let opts = self.login_chatgpt_common(codex_streamlined_login).await?;
|
||||
let server = run_login_server(opts)
|
||||
.map_err(|err| internal_error(format!("failed to start login server: {err}")))?;
|
||||
let login_id = Uuid::new_v4();
|
||||
@@ -1573,7 +1585,9 @@ impl CodexMessageProcessor {
|
||||
async fn login_chatgpt_device_code_response(
|
||||
&self,
|
||||
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
|
||||
let opts = self.login_chatgpt_common().await?;
|
||||
let opts = self
|
||||
.login_chatgpt_common(/*codex_streamlined_login*/ false)
|
||||
.await?;
|
||||
let device_code = request_device_code(&opts)
|
||||
.await
|
||||
.map_err(Self::login_chatgpt_device_code_start_error)?;
|
||||
|
||||
@@ -6,5 +6,6 @@ codex_rust_crate(
|
||||
compile_data = [
|
||||
"src/assets/error.html",
|
||||
"src/assets/success.html",
|
||||
"src/assets/success_legacy.html",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -2,197 +2,236 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Sign into Codex</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">
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>Signed in to Codex</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;
|
||||
:root {
|
||||
--font-use: "SF Pro", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--gray-0: #ffffff;
|
||||
--gray-50: #f9f9f9;
|
||||
--gray-100: #ededed;
|
||||
--gray-150: #dfdfdf;
|
||||
--gray-300: #afafaf;
|
||||
--gray-500: #5d5d5d;
|
||||
--gray-900: #181818;
|
||||
--gray-1000: #0d0d0d;
|
||||
--color-background-surface: var(--gray-0);
|
||||
--color-text-foreground: var(--gray-1000);
|
||||
--color-border: rgb(13 13 13 / 10%);
|
||||
--color-button-background: var(--gray-0);
|
||||
--color-button-background-hover: var(--gray-50);
|
||||
--color-button-border: var(--gray-100);
|
||||
--color-button-border-hover: var(--gray-150);
|
||||
--interactive-label-secondary-default: var(--gray-1000);
|
||||
--text-secondary: var(--gray-500);
|
||||
color: var(--color-text-foreground);
|
||||
background: var(--color-background-surface);
|
||||
font-family: var(--font-use);
|
||||
}
|
||||
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background-surface: var(--gray-900);
|
||||
--color-text-foreground: var(--gray-0);
|
||||
--color-border: rgb(255 255 255 / 8%);
|
||||
--color-button-background: rgb(255 255 255 / 5%);
|
||||
--color-button-background-hover: rgb(255 255 255 / 8%);
|
||||
--color-button-border: rgb(255 255 255 / 8%);
|
||||
--color-button-border-hover: rgb(255 255 255 / 16%);
|
||||
--interactive-label-secondary-default: var(--gray-300);
|
||||
--text-secondary: rgb(255 255 255 / 65%);
|
||||
}
|
||||
}
|
||||
.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;
|
||||
margin-top: 15vh;
|
||||
}
|
||||
.svg-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
color: var(--text-primary, #0D0D0D);
|
||||
font-size: 32px;
|
||||
font-weight: 400;
|
||||
line-height: 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;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 16px;
|
||||
border: .5px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px 0px;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--color-background-surface);
|
||||
}
|
||||
|
||||
.wordmark {
|
||||
position: fixed;
|
||||
top: 18px;
|
||||
left: 20px;
|
||||
font-size: 21px;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.45px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 56px 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: min(100%, 560px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0;
|
||||
color: var(--text-secondary, #5d5d5d);
|
||||
font-feature-settings: "liga" off, "clig" off;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.18px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--color-button-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-button-background);
|
||||
color: var(--interactive-label-secondary-default, #0d0d0d);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-use, "SF Pro");
|
||||
font-feature-settings: "liga" off, "clig" off;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 510;
|
||||
letter-spacing: -0.154px;
|
||||
line-height: 20px;
|
||||
padding: 8px 16px 8px 14px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--color-button-background-hover);
|
||||
border-color: var(--color-button-border-hover);
|
||||
}
|
||||
|
||||
.button:focus-visible {
|
||||
outline: 2px solid var(--color-text-foreground);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.codex-mark {
|
||||
flex: 0 0 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.setup-box {
|
||||
display: none;
|
||||
width: min(100vw - 32px, 560px);
|
||||
padding: 16px 20px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.setup-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 510;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.setup-description {
|
||||
margin: 0;
|
||||
color: var(--text-secondary, #5d5d5d);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="inner-container">
|
||||
<div class="content">
|
||||
<div class="logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path stroke="#000" 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"></path></svg>
|
||||
</div>
|
||||
<div class="title">Signed in to Codex</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 class="wordmark" aria-label="ChatGPT">ChatGPT</div>
|
||||
<main>
|
||||
<div class="content">
|
||||
<p class="message" id="status-message">You’re signed in and may close this tab</p>
|
||||
<button class="button" type="button" id="open-codex">
|
||||
<svg
|
||||
class="codex-mark"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2.484"
|
||||
/>
|
||||
</svg>
|
||||
<span>Open Codex</span>
|
||||
</button>
|
||||
<div class="setup-box" id="setup-box">
|
||||
<p class="setup-title">Finish setting up your API organization</p>
|
||||
<p class="setup-description">Add a payment method to use your organization. Redirecting in <span id="countdown">3</span>s...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
const CODEX_URL = "codex://threads/new";
|
||||
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
|
||||
const preview = params.get("preview") === "true";
|
||||
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");
|
||||
|
||||
window.history.replaceState(null, "", window.location.pathname);
|
||||
|
||||
function openCodex() {
|
||||
window.location.href = CODEX_URL;
|
||||
}
|
||||
|
||||
document.getElementById("open-codex").addEventListener("click", openCodex);
|
||||
if (!preview) {
|
||||
setTimeout(openCodex, 250);
|
||||
}
|
||||
|
||||
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');
|
||||
const setupBox = document.getElementById("setup-box");
|
||||
const countdownNode = document.getElementById("countdown");
|
||||
setupBox.style.display = "block";
|
||||
const redirectUrlObj = new URL("/org-setup", platformUrl);
|
||||
redirectUrlObj.search = new URLSearchParams({
|
||||
p: planType,
|
||||
t: idToken,
|
||||
with_org: orgId,
|
||||
project_id: projectId,
|
||||
}).toString();
|
||||
let countdown = 3;
|
||||
function tick() {
|
||||
message.textContent =
|
||||
'Redirecting in ' + countdown + 's…';
|
||||
countdownNode.textContent = String(countdown);
|
||||
if (countdown === 0) {
|
||||
window.location.replace(redirectUrl);
|
||||
} else {
|
||||
countdown -= 1;
|
||||
setTimeout(tick, 1000);
|
||||
window.location.replace(redirectUrlObj.toString());
|
||||
return;
|
||||
}
|
||||
countdown -= 1;
|
||||
setTimeout(tick, 1000);
|
||||
}
|
||||
tick();
|
||||
} else {
|
||||
const closeBox = document.querySelector('.close-box');
|
||||
closeBox.style.display = 'flex';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Sign into Codex</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;
|
||||
margin-top: 15vh;
|
||||
}
|
||||
.svg-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
color: var(--text-primary, #0D0D0D);
|
||||
font-size: 32px;
|
||||
font-weight: 400;
|
||||
line-height: 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;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 16px;
|
||||
border: .5px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px 0px;
|
||||
box-sizing: border-box;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="inner-container">
|
||||
<div class="content">
|
||||
<div class="logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path stroke="#000" 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"></path></svg>
|
||||
</div>
|
||||
<div class="title">Signed in to Codex</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>
|
||||
@@ -67,6 +67,7 @@ pub struct ServerOptions {
|
||||
pub open_browser: bool,
|
||||
pub force_state: Option<String>,
|
||||
pub forced_chatgpt_workspace_id: Option<String>,
|
||||
pub codex_streamlined_login: bool,
|
||||
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
}
|
||||
|
||||
@@ -86,6 +87,7 @@ impl ServerOptions {
|
||||
open_browser: true,
|
||||
force_state: None,
|
||||
forced_chatgpt_workspace_id,
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_credentials_store_mode,
|
||||
}
|
||||
}
|
||||
@@ -374,6 +376,7 @@ async fn process_request(
|
||||
&opts.issuer,
|
||||
&tokens.id_token,
|
||||
&tokens.access_token,
|
||||
opts.codex_streamlined_login,
|
||||
);
|
||||
match tiny_http::Header::from_bytes(&b"Location"[..], success_url.as_bytes()) {
|
||||
Ok(header) => HandledRequest::RedirectWithHeader(header),
|
||||
@@ -398,7 +401,14 @@ async fn process_request(
|
||||
}
|
||||
}
|
||||
"/success" => {
|
||||
let body = include_str!("assets/success.html");
|
||||
let use_streamlined_success = parsed_url
|
||||
.query_pairs()
|
||||
.any(|(key, value)| key == "codex_streamlined_login" && value == "true");
|
||||
let body = if use_streamlined_success {
|
||||
include_str!("assets/success.html")
|
||||
} else {
|
||||
include_str!("assets/success_legacy.html")
|
||||
};
|
||||
HandledRequest::ResponseAndExit {
|
||||
headers: match Header::from_bytes(
|
||||
&b"Content-Type"[..],
|
||||
@@ -713,13 +723,15 @@ pub(crate) async fn exchange_code_for_tokens(
|
||||
}
|
||||
|
||||
let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
|
||||
let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/'));
|
||||
info!(
|
||||
issuer = %sanitize_url_for_logging(issuer),
|
||||
token_endpoint = %sanitize_url_for_logging(&token_endpoint),
|
||||
redirect_uri = %redirect_uri,
|
||||
"starting oauth token exchange"
|
||||
);
|
||||
let resp = client
|
||||
.post(format!("{issuer}/oauth/token"))
|
||||
.post(token_endpoint)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(format!(
|
||||
"grant_type=authorization_code&code={}&redirect_uri={}&client_id={}&code_verifier={}",
|
||||
@@ -806,7 +818,13 @@ pub(crate) async fn persist_tokens_async(
|
||||
.map_err(|e| io::Error::other(format!("persist task failed: {e}")))?
|
||||
}
|
||||
|
||||
fn compose_success_url(port: u16, issuer: &str, id_token: &str, access_token: &str) -> String {
|
||||
fn compose_success_url(
|
||||
port: u16,
|
||||
issuer: &str,
|
||||
id_token: &str,
|
||||
access_token: &str,
|
||||
codex_streamlined_login: bool,
|
||||
) -> String {
|
||||
let token_claims = jwt_auth_claims(id_token);
|
||||
let access_claims = jwt_auth_claims(access_token);
|
||||
|
||||
@@ -846,6 +864,9 @@ fn compose_success_url(port: u16, issuer: &str, id_token: &str, access_token: &s
|
||||
("plan_type", plan_type.to_string()),
|
||||
("platform_url", platform_url.to_string()),
|
||||
];
|
||||
if codex_streamlined_login {
|
||||
params.push(("codex_streamlined_login", "true".to_string()));
|
||||
}
|
||||
let qs = params
|
||||
.drain(..)
|
||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(&v)))
|
||||
@@ -1085,8 +1106,9 @@ pub(crate) async fn obtain_api_key(
|
||||
access_token: String,
|
||||
}
|
||||
let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
|
||||
let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/'));
|
||||
let resp = client
|
||||
.post(format!("{issuer}/oauth/token"))
|
||||
.post(token_endpoint)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(format!(
|
||||
"grant_type={}&client_id={}&requested_token={}&subject_token={}&subject_token_type={}",
|
||||
@@ -1112,7 +1134,9 @@ pub(crate) async fn obtain_api_key(
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::DEFAULT_ISSUER;
|
||||
use super::TokenEndpointErrorDetail;
|
||||
use super::compose_success_url;
|
||||
use super::html_escape;
|
||||
use super::is_missing_codex_entitlement_error;
|
||||
use super::parse_token_endpoint_error;
|
||||
@@ -1219,6 +1243,43 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_success_url_omits_streamlined_success_by_default() {
|
||||
let url = url::Url::parse(&compose_success_url(
|
||||
/*port*/ 1455,
|
||||
DEFAULT_ISSUER,
|
||||
"e30.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnt9fQ.sig",
|
||||
"e30.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnt9fQ.sig",
|
||||
/*codex_streamlined_login*/ false,
|
||||
))
|
||||
.expect("success url should parse");
|
||||
|
||||
assert_eq!(
|
||||
url.query_pairs()
|
||||
.find(|(key, _)| key == "codex_streamlined_login"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_success_url_includes_streamlined_success_when_requested() {
|
||||
let url = url::Url::parse(&compose_success_url(
|
||||
/*port*/ 1455,
|
||||
DEFAULT_ISSUER,
|
||||
"e30.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnt9fQ.sig",
|
||||
"e30.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnt9fQ.sig",
|
||||
/*codex_streamlined_login*/ true,
|
||||
))
|
||||
.expect("success url should parse");
|
||||
|
||||
assert_eq!(
|
||||
url.query_pairs()
|
||||
.find(|(key, _)| key == "codex_streamlined_login")
|
||||
.map(|(_, value)| value.into_owned()),
|
||||
Some("true".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_login_error_page_escapes_dynamic_fields() {
|
||||
let body = String::from_utf8(render_login_error_page(
|
||||
|
||||
@@ -122,6 +122,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
|
||||
open_browser: false,
|
||||
force_state: Some(state),
|
||||
forced_chatgpt_workspace_id: Some(chatgpt_account_id.to_string()),
|
||||
codex_streamlined_login: false,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
assert!(
|
||||
@@ -183,6 +184,7 @@ async fn creates_missing_codex_home_dir() -> Result<()> {
|
||||
open_browser: false,
|
||||
force_state: Some(state),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -222,6 +224,7 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
|
||||
open_browser: false,
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: Some("org-required".to_string()),
|
||||
codex_streamlined_login: false,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
assert!(
|
||||
@@ -279,6 +282,7 @@ async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error()
|
||||
open_browser: false,
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -346,6 +350,7 @@ async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result<
|
||||
open_browser: false,
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -490,6 +495,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
open_browser: false,
|
||||
force_state: Some("cancel_state".to_string()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
};
|
||||
|
||||
let first_server = run_login_server(first_opts)?;
|
||||
@@ -510,6 +516,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
open_browser: false,
|
||||
force_state: Some("cancel_state_2".to_string()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
};
|
||||
|
||||
let second_server = run_login_server(second_opts)?;
|
||||
|
||||
@@ -876,7 +876,9 @@ impl AuthModeWidget {
|
||||
match request_handle
|
||||
.request_typed::<LoginAccountResponse>(ClientRequest::LoginAccount {
|
||||
request_id: onboarding_request_id(),
|
||||
params: LoginAccountParams::Chatgpt,
|
||||
params: LoginAccountParams::Chatgpt {
|
||||
codex_streamlined_login: false,
|
||||
},
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user