Files
pangolin/web/usercenter/lib/api/mock.ts
T
wangjia ec0942e1e7
Deploy Server / deploy-server (push) Successful in 2m47s
feat(usercenter): SSO /sso 落地页(拿票兑会话→自动登录)
App 打开 app.yanmeiai.com/sso?t=<ticket>&redirect=<路径>;本页 exchangeWebTicket→setSession
(与正常登录同路径)→跳白名单相对路径。镜像 jiu sso.njk。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 00:02:48 +08:00

174 lines
5.4 KiB
TypeScript

// mock.ts — MockClient:演示数据,供阶段 A 的 UI 验收使用,零网络依赖。
import {
ApiClient,
ApiError,
Device,
LoginResult,
Me,
RedeemResult,
Session,
SubscriptionInfo,
TotpSetup,
} from './types';
import { setSession, clearSession } from './session';
const delay = (ms = 420) => new Promise((r) => setTimeout(r, ms));
function makeSession(): Session {
return {
accessToken: 'mock-access-' + Math.random().toString(36).slice(2),
refreshToken: 'mock-refresh-' + Math.random().toString(36).slice(2),
accessExpiresAt: Date.now() + 10 * 60_000,
};
}
export class MockClient implements ApiClient {
// 演示态:PRO 会员,已开启 2FA(故登录走二段式)
private totpEnabled = true;
private pending: string | null = null;
private subUrl = 'https://sub.pangolin.vpn/s/8f3kx92m';
private devices: Device[] = [
{ id: 'd1', name: 'iPhone 15 Pro', platform: 'iOS 18', lastActive: '刚刚', current: true },
{ id: 'd2', name: 'MacBook Air', platform: 'macOS 15', lastActive: '2 小时前', current: false },
];
async login(email: string, password: string): Promise<LoginResult> {
await delay();
// 演示锁定/失败:用特定凭证触发,便于验收双语文案
if (password === 'locked') {
throw new ApiError(
{ code: 'account_locked', message_zh: '失败次数过多,账户已临时锁定', message_en: 'Too many attempts — account temporarily locked' },
30,
);
}
if (password === 'wrong' || !/\S+@\S+\.\S+/.test(email)) {
throw new ApiError({ code: 'invalid_credentials', message_zh: '邮箱或密码错误', message_en: 'Wrong email or password' });
}
if (this.totpEnabled) {
this.pending = 'mock-pending-' + Math.random().toString(36).slice(2);
return { kind: 'totp_required', pendingToken: this.pending };
}
const s = makeSession();
setSession(s);
return { kind: 'session', session: s };
}
async loginTotp(_pendingToken: string, code: string): Promise<Session> {
await delay();
if (code === '000000') {
throw new ApiError({ code: 'totp_invalid', message_zh: '动态码不正确,请重试', message_en: 'Invalid code, try again' });
}
const s = makeSession();
setSession(s);
this.pending = null;
return s;
}
// 演示态:任意非 'invalid' 票据都兑换成功;'invalid' 用于验收失败态文案。
async exchangeWebTicket(ticket: string): Promise<Session> {
await delay(300);
if (ticket === 'invalid') {
throw new ApiError({
code: 'auth.ticket_invalid',
message_zh: '登录票据无效或已过期,请重新从 App 打开',
message_en: 'Login ticket is invalid or expired, please reopen from the app',
});
}
const s = makeSession();
setSession(s);
return s;
}
async refresh(): Promise<Session> {
await delay(150);
const s = makeSession();
setSession(s);
return s;
}
async getMe(): Promise<Me> {
await delay(260);
return {
email: 'me@pangolin.vpn',
plan: 'pro',
expiresAt: '2026-12-31',
devicesUsed: 2,
devicesMax: 5,
quotaTodayMin: null,
dataTodayGB: 0.8,
weeklyGB: [1.8, 2.4, 1.2, 3.1, 4.0, 5.2, 2.6],
totpEnabled: this.totpEnabled,
};
}
async getSubscription(): Promise<SubscriptionInfo> {
await delay(160);
return { url: this.subUrl };
}
async resetSubscription(): Promise<SubscriptionInfo> {
await delay(500);
this.subUrl = 'https://sub.pangolin.vpn/s/' + Math.random().toString(36).slice(2, 10);
return { url: this.subUrl };
}
async listDevices(): Promise<Device[]> {
await delay(220);
return [...this.devices];
}
async removeDevice(id: string): Promise<void> {
await delay(360);
this.devices = this.devices.filter((d) => d.id !== id);
}
async redeem(code: string): Promise<RedeemResult> {
await delay(520);
const c = code.trim().toUpperCase();
if (c === 'LOCKED') {
throw new ApiError(
{ code: 'rate_limited', message_zh: '操作过于频繁,请稍后再试', message_en: 'Too many requests, slow down' },
20,
);
}
if (c === 'USED') {
throw new ApiError({ code: 'code_used', message_zh: '激活码已被使用', message_en: 'Activation code already used' });
}
if (c.length < 4 || c === 'INVALID') {
throw new ApiError({ code: 'code_invalid', message_zh: '激活码无效', message_en: 'Invalid activation code' });
}
return { plan: 'pro', expiresAt: '2027-12-31' };
}
async totpSetup(): Promise<TotpSetup> {
await delay(260);
const secret = 'JBSWY3DPEHPK3PXP';
return {
secret,
otpauthUri: `otpauth://totp/Pangolin:me@pangolin.vpn?secret=${secret}&issuer=Pangolin`,
};
}
async totpVerify(code: string): Promise<void> {
await delay(300);
if (code === '000000' || code.length !== 6) {
throw new ApiError({ code: 'totp_invalid', message_zh: '动态码不正确,请重试', message_en: 'Invalid code, try again' });
}
this.totpEnabled = true;
}
async totpDisable(code: string): Promise<void> {
await delay(300);
if (code === '000000' || code.length !== 6) {
throw new ApiError({ code: 'totp_invalid', message_zh: '动态码不正确,请重试', message_en: 'Invalid code, try again' });
}
this.totpEnabled = false;
}
async logout(): Promise<void> {
await delay(120);
this.pending = null;
clearSession();
}
}