313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
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<LoginResponse> {
|
|
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<void> {
|
|
if (mockOn) return mocked(undefined, 100)
|
|
return request('/auth/logout', { method: 'POST' })
|
|
}
|
|
|
|
// ---- 会话 ----
|
|
|
|
/** 撤销全部会话:旧 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', 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' })
|
|
}
|
|
|
|
// ---- 登录凭据 ----
|
|
|
|
export function getCredentials(): Promise<CredentialsInfo> {
|
|
if (mockOn) return mocked({ username: 'admin', passwordLoginDisabled: false })
|
|
return request('/auth/credentials')
|
|
}
|
|
|
|
/** 修改用户名 / 密码;成功后旧 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, 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 },
|
|
refreshesSession: true,
|
|
})
|
|
}
|
|
|
|
// ---- 两步验证(TOTP) ----
|
|
|
|
export function getTotpStatus(): Promise<TotpStatus> {
|
|
if (mockOn) return mocked({ enabled: false })
|
|
return request('/auth/totp')
|
|
}
|
|
|
|
/** 生成待激活密钥;10 分钟内输入验证码激活,重复调用覆盖旧暂存 */
|
|
export function setupTotp(): Promise<TotpSetup> {
|
|
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<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 },
|
|
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, 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<SessionRefresh> {
|
|
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<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 配置(设置页) ----
|
|
|
|
const mockOauth: OAuthSettings = {
|
|
oidcIssuer: '',
|
|
oidcClientId: '',
|
|
oidcSecretSet: false,
|
|
oidcDisplayName: '',
|
|
oidcDisabled: false,
|
|
githubClientId: '',
|
|
githubSecretSet: false,
|
|
githubDisplayName: '',
|
|
githubDisabled: false,
|
|
}
|
|
|
|
export function getOAuthSettings(): Promise<OAuthSettings> {
|
|
if (mockOn) return mocked({ ...mockOauth })
|
|
return request('/settings/oauth')
|
|
}
|
|
|
|
/** 字段补丁部分更新:缺省字段沿用现值,并发编辑不同 provider 互不回滚 */
|
|
export function updateOAuthSettings(body: UpdateOAuthRequest): Promise<OAuthSettings> {
|
|
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 })
|
|
}
|