Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand Down
36 changes: 36 additions & 0 deletions server/app/controller/user/login_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions server/app/model/user/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class LoginByPasswordIn(BaseModel):
class LoginResponse(BaseModel):
token: str
email: EmailStr
redirect_url: str | None = None


class UserIn(BaseModel):
Expand Down
8 changes: 4 additions & 4 deletions src/pages/History.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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"
Expand All @@ -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}
Expand Down
Loading
Loading