feat: add delete confirmation modal and enhance token handling

- Implemented a DeleteConfirmModal component for confirming deletions across various pages (Accounts, Sites, Tokens).
- Updated Accounts and Sites pages to utilize the new modal for single and batch deletions.
- Enhanced TokensPanel to support group selection and improved token deletion handling with confirmation.
- Added functionality to skip confirmation for batch actions and handle token group fetching with error management.
- Improved error handling for expired sessions in NewApiAdapter and OneApiAdapter.
- Added tests for new delete confirmation behavior and token editing interactions.
This commit is contained in:
ciat-777
2026-03-14 13:31:15 +08:00
parent ad791d87bb
commit 1a1f0452b3
18 changed files with 595 additions and 58 deletions
@@ -16,6 +16,7 @@ const CHECKIN_ALREADY_TOKEN = 'checkin-already-token';
const CHECKIN_INVALID_URL_TOKEN = 'checkin-invalid-url-token';
const CHECKIN_CLOUDFLARE_530_TOKEN = 'checkin-cloudflare-530-token';
const BALANCE_FAIL_TOKEN = 'balance-fail-token';
const GROUP_EXPIRED_TOKEN = 'group-expired-token';
const SHIELD_LOGIN_USERNAME = 'shield-user';
const SHIELD_LOGIN_PASSWORD = 'shield-pass';
const SHIELD_LOGIN_TOKEN = 'login-session-token';
@@ -366,6 +367,19 @@ describe('NewApiAdapter', () => {
}
}
if (req.url === '/api/user/self/groups') {
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${GROUP_EXPIRED_TOKEN}`) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, message: 'access token expired' }));
return;
}
if (typeof req.headers.authorization === 'string' && req.headers.authorization === 'Bearer session-token') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, data: { default: true, gemini: true } }));
return;
}
}
if (req.url === '/api/user/sign_in') {
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${CHECKIN_ALREADY_TOKEN}`)) {
res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -556,6 +570,20 @@ describe('NewApiAdapter', () => {
expect(result.message).toBe('今天已经签到过啦');
});
it('returns clean groups from data object without envelope keys', async () => {
const adapter = new NewApiAdapter();
const groups = await adapter.getUserGroups(baseUrl, 'session-token', 11494);
expect(groups).toEqual(['default', 'gemini']);
expect(groups).not.toContain('success');
expect(groups).not.toContain('message');
});
it('throws expired-session error when group endpoint reports invalid access token', async () => {
const adapter = new NewApiAdapter();
await expect(adapter.getUserGroups(baseUrl, GROUP_EXPIRED_TOKEN, 11494)).rejects.toThrow('账号会话可能已过期');
});
it('sends all compatibility user-id headers when userId is known', async () => {
await new Promise<void>((resolve, reject) => {
server.close((err?: Error) => (err ? reject(err) : resolve()));
+37
View File
@@ -248,6 +248,10 @@ export class NewApiAdapter extends BasePlatformAdapter {
}
private parseGroupKeys(payload: any): string[] {
if (payload && typeof payload === 'object' && payload?.success === false) {
return [];
}
const source = payload?.data ?? payload;
if (Array.isArray(source)) {
return source
@@ -258,12 +262,28 @@ export class NewApiAdapter extends BasePlatformAdapter {
if (source && typeof source === 'object') {
return Object.keys(source)
.map((key) => key.trim())
.filter((key) => !['success', 'message', 'code', 'data', 'error'].includes(key.toLowerCase()))
.filter(Boolean);
}
return [];
}
private resolveGroupFetchErrorMessage(payload: any): string {
const message = typeof payload?.message === 'string' ? payload.message.trim() : '';
const normalized = message.toLowerCase();
const indicatesExpired = normalized.includes('expired')
|| normalized.includes('invalid token')
|| normalized.includes('access token')
|| normalized.includes('unauthorized')
|| normalized.includes('forbidden')
|| normalized.includes('未登录')
|| normalized.includes('登录')
|| normalized.includes('过期');
if (indicatesExpired) return '账号会话可能已过期,请重新登录后再拉取分组';
return message || '拉取分组失败';
}
private normalizeTokenItems(items: any[]): ApiTokenInfo[] {
const normalized: ApiTokenInfo[] = [];
for (let index = 0; index < items.length; index += 1) {
@@ -1141,11 +1161,15 @@ export class NewApiAdapter extends BasePlatformAdapter {
async getUserGroups(baseUrl: string, accessToken: string, platformUserId?: number): Promise<string[]> {
const resolvedUserId = platformUserId || await this.discoverUserId(baseUrl, accessToken);
const dedupe = (groups: string[]) => Array.from(new Set(groups.map((item) => item.trim()).filter(Boolean)));
let terminalError: string | null = null;
try {
const res = await this.fetchJson<any>(`${baseUrl}/api/user/self/groups`, {
headers: this.authHeaders(accessToken, resolvedUserId || undefined),
});
if (res?.success === false) {
terminalError = this.resolveGroupFetchErrorMessage(res);
}
const parsed = dedupe(this.parseGroupKeys(res));
if (parsed.length > 0) return parsed;
} catch {}
@@ -1154,6 +1178,9 @@ export class NewApiAdapter extends BasePlatformAdapter {
const res = await this.fetchJson<any>(`${baseUrl}/api/user_group_map`, {
headers: this.authHeaders(accessToken, resolvedUserId || undefined),
});
if (res?.success === false) {
terminalError = this.resolveGroupFetchErrorMessage(res);
}
const parsed = dedupe(this.parseGroupKeys(res));
if (parsed.length > 0) return parsed;
} catch {}
@@ -1165,17 +1192,27 @@ export class NewApiAdapter extends BasePlatformAdapter {
try {
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/self/groups`, { headers });
if (res?.success === false) {
terminalError = this.resolveGroupFetchErrorMessage(res);
}
const parsed = dedupe(this.parseGroupKeys(res));
if (parsed.length > 0) return parsed;
} catch {}
try {
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user_group_map`, { headers });
if (res?.success === false) {
terminalError = this.resolveGroupFetchErrorMessage(res);
}
const parsed = dedupe(this.parseGroupKeys(res));
if (parsed.length > 0) return parsed;
} catch {}
}
if (terminalError) {
throw new Error(terminalError);
}
return ['default'];
}
+26 -1
View File
@@ -129,24 +129,49 @@ export class OneApiAdapter extends BasePlatformAdapter {
async getUserGroups(baseUrl: string, accessToken: string): Promise<string[]> {
const headers = { Authorization: `Bearer ${accessToken}` };
const resolveGroupFetchErrorMessage = (payload: any): string => {
const message = typeof payload?.message === 'string' ? payload.message.trim() : '';
const normalized = message.toLowerCase();
const indicatesExpired = normalized.includes('expired')
|| normalized.includes('invalid token')
|| normalized.includes('access token')
|| normalized.includes('unauthorized')
|| normalized.includes('forbidden')
|| normalized.includes('未登录')
|| normalized.includes('登录')
|| normalized.includes('过期');
if (indicatesExpired) return '账号会话可能已过期,请重新登录后再拉取分组';
return message || '拉取分组失败';
};
const extractGroupKeys = (payload: any): string[] => {
if (payload && typeof payload === 'object' && payload?.success === false) return [];
const source = payload?.data || payload;
if (!source || typeof source !== 'object') return [];
return Object.keys(source).map((key) => key.trim()).filter(Boolean);
return Object.keys(source)
.map((key) => key.trim())
.filter((key) => !['success', 'message', 'code', 'data', 'error'].includes(key.toLowerCase()))
.filter(Boolean);
};
let terminalError: string | null = null;
try {
const groupMap = await this.fetchJson<any>(`${baseUrl}/api/user_group_map`, { headers });
if (groupMap?.success === false) terminalError = resolveGroupFetchErrorMessage(groupMap);
const keys = extractGroupKeys(groupMap);
if (keys.length > 0) return Array.from(new Set(keys));
} catch {}
try {
const groups = await this.fetchJson<any>(`${baseUrl}/api/user/self/groups`, { headers });
if (groups?.success === false) terminalError = resolveGroupFetchErrorMessage(groups);
const keys = extractGroupKeys(groups);
if (keys.length > 0) return Array.from(new Set(keys));
} catch {}
if (terminalError) {
throw new Error(terminalError);
}
return ['default'];
}