Add OAuth 2.0 authorization server to enable ChatGPT Web integration (#413)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
Co-authored-by: samanhappy <samanhappy@gmail.com>
This commit is contained in:
Copilot
2025-11-21 13:25:02 +08:00
committed by GitHub
parent 1869f283ba
commit 449e6ea4fd
34 changed files with 4930 additions and 103 deletions

View File

@@ -1,11 +1,34 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import React, { useState, useMemo, useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import { getToken } from '../services/authService';
import ThemeSwitch from '@/components/ui/ThemeSwitch';
import LanguageSwitch from '@/components/ui/LanguageSwitch';
import DefaultPasswordWarningModal from '@/components/ui/DefaultPasswordWarningModal';
const sanitizeReturnUrl = (value: string | null): string | null => {
if (!value) {
return null;
}
try {
// Support both relative paths and absolute URLs on the same origin
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
const url = new URL(value, origin);
if (url.origin !== origin) {
return null;
}
const relativePath = `${url.pathname}${url.search}${url.hash}`;
return relativePath || '/';
} catch {
if (value.startsWith('/') && !value.startsWith('//')) {
return value;
}
return null;
}
};
const LoginPage: React.FC = () => {
const { t } = useTranslation();
const [username, setUsername] = useState('');
@@ -14,7 +37,46 @@ const LoginPage: React.FC = () => {
const [loading, setLoading] = useState(false);
const [showDefaultPasswordWarning, setShowDefaultPasswordWarning] = useState(false);
const { login } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const returnUrl = useMemo(() => {
const params = new URLSearchParams(location.search);
return sanitizeReturnUrl(params.get('returnUrl'));
}, [location.search]);
const buildRedirectTarget = useCallback(() => {
if (!returnUrl) {
return '/';
}
// Only attach JWT when returning to the OAuth authorize endpoint
if (!returnUrl.startsWith('/oauth/authorize')) {
return returnUrl;
}
const token = getToken();
if (!token) {
return returnUrl;
}
try {
const origin = window.location.origin;
const url = new URL(returnUrl, origin);
url.searchParams.set('token', token);
return `${url.pathname}${url.search}${url.hash}`;
} catch {
const separator = returnUrl.includes('?') ? '&' : '?';
return `${returnUrl}${separator}token=${encodeURIComponent(token)}`;
}
}, [returnUrl]);
const redirectAfterLogin = useCallback(() => {
if (returnUrl) {
window.location.assign(buildRedirectTarget());
} else {
navigate('/');
}
}, [buildRedirectTarget, navigate, returnUrl]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -35,7 +97,7 @@ const LoginPage: React.FC = () => {
// Show warning modal instead of navigating immediately
setShowDefaultPasswordWarning(true);
} else {
navigate('/');
redirectAfterLogin();
}
} else {
setError(t('auth.loginFailed'));
@@ -49,7 +111,7 @@ const LoginPage: React.FC = () => {
const handleCloseWarning = () => {
setShowDefaultPasswordWarning(false);
navigate('/');
redirectAfterLogin();
};
return (
@@ -160,4 +222,4 @@ const LoginPage: React.FC = () => {
);
};
export default LoginPage;
export default LoginPage;