import type { AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, } from '@simplewebauthn/browser' import { mockOn, mocked, request } from './request' import type { CredentialsInfo, OauthProviderInfo, LoginRequest, LoginResponse, OAuthSettings, PasskeyCeremonyOptions, PasskeyInfo, SessionItem, SessionRefresh, TotpSetup, TotpStatus, UpdateCredentialsRequest, UpdateOAuthRequest, UserIdentityInfo, } from '@/types/api' export function login(body: LoginRequest): Promise { if (mockOn) { if (body.password === 'wrong') return Promise.reject(new Error('用户名或密码错误')) return mocked({ token: 'mock-token', expiresAt: '2099-01-01T00:00:00Z' }, 400) } return request('/auth/login', { method: 'POST', body }) } /** 服务端登出:当前令牌拉黑至自然过期;调用方无论成败都应清本地会话 */ export function logout(): Promise { if (mockOn) return mocked(undefined, 100) return request('/auth/logout', { method: 'POST' }) } // ---- 会话 ---- /** 撤销全部会话:旧 token 全部失效,响应带操作者的新会话 */ export function revokeSessions(): Promise { if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/revoke-sessions', { method: 'POST', refreshesSession: true }) } /** mock 会话样例:相对当前时间偏移,保证「最近活跃」展示合理 */ function mockSessions(): { items: SessionItem[] } { const ago = (min: number) => new Date(Date.now() - min * 60000).toISOString() const later = new Date(Date.now() + 23 * 3600000).toISOString() const mk = (id: number, method: string, clientIp: string, userAgent: string, seen: number, created: number, current = false): SessionItem => ({ id, method, clientIp, userAgent, createdAt: ago(created), lastSeenAt: ago(seen), expiresAt: later, current, }) return { items: [ mk(1, 'password', '198.51.100.7', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/126.0 Safari/537.36', 0, 180, true), mk(2, 'passkey', '203.0.113.24', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 Version/17.5 Mobile/15E148 Safari/604.1', 185, 205), mk(3, 'wallet', '192.0.2.88', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0 Safari/537.36 Edg/126.0', 780, 800), ], } } /** 活跃会话列表:未撤销、未过期、版本为当前,最近活跃在前 */ export function listSessions(): Promise<{ items: SessionItem[] }> { if (mockOn) return mocked(mockSessions(), 300) return request('/auth/sessions') } /** 定点撤销一个其他会话;当前会话不可撤销(请走退出登录) */ export function revokeSession(id: number): Promise { if (mockOn) return mocked(undefined, 250) return request(`/auth/sessions/${id}`, { method: 'DELETE' }) } // ---- 登录凭据 ---- export function getCredentials(): Promise { if (mockOn) return mocked({ username: 'admin', passwordLoginDisabled: false }) return request('/auth/credentials') } /** 修改用户名 / 密码;成功后旧 token 全部失效,响应带操作者的新会话 */ export function updateCredentials(body: UpdateCredentialsRequest): Promise { if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 400) return request('/auth/credentials', { method: 'PUT', body, refreshesSession: true }) } /** 保存密码登录禁用开关;成功后旧 token 全部失效,响应带新会话 */ export function updatePasswordLogin(disabled: boolean): Promise { if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/password-login', { method: 'PUT', body: { disabled }, refreshesSession: true, }) } // ---- 两步验证(TOTP) ---- export function getTotpStatus(): Promise { if (mockOn) return mocked({ enabled: false }) return request('/auth/totp') } /** 生成待激活密钥;10 分钟内输入验证码激活,重复调用覆盖旧暂存 */ export function setupTotp(): Promise { if (mockOn) return mocked({ secret: 'JBSWY3DPEHPK3PXP', otpauthUri: 'otpauth://totp/oci-portal:admin?secret=JBSWY3DPEHPK3PXP&issuer=oci-portal', }) return request('/auth/totp/setup', { method: 'POST' }) } /** 激活两步验证;成功后旧 token 全部失效,响应带新会话 */ export function activateTotp(code: string): Promise { if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/totp/activate', { method: 'POST', body: { code }, refreshesSession: true, }) } /** 停用两步验证;密码或当前验证码任一确认,响应带新会话 */ export function disableTotp(body: { password?: string; code?: string }): Promise { if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/totp/disable', { method: 'POST', body, refreshesSession: true }) } // ---- 外部身份(OAuth) ---- /** 可登录的 provider 列表(登录页公开接口;已禁用的不返回); * passwordLoginDisabled 为 true 且有可用 provider 时,登录页隐藏密码表单; * passkeyLogin / walletLogin 为 true 表示存在对应凭据,登录页显示入口 */ export function getOauthProviders(): Promise<{ providers: OauthProviderInfo[] passwordLoginDisabled: boolean passkeyLogin: boolean walletLogin: boolean }> { if (mockOn) return mocked({ providers: [ { provider: 'github', displayName: 'GitHub' }, { provider: 'oidc', displayName: 'OIDC SSO' }, ], passwordLoginDisabled: false, passkeyLogin: true, walletLogin: true, }) return request('/auth/oauth/providers') } /** 获取授权跳转 URL;bind 模式要求已登录 */ export function getOauthAuthorizeUrl( provider: string, mode: 'login' | 'bind', ): Promise<{ url: string }> { if (mockOn) return mocked({ url: 'https://example.com/oauth/authorize?mock=1' }) return request(`/auth/oauth/${provider}/authorize`, { query: { mode } }) } export function listIdentities(): Promise<{ items: UserIdentityInfo[] }> { if (mockOn) return mocked({ items: [ { id: 1, provider: 'github', display: 'octocat', createdAt: '2026-07-07T10:00:00+08:00' }, ], }) return request('/auth/identities') } /** 解绑外部身份;成功后旧 token 全部失效,响应带新会话 */ export function unbindIdentity(id: number): Promise { if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request(`/auth/identities/${id}`, { method: 'DELETE', refreshesSession: true }) } // ---- 通行密钥(Passkey) ---- const mockPasskeyRegisterOptions: PasskeyCeremonyOptions = { sessionId: 'mock-session', options: { publicKey: { challenge: 'bW9jaw', rp: { id: 'localhost', name: 'OCI Portal' }, user: { id: 'AQ', name: 'admin', displayName: 'admin' }, pubKeyCredParams: [{ type: 'public-key', alg: -7 }], }, }, } /** 发起通行密钥注册;sessionId 需原样带回 finish */ export function beginPasskeyRegister(): Promise< PasskeyCeremonyOptions > { if (mockOn) return mocked(mockPasskeyRegisterOptions, 300) return request('/auth/passkey/register/begin', { method: 'POST' }) } /** 完成通行密钥注册;成功后旧 token 全部失效,响应带新会话 */ export function finishPasskeyRegister( sessionId: string, name: string, credential: RegistrationResponseJSON, ): Promise { if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/passkey/register/finish', { method: 'POST', body: { sessionId, name, credential }, refreshesSession: true, }) } export function listPasskeys(): Promise<{ items: PasskeyInfo[] }> { if (mockOn) return mocked({ items: [ { id: 1, name: 'MacBook Touch ID', createdAt: '2026-07-20T10:00:00+08:00', lastUsedAt: null }, ], }) return request('/auth/passkeys') } /** 删除通行密钥;成功后旧 token 全部失效,响应带新会话 */ export function removePasskey(id: number): Promise { if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request(`/auth/passkeys/${id}`, { method: 'DELETE', refreshesSession: true }) } /** 发起通行密钥登录(公开接口) */ export function beginPasskeyLogin(): Promise< PasskeyCeremonyOptions > { if (mockOn) return mocked({ sessionId: 'mock-session', options: { publicKey: { challenge: 'bW9jaw', rpId: 'localhost' } }, }) return request('/auth/passkey/login/begin', { method: 'POST' }) } /** 完成通行密钥登录;UV 通过后豁免 TOTP */ export function finishPasskeyLogin( sessionId: string, credential: AuthenticationResponseJSON, ): Promise { if (mockOn) return mocked({ token: 'mock-token', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/passkey/login/finish', { method: 'POST', body: { sessionId, credential }, }) } // ---- Web3 钱包(SIWE) ---- /** 发起钱包签名挑战;mode=bind 要求已登录,message 需原样 personal_sign */ export function getWalletChallenge( address: string, mode: 'login' | 'bind', ): Promise<{ nonce: string; message: string }> { if (mockOn) return mocked({ nonce: 'mock-nonce', message: `localhost wants you to sign in with your Ethereum account:\n${address}\n\n登录 OCI Portal 面板\n\nNonce: mock-nonce`, }) return request('/auth/wallet/challenge', { method: 'POST', body: { address, mode } }) } /** 校验钱包签名:login 返回新会话;bind 返回换发的新 token(旧会话已失效) */ export function verifyWallet(nonce: string, signature: string): Promise { if (mockOn) return mocked({ token: 'mock-token', expiresAt: '2099-01-01T00:00:00Z' }, 300) return request('/auth/wallet/verify', { method: 'POST', body: { nonce, signature }, refreshesSession: true, }) } // ---- OAuth provider 配置(设置页) ---- const mockOauth: OAuthSettings = { oidcIssuer: '', oidcClientId: '', oidcSecretSet: false, oidcDisplayName: '', oidcDisabled: false, githubClientId: '', githubSecretSet: false, githubDisplayName: '', githubDisabled: false, } export function getOAuthSettings(): Promise { if (mockOn) return mocked({ ...mockOauth }) return request('/settings/oauth') } /** 字段补丁部分更新:缺省字段沿用现值,并发编辑不同 provider 互不回滚 */ export function updateOAuthSettings(body: UpdateOAuthRequest): Promise { if (mockOn) { const { oidcClientSecret, githubClientSecret, ...rest } = body Object.assign(mockOauth, Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined))) if (oidcClientSecret !== undefined) mockOauth.oidcSecretSet = oidcClientSecret !== '' if (githubClientSecret !== undefined) mockOauth.githubSecretSet = githubClientSecret !== '' return mocked({ ...mockOauth }) } return request('/settings/oauth', { method: 'PATCH', body }) }