Merge pull request #490 from presenton/feat/codex-username-tier
feat: enhance Codex user profile management with username, email, and…
This commit is contained in:
commit
9b0803dc42
10 changed files with 290 additions and 33 deletions
|
|
@ -16,25 +16,32 @@ from fastapi import APIRouter, HTTPException
|
|||
from pydantic import BaseModel
|
||||
|
||||
from utils.oauth.openai_codex import (
|
||||
CodexAccountProfile,
|
||||
OAuthCallbackServer,
|
||||
TokenSuccess,
|
||||
create_authorization_flow,
|
||||
exchange_authorization_code,
|
||||
get_account_id,
|
||||
get_account_profile,
|
||||
parse_authorization_input,
|
||||
refresh_access_token,
|
||||
)
|
||||
from utils.get_env import (
|
||||
get_codex_access_token_env,
|
||||
get_codex_email_env,
|
||||
get_codex_is_pro_env,
|
||||
get_codex_refresh_token_env,
|
||||
get_codex_token_expires_env,
|
||||
get_codex_username_env,
|
||||
)
|
||||
from utils.set_env import (
|
||||
set_codex_access_token_env,
|
||||
set_codex_account_id_env,
|
||||
set_codex_email_env,
|
||||
set_codex_is_pro_env,
|
||||
set_codex_refresh_token_env,
|
||||
set_codex_token_expires_env,
|
||||
set_codex_model_env,
|
||||
set_codex_username_env,
|
||||
)
|
||||
from utils.user_config import save_codex_tokens_to_user_config
|
||||
|
||||
|
|
@ -60,6 +67,9 @@ class InitiateResponse(BaseModel):
|
|||
class StatusResponse(BaseModel):
|
||||
status: str # "pending" | "success" | "failed"
|
||||
account_id: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
is_pro: Optional[bool] = None
|
||||
detail: Optional[str] = None
|
||||
|
||||
|
||||
|
|
@ -69,11 +79,17 @@ class ExchangeRequest(BaseModel):
|
|||
|
||||
|
||||
class ExchangeResponse(BaseModel):
|
||||
account_id: str
|
||||
account_id: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
is_pro: Optional[bool] = None
|
||||
|
||||
|
||||
class RefreshResponse(BaseModel):
|
||||
account_id: Optional[str]
|
||||
username: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
is_pro: Optional[bool] = None
|
||||
detail: str
|
||||
|
||||
|
||||
|
|
@ -81,16 +97,31 @@ class RefreshResponse(BaseModel):
|
|||
# Helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _store_token(result: TokenSuccess) -> Optional[str]:
|
||||
"""Persist token fields in env vars and userConfig.json. Returns account_id or None."""
|
||||
def _parse_optional_bool(value: Optional[str]) -> Optional[bool]:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = value.strip().lower()
|
||||
if normalized in {"true", "1", "yes", "y"}:
|
||||
return True
|
||||
if normalized in {"false", "0", "no", "n"}:
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def _store_token(result: TokenSuccess) -> CodexAccountProfile:
|
||||
"""Persist token fields in env vars and userConfig.json. Returns parsed profile."""
|
||||
set_codex_access_token_env(result.access)
|
||||
set_codex_refresh_token_env(result.refresh)
|
||||
set_codex_token_expires_env(str(result.expires))
|
||||
account_id = get_account_id(result.access)
|
||||
if account_id:
|
||||
set_codex_account_id_env(account_id)
|
||||
|
||||
profile = get_account_profile(result.access, result.id_token)
|
||||
set_codex_account_id_env(profile.account_id or "")
|
||||
set_codex_username_env(profile.username or "")
|
||||
set_codex_email_env(profile.email or "")
|
||||
set_codex_is_pro_env("" if profile.is_pro is None else str(profile.is_pro))
|
||||
|
||||
save_codex_tokens_to_user_config()
|
||||
return account_id
|
||||
return profile
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -166,8 +197,14 @@ async def poll_codex_auth_status(session_id: str):
|
|||
if not isinstance(result, TokenSuccess):
|
||||
return StatusResponse(status="failed", detail=result.reason)
|
||||
|
||||
account_id = _store_token(result)
|
||||
return StatusResponse(status="success", account_id=account_id)
|
||||
profile = _store_token(result)
|
||||
return StatusResponse(
|
||||
status="success",
|
||||
account_id=profile.account_id,
|
||||
username=profile.username,
|
||||
email=profile.email,
|
||||
is_pro=profile.is_pro,
|
||||
)
|
||||
|
||||
|
||||
@CODEX_AUTH_ROUTER.post("/exchange", response_model=ExchangeResponse)
|
||||
|
|
@ -207,11 +244,16 @@ async def exchange_codex_code(body: ExchangeRequest):
|
|||
if not isinstance(result, TokenSuccess):
|
||||
raise HTTPException(status_code=502, detail=f"Token exchange failed: {result.reason}")
|
||||
|
||||
account_id = _store_token(result)
|
||||
if not account_id:
|
||||
profile = _store_token(result)
|
||||
if not profile.account_id:
|
||||
raise HTTPException(status_code=502, detail="Token exchanged but could not extract account ID")
|
||||
|
||||
return ExchangeResponse(account_id=account_id)
|
||||
return ExchangeResponse(
|
||||
account_id=profile.account_id,
|
||||
username=profile.username,
|
||||
email=profile.email,
|
||||
is_pro=profile.is_pro,
|
||||
)
|
||||
|
||||
|
||||
@CODEX_AUTH_ROUTER.post("/refresh", response_model=RefreshResponse)
|
||||
|
|
@ -232,9 +274,12 @@ async def refresh_codex_token():
|
|||
if not isinstance(result, TokenSuccess):
|
||||
raise HTTPException(status_code=502, detail=f"Token refresh failed: {result.reason}")
|
||||
|
||||
account_id = _store_token(result)
|
||||
profile = _store_token(result)
|
||||
return RefreshResponse(
|
||||
account_id=account_id,
|
||||
account_id=profile.account_id,
|
||||
username=profile.username,
|
||||
email=profile.email,
|
||||
is_pro=profile.is_pro,
|
||||
detail="Token refreshed successfully",
|
||||
)
|
||||
|
||||
|
|
@ -260,8 +305,18 @@ async def get_codex_auth_status():
|
|||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
account_id = get_account_id(access_token)
|
||||
return StatusResponse(status="authenticated", account_id=account_id)
|
||||
profile = get_account_profile(access_token)
|
||||
return StatusResponse(
|
||||
status="authenticated",
|
||||
account_id=profile.account_id,
|
||||
username=profile.username or get_codex_username_env(),
|
||||
email=profile.email or get_codex_email_env(),
|
||||
is_pro=(
|
||||
profile.is_pro
|
||||
if profile.is_pro is not None
|
||||
else _parse_optional_bool(get_codex_is_pro_env())
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@CODEX_AUTH_ROUTER.post("/logout")
|
||||
|
|
@ -273,6 +328,9 @@ async def logout_codex():
|
|||
set_codex_refresh_token_env("")
|
||||
set_codex_token_expires_env("")
|
||||
set_codex_account_id_env("")
|
||||
set_codex_username_env("")
|
||||
set_codex_email_env("")
|
||||
set_codex_is_pro_env("")
|
||||
set_codex_model_env("")
|
||||
save_codex_tokens_to_user_config()
|
||||
return {"detail": "Logged out successfully"}
|
||||
|
|
|
|||
|
|
@ -55,3 +55,6 @@ class UserConfig(BaseModel):
|
|||
CODEX_REFRESH_TOKEN: Optional[str] = None
|
||||
CODEX_TOKEN_EXPIRES: Optional[str] = None
|
||||
CODEX_ACCOUNT_ID: Optional[str] = None
|
||||
CODEX_USERNAME: Optional[str] = None
|
||||
CODEX_EMAIL: Optional[str] = None
|
||||
CODEX_IS_PRO: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -136,6 +136,18 @@ def get_codex_account_id_env():
|
|||
return os.getenv("CODEX_ACCOUNT_ID")
|
||||
|
||||
|
||||
def get_codex_username_env():
|
||||
return os.getenv("CODEX_USERNAME")
|
||||
|
||||
|
||||
def get_codex_email_env():
|
||||
return os.getenv("CODEX_EMAIL")
|
||||
|
||||
|
||||
def get_codex_is_pro_env():
|
||||
return os.getenv("CODEX_IS_PRO")
|
||||
|
||||
|
||||
def get_codex_model_env():
|
||||
return os.getenv("CODEX_MODEL")
|
||||
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ class TokenSuccess:
|
|||
access: str
|
||||
refresh: str
|
||||
expires: int # Unix ms timestamp when the token expires
|
||||
id_token: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -261,6 +262,14 @@ class AuthorizationFlow:
|
|||
url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodexAccountProfile:
|
||||
account_id: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
is_pro: Optional[bool] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JWT helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -296,6 +305,51 @@ def get_account_id(access_token: str) -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
def _as_non_empty_str(value) -> Optional[str]:
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
return None
|
||||
|
||||
|
||||
def get_account_profile(access_token: str, id_token: Optional[str] = None) -> CodexAccountProfile:
|
||||
"""Extract profile from exact observed JWT paths in access/id tokens."""
|
||||
access_payload = _decode_jwt_payload(access_token) or {}
|
||||
access_auth = access_payload.get(JWT_CLAIM_PATH)
|
||||
access_auth = access_auth if isinstance(access_auth, dict) else {}
|
||||
|
||||
access_profile = access_payload.get("https://api.openai.com/profile")
|
||||
access_profile = access_profile if isinstance(access_profile, dict) else {}
|
||||
|
||||
id_payload = _decode_jwt_payload(id_token) if id_token else None
|
||||
id_payload = id_payload if isinstance(id_payload, dict) else {}
|
||||
id_auth = id_payload.get(JWT_CLAIM_PATH)
|
||||
id_auth = id_auth if isinstance(id_auth, dict) else {}
|
||||
|
||||
account_id = _as_non_empty_str(access_auth.get("chatgpt_account_id")) or _as_non_empty_str(
|
||||
id_auth.get("chatgpt_account_id")
|
||||
)
|
||||
username = _as_non_empty_str(id_payload.get("name"))
|
||||
email = _as_non_empty_str(access_profile.get("email")) or _as_non_empty_str(
|
||||
id_payload.get("email")
|
||||
)
|
||||
|
||||
plan_type = _as_non_empty_str(access_auth.get("chatgpt_plan_type")) or _as_non_empty_str(
|
||||
id_auth.get("chatgpt_plan_type")
|
||||
)
|
||||
if plan_type:
|
||||
is_pro = plan_type.strip().lower() != "free"
|
||||
else:
|
||||
is_pro = None
|
||||
|
||||
return CodexAccountProfile(
|
||||
account_id=account_id,
|
||||
username=username,
|
||||
email=email,
|
||||
is_pro=is_pro,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Authorization URL + PKCE
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -474,6 +528,7 @@ def exchange_authorization_code(
|
|||
return TokenFailure(reason=f"HTTP {response.status_code}: {response.text[:200]}")
|
||||
|
||||
body = response.json()
|
||||
|
||||
access = body.get("access_token")
|
||||
refresh = body.get("refresh_token")
|
||||
expires_in = body.get("expires_in")
|
||||
|
|
@ -482,7 +537,9 @@ def exchange_authorization_code(
|
|||
return TokenFailure(reason=f"Token response missing fields: {list(body.keys())}")
|
||||
|
||||
expires_ms = int(time.time() * 1000) + int(expires_in) * 1000
|
||||
return TokenSuccess(access=access, refresh=refresh, expires=expires_ms)
|
||||
id_token = body.get("id_token")
|
||||
id_token = id_token if isinstance(id_token, str) else None
|
||||
return TokenSuccess(access=access, refresh=refresh, expires=expires_ms, id_token=id_token)
|
||||
except Exception as exc:
|
||||
return TokenFailure(reason=str(exc))
|
||||
|
||||
|
|
|
|||
|
|
@ -122,5 +122,17 @@ def set_codex_account_id_env(value: str):
|
|||
os.environ["CODEX_ACCOUNT_ID"] = value
|
||||
|
||||
|
||||
def set_codex_username_env(value: str):
|
||||
os.environ["CODEX_USERNAME"] = value
|
||||
|
||||
|
||||
def set_codex_email_env(value: str):
|
||||
os.environ["CODEX_EMAIL"] = value
|
||||
|
||||
|
||||
def set_codex_is_pro_env(value: str):
|
||||
os.environ["CODEX_IS_PRO"] = value
|
||||
|
||||
|
||||
def set_codex_model_env(value: str):
|
||||
os.environ["CODEX_MODEL"] = value
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ from utils.get_env import (
|
|||
get_codex_refresh_token_env,
|
||||
get_codex_token_expires_env,
|
||||
get_codex_account_id_env,
|
||||
get_codex_username_env,
|
||||
get_codex_email_env,
|
||||
get_codex_is_pro_env,
|
||||
get_codex_model_env,
|
||||
)
|
||||
from utils.parsers import parse_bool_or_none
|
||||
|
|
@ -64,6 +67,9 @@ from utils.set_env import (
|
|||
set_codex_refresh_token_env,
|
||||
set_codex_token_expires_env,
|
||||
set_codex_account_id_env,
|
||||
set_codex_username_env,
|
||||
set_codex_email_env,
|
||||
set_codex_is_pro_env,
|
||||
set_codex_model_env,
|
||||
)
|
||||
|
||||
|
|
@ -133,6 +139,13 @@ def get_user_config():
|
|||
CODEX_REFRESH_TOKEN=existing_config.CODEX_REFRESH_TOKEN or get_codex_refresh_token_env(),
|
||||
CODEX_TOKEN_EXPIRES=existing_config.CODEX_TOKEN_EXPIRES or get_codex_token_expires_env(),
|
||||
CODEX_ACCOUNT_ID=existing_config.CODEX_ACCOUNT_ID or get_codex_account_id_env(),
|
||||
CODEX_USERNAME=existing_config.CODEX_USERNAME or get_codex_username_env(),
|
||||
CODEX_EMAIL=existing_config.CODEX_EMAIL or get_codex_email_env(),
|
||||
CODEX_IS_PRO=(
|
||||
existing_config.CODEX_IS_PRO
|
||||
if existing_config.CODEX_IS_PRO is not None
|
||||
else parse_bool_or_none(get_codex_is_pro_env())
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -196,6 +209,12 @@ def update_env_with_user_config():
|
|||
set_codex_token_expires_env(user_config.CODEX_TOKEN_EXPIRES)
|
||||
if user_config.CODEX_ACCOUNT_ID:
|
||||
set_codex_account_id_env(user_config.CODEX_ACCOUNT_ID)
|
||||
if user_config.CODEX_USERNAME:
|
||||
set_codex_username_env(user_config.CODEX_USERNAME)
|
||||
if user_config.CODEX_EMAIL:
|
||||
set_codex_email_env(user_config.CODEX_EMAIL)
|
||||
if user_config.CODEX_IS_PRO is not None:
|
||||
set_codex_is_pro_env(str(user_config.CODEX_IS_PRO))
|
||||
|
||||
|
||||
def save_codex_tokens_to_user_config() -> None:
|
||||
|
|
@ -220,6 +239,9 @@ def save_codex_tokens_to_user_config() -> None:
|
|||
existing["CODEX_REFRESH_TOKEN"] = get_codex_refresh_token_env()
|
||||
existing["CODEX_TOKEN_EXPIRES"] = get_codex_token_expires_env()
|
||||
existing["CODEX_ACCOUNT_ID"] = get_codex_account_id_env()
|
||||
existing["CODEX_USERNAME"] = get_codex_username_env()
|
||||
existing["CODEX_EMAIL"] = get_codex_email_env()
|
||||
existing["CODEX_IS_PRO"] = parse_bool_or_none(get_codex_is_pro_env())
|
||||
|
||||
try:
|
||||
with open(user_config_path, "w") as f:
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ export async function POST(request: Request) {
|
|||
CODEX_REFRESH_TOKEN: existingConfig.CODEX_REFRESH_TOKEN,
|
||||
CODEX_TOKEN_EXPIRES: existingConfig.CODEX_TOKEN_EXPIRES,
|
||||
CODEX_ACCOUNT_ID: existingConfig.CODEX_ACCOUNT_ID,
|
||||
CODEX_USERNAME: existingConfig.CODEX_USERNAME,
|
||||
CODEX_EMAIL: existingConfig.CODEX_EMAIL,
|
||||
CODEX_IS_PRO: existingConfig.CODEX_IS_PRO,
|
||||
USE_CUSTOM_URL:
|
||||
userConfig.USE_CUSTOM_URL === undefined
|
||||
? existingConfig.USE_CUSTOM_URL
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
Loader2,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
Crown,
|
||||
User,
|
||||
UserCheck,
|
||||
ArrowRight,
|
||||
|
|
@ -34,6 +35,9 @@ type AuthStatus = "checking" | "unauthenticated" | "polling" | "authenticated";
|
|||
interface StatusResponse {
|
||||
status: string;
|
||||
account_id?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
is_pro?: boolean;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +64,9 @@ export default function CodexConfig({
|
|||
}: CodexConfigProps) {
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>("checking");
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState<string | null>(null);
|
||||
const [isPro, setIsPro] = useState<boolean | null>(null);
|
||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
const [manualCode, setManualCode] = useState("");
|
||||
const [isExchanging, setIsExchanging] = useState(false);
|
||||
|
|
@ -80,22 +87,32 @@ export default function CodexConfig({
|
|||
return () => stopPolling();
|
||||
}, []);
|
||||
|
||||
const applyProfile = (data: Partial<StatusResponse>) => {
|
||||
setAccountId(data.account_id ?? null);
|
||||
setUsername(data.username ?? null);
|
||||
setEmail(data.email ?? null);
|
||||
setIsPro(typeof data.is_pro === "boolean" ? data.is_pro : null);
|
||||
};
|
||||
|
||||
const checkCurrentAuthStatus = async () => {
|
||||
try {
|
||||
const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/status"));
|
||||
if (!res.ok) {
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
return;
|
||||
}
|
||||
const data: StatusResponse = await res.json();
|
||||
if (data.status === "authenticated") {
|
||||
setAuthStatus("authenticated");
|
||||
setAccountId(data.account_id ?? null);
|
||||
applyProfile(data);
|
||||
} else {
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
}
|
||||
} catch {
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -125,7 +142,7 @@ export default function CodexConfig({
|
|||
if (pollData.status === "success") {
|
||||
stopPolling();
|
||||
setAuthStatus("authenticated");
|
||||
setAccountId(pollData.account_id ?? null);
|
||||
applyProfile(pollData);
|
||||
setSessionId(null);
|
||||
if (!codexModel) {
|
||||
onInputChange(DEFAULT_CODEX_MODEL, "codex_model");
|
||||
|
|
@ -134,6 +151,7 @@ export default function CodexConfig({
|
|||
} else if (pollData.status === "failed") {
|
||||
stopPolling();
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
toast.error("Authentication failed. Please try again.");
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -143,6 +161,7 @@ export default function CodexConfig({
|
|||
} catch (err) {
|
||||
toast.error("Failed to start sign-in flow");
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -162,7 +181,7 @@ export default function CodexConfig({
|
|||
const data = await res.json();
|
||||
stopPolling();
|
||||
setAuthStatus("authenticated");
|
||||
setAccountId(data.account_id);
|
||||
applyProfile(data);
|
||||
setSessionId(null);
|
||||
setManualCode("");
|
||||
if (!codexModel) {
|
||||
|
|
@ -189,6 +208,9 @@ export default function CodexConfig({
|
|||
await fetch(getApiUrl("/api/v1/ppt/codex/auth/logout"), { method: "POST" });
|
||||
setAuthStatus("unauthenticated");
|
||||
setAccountId(null);
|
||||
setUsername(null);
|
||||
setEmail(null);
|
||||
setIsPro(null);
|
||||
onInputChange("", "codex_model");
|
||||
toast.success("Signed out from ChatGPT");
|
||||
} catch {
|
||||
|
|
@ -206,11 +228,12 @@ export default function CodexConfig({
|
|||
});
|
||||
if (!res.ok) throw new Error("Refresh failed");
|
||||
const data = await res.json();
|
||||
if (data.account_id) setAccountId(data.account_id);
|
||||
applyProfile(data);
|
||||
toast.success("Token refreshed successfully");
|
||||
} catch {
|
||||
toast.error("Token refresh failed. Please sign in again.");
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
|
|
@ -287,17 +310,39 @@ export default function CodexConfig({
|
|||
}
|
||||
|
||||
if (authStatus === "authenticated") {
|
||||
const planLabel = isPro === true ? "Pro" : isPro === false ? "Free" : "Unknown";
|
||||
|
||||
return (
|
||||
<div className=" mb-5">
|
||||
<div className="flex items-center justify-between gap-3 p-5 border border-[#EDEEEF] rounded-[8px]">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
<UserCheck className="w-6 h-6 text-black shrink-0" />
|
||||
<div className="flex-gpt 5.4 mini1 min-w-0">
|
||||
{accountId && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-800 truncate">
|
||||
Acc: {accountId}
|
||||
{username || email || (accountId ? `Account ${accountId}` : "ChatGPT Account")}
|
||||
</p>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium shrink-0",
|
||||
isPro === true ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-700"
|
||||
)}
|
||||
title={`Subscription: ${planLabel}`}
|
||||
>
|
||||
{isPro === true ? (
|
||||
<Crown className="w-3 h-3" />
|
||||
) : (
|
||||
<User className="w-3 h-3" />
|
||||
)}
|
||||
{planLabel}
|
||||
</span>
|
||||
</div>
|
||||
{email && username && (
|
||||
<p className="text-xs text-gray-500 truncate">{email}</p>
|
||||
)}
|
||||
{!email && accountId && (
|
||||
<p className="text-xs text-gray-500 truncate">ID: {accountId}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-400">Signed in to ChatGPT</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
Loader2,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
Crown,
|
||||
User,
|
||||
UserCheck,
|
||||
} from "lucide-react";
|
||||
|
|
@ -33,6 +34,9 @@ type AuthStatus = "checking" | "unauthenticated" | "polling" | "authenticated";
|
|||
interface StatusResponse {
|
||||
status: string;
|
||||
account_id?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
is_pro?: boolean;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +63,9 @@ export default function CodexConfig({
|
|||
}: CodexConfigProps) {
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>("checking");
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState<string | null>(null);
|
||||
const [isPro, setIsPro] = useState<boolean | null>(null);
|
||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
const [manualCode, setManualCode] = useState("");
|
||||
const [isExchanging, setIsExchanging] = useState(false);
|
||||
|
|
@ -79,22 +86,32 @@ export default function CodexConfig({
|
|||
return () => stopPolling();
|
||||
}, []);
|
||||
|
||||
const applyProfile = (data: Partial<StatusResponse>) => {
|
||||
setAccountId(data.account_id ?? null);
|
||||
setUsername(data.username ?? null);
|
||||
setEmail(data.email ?? null);
|
||||
setIsPro(typeof data.is_pro === "boolean" ? data.is_pro : null);
|
||||
};
|
||||
|
||||
const checkCurrentAuthStatus = async () => {
|
||||
try {
|
||||
const res = await fetch(getApiUrl("/api/v1/ppt/codex/auth/status"));
|
||||
if (!res.ok) {
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
return;
|
||||
}
|
||||
const data: StatusResponse = await res.json();
|
||||
if (data.status === "authenticated") {
|
||||
setAuthStatus("authenticated");
|
||||
setAccountId(data.account_id ?? null);
|
||||
applyProfile(data);
|
||||
} else {
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
}
|
||||
} catch {
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -122,7 +139,7 @@ export default function CodexConfig({
|
|||
if (pollData.status === "success") {
|
||||
stopPolling();
|
||||
setAuthStatus("authenticated");
|
||||
setAccountId(pollData.account_id ?? null);
|
||||
applyProfile(pollData);
|
||||
setSessionId(null);
|
||||
if (!codexModel) {
|
||||
onInputChange(DEFAULT_CODEX_MODEL, "codex_model");
|
||||
|
|
@ -131,6 +148,7 @@ export default function CodexConfig({
|
|||
} else if (pollData.status === "failed") {
|
||||
stopPolling();
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
toast.error("Authentication failed. Please try again.");
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -140,6 +158,7 @@ export default function CodexConfig({
|
|||
} catch (err) {
|
||||
toast.error("Failed to start sign-in flow");
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -159,7 +178,7 @@ export default function CodexConfig({
|
|||
const data = await res.json();
|
||||
stopPolling();
|
||||
setAuthStatus("authenticated");
|
||||
setAccountId(data.account_id);
|
||||
applyProfile(data);
|
||||
setSessionId(null);
|
||||
setManualCode("");
|
||||
if (!codexModel) {
|
||||
|
|
@ -185,7 +204,7 @@ export default function CodexConfig({
|
|||
try {
|
||||
await fetch(getApiUrl("/api/v1/ppt/codex/auth/logout"), { method: "POST" });
|
||||
setAuthStatus("unauthenticated");
|
||||
setAccountId(null);
|
||||
applyProfile({});
|
||||
onInputChange("", "codex_model");
|
||||
toast.success("Signed out from ChatGPT");
|
||||
} catch {
|
||||
|
|
@ -203,11 +222,12 @@ export default function CodexConfig({
|
|||
});
|
||||
if (!res.ok) throw new Error("Refresh failed");
|
||||
const data = await res.json();
|
||||
if (data.account_id) setAccountId(data.account_id);
|
||||
applyProfile(data);
|
||||
toast.success("Token refreshed successfully");
|
||||
} catch {
|
||||
toast.error("Token refresh failed. Please sign in again.");
|
||||
setAuthStatus("unauthenticated");
|
||||
applyProfile({});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
|
|
@ -266,15 +286,37 @@ export default function CodexConfig({
|
|||
}
|
||||
|
||||
if (authStatus === "authenticated") {
|
||||
const planLabel = isPro === true ? "Pro" : isPro === false ? "Free" : "Unknown";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 p-3 border border-[#EDEEEF] rounded-[8px]">
|
||||
<div className="flex items-center gap-3 p-3 border border-[#EDEEEF] rounded-lg">
|
||||
<UserCheck className="w-5 h-5 text-black shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
{accountId && (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-800 truncate">
|
||||
Acc: {accountId}
|
||||
{username || email || (accountId ? `Account ${accountId}` : "ChatGPT Account")}
|
||||
</p>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium shrink-0",
|
||||
isPro === true ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-700"
|
||||
)}
|
||||
title={`Subscription: ${planLabel}`}
|
||||
>
|
||||
{isPro === true ? (
|
||||
<Crown className="w-3 h-3" />
|
||||
) : (
|
||||
<User className="w-3 h-3" />
|
||||
)}
|
||||
{planLabel}
|
||||
</span>
|
||||
</div>
|
||||
{email && username && (
|
||||
<p className="text-xs text-gray-500 truncate">{email}</p>
|
||||
)}
|
||||
{!email && accountId && (
|
||||
<p className="text-xs text-gray-500 truncate">ID: {accountId}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-400">Signed in to ChatGPT</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ export interface LLMConfig {
|
|||
CODEX_REFRESH_TOKEN?: string;
|
||||
CODEX_TOKEN_EXPIRES?: string;
|
||||
CODEX_ACCOUNT_ID?: string;
|
||||
CODEX_USERNAME?: string;
|
||||
CODEX_EMAIL?: string;
|
||||
CODEX_IS_PRO?: boolean;
|
||||
|
||||
// Only used in UI settings
|
||||
USE_CUSTOM_URL?: boolean;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue