Skip to content

Commit 44a393c

Browse files
lidge-junWibias
andcommitted
feat(gui): warn before high-risk subscription OAuth login (#176)
Adds OAuthTosWarningModal with per-provider risk levels (high: Anthropic, Google Antigravity; elevated: GitHub Copilot, Cursor). Warning shown before OAuth flow starts, requiring explicit acknowledgement. Escape/cancel dismiss without starting OAuth. i18n keys in all four locales. Squash-merged from Wibias:feat/oauth-tos-warnings (6bc022c). Co-Authored-By: Wibias <37517432+Wibias@users.noreply.github.com>
1 parent fbac9f0 commit 44a393c

5 files changed

Lines changed: 24 additions & 42 deletions

File tree

gui/src/components/OAuthTosWarningModal.tsx

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Modal shown before starting OAuth for providers whose subscription tokens
33
* are restricted (or risky) when used outside the official client.
44
*/
5-
import { useEffect, useId, useRef, useState } from "react";
5+
import { useCallback, useEffect, useId, useRef, useState } from "react";
66
import { useT } from "../i18n";
77
import { IconAlert } from "../icons";
88
import {
@@ -25,40 +25,24 @@ export default function OAuthTosWarningModal({
2525
const t = useT();
2626
const titleId = useId();
2727
const bodyId = useId();
28-
const dialogRef = useRef<HTMLDivElement>(null);
29-
const previousFocusRef = useRef<HTMLElement | null>(null);
28+
const dialogRef = useRef<HTMLDialogElement>(null);
3029
const submittedRef = useRef(false);
3130
const [acknowledged, setAcknowledged] = useState(false);
3231
const [submitted, setSubmitted] = useState(false);
3332
const level = oauthTosRisk(providerId);
3433

35-
// Capture-phase Escape so a parent modal (e.g. Add Provider) does not also close.
34+
// Open as a native modal dialog — provides focus trapping and backdrop for free.
3635
useEffect(() => {
37-
const onKey = (e: KeyboardEvent) => {
38-
if (e.key !== "Escape") return;
39-
e.preventDefault();
40-
e.stopImmediatePropagation();
41-
onCancel();
42-
};
43-
window.addEventListener("keydown", onKey, true);
44-
return () => window.removeEventListener("keydown", onKey, true);
45-
}, [onCancel]);
46-
47-
// Focus first control on open; restore on close.
48-
useEffect(() => {
49-
previousFocusRef.current = document.activeElement as HTMLElement | null;
5036
const dialog = dialogRef.current;
51-
if (dialog) {
52-
const focusable = dialog.querySelector<HTMLElement>(
53-
"input:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])",
54-
);
55-
focusable?.focus();
56-
}
57-
return () => {
58-
previousFocusRef.current?.focus();
59-
};
37+
if (dialog && !dialog.open) dialog.showModal();
6038
}, []);
6139

40+
// Native <dialog> fires "cancel" on Escape — forward it to our handler.
41+
const handleCancel = useCallback((e: React.SyntheticEvent) => {
42+
e.preventDefault();
43+
onCancel();
44+
}, [onCancel]);
45+
6246
// Unmarked provider: render nothing (callers must gate with oauthTosRisk).
6347
if (!level) return null;
6448

@@ -70,17 +54,15 @@ export default function OAuthTosWarningModal({
7054
};
7155

7256
return (
73-
<div
74-
role="dialog"
75-
aria-modal="true"
57+
<dialog
58+
ref={dialogRef}
7659
aria-labelledby={titleId}
77-
aria-describedby={bodyId}
60+
aria-describedby={bodyId}
7861
className="modal-overlay"
62+
onCancel={handleCancel}
7963
onClick={onCancel}
80-
style={{ zIndex: 60 }}
8164
>
8265
<div
83-
ref={dialogRef}
8466
className="modal-card"
8567
onClick={e => e.stopPropagation()}
8668
style={{ maxWidth: 460 }}
@@ -123,6 +105,6 @@ export default function OAuthTosWarningModal({
123105
</button>
124106
</div>
125107
</div>
126-
</div>
108+
</dialog>
127109
);
128110
}

gui/src/i18n/de.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@ export const de = {
147147
"prov.loginOk": "Bei {provider} angemeldet. Führe {cmd} aus (oder es gilt live), um seine Modelle aufzulisten.",
148148
"oauthTos.highTitle": "{provider}: Risiko bei Abo-OAuth",
149149
"oauthTos.elevatedTitle": "{provider}: inoffizielle OAuth-Brücke",
150-
"oauthTos.highBody": "OpenCodex ist ein Drittanbieter-Proxy. {provider} beschränkt Abo-OAuth auf offizielle Apps. Die Anmeldung hier kann gegen die Nutzungsbedingungen verstoßen und zu Limits oder Kontosperrung führen. Ein API-Schlüssel ist der sicherere Weg, sofern verfügbar.",
150+
"oauthTos.highBody": "OpenCodex ist ein Drittanbieter-Proxy. {provider} kann Abo-OAuth auf offizielle Apps oder genehmigte Integrationen beschränken. Die Weiterleitung von Tokens über nicht genehmigte Pfade kann gegen die Nutzungsbedingungen verstoßen und zu Limits oder Kontosperrung führen.",
151151
"oauthTos.elevatedBody": "OpenCodex verbindet {provider} über einen inoffiziellen OAuth-Pfad. Anbieter können ungewöhnlichen oder automatisierten Traffic als Missbrauch werten und den Zugang sperren. Bevorzuge den offiziellen Client oder einen API-Schlüssel.",
152-
"oauthTos.saferPath": "Sicherere Optionen: die offizielle App mit deinem Abo nutzen oder in OpenCodex einen API-Schlüssel hinterlegen.",
152+
"oauthTos.saferPath": "Sicherere Optionen: die offizielle App, eine genehmigte SDK-Integration oder einen API-Schlüssel verwenden.",
153153
"oauthTos.acknowledge": "Ich verstehe das Risiko und möchte trotzdem mit OAuth fortfahren.",
154154
"oauthTos.continue": "Mit OAuth fortfahren",
155155
"prov.logoutOk": "Von {provider} abgemeldet.",

gui/src/i18n/en.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,9 @@ export const en = {
158158
"prov.loginOk": "Logged in to {provider}. Run {cmd} (or it applies live) to list its models.",
159159
"oauthTos.highTitle": "{provider}: subscription OAuth risk",
160160
"oauthTos.elevatedTitle": "{provider}: unofficial OAuth bridge",
161-
"oauthTos.highBody": "OpenCodex is a third-party proxy. {provider} restricts subscription OAuth to its official apps. Using that login here can violate their terms and may lead to rate limits or account suspension. An API key is the safer path when available.",
162-
"oauthTos.elevatedBody": "OpenCodex bridges {provider} through an unofficial OAuth path. Providers may treat unusual or automated traffic as abuse and suspend access. Prefer an official client or an API key when you can.",
163-
"oauthTos.saferPath": "Safer options: use the providers official app with your subscription, or configure an API key in OpenCodex.",
161+
"oauthTos.highBody": "OpenCodex is a third-party proxy. {provider} may restrict subscription OAuth to official apps or approved integrations. Routing tokens through an unapproved path can violate their terms and may lead to rate limits or account suspension.",
162+
"oauthTos.elevatedBody": "OpenCodex bridges {provider} through an unofficial OAuth path. Providers may treat unusual or automated traffic as abuse and suspend access. Prefer an official client or an API key when available.",
163+
"oauthTos.saferPath": "Safer options: use the provider's official app, an approved SDK integration, or configure an API key when available.",
164164
"oauthTos.acknowledge": "I understand the risk and want to continue with OAuth anyway.",
165165
"oauthTos.continue": "Continue with OAuth",
166166
"prov.logoutOk": "Logged out of {provider}.",

gui/src/i18n/ko.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ export const ko: Record<TKey, string> = {
153153
"prov.loginOk": "{provider} 에 로그인했습니다. 모델을 표시하려면 {cmd} 를 실행하세요(또는 실시간 적용됩니다).",
154154
"oauthTos.highTitle": "{provider}: 구독 OAuth 위험",
155155
"oauthTos.elevatedTitle": "{provider}: 비공식 OAuth 브리지",
156-
"oauthTos.highBody": "OpenCodex는 타사 프록시입니다. {provider}는 구독 OAuth를 공식 앱으로 제한합니다. 여기서 로그인하면 약관 위반으로 제한이나 계정 정지가 발생할 수 있습니다. 가능하면 API 키가 더 안전합니다.",
156+
"oauthTos.highBody": "OpenCodex는 타사 프록시입니다. {provider}는 구독 OAuth를 공식 앱이나 승인된 연동으로 제한할 수 있습니다. 비승인 경로로 토큰을 우회하면 약관 위반으로 제한이나 계정 정지가 발생할 수 있습니다.",
157157
"oauthTos.elevatedBody": "OpenCodex는 {provider}를 비공식 OAuth 경로로 연결합니다. 비정상/자동화 트래픽은 남용으로 간주되어 접근이 정지될 수 있습니다. 가능하면 공식 클라이언트나 API 키를 사용하세요.",
158-
"oauthTos.saferPath": "더 안전한 방법: 구독은 공식 앱에서 쓰거나 OpenCodex에 API 키를 설정하세요.",
158+
"oauthTos.saferPath": "더 안전한 방법: 공식 앱, 승인된 SDK 연동, 또는 API 키를 사용하세요.",
159159
"oauthTos.acknowledge": "위험을 이해했으며 OAuth로 계속 진행합니다.",
160160
"oauthTos.continue": "OAuth로 계속",
161161
"prov.logoutOk": "{provider} 에서 로그아웃했습니다.",

gui/src/i18n/zh.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ export const zh: Record<TKey, string> = {
153153
"prov.loginOk": "已登录到 {provider}。运行 {cmd}(或实时生效)以列出其模型。",
154154
"oauthTos.highTitle": "{provider}:订阅 OAuth 风险",
155155
"oauthTos.elevatedTitle": "{provider}:非官方 OAuth 桥接",
156-
"oauthTos.highBody": "OpenCodex 是第三方代理。{provider} 将订阅 OAuth 限制在官方应用内。在此登录可能违反其条款,并导致限流或账号停用。如有 API 密钥,更安全。",
156+
"oauthTos.highBody": "OpenCodex 是第三方代理。{provider} 可能将订阅 OAuth 限制在官方应用或已授权集成内。通过未授权路径转发令牌可能违反其条款,导致限流或账号停用。",
157157
"oauthTos.elevatedBody": "OpenCodex 通过非官方 OAuth 路径连接 {provider}。异常或自动化流量可能被判定为滥用并暂停访问。请优先使用官方客户端或 API 密钥。",
158-
"oauthTos.saferPath": "更安全的做法:用官方应用使用订阅,或在 OpenCodex 中配置 API 密钥。",
158+
"oauthTos.saferPath": "更安全的做法:使用官方应用、已授权的 SDK 集成,或配置 API 密钥。",
159159
"oauthTos.acknowledge": "我了解风险,仍要继续使用 OAuth。",
160160
"oauthTos.continue": "继续使用 OAuth",
161161
"prov.logoutOk": "已退出 {provider}。",

0 commit comments

Comments
 (0)