mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
revert(assistant): drop the ChatGPT relay, open-source apps keep the loopback sign-in
OpenAI offers a fixed web redirect for Sign in with ChatGPT only to selected commercial partners; open-source apps sign in through the 127.0.0.1 loopback callback. The relay could never be switched on, so it goes again.
This commit is contained in:
@@ -5,7 +5,6 @@ import { CHATGPT_USAGE_URL, providerLabel } from '../../Assistant/providerCatalo
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import { INPUT } from '../../shared/dialogParts';
|
||||
import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SettingsHint } from '../settingsKit';
|
||||
import { rememberChatgptFlow } from './aiConnectModel';
|
||||
import type { ConnectError, useChatgptConnect } from './useAiConnect';
|
||||
|
||||
type ChatgptConnect = ReturnType<typeof useChatgptConnect>;
|
||||
@@ -98,12 +97,9 @@ export default function ChatGptPasteStep({
|
||||
<div className="flex">
|
||||
<a
|
||||
href={flow?.authorizeUrl}
|
||||
// A sign-in that comes back by itself leaves this tab; one to paste from opens beside it.
|
||||
{...(flow?.returns ? {} : { target: '_blank', rel: 'noopener noreferrer' })}
|
||||
onClick={() => {
|
||||
if (flow?.returns) rememberChatgptFlow(flow.flowId);
|
||||
markOpened();
|
||||
}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={markOpened}
|
||||
aria-disabled={!flow}
|
||||
className={`${SETTINGS_BUTTON_PRIMARY} ${flow ? '' : 'pointer-events-none opacity-50'}`}
|
||||
style={fs(13, 'body')}
|
||||
@@ -113,58 +109,56 @@ export default function ChatGptPasteStep({
|
||||
</a>
|
||||
</div>
|
||||
</Step>
|
||||
{!flow?.returns && (
|
||||
<Step n={2} text={t('assistant.connect.chatgptStep2')}>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
id={inputId}
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
dir="ltr"
|
||||
value={pasted}
|
||||
onChange={(e) => change(e.target.value)}
|
||||
onPaste={(e) => {
|
||||
if (paste(e.clipboardData.getData('text'))) e.preventDefault();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void submit();
|
||||
}
|
||||
}}
|
||||
placeholder="http://127.0.0.1:1455/auth/callback?code=…"
|
||||
aria-label={t('assistant.connect.chatgptPaste')}
|
||||
className={`${INPUT} font-mono`}
|
||||
/>
|
||||
{pasted.trim() ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void submit()}
|
||||
disabled={busy || !flow}
|
||||
className={SETTINGS_BUTTON_PRIMARY}
|
||||
style={fs(13, 'body')}
|
||||
>
|
||||
{busy && <Loader2 size={14} className="animate-spin" />}
|
||||
{t('assistant.connect.save')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={pasteFromClipboard}
|
||||
disabled={!flow}
|
||||
className={`${SETTINGS_BUTTON} whitespace-nowrap`}
|
||||
style={fs(13, 'body')}
|
||||
>
|
||||
<ClipboardPaste size={14} strokeWidth={2} />
|
||||
{t('assistant.connect.chatgptPaste')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Step>
|
||||
)}
|
||||
<Step n={2} text={t('assistant.connect.chatgptStep2')}>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
id={inputId}
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
dir="ltr"
|
||||
value={pasted}
|
||||
onChange={(e) => change(e.target.value)}
|
||||
onPaste={(e) => {
|
||||
if (paste(e.clipboardData.getData('text'))) e.preventDefault();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void submit();
|
||||
}
|
||||
}}
|
||||
placeholder="http://127.0.0.1:1455/auth/callback?code=…"
|
||||
aria-label={t('assistant.connect.chatgptPaste')}
|
||||
className={`${INPUT} font-mono`}
|
||||
/>
|
||||
{pasted.trim() ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void submit()}
|
||||
disabled={busy || !flow}
|
||||
className={SETTINGS_BUTTON_PRIMARY}
|
||||
style={fs(13, 'body')}
|
||||
>
|
||||
{busy && <Loader2 size={14} className="animate-spin" />}
|
||||
{t('assistant.connect.save')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={pasteFromClipboard}
|
||||
disabled={!flow}
|
||||
className={`${SETTINGS_BUTTON} whitespace-nowrap`}
|
||||
style={fs(13, 'body')}
|
||||
>
|
||||
<ClipboardPaste size={14} strokeWidth={2} />
|
||||
{t('assistant.connect.chatgptPaste')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Step>
|
||||
</ol>
|
||||
{error && (
|
||||
<ConnectErrorNote error={error} provider={providerLabel('chatgpt', '')} onOtherProvider={onOtherProvider} />
|
||||
|
||||
@@ -71,28 +71,6 @@ export function rememberOpenrouterFlow(flowId: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
const CHATGPT_FLOW_KEY = 'trek.ai.chatgptFlow';
|
||||
|
||||
/** Keeps the ChatGPT sign-in that left for OpenAI and comes back by itself, for the page it returns to. */
|
||||
export function rememberChatgptFlow(flowId: string): void {
|
||||
try {
|
||||
sessionStorage.setItem(CHATGPT_FLOW_KEY, flowId);
|
||||
} catch {
|
||||
// Storage refused (private mode): the return then reads as an expired sign-in.
|
||||
}
|
||||
}
|
||||
|
||||
/** The ChatGPT sign-in this tab started, read once. */
|
||||
export function takeChatgptFlow(): string | null {
|
||||
try {
|
||||
const flowId = sessionStorage.getItem(CHATGPT_FLOW_KEY);
|
||||
sessionStorage.removeItem(CHATGPT_FLOW_KEY);
|
||||
return flowId;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The flow the OpenRouter sign-in started, read once. */
|
||||
export function takeOpenrouterFlow(): string | null {
|
||||
try {
|
||||
|
||||
@@ -17,15 +17,9 @@ const toast = vi.hoisted(() => ({ success: vi.fn(), error: vi.fn() }));
|
||||
vi.mock('../../../repo/aiConnectionsRepo', () => ({ aiConnectionsRepo: repo }));
|
||||
vi.mock('../../shared/Toast', () => ({ useToast: () => toast }));
|
||||
|
||||
const {
|
||||
useChatgptConnect,
|
||||
useChatgptReturn,
|
||||
useCustomConnect,
|
||||
useKeyConnect,
|
||||
useOpenrouterReturn,
|
||||
useOpenrouterStart,
|
||||
} = await import('./useAiConnect');
|
||||
const { rememberChatgptFlow, rememberOpenrouterFlow } = await import('./aiConnectModel');
|
||||
const { useChatgptConnect, useCustomConnect, useKeyConnect, useOpenrouterReturn, useOpenrouterStart } =
|
||||
await import('./useAiConnect');
|
||||
const { rememberOpenrouterFlow } = await import('./aiConnectModel');
|
||||
|
||||
function wrapperAt(url = '/settings') {
|
||||
return ({ children }: { children: ReactNode }) =>
|
||||
@@ -256,26 +250,3 @@ describe('OpenRouter', () => {
|
||||
expect(repo.openrouterFinish).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useChatgptReturn', () => {
|
||||
it('FE-HOOK-AI-CONNECT-030: a sign-in back through the relay finishes once, as the address it came back to', async () => {
|
||||
rememberChatgptFlow(FLOW.flowId);
|
||||
const onConnected = vi.fn();
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
useChatgptReturn(onConnected);
|
||||
return useLocation();
|
||||
},
|
||||
{ wrapper: wrapperAt('/settings?tab=integrations&ai=chatgpt&code=c1&state=s1.aA') }
|
||||
);
|
||||
await waitFor(() => expect(onConnected).toHaveBeenCalledTimes(1));
|
||||
// The address as the browser holds it; the server checks it as it checks a pasted one.
|
||||
expect(repo.chatgptFinish).toHaveBeenCalledWith({ flowId: FLOW.flowId, pastedUrl: window.location.href });
|
||||
expect(result.current.search).toBe('?tab=integrations');
|
||||
|
||||
vi.clearAllMocks();
|
||||
renderHook(() => useChatgptReturn(onConnected), { wrapper: wrapperAt('/settings?ai=chatgpt&code=again') });
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalledTimes(1));
|
||||
expect(repo.chatgptFinish).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,14 +5,7 @@ import { useTranslation } from '../../../i18n';
|
||||
import { aiConnectionsRepo } from '../../../repo/aiConnectionsRepo';
|
||||
import { detectKeyKind, isChatgptCallback, providerLabel } from '../../Assistant/providerCatalog';
|
||||
import { useToast } from '../../shared/Toast';
|
||||
import {
|
||||
connectErrorKey,
|
||||
httpHostOf,
|
||||
refusalCode,
|
||||
rememberOpenrouterFlow,
|
||||
takeChatgptFlow,
|
||||
takeOpenrouterFlow,
|
||||
} from './aiConnectModel';
|
||||
import { connectErrorKey, httpHostOf, refusalCode, rememberOpenrouterFlow, takeOpenrouterFlow } from './aiConnectModel';
|
||||
|
||||
/**
|
||||
* The ways to connect a provider, one hook each, behind `AiConnectPanel`: a
|
||||
@@ -300,49 +293,6 @@ export function useOpenrouterStart(scope: AiConnectionScope) {
|
||||
return { start, busy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes a ChatGPT sign-in the page came back from through the relay
|
||||
* (`?ai=chatgpt&code=…&state=…`): the server reads the address as it reads a
|
||||
* pasted one. The parameters leave the address at once, so a reload does not
|
||||
* try again.
|
||||
*/
|
||||
export function useChatgptReturn(onConnected: () => void) {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [params, setParams] = useSearchParams();
|
||||
const handled = useRef(false);
|
||||
|
||||
const finish = useEffectEvent(async (returned: string) => {
|
||||
const flowId = takeChatgptFlow();
|
||||
if (!flowId) {
|
||||
toast.error(t('assistant.connect.chatgptExpired'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await aiConnectionsRepo.chatgptFinish({ flowId, pastedUrl: returned });
|
||||
toast.success(t('assistant.connect.ok'));
|
||||
onConnected();
|
||||
} catch (err) {
|
||||
toast.error(t(connectErrorKey(err)));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (handled.current || params.get('ai') !== 'chatgpt') return;
|
||||
handled.current = true;
|
||||
const returned = window.location.href;
|
||||
setParams(
|
||||
(prev) => {
|
||||
const next = new URLSearchParams(prev);
|
||||
for (const key of ['ai', 'code', 'state', 'scope', 'client_id', 'error']) next.delete(key);
|
||||
return next;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
void finish(returned);
|
||||
}, [params, setParams]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes an OpenRouter sign-in the page came back from (`?ai=openrouter&code=…`)
|
||||
* once, and takes both parameters out of the address so a reload does not try again.
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useAuthStore } from '../../../store/authStore';
|
||||
import { providerLabel } from '../../Assistant/providerCatalog';
|
||||
import { useToast } from '../../shared/Toast';
|
||||
import { type AiConnectMode, connectErrorKey, repairModeOf } from './aiConnectModel';
|
||||
import { useChatgptReturn, useOpenrouterReturn } from './useAiConnect';
|
||||
import { useOpenrouterReturn } from './useAiConnect';
|
||||
|
||||
/**
|
||||
* What the AI connection card shows:
|
||||
@@ -157,7 +157,6 @@ export function useAiConnections({ phone = false }: { phone?: boolean } = {}) {
|
||||
void reload();
|
||||
};
|
||||
useOpenrouterReturn(onConnected);
|
||||
useChatgptReturn(onConnected);
|
||||
|
||||
const own = status?.own ?? null;
|
||||
const view = viewOf(status, loaded);
|
||||
|
||||
Reference in New Issue
Block a user