mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-04-28 03:30:06 +00:00
feat: local auto-login + eigent.ai callback for Electron hybrid login (#1497)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run
This commit is contained in:
parent
ea44f1e0b4
commit
bebd1aafd7
7 changed files with 319 additions and 229 deletions
|
|
@ -471,10 +471,12 @@ function handleProtocolUrl(url: string) {
|
|||
function processProtocolUrl(url: string) {
|
||||
const urlObj = new URL(url);
|
||||
const code = urlObj.searchParams.get('code');
|
||||
const token = urlObj.searchParams.get('token');
|
||||
const share_token = urlObj.searchParams.get('share_token');
|
||||
|
||||
log.info('urlObj', urlObj);
|
||||
log.info('code', code);
|
||||
log.info('token', token);
|
||||
log.info('share_token', share_token);
|
||||
|
||||
if (win && !win.isDestroyed()) {
|
||||
|
|
@ -489,6 +491,12 @@ function processProtocolUrl(url: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
log.info('protocol token received');
|
||||
win.webContents.send('auth-token-received', token);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
log.error('protocol code:', code);
|
||||
win.webContents.send('auth-code-received', code);
|
||||
|
|
@ -524,6 +532,58 @@ function processQueuedProtocolUrls() {
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== auth callback server ====================
|
||||
// Local HTTP server for receiving auth callbacks from external login (eigent.ai)
|
||||
// Works in both dev and production mode, avoids eigent:// protocol issues in dev
|
||||
let authCallbackServer: http.Server | null = null;
|
||||
let authCallbackPort: number | null = null;
|
||||
|
||||
async function startAuthCallbackServer() {
|
||||
if (authCallbackServer) return authCallbackPort;
|
||||
|
||||
const port = await findAvailablePort(19836, 19900);
|
||||
|
||||
authCallbackServer = http.createServer((req, res) => {
|
||||
const url = new URL(req.url || '', `http://localhost:${port}`);
|
||||
|
||||
if (url.pathname === '/auth/callback') {
|
||||
const token = url.searchParams.get('token');
|
||||
log.info('Auth callback URL:', req.url);
|
||||
log.info('Auth callback token present:', !!token);
|
||||
log.info('Auth callback win available:', !!win && !win.isDestroyed());
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html><head><title>Login Successful</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #f4f4f9; color: #333; }
|
||||
.container { padding: 40px; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); text-align: center; }
|
||||
</style></head>
|
||||
<body><div class="container">
|
||||
<h1>Login Successful</h1>
|
||||
<p>You can close this tab and return to Eigent.</p>
|
||||
</div></body></html>
|
||||
`);
|
||||
|
||||
if (token && win && !win.isDestroyed()) {
|
||||
log.info('Auth callback received token');
|
||||
win.webContents.send('auth-token-received', token);
|
||||
win.show();
|
||||
win.focus();
|
||||
}
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
authCallbackServer.listen(port);
|
||||
authCallbackPort = port;
|
||||
log.info(`Auth callback server started on port ${port}`);
|
||||
return port;
|
||||
}
|
||||
|
||||
// ==================== single instance lock ====================
|
||||
const setupSingleInstanceLock = () => {
|
||||
// The lock is already acquired at module level (requestSingleInstanceLock
|
||||
|
|
@ -603,6 +663,12 @@ const checkManagerInstance = (manager: any, name: string) => {
|
|||
};
|
||||
|
||||
function registerIpcHandlers() {
|
||||
// ==================== auth callback ====================
|
||||
ipcMain.handle('get-auth-callback-url', async () => {
|
||||
const port = await startAuthCallbackServer();
|
||||
return `http://localhost:${port}/auth/callback`;
|
||||
});
|
||||
|
||||
// ==================== basic info handler ====================
|
||||
ipcMain.handle('get-browser-port', () => {
|
||||
log.info('Getting browser port');
|
||||
|
|
|
|||
|
|
@ -157,6 +157,42 @@ async def by_stack_auth(
|
|||
return LoginResponse(token=Auth.create_access_token(user.id), email=user.email)
|
||||
|
||||
|
||||
@router.post("/auto-login", name="auto login for local mode")
|
||||
async def auto_login(session: Session = Depends(session)) -> LoginResponse:
|
||||
"""
|
||||
Auto login for fully local mode (VITE_USE_LOCAL_PROXY=true).
|
||||
Returns the most recently active user, or creates a default admin user if none exists.
|
||||
"""
|
||||
# Find the most recently active user
|
||||
user = User.by(
|
||||
User.status == Status.Normal,
|
||||
order_by=User.updated_at.desc(),
|
||||
limit=1,
|
||||
s=session,
|
||||
).one_or_none()
|
||||
|
||||
if not user:
|
||||
# Create default admin user
|
||||
with session as s:
|
||||
try:
|
||||
user = User(
|
||||
email="admin@eigent.local",
|
||||
username="admin",
|
||||
nickname="Admin",
|
||||
)
|
||||
s.add(user)
|
||||
s.commit()
|
||||
s.refresh(user)
|
||||
logger.info("Default admin user created", extra={"user_id": user.id})
|
||||
except Exception as e:
|
||||
s.rollback()
|
||||
logger.error("Failed to create default admin user", extra={"error": str(e)}, exc_info=True)
|
||||
raise UserException(code.error, _("Failed to create default user"))
|
||||
|
||||
logger.info("Auto login successful", extra={"user_id": user.id, "email": user.email})
|
||||
return LoginResponse(token=Auth.create_access_token(user.id), email=user.email)
|
||||
|
||||
|
||||
@router.post("/register", name="register by email/password")
|
||||
async def register(data: RegisterIn, session: Session = Depends(session)):
|
||||
email = data.email
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class LoginByPasswordIn(BaseModel):
|
|||
class LoginResponse(BaseModel):
|
||||
token: str
|
||||
email: EmailStr
|
||||
redirect_url: str | None = None
|
||||
|
||||
|
||||
class UserIn(BaseModel):
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default function Home() {
|
|||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const { username, email } = useAuthStore();
|
||||
const displayName = username ?? email ?? '';
|
||||
const displayName = username || email || '';
|
||||
|
||||
// Compute activeTab from URL, fallback to 'projects' if not in URL or invalid
|
||||
const activeTab = useMemo(() => {
|
||||
|
|
@ -127,7 +127,7 @@ export default function Home() {
|
|||
cancelText={t('layout.cancel')}
|
||||
/>
|
||||
{/* welcome text */}
|
||||
<div className="from-surface-primary to-surface-primary px-20 pt-16 flex w-full flex-row bg-gradient-to-b">
|
||||
<div className="flex w-full flex-row bg-gradient-to-b from-surface-primary to-surface-primary px-20 pt-16">
|
||||
<WordCarousel
|
||||
words={[`${t('layout.welcome')}, ${welcomeName} !`]}
|
||||
className="text-heading-xl font-bold tracking-tight"
|
||||
|
|
@ -145,10 +145,10 @@ export default function Home() {
|
|||
{/* Navbar */}
|
||||
{/* -top-px avoids a visible hairline: at top-0 subpixel rounding can leave a gap; */}
|
||||
<div
|
||||
className={`border-border-disabled bg-bg-page-default px-20 pb-4 pt-10 sticky -top-px z-20 flex flex-col items-center justify-between border-x-0 border-t-0 border-solid`}
|
||||
className={`sticky -top-px z-20 flex flex-col items-center justify-between border-x-0 border-t-0 border-solid border-border-disabled bg-bg-page-default px-20 pb-4 pt-10`}
|
||||
>
|
||||
<div className="mx-auto flex w-full flex-row items-center justify-between">
|
||||
<div className="gap-2 flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<MenuToggleGroup
|
||||
type="single"
|
||||
value={activeTab}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,7 @@ import { useStackApp } from '@stackframe/react';
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
import { proxyFetchPost } from '@/api/http';
|
||||
import eyeOff from '@/assets/eye-off.svg';
|
||||
import eye from '@/assets/eye.svg';
|
||||
import github2 from '@/assets/github2.svg';
|
||||
import google from '@/assets/google.svg';
|
||||
import { proxyFetchGet, proxyFetchPost } from '@/api/http';
|
||||
import WindowControls from '@/components/WindowControls';
|
||||
import { hasStackKeys } from '@/lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -33,56 +27,29 @@ import background from '@/assets/background.png';
|
|||
import eigentLogo from '@/assets/logo/eigent_icon.png';
|
||||
|
||||
const HAS_STACK_KEYS = hasStackKeys();
|
||||
const IS_LOCAL_MODE = import.meta.env.VITE_USE_LOCAL_PROXY === 'true';
|
||||
let lock = false;
|
||||
|
||||
export default function Login() {
|
||||
// Always call hooks unconditionally - React Hooks must be called in the same order
|
||||
const stackApp = useStackApp();
|
||||
const app = HAS_STACK_KEYS ? stackApp : null;
|
||||
const { setAuth, setModelType, setLocalProxyValue } = useAuthStore();
|
||||
const {
|
||||
setAuth,
|
||||
setModelType,
|
||||
setLocalProxyValue,
|
||||
setInitState,
|
||||
setIsFirstLaunch,
|
||||
} = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [hidePassword, setHidePassword] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
});
|
||||
const [errors, setErrors] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [generalError, setGeneralError] = useState('');
|
||||
const [callbackUrl, setCallbackUrl] = useState<string | null>(null);
|
||||
const titlebarRef = useRef<HTMLDivElement>(null);
|
||||
const [platform, setPlatform] = useState<string>('');
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
if (!formData.email) {
|
||||
newErrors.email = t('layout.please-enter-email-address');
|
||||
} else if (!validateEmail(formData.email)) {
|
||||
newErrors.email = t('layout.please-enter-a-valid-email-address');
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
newErrors.password = t('layout.please-enter-password');
|
||||
} else if (formData.password.length < 8) {
|
||||
newErrors.password = t('layout.password-must-be-at-least-8-characters');
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return !newErrors.email && !newErrors.password;
|
||||
};
|
||||
|
||||
const getLoginErrorMessage = useCallback(
|
||||
(data: any) => {
|
||||
if (!data || typeof data !== 'object' || typeof data.code !== 'number') {
|
||||
|
|
@ -122,37 +89,12 @@ export default function Login() {
|
|||
[t]
|
||||
);
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
|
||||
if (errors[field as keyof typeof errors]) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[field]: '',
|
||||
}));
|
||||
}
|
||||
|
||||
if (generalError) {
|
||||
setGeneralError('');
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
const handleLogin = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto login for local mode - calls /api/auto-login
|
||||
const handleAutoLogin = async () => {
|
||||
setGeneralError('');
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await proxyFetchPost('/api/login', {
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
});
|
||||
const data = await proxyFetchPost('/api/auto-login', {});
|
||||
|
||||
const errorMessage = getLoginErrorMessage(data);
|
||||
if (errorMessage) {
|
||||
|
|
@ -160,22 +102,21 @@ export default function Login() {
|
|||
return;
|
||||
}
|
||||
|
||||
setAuth({ email: formData.email, ...data });
|
||||
setModelType('cloud');
|
||||
// Record VITE_USE_LOCAL_PROXY value at login
|
||||
const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null;
|
||||
setLocalProxyValue(localProxyValue);
|
||||
setAuth({ email: data.email, ...data });
|
||||
setLocalProxyValue(import.meta.env.VITE_USE_LOCAL_PROXY || null);
|
||||
setModelType('custom');
|
||||
setInitState('done');
|
||||
setIsFirstLaunch(false);
|
||||
navigate('/');
|
||||
} catch (error: any) {
|
||||
console.error('Login failed:', error);
|
||||
setGeneralError(
|
||||
t('layout.login-failed-please-check-your-email-and-password')
|
||||
);
|
||||
console.error('Auto login failed:', error);
|
||||
setGeneralError(t('layout.login-failed-please-try-again'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Hybrid/app mode: handle Stack Auth callback (reuse existing OAuth flow)
|
||||
const handleLoginByStack = useCallback(
|
||||
async (token: string) => {
|
||||
try {
|
||||
|
|
@ -191,10 +132,8 @@ export default function Login() {
|
|||
setGeneralError(errorMessage);
|
||||
return;
|
||||
}
|
||||
console.log('data', data);
|
||||
setModelType('cloud');
|
||||
setAuth({ email: formData.email, ...data });
|
||||
// Record VITE_USE_LOCAL_PROXY value at login
|
||||
setAuth({ email: data.email, ...data });
|
||||
const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null;
|
||||
setLocalProxyValue(localProxyValue);
|
||||
navigate('/');
|
||||
|
|
@ -208,7 +147,6 @@ export default function Login() {
|
|||
}
|
||||
},
|
||||
[
|
||||
formData.email,
|
||||
navigate,
|
||||
setAuth,
|
||||
setModelType,
|
||||
|
|
@ -220,33 +158,10 @@ export default function Login() {
|
|||
]
|
||||
);
|
||||
|
||||
const handleReloadBtn = async (type: string) => {
|
||||
if (!app) {
|
||||
console.error('Stack app not initialized');
|
||||
return;
|
||||
}
|
||||
console.log('handleReloadBtn1', type);
|
||||
const cookies = document.cookie.split('; ');
|
||||
cookies.forEach((cookie) => {
|
||||
const [name] = cookie.split('=');
|
||||
if (name.startsWith('stack-oauth-outer-')) {
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
|
||||
}
|
||||
});
|
||||
console.log('handleReloadBtn2', type);
|
||||
await app.signInWithOAuth(type);
|
||||
};
|
||||
|
||||
const handleGetToken = useCallback(
|
||||
async (code: string) => {
|
||||
const code_verifier = localStorage.getItem('stack-oauth-outer-');
|
||||
const formData = new URLSearchParams();
|
||||
console.log(
|
||||
'import.meta.env.PROD',
|
||||
import.meta.env.PROD
|
||||
? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback`
|
||||
: `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback`
|
||||
);
|
||||
formData.append(
|
||||
'redirect_uri',
|
||||
import.meta.env.PROD
|
||||
|
|
@ -273,7 +188,7 @@ export default function Login() {
|
|||
body: formData,
|
||||
}
|
||||
);
|
||||
const data = await res.json(); // parse response data
|
||||
const data = await res.json();
|
||||
return data.access_token;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
@ -298,6 +213,44 @@ export default function Login() {
|
|||
[location.pathname, handleLoginByStack, handleGetToken, setIsLoading]
|
||||
);
|
||||
|
||||
// Listen for direct token callback from Electron (eigent.ai login redirect)
|
||||
useEffect(() => {
|
||||
const handleTokenReceived = async (_event: any, token: string) => {
|
||||
if (!token) return;
|
||||
setIsLoading(true);
|
||||
// Temporarily set token so proxyFetchGet can use it for auth
|
||||
setAuth({ email: '', token, username: '', user_id: 0 });
|
||||
setLocalProxyValue(import.meta.env.VITE_USE_LOCAL_PROXY || null);
|
||||
try {
|
||||
const userInfo = await proxyFetchGet('/api/user');
|
||||
if (userInfo && userInfo.email) {
|
||||
setAuth({
|
||||
token,
|
||||
email: userInfo.email,
|
||||
username:
|
||||
userInfo.username ||
|
||||
userInfo.nickname ||
|
||||
userInfo.fullname ||
|
||||
userInfo.email?.split('@')[0] ||
|
||||
'',
|
||||
user_id:
|
||||
userInfo.id || JSON.parse(atob(token.split('.')[1])).id || 0,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch user info:', e);
|
||||
}
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
window.ipcRenderer?.on('auth-token-received', handleTokenReceived);
|
||||
|
||||
return () => {
|
||||
window.ipcRenderer?.off('auth-token-received', handleTokenReceived);
|
||||
};
|
||||
}, [setAuth, setLocalProxyValue, navigate]);
|
||||
|
||||
// Listen for auth code callback from Electron (Stack Auth OAuth flow)
|
||||
useEffect(() => {
|
||||
window.ipcRenderer?.on('auth-code-received', handleAuthCode);
|
||||
|
||||
|
|
@ -318,7 +271,6 @@ export default function Login() {
|
|||
// Handle before-close event for login page
|
||||
useEffect(() => {
|
||||
const handleBeforeClose = () => {
|
||||
// On login page, always close directly without confirmation
|
||||
window.electronAPI.closeWindow(true);
|
||||
};
|
||||
|
||||
|
|
@ -329,6 +281,91 @@ export default function Login() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Hybrid/app mode: prepare auth callback URL on mount (don't auto-open browser)
|
||||
useEffect(() => {
|
||||
if (IS_LOCAL_MODE) return;
|
||||
|
||||
const prepareCallbackUrl = async () => {
|
||||
let cbUrl: string;
|
||||
if (import.meta.env.PROD) {
|
||||
cbUrl = 'eigent://auth/callback';
|
||||
} else {
|
||||
cbUrl = 'eigent://auth/callback';
|
||||
try {
|
||||
const url = await window.ipcRenderer?.invoke('get-auth-callback-url');
|
||||
if (url) cbUrl = url;
|
||||
} catch (e) {
|
||||
// Fallback to eigent:// protocol
|
||||
}
|
||||
}
|
||||
setCallbackUrl(cbUrl);
|
||||
};
|
||||
|
||||
prepareCallbackUrl();
|
||||
}, []);
|
||||
|
||||
// Render local mode: "Start Eigent" button only
|
||||
const renderLocalMode = () => (
|
||||
<div className="relative flex w-80 flex-1 flex-col items-center justify-center pt-8">
|
||||
<img
|
||||
src={eigentLogo}
|
||||
className="absolute left-1/2 top-10 h-16 w-16 -translate-x-1/2"
|
||||
/>
|
||||
<div className="mb-8 text-heading-lg font-bold text-text-heading">
|
||||
Eigent
|
||||
</div>
|
||||
{generalError && (
|
||||
<p className="mb-4 mt-1 text-label-md text-text-cuation">
|
||||
{generalError}
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleAutoLogin}
|
||||
size="lg"
|
||||
variant="primary"
|
||||
className="w-full rounded-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<span className="flex-1">
|
||||
{isLoading ? t('layout.logging-in') : 'Start Eigent'}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Render hybrid/app mode: waiting for external login callback
|
||||
const renderHybridMode = () => (
|
||||
<div className="relative flex w-80 flex-1 flex-col items-center justify-center pt-8">
|
||||
<img
|
||||
src={eigentLogo}
|
||||
className="absolute left-1/2 top-10 h-16 w-16 -translate-x-1/2"
|
||||
/>
|
||||
<div className="mb-4 text-heading-lg font-bold text-text-heading">
|
||||
{t('layout.login')}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<p className="mb-6 text-center text-label-md text-text-secondary">
|
||||
{t('layout.logging-in')}...
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
window.open(
|
||||
`https://www.eigent.ai/signin?callbackUrl=${encodeURIComponent(callbackUrl || 'eigent://auth/callback')}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
);
|
||||
}}
|
||||
size="lg"
|
||||
variant="primary"
|
||||
className="w-full rounded-full"
|
||||
>
|
||||
<span className="flex-1">{t('layout.log-in')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full flex-col overflow-hidden">
|
||||
{/* Titlebar with drag region and window controls */}
|
||||
|
|
@ -373,119 +410,7 @@ export default function Login() {
|
|||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
<div className="relative flex w-80 flex-1 flex-col items-center justify-center pt-8">
|
||||
<img
|
||||
src={eigentLogo}
|
||||
className="absolute left-1/2 top-10 h-16 w-16 -translate-x-1/2"
|
||||
/>
|
||||
<div className="mb-4 flex items-end justify-between self-stretch">
|
||||
<div className="text-heading-lg font-bold text-text-heading">
|
||||
{t('layout.login')}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (import.meta.env.VITE_USE_LOCAL_PROXY === 'true') {
|
||||
navigate('/signup');
|
||||
} else {
|
||||
window.open(
|
||||
'https://www.eigent.ai/signup',
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('layout.sign-up')}
|
||||
</Button>
|
||||
</div>
|
||||
{HAS_STACK_KEYS && (
|
||||
<div className="w-full pt-6">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => handleReloadBtn('google')}
|
||||
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<img src={google} className="h-5 w-5" />
|
||||
<span className="ml-2">
|
||||
{t('layout.continue-with-google-login')}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => handleReloadBtn('github')}
|
||||
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<img src={github2} className="h-5 w-5" />
|
||||
<span className="ml-2">
|
||||
{t('layout.continue-with-github-login')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{HAS_STACK_KEYS && (
|
||||
<div className="mb-6 mt-2 w-full text-center font-inter text-[15px] font-medium leading-[22px] text-[#222]">
|
||||
{t('layout.or')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{generalError && (
|
||||
<p className="mb-4 mt-1 text-label-md text-text-cuation">
|
||||
{generalError}
|
||||
</p>
|
||||
)}
|
||||
<div className="relative mb-4 flex w-full flex-col gap-4">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
size="default"
|
||||
title={t('layout.email')}
|
||||
placeholder={t('layout.enter-your-email')}
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
state={errors.email ? 'error' : undefined}
|
||||
note={errors.email}
|
||||
onEnter={handleLogin}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
title={t('layout.password')}
|
||||
size="default"
|
||||
type={hidePassword ? 'password' : 'text'}
|
||||
required
|
||||
placeholder={t('layout.enter-your-password')}
|
||||
value={formData.password}
|
||||
onChange={(e) =>
|
||||
handleInputChange('password', e.target.value)
|
||||
}
|
||||
state={errors.password ? 'error' : undefined}
|
||||
note={errors.password}
|
||||
backIcon={<img src={hidePassword ? eye : eyeOff} />}
|
||||
onBackIconClick={() => setHidePassword(!hidePassword)}
|
||||
onEnter={handleLogin}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
size="md"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="w-full rounded-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<span className="flex-1">
|
||||
{isLoading ? t('layout.logging-in') : t('layout.log-in')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{IS_LOCAL_MODE ? renderLocalMode() : renderHybridMode()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { hasStackKeys } from '@/lib';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const HAS_STACK_KEYS = hasStackKeys();
|
||||
const IS_LOCAL_MODE = import.meta.env.VITE_USE_LOCAL_PROXY === 'true';
|
||||
let lock = false;
|
||||
export default function SignUp() {
|
||||
// Always call hooks unconditionally - React Hooks must be called in the same order
|
||||
|
|
@ -40,6 +41,25 @@ export default function SignUp() {
|
|||
const { setAuth, initState: _initState } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Local mode: no signup needed, redirect to home
|
||||
useEffect(() => {
|
||||
if (IS_LOCAL_MODE) {
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
// Hybrid/app mode without Stack keys: redirect to external signup
|
||||
useEffect(() => {
|
||||
if (!IS_LOCAL_MODE && !HAS_STACK_KEYS) {
|
||||
window.open(
|
||||
'https://www.eigent.ai/signup',
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
);
|
||||
navigate('/login', { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
const [hidePassword, setHidePassword] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState({
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { proxyFetchPost } from '@/api/http';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { lazy, useEffect, useReducer } from 'react';
|
||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
|
|
@ -24,6 +25,8 @@ const Home = lazy(() => import('@/pages/Home'));
|
|||
const History = lazy(() => import('@/pages/History'));
|
||||
const NotFound = lazy(() => import('@/pages/NotFound'));
|
||||
|
||||
const IS_LOCAL_MODE = import.meta.env.VITE_USE_LOCAL_PROXY === 'true';
|
||||
|
||||
interface AuthState {
|
||||
loading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
|
|
@ -61,7 +64,16 @@ const ProtectedRoute = () => {
|
|||
initialized: false,
|
||||
});
|
||||
|
||||
const { token, localProxyValue, logout } = useAuthStore();
|
||||
const {
|
||||
token,
|
||||
localProxyValue,
|
||||
logout,
|
||||
setAuth,
|
||||
setLocalProxyValue,
|
||||
setInitState,
|
||||
setIsFirstLaunch,
|
||||
setModelType,
|
||||
} = useAuthStore();
|
||||
useEffect(() => {
|
||||
// Check VITE_USE_LOCAL_PROXY value on app startup
|
||||
if (token) {
|
||||
|
|
@ -77,8 +89,38 @@ const ProtectedRoute = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Local mode: auto-login when no token
|
||||
if (IS_LOCAL_MODE && !token) {
|
||||
proxyFetchPost('/api/auto-login', {})
|
||||
.then((data) => {
|
||||
if (data && data.token) {
|
||||
setAuth({ email: data.email, ...data });
|
||||
setLocalProxyValue(import.meta.env.VITE_USE_LOCAL_PROXY || null);
|
||||
setModelType('custom');
|
||||
setInitState('done');
|
||||
setIsFirstLaunch(false);
|
||||
dispatch({
|
||||
type: 'INITIALIZE',
|
||||
payload: { isAuthenticated: true },
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'INITIALIZE',
|
||||
payload: { isAuthenticated: false },
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch({
|
||||
type: 'INITIALIZE',
|
||||
payload: { isAuthenticated: false },
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'INITIALIZE', payload: { isAuthenticated: !!token } });
|
||||
}, [token, localProxyValue, logout]);
|
||||
}, [token, localProxyValue, logout, setAuth, setLocalProxyValue]);
|
||||
|
||||
if (state.loading || !state.initialized) {
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue