From e28beff3a6fe8774c7b21394c58b882ff4eca979 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Sun, 11 Oct 2026 12:04:01 +0800 Subject: [PATCH] Revert "feat(makelore): add platform oidc login" This reverts commit 7236def07d6a9a5cfde70c3239c6ac53764bcd7d. --- README.md | 4 +- electron/api/routes/auth.ts | 22 ---- electron/main/app-deep-link.ts | 24 +--- electron/main/index.ts | 62 +--------- electron/services/platform-auth.ts | 179 ----------------------------- src/App.tsx | 9 +- src/pages/Login/index.tsx | 36 ------ tests/unit/app-deep-link.test.ts | 10 -- tests/unit/auth-routes.test.ts | 31 ----- tests/unit/login-page.test.tsx | 35 +----- tests/unit/platform-auth.test.ts | 73 ------------ 11 files changed, 10 insertions(+), 475 deletions(-) delete mode 100644 electron/services/platform-auth.ts delete mode 100644 tests/unit/platform-auth.test.ts diff --git a/README.md b/README.md index 42dff735..aeef282b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智 - `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏负责新建、切换和管理 Workspace,并在桌面设计模式下以 256px 宽度常驻展开;中央沿用 AI 编程的安静对话画布、自然消息流和底部悬浮输入器,AI 整理出的制作方案作为对话内的轻量可编辑稿持续更新;桌面端右侧同为 256px 的全高历史作品栏集中展示当前项目的制作记录与生成结果。任务中的已生成图片可通过“放大查看图片”按钮直接打开大图,支持适应窗口、原始尺寸及 Esc 关闭,无需先下载。紧凑窗口通过左侧抽屉访问项目列表,历史记录保留在时间线中。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。 - `Makelore Robot|AI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中,Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。 -应用启动默认进入 AI 模块入口选择页。入口页可在未登录状态浏览;未登录用户点击已开通模块时进入客户端原生登录页,可使用平台账号 OIDC 登录,也可使用账号密码或手机号短信验证码登录。平台账号登录由 Electron Main 从 `PLATFORM_AUTH_ISSUER` 发现授权端点,使用系统浏览器完成 Authorization Code + PKCE(S256),回到 `niancode://auth/callback` 后由 Main 校验 state、交换令牌并保存会话;Renderer 不接触 client secret、refresh token 或授权回调凭据。密码登录可选“记住密码”:正式安装包仅由 Electron Main 使用系统受保护凭据存储加密保存和回填账号密码,不写入 Renderer 持久状态,未打包开发版或系统安全存储不可用时禁用该选项。登录请求由 Renderer 经 Host API 交给 Electron Main,再由 Main 调用 Works Square;成功后回到入口选择页。已登录时,Electron Main 会从 Works Square `/api/auth/me` 读取当前账号,只向 Renderer 投影用户名、账号/租户/部门标识、权限名列表与四个模块布尔开关,不透传上游资料或凭据。工作区门禁同时要求有效 Token 和完整用户身份;旧状态缺失身份时会先尝试从 Main 恢复,仍无法确认则清除残留会话并返回登录页。被管理员关闭的模块会在入口页置灰且无法点击,直接访问其工作区路径也会返回入口页。旧服务端未返回策略或缺少单项字段时默认开放;这个客户端门禁不替代服务端 API 授权。 +应用启动默认进入 AI 模块入口选择页。入口页可在未登录状态浏览;未登录用户点击已开通模块时进入客户端原生登录页,可使用账号密码或手机号短信验证码登录。密码登录可选“记住密码”:正式安装包仅由 Electron Main 使用系统受保护凭据存储加密保存和回填账号密码,不写入 Renderer 持久状态,未打包开发版或系统安全存储不可用时禁用该选项。登录请求由 Renderer 经 Host API 交给 Electron Main,再由 Main 调用 Works Square;成功后回到入口选择页。已登录时,Electron Main 会从 Works Square `/api/auth/me` 读取当前账号,只向 Renderer 投影用户名、账号/租户/部门标识、权限名列表与四个模块布尔开关,不透传上游资料或凭据。工作区门禁同时要求有效 Token 和完整用户身份;旧状态缺失身份时会先尝试从 Main 恢复,仍无法确认则清除残留会话并返回登录页。被管理员关闭的模块会在入口页置灰且无法点击,直接访问其工作区路径也会返回入口页。旧服务端未返回策略或缺少单项字段时默认开放;这个客户端门禁不替代服务端 API 授权。 作品广场、素材广场、独立发布上传和云部署页面不属于 Makelore 2.0 工作台。新建 Code 项目只要求选择目录:Main 自动生成内部项目 ID,并以内部 `interactive_ai_app` 类型创建 `.makelore/project.json` 与 `knowledge/`,不再让用户选择或查看项目身份、项目类型和模板;缺少项目 ID 的旧项目在读取时由 Main 自动补全。从列表移除项目只取消登记,不删除磁盘文件;在“新建项目”中直接选择已有项目文件夹会重新打开,并保留原有项目身份、类型、智能体、对话和知识文件。“新建下级文件夹”仍拒绝已存在的同名目录。创建成功后直接进入对话工作区,未创建智能体时只显示可选的设置入口,不再用初始化门禁遮挡工作区。已有 `custom` 项目继续受支持;历史 `mini_game` / `mini_program` 配置在读取时归一为交互式 AI 应用,但不会因读取被改写。用户获取并为项目启用官方 bundled `makelore.project-scaffold` 插件后,每个父智能体都可按需明确调用 `makelore-project-scaffold` Skill,无需伙伴分配;它以不覆盖既有路径的方式生成固定六文件 Vite 起步工程,不是创建前置条件,也不安装依赖、不联网、不构建、不上传或提审。交互式 AI 应用的项目配置底部提供“一键提交审核”;Main 自动预检、安全打包并提交,构建通过后进入运营审核,审核通过即直接发布。首次创建必须选择 PNG、JPEG 或 WebP 项目封面,并通过 Main-owned multipart 原子接口同时保存资料与封面;已有 draft/published 只提交新版本并沿用平台现有资料与封面。项目成果预览 `/deliverables` 继续保留。 @@ -111,7 +111,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p - Renderer 的后端调用统一经过 `src/lib/host-api.ts` 或 `src/lib/api-client.ts`;请求先经 Main-owned IPC,再由兼容 Host API 路由处理。只有真正需要 URL 的资源和流会把 loopback 地址暴露给 Renderer。 - Renderer 不直接调用 Electron IPC 或本地运行时 HTTP 地址。 - Electron Main 负责认证、秘密存储、运行时生命周期、代理、同步和系统集成;所有 stream、watcher、poller、loopback server 与子进程必须登记到模块活动和任务租约,不允许页面自行创建无托管后台任务。 -- 平台账号 OIDC 登录与 Works Square 原生密码、短信登录均沿 Renderer → Host API → Electron Main → Works Square 链路完成。OIDC 使用公开 `makelore` client、精确回调 `niancode://auth/callback`、state/nonce 和 S256 PKCE;Main 在会话切换前清理旧账号的本地运行时,再持久化新的平台 access/refresh token。登录态按真实键盘、鼠标或触摸活动滑动续期;持续使用无需反复登录,连续 7 天未使用才清除会话并要求重新登录。刷新凭据始终只由 Electron Main 持有,并在正式安装包中通过系统受保护凭据存储加密落盘;可选的记住密码记录使用独立的 Main-owned 加密存储,退出登录不会清除它,只有成功的未勾选密码登录才清除旧记录。未打包开发版只在内存持有会话且禁用记住密码,避免未签名 Electron 调试进程触发 macOS 钥匙串。Renderer 现有的短效公开 access-token 会话快照与持久化保持不变(旧版升级迁移时仅暂存既有刷新凭据,Main 成功接管后立即删除),账号密码不进入 Renderer 持久状态。 +- Works Square 原生密码与短信登录均沿 Renderer → Host API → Electron Main → Works Square 链路完成。登录态按真实键盘、鼠标或触摸活动滑动续期;持续使用无需反复登录,连续 7 天未使用才清除会话并要求重新登录。刷新凭据始终只由 Electron Main 持有,并在正式安装包中通过系统受保护凭据存储加密落盘;可选的记住密码记录使用独立的 Main-owned 加密存储,退出登录不会清除它,只有成功的未勾选密码登录才清除旧记录。未打包开发版只在内存持有会话且禁用记住密码,避免未签名 Electron 调试进程触发 macOS 钥匙串。Renderer 现有的短效公开 access-token 会话快照与持久化保持不变(旧版升级迁移时仅暂存既有刷新凭据,Main 成功接管后立即删除),账号密码不进入 Renderer 持久状态。 - AI 编程发布只经过 Main-owned Host API:Renderer 仅提交本地项目标识、非敏感作品资料和有界封面 DTO;Main 持有源码快照、本地 npm/Vite 构建、精确产物预检、双归档、Works Token、版本生成、幂等重试和安全状态投影。发布构建同时提供 Main-owned `ReleaseJob` 的 start/progress/status/cancel 契约,同一项目串行执行并支持取消;异步 Job 的扫描、依赖安装、构建和双归档均在独立 `utilityProcess` 中以流式文件处理,Main 只接收进度、摘要和契约,旧的同步提交接口继续兼容已有客户端。首次项目 create 使用 `/api/projects/with-cover` multipart 原子写入资料与封面;已有项目只提交版本,状态竞态会固定失败并要求重新确认,不执行无条件 metadata PATCH 或封面替换。项目的 Vite config/plugins 会以当前桌面用户权限执行,因此该链路只适用于用户信任的本地项目,不是 sandbox。 - AI 编程项目配置只以项目内 `.makelore/project.json` 为准;项目文件和会话主数据保持本地,问答观察快照按个人资料同步规则单向上行。Main 不探测、读取或迁移 `.niancode` 与 `.opencode` 项目数据。 - AI 绘画 Renderer 只调用 Main-owned Host API;Main 负责 Works Square Token 刷新、Workspace 所属的持久 Agent Session、稳定命令身份、有界 Run 查询、可恢复事件订阅与契约映射,并通过本机 Host API 的 SSE 投影同步表单、任务和资产状态。切换 Workspace 只重连对应流;注销或退出时关闭本地流,不删除服务端持久 Session。远端 Token、Provider Prompt 与存储地址不进入 Renderer。 diff --git a/electron/api/routes/auth.ts b/electron/api/routes/auth.ts index b16e3817..e6d549e0 100644 --- a/electron/api/routes/auth.ts +++ b/electron/api/routes/auth.ts @@ -27,7 +27,6 @@ import { getRememberedPasswordState, updateRememberedPassword, } from '../../services/remembered-password'; -import { startPlatformAuthorization } from '../../services/platform-auth'; type PasswordLoginInput = { username?: unknown; @@ -336,22 +335,6 @@ async function handlePasswordLogin( }); } -async function handlePlatformAuthorizationStart( - req: IncomingMessage, - res: ServerResponse, -): Promise { - // Keep the route body intentionally empty: the Main process owns the OIDC - // client configuration and all PKCE state. The renderer only receives a - // browser URL and never handles a client secret. - readExactJsonObject(await parseJsonBody>(req), []); - const authorization = await startPlatformAuthorization(); - sendJson(res, 200, { - success: true, - requestId: authorization.requestId, - authorizationUrl: authorization.authorizationUrl, - }); -} - async function handleRememberedPassword(res: ServerResponse): Promise { const state = await getRememberedPasswordState(); res.setHeader('Cache-Control', 'no-store'); @@ -808,11 +791,6 @@ export async function handleAuthRoutes( return true; } - if (url.pathname === '/api/auth/platform/start' && req.method === 'POST') { - await handlePlatformAuthorizationStart(req, res); - return true; - } - if (url.pathname === '/api/auth/remembered-password' && req.method === 'GET') { await handleRememberedPassword(res); return true; diff --git a/electron/main/app-deep-link.ts b/electron/main/app-deep-link.ts index 039e5dbd..59204495 100644 --- a/electron/main/app-deep-link.ts +++ b/electron/main/app-deep-link.ts @@ -2,11 +2,8 @@ export const NIANCODE_APP_PROTOCOL = 'niancode'; export type NianCodeDeepLink = { type: 'desktop-auth-callback'; - /** Legacy desktop auth callbacks carry a request id. OIDC callbacks use state. */ - requestId?: string; + requestId: string; url: string; - code?: string; - state?: string; } | { type: 'cloud-agent'; slug: string; url: string } | { type: 'teacher-preview'; draftRevision: number; url: string }; @@ -32,7 +29,6 @@ const SENSITIVE_QUERY_KEYS = new Set([ 'device_secret', 'token', ]); -const AUTH_CALLBACK_QUERY_KEYS = new Set(['request_id', 'code', 'state']); export function parseNianCodeDeepLinkUrl(rawUrl: string): NianCodeDeepLink | null { let url: URL; @@ -71,27 +67,15 @@ export function parseNianCodeDeepLinkUrl(rawUrl: string): NianCodeDeepLink | nul } } - if ([...url.searchParams.keys()].some((key) => !AUTH_CALLBACK_QUERY_KEYS.has(key))) { + const requestId = url.searchParams.get('request_id')?.trim(); + if (!requestId || requestId.length > 128) { return null; } - const requestId = url.searchParams.get('request_id')?.trim() || undefined; - if (requestId && requestId.length > 128) return null; - - const code = url.searchParams.get('code')?.trim() || undefined; - const state = url.searchParams.get('state')?.trim() || undefined; - if (Boolean(code) !== Boolean(state) || (code && code.length > 4096) || (state && state.length > 512)) { - return null; - } - // The platform OIDC redirect is registered as niancode://auth/callback and - // therefore returns code/state without the legacy request_id parameter. - if (!requestId && !(code && state)) return null; - return { type: 'desktop-auth-callback', - ...(requestId ? { requestId } : {}), + requestId, url: rawUrl, - ...(code ? { code, state } : {}), }; } diff --git a/electron/main/index.ts b/electron/main/index.ts index 881215b9..d3c58216 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -65,7 +65,6 @@ import { createReleaseUtilityPreparer } from '../services/release-utility-proces import { createStaticArtifactSnapshot } from '../services/static-release-server'; import { consumeWorksSquareStartupRuntimeCleanupRequired, - commitWorksSquareSessionFromTokenPayload, getWorksSquareSessionRestoreStatus, getWorksSquareSessionSnapshot, initializeWorksSquareSession, @@ -74,11 +73,7 @@ import { } from '../services/works-square-session'; import { shouldUseSecureWorksSquareSessionPersistence } from '../services/works-square-session-persistence-policy'; import { initializeRememberedPassword } from '../services/remembered-password'; -import { - clearManagedWorksSquareRuntimeBestEffort, - ensureManagedWorksSquareRuntimeClean, -} from '../services/works-square-runtime'; -import { completePlatformAuthorization } from '../services/platform-auth'; +import { clearManagedWorksSquareRuntimeBestEffort } from '../services/works-square-runtime'; import { WorksSquareDesignWorkspace } from '../image-workspace/works-square-workspace'; import type { DesignWorkspaceModule } from '../image-workspace/module'; import { @@ -212,12 +207,6 @@ let releaseJobs: ReleaseJobManager | null = null; let codingProducts: CodingProductComposition | null = null; let windowIpcBindings: ReturnType | null = null; let unsubscribeAuthSession: (() => void) | null = null; -let platformAuthReady = false; -let queuedPlatformAuthCallback: { - requestId: string | null; - code: string; - state: string; -} | null = null; const mainWindowFocusState = createMainWindowFocusState(); const quitLifecycleState = createQuitLifecycleState(); const launchDeepLinkUrl = findNianCodeDeepLinkUrl(process.argv); @@ -376,33 +365,6 @@ function requestMainWindowFocus(reason: string): void { logger.debug(`Main window is not ready yet; deferring focus for ${reason}`); } -function completePlatformAuthCallback(callback: { - requestId: string | null; - code: string; - state: string; -}): void { - void (async () => { - // Match the password/mobile login path: clean the previous account's - // derived runtime before making the new account active. Keeping the - // authorization pending until cleanup succeeds lets the user retry after - // a transient local cleanup failure. - await ensureManagedWorksSquareRuntimeClean({ - codingProducts: codingProducts ?? undefined, - imageWorkspace: imageWorkspaceModule ?? undefined, - }); - const payload = await completePlatformAuthorization( - callback.requestId, - callback.code, - callback.state, - ); - await commitWorksSquareSessionFromTokenPayload(payload); - requestMainWindowFocus('platform OIDC callback'); - })().catch((error) => { - logger.warn('[auth] Platform OIDC callback was not completed', error); - requestMainWindowFocus('platform OIDC callback failure'); - }); -} - function handleAppDeepLinkActivation(rawUrl: string): boolean { const deepLink = parseNianCodeDeepLinkUrl(rawUrl); if (!deepLink) { @@ -416,21 +378,7 @@ function handleAppDeepLinkActivation(rawUrl: string): boolean { queueTeacherPreviewRevision(deepLink.draftRevision); mainWindow?.webContents.send('navigate', '/coding-teacher-preview?draftRevision=' + deepLink.draftRevision); } else { - if (deepLink.code && deepLink.state) { - const callback = { - requestId: deepLink.requestId ?? null, - code: deepLink.code, - state: deepLink.state, - }; - if (platformAuthReady) { - completePlatformAuthCallback(callback); - } else { - queuedPlatformAuthCallback = callback; - logger.debug('[auth] Queued platform OIDC callback until session restore is ready'); - } - } else { - logger.info(`Received Makelore app link: type=${deepLink.type}, request_id=${deepLink.requestId ?? 'none'}`); - } + logger.info(`Received Makelore app link: type=${deepLink.type}, request_id=${deepLink.requestId}`); } requestMainWindowFocus('app deep link'); return true; @@ -716,12 +664,6 @@ async function initialize(): Promise { }, 'expired persisted session during startup'); }); } - platformAuthReady = true; - if (queuedPlatformAuthCallback) { - const callback = queuedPlatformAuthCallback; - queuedPlatformAuthCallback = null; - completePlatformAuthCallback(callback); - } if (!isE2EMode) { projectProgressSync = createProjectProgressSync(codingProjectStore); const activateProgrammingServices = (): void => { diff --git a/electron/services/platform-auth.ts b/electron/services/platform-auth.ts deleted file mode 100644 index 35157be0..00000000 --- a/electron/services/platform-auth.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { createHash, randomBytes, randomUUID } from 'node:crypto'; -import { WORKS_SQUARE_CONFIG } from '../api/works-config'; -import { proxyAwareFetch } from '../utils/proxy-fetch'; -import type { WorksSquareTokenPayload } from './works-square-session'; - -const DEFAULT_CLIENT_ID = 'makelore'; -const DEFAULT_SCOPE = 'openid profile phone offline_access'; -const REDIRECT_URI = 'niancode://auth/callback'; -const STATE_TTL_MS = 10 * 60_000; - -type PlatformAuthMetadata = { - authorization_endpoint: string; - token_endpoint: string; -}; - -type PendingAuthorization = { - state: string; - codeVerifier: string; - expiresAt: number; -}; - -const pendingAuthorizations = new Map(); -let metadataPromise: Promise | null = null; - -function configuredIssuer(): string { - const raw = (process.env.PLATFORM_AUTH_ISSUER || WORKS_SQUARE_CONFIG.apiBaseUrl).trim().replace(/\/+$/, ''); - const parsed = new URL(raw); - if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) { - throw new Error('PLATFORM_AUTH_ISSUER must be an absolute HTTP(S) URL without credentials or query parameters'); - } - return parsed.toString().replace(/\/$/, ''); -} - -function configuredClientId(): string { - return process.env.PLATFORM_AUTH_CLIENT_ID?.trim() || DEFAULT_CLIENT_ID; -} - -function configuredScope(): string { - return process.env.PLATFORM_AUTH_SCOPE?.trim() || DEFAULT_SCOPE; -} - -function absoluteHttpUrl(value: unknown, field: string): string { - if (typeof value !== 'string' || !value.trim()) throw new Error(`OIDC discovery missing ${field}`); - const parsed = new URL(value); - if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) { - throw new Error(`OIDC discovery returned an invalid ${field}`); - } - return parsed.toString(); -} - -async function loadMetadata(): Promise { - const issuer = configuredIssuer(); - const response = await proxyAwareFetch(`${issuer}/.well-known/openid-configuration`, { - method: 'GET', - headers: { Accept: 'application/json' }, - }); - if (!response.ok) throw new Error(`OIDC discovery failed (${response.status})`); - const payload = await response.json() as Record; - return { - authorization_endpoint: absoluteHttpUrl(payload.authorization_endpoint, 'authorization_endpoint'), - token_endpoint: absoluteHttpUrl(payload.token_endpoint, 'token_endpoint'), - }; -} - -async function getMetadata(): Promise { - if (!metadataPromise) { - metadataPromise = loadMetadata().catch((error) => { - metadataPromise = null; - throw error; - }); - } - return metadataPromise; -} - -function takePendingAuthorization( - requestId: string | null, - returnedState: string, -): PendingAuthorization | null { - if (requestId) { - const pending = pendingAuthorizations.get(requestId); - pendingAuthorizations.delete(requestId); - return pending ?? null; - } - - // The platform redirect is a fixed niancode:// URI and does not append our - // local request id. The state value is the OIDC correlation handle instead. - for (const [candidateId, pending] of pendingAuthorizations) { - if (pending.state === returnedState) { - pendingAuthorizations.delete(candidateId); - return pending; - } - } - return null; -} - -function cleanupPendingAuthorizations(now = Date.now()): void { - for (const [requestId, pending] of pendingAuthorizations) { - if (pending.expiresAt <= now) pendingAuthorizations.delete(requestId); - } -} - -function codeChallengeS256(verifier: string): string { - return createHash('sha256').update(verifier, 'ascii').digest('base64url'); -} - -function readTokenPayload(value: unknown): WorksSquareTokenPayload { - if (!value || typeof value !== 'object' || Array.isArray(value)) { - throw new Error('OIDC token endpoint returned an invalid response'); - } - const payload = value as WorksSquareTokenPayload; - if (typeof payload.access_token !== 'string' || !payload.access_token.trim()) { - throw new Error('OIDC token endpoint did not return access_token'); - } - return payload; -} - -export async function startPlatformAuthorization(): Promise<{ - requestId: string; - authorizationUrl: string; -}> { - cleanupPendingAuthorizations(); - const metadata = await getMetadata(); - const requestId = randomUUID(); - const state = randomBytes(32).toString('base64url'); - const codeVerifier = randomBytes(64).toString('base64url'); - pendingAuthorizations.set(requestId, { - state, - codeVerifier, - expiresAt: Date.now() + STATE_TTL_MS, - }); - - const url = new URL(metadata.authorization_endpoint); - url.searchParams.set('client_id', configuredClientId()); - url.searchParams.set('response_type', 'code'); - url.searchParams.set('redirect_uri', REDIRECT_URI); - url.searchParams.set('scope', configuredScope()); - url.searchParams.set('state', state); - url.searchParams.set('nonce', randomBytes(32).toString('base64url')); - url.searchParams.set('code_challenge', codeChallengeS256(codeVerifier)); - url.searchParams.set('code_challenge_method', 'S256'); - - return { requestId, authorizationUrl: url.toString() }; -} - -export async function completePlatformAuthorization( - requestId: string | null, - code: string, - returnedState: string, -): Promise { - cleanupPendingAuthorizations(); - const pending = takePendingAuthorization(requestId, returnedState); - if (!pending || pending.expiresAt <= Date.now()) throw new Error('OIDC authorization request expired'); - if (pending.state !== returnedState) throw new Error('OIDC authorization state mismatch'); - - const metadata = await getMetadata(); - const response = await proxyAwareFetch(metadata.token_endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - code, - redirect_uri: REDIRECT_URI, - client_id: configuredClientId(), - code_verifier: pending.codeVerifier, - }).toString(), - }); - if (!response.ok) { - const detail = await response.text(); - throw new Error(`OIDC token exchange failed (${response.status}): ${detail.slice(0, 180)}`); - } - return readTokenPayload(await response.json()); -} - -export function resetPlatformAuthorizationForTests(): void { - pendingAuthorizations.clear(); - metadataPromise = null; -} - -export const PLATFORM_AUTH_REDIRECT_URI = REDIRECT_URI; diff --git a/src/App.tsx b/src/App.tsx index c133a156..c6c028e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -284,14 +284,7 @@ function App() { useEffect(() => { const unsubscribe = subscribeHostEvent('auth:session-changed', (session) => { - const auth = useAuthStore.getState(); - auth.applyMainSession(session); - // A platform OIDC callback is committed by Main and only carries the - // session credentials. Reload the current account so the renderer gets - // the stable username and module permissions before routing. - if (session) { - void auth.init().catch(() => undefined); - } + useAuthStore.getState().applyMainSession(session); }); return unsubscribe; }, []); diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index f68c28ea..263db02b 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -6,7 +6,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { hostApiFetch } from '@/lib/host-api'; -import { invokeIpc } from '@/lib/api-client'; import { useAuthStore } from '@/stores/auth'; import { useProviderStore } from '@/stores/providers'; import logoSvg from '@/assets/logo.svg'; @@ -308,30 +307,6 @@ export function Login() { } }; - const handlePlatformLogin = async () => { - if (!agreed || busy) return; - setSubmitError(null); - setSubmitting(true); - try { - const response = await hostApiFetch<{ - success?: unknown; - authorizationUrl?: unknown; - }>('/api/auth/platform/start', { - method: 'POST', - cache: 'no-store', - body: JSON.stringify({}), - }); - if (response.success !== true) throw new Error('平台登录暂时不可用,请稍后重试。'); - const authorizationUrl = getSafeExternalUrl(response.authorizationUrl); - if (!authorizationUrl) throw new Error('平台登录地址无效,请稍后重试。'); - await invokeIpc('shell:openExternal', authorizationUrl); - } catch (loginError) { - setSubmitError(loginError instanceof Error ? loginError.message : String(loginError)); - } finally { - setSubmitting(false); - } - }; - const handleSendCode = async () => { if ( sendingCode @@ -489,17 +464,6 @@ export function Login() { )} - -