Skip to content

Commit e9a7d1d

Browse files
4pmtongbitloi
authored andcommitted
feat: local auto-login + eigent.ai callback for Electron hybrid login (eigent-ai#1497)
1 parent 2c2d58f commit e9a7d1d

7 files changed

Lines changed: 319 additions & 248 deletions

File tree

electron/main/index.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,12 @@ function handleProtocolUrl(url: string) {
471471
function processProtocolUrl(url: string) {
472472
const urlObj = new URL(url);
473473
const code = urlObj.searchParams.get('code');
474+
const token = urlObj.searchParams.get('token');
474475
const share_token = urlObj.searchParams.get('share_token');
475476

476477
log.info('urlObj', urlObj);
477478
log.info('code', code);
479+
log.info('token', token);
478480
log.info('share_token', share_token);
479481

480482
if (win && !win.isDestroyed()) {
@@ -489,6 +491,12 @@ function processProtocolUrl(url: string) {
489491
return;
490492
}
491493

494+
if (token) {
495+
log.info('protocol token received');
496+
win.webContents.send('auth-token-received', token);
497+
return;
498+
}
499+
492500
if (code) {
493501
log.error('protocol code:', code);
494502
win.webContents.send('auth-code-received', code);
@@ -524,6 +532,58 @@ function processQueuedProtocolUrls() {
524532
}
525533
}
526534

535+
// ==================== auth callback server ====================
536+
// Local HTTP server for receiving auth callbacks from external login (eigent.ai)
537+
// Works in both dev and production mode, avoids eigent:// protocol issues in dev
538+
let authCallbackServer: http.Server | null = null;
539+
let authCallbackPort: number | null = null;
540+
541+
async function startAuthCallbackServer() {
542+
if (authCallbackServer) return authCallbackPort;
543+
544+
const port = await findAvailablePort(19836, 19900);
545+
546+
authCallbackServer = http.createServer((req, res) => {
547+
const url = new URL(req.url || '', `http://localhost:${port}`);
548+
549+
if (url.pathname === '/auth/callback') {
550+
const token = url.searchParams.get('token');
551+
log.info('Auth callback URL:', req.url);
552+
log.info('Auth callback token present:', !!token);
553+
log.info('Auth callback win available:', !!win && !win.isDestroyed());
554+
555+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
556+
res.end(`
557+
<!DOCTYPE html>
558+
<html><head><title>Login Successful</title>
559+
<style>
560+
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; }
561+
.container { padding: 40px; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); text-align: center; }
562+
</style></head>
563+
<body><div class="container">
564+
<h1>Login Successful</h1>
565+
<p>You can close this tab and return to Eigent.</p>
566+
</div></body></html>
567+
`);
568+
569+
if (token && win && !win.isDestroyed()) {
570+
log.info('Auth callback received token');
571+
win.webContents.send('auth-token-received', token);
572+
win.show();
573+
win.focus();
574+
}
575+
} else {
576+
res.writeHead(404);
577+
res.end('Not Found');
578+
}
579+
});
580+
581+
authCallbackServer.listen(port);
582+
authCallbackPort = port;
583+
log.info(`Auth callback server started on port ${port}`);
584+
return port;
585+
}
586+
527587
// ==================== single instance lock ====================
528588
const setupSingleInstanceLock = () => {
529589
// The lock is already acquired at module level (requestSingleInstanceLock
@@ -603,6 +663,12 @@ const checkManagerInstance = (manager: any, name: string) => {
603663
};
604664

605665
function registerIpcHandlers() {
666+
// ==================== auth callback ====================
667+
ipcMain.handle('get-auth-callback-url', async () => {
668+
const port = await startAuthCallbackServer();
669+
return `http://localhost:${port}/auth/callback`;
670+
});
671+
606672
// ==================== basic info handler ====================
607673
ipcMain.handle('get-browser-port', () => {
608674
log.info('Getting browser port');

server/app/controller/user/login_controller.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,42 @@ async def by_stack_auth(
157157
return LoginResponse(token=Auth.create_access_token(user.id), email=user.email)
158158

159159

160+
@router.post("/auto-login", name="auto login for local mode")
161+
async def auto_login(session: Session = Depends(session)) -> LoginResponse:
162+
"""
163+
Auto login for fully local mode (VITE_USE_LOCAL_PROXY=true).
164+
Returns the most recently active user, or creates a default admin user if none exists.
165+
"""
166+
# Find the most recently active user
167+
user = User.by(
168+
User.status == Status.Normal,
169+
order_by=User.updated_at.desc(),
170+
limit=1,
171+
s=session,
172+
).one_or_none()
173+
174+
if not user:
175+
# Create default admin user
176+
with session as s:
177+
try:
178+
user = User(
179+
email="admin@eigent.local",
180+
username="admin",
181+
nickname="Admin",
182+
)
183+
s.add(user)
184+
s.commit()
185+
s.refresh(user)
186+
logger.info("Default admin user created", extra={"user_id": user.id})
187+
except Exception as e:
188+
s.rollback()
189+
logger.error("Failed to create default admin user", extra={"error": str(e)}, exc_info=True)
190+
raise UserException(code.error, _("Failed to create default user"))
191+
192+
logger.info("Auto login successful", extra={"user_id": user.id, "email": user.email})
193+
return LoginResponse(token=Auth.create_access_token(user.id), email=user.email)
194+
195+
160196
@router.post("/register", name="register by email/password")
161197
async def register(data: RegisterIn, session: Session = Depends(session)):
162198
email = data.email

server/app/model/user/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class LoginByPasswordIn(BaseModel):
6060
class LoginResponse(BaseModel):
6161
token: str
6262
email: EmailStr
63+
redirect_url: str | None = None
6364

6465

6566
class UserIn(BaseModel):

src/pages/History.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default function Home() {
6161
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
6262
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
6363
const { username, email } = useAuthStore();
64-
const displayName = username ?? email ?? '';
64+
const displayName = username || email || '';
6565

6666
// Compute activeTab from URL, fallback to 'projects' if not in URL or invalid
6767
const activeTab = useMemo(() => {
@@ -127,7 +127,7 @@ export default function Home() {
127127
cancelText={t('layout.cancel')}
128128
/>
129129
{/* welcome text */}
130-
<div className="from-surface-primary to-surface-primary px-20 pt-16 flex w-full flex-row bg-gradient-to-b">
130+
<div className="flex w-full flex-row bg-gradient-to-b from-surface-primary to-surface-primary px-20 pt-16">
131131
<WordCarousel
132132
words={[`${t('layout.welcome')}, ${welcomeName} !`]}
133133
className="text-heading-xl font-bold tracking-tight"
@@ -145,10 +145,10 @@ export default function Home() {
145145
{/* Navbar */}
146146
{/* -top-px avoids a visible hairline: at top-0 subpixel rounding can leave a gap; */}
147147
<div
148-
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`}
148+
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`}
149149
>
150150
<div className="mx-auto flex w-full flex-row items-center justify-between">
151-
<div className="gap-2 flex items-center">
151+
<div className="flex items-center gap-2">
152152
<MenuToggleGroup
153153
type="single"
154154
value={activeTab}

0 commit comments

Comments
 (0)