+160
-7
@@ -1,3 +1,10 @@
|
||||
import type {
|
||||
AuthenticationResponseJSON,
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/browser'
|
||||
|
||||
import { mockOn, mocked, request } from './request'
|
||||
import type {
|
||||
CredentialsInfo,
|
||||
@@ -5,6 +12,9 @@ import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
OAuthSettings,
|
||||
PasskeyCeremonyOptions,
|
||||
PasskeyInfo,
|
||||
SessionItem,
|
||||
SessionRefresh,
|
||||
TotpSetup,
|
||||
TotpStatus,
|
||||
@@ -32,7 +42,36 @@ export function logout(): Promise<void> {
|
||||
/** 撤销全部会话:旧 token 全部失效,响应带操作者的新会话 */
|
||||
export function revokeSessions(): Promise<SessionRefresh> {
|
||||
if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300)
|
||||
return request('/auth/revoke-sessions', { method: 'POST' })
|
||||
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<void> {
|
||||
if (mockOn) return mocked(undefined, 250)
|
||||
return request(`/auth/sessions/${id}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
// ---- 登录凭据 ----
|
||||
@@ -45,13 +84,17 @@ export function getCredentials(): Promise<CredentialsInfo> {
|
||||
/** 修改用户名 / 密码;成功后旧 token 全部失效,响应带操作者的新会话 */
|
||||
export function updateCredentials(body: UpdateCredentialsRequest): Promise<SessionRefresh> {
|
||||
if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 400)
|
||||
return request('/auth/credentials', { method: 'PUT', body })
|
||||
return request('/auth/credentials', { method: 'PUT', body, refreshesSession: true })
|
||||
}
|
||||
|
||||
/** 保存密码登录禁用开关;成功后旧 token 全部失效,响应带新会话 */
|
||||
export function updatePasswordLogin(disabled: boolean): Promise<SessionRefresh> {
|
||||
if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300)
|
||||
return request('/auth/password-login', { method: 'PUT', body: { disabled } })
|
||||
return request('/auth/password-login', {
|
||||
method: 'PUT',
|
||||
body: { disabled },
|
||||
refreshesSession: true,
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 两步验证(TOTP) ----
|
||||
@@ -74,22 +117,29 @@ export function setupTotp(): Promise<TotpSetup> {
|
||||
/** 激活两步验证;成功后旧 token 全部失效,响应带新会话 */
|
||||
export function activateTotp(code: string): Promise<SessionRefresh> {
|
||||
if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300)
|
||||
return request('/auth/totp/activate', { method: 'POST', body: { code } })
|
||||
return request('/auth/totp/activate', {
|
||||
method: 'POST',
|
||||
body: { code },
|
||||
refreshesSession: true,
|
||||
})
|
||||
}
|
||||
|
||||
/** 停用两步验证;密码或当前验证码任一确认,响应带新会话 */
|
||||
export function disableTotp(body: { password?: string; code?: string }): Promise<SessionRefresh> {
|
||||
if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300)
|
||||
return request('/auth/totp/disable', { method: 'POST', body })
|
||||
return request('/auth/totp/disable', { method: 'POST', body, refreshesSession: true })
|
||||
}
|
||||
|
||||
// ---- 外部身份(OAuth) ----
|
||||
|
||||
/** 可登录的 provider 列表(登录页公开接口;已禁用的不返回);
|
||||
* passwordLoginDisabled 为 true 且有可用 provider 时,登录页隐藏密码表单 */
|
||||
* passwordLoginDisabled 为 true 且有可用 provider 时,登录页隐藏密码表单;
|
||||
* passkeyLogin / walletLogin 为 true 表示存在对应凭据,登录页显示入口 */
|
||||
export function getOauthProviders(): Promise<{
|
||||
providers: OauthProviderInfo[]
|
||||
passwordLoginDisabled: boolean
|
||||
passkeyLogin: boolean
|
||||
walletLogin: boolean
|
||||
}> {
|
||||
if (mockOn)
|
||||
return mocked({
|
||||
@@ -98,6 +148,8 @@ export function getOauthProviders(): Promise<{
|
||||
{ provider: 'oidc', displayName: 'OIDC SSO' },
|
||||
],
|
||||
passwordLoginDisabled: false,
|
||||
passkeyLogin: true,
|
||||
walletLogin: true,
|
||||
})
|
||||
return request('/auth/oauth/providers')
|
||||
}
|
||||
@@ -124,7 +176,108 @@ export function listIdentities(): Promise<{ items: UserIdentityInfo[] }> {
|
||||
/** 解绑外部身份;成功后旧 token 全部失效,响应带新会话 */
|
||||
export function unbindIdentity(id: number): Promise<SessionRefresh> {
|
||||
if (mockOn) return mocked({ token: 'mock-token-2', expiresAt: '2099-01-01T00:00:00Z' }, 300)
|
||||
return request(`/auth/identities/${id}`, { method: 'DELETE' })
|
||||
return request(`/auth/identities/${id}`, { method: 'DELETE', refreshesSession: true })
|
||||
}
|
||||
|
||||
// ---- 通行密钥(Passkey) ----
|
||||
|
||||
const mockPasskeyRegisterOptions: PasskeyCeremonyOptions<PublicKeyCredentialCreationOptionsJSON> = {
|
||||
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<PublicKeyCredentialCreationOptionsJSON>
|
||||
> {
|
||||
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<SessionRefresh> {
|
||||
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<SessionRefresh> {
|
||||
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<PublicKeyCredentialRequestOptionsJSON>
|
||||
> {
|
||||
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<LoginResponse> {
|
||||
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<LoginResponse> {
|
||||
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 配置(设置页) ----
|
||||
|
||||
Reference in New Issue
Block a user