mirror of
https://gitee.com/wanwujie/sub2api-mobile
synced 2026-04-03 06:52:14 +08:00
345 lines
9.9 KiB
TypeScript
345 lines
9.9 KiB
TypeScript
import * as SecureStore from 'expo-secure-store';
|
|
import { Platform } from 'react-native';
|
|
const { proxy } = require('valtio');
|
|
|
|
const BASE_URL_KEY = 'sub2api_base_url';
|
|
const ADMIN_KEY_KEY = 'sub2api_admin_api_key';
|
|
const ACCOUNTS_KEY = 'sub2api_accounts';
|
|
const ACTIVE_ACCOUNT_ID_KEY = 'sub2api_active_account_id';
|
|
const IS_WEB = Platform.OS === 'web';
|
|
|
|
export type AdminAccountProfile = {
|
|
id: string;
|
|
label: string;
|
|
baseUrl: string;
|
|
adminApiKey: string;
|
|
updatedAt: string;
|
|
enabled?: boolean;
|
|
};
|
|
|
|
function createAccountId() {
|
|
return `acct_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
function getAccountLabel(baseUrl: string) {
|
|
try {
|
|
const url = new URL(baseUrl);
|
|
return url.host || baseUrl;
|
|
} catch {
|
|
return baseUrl;
|
|
}
|
|
}
|
|
|
|
function normalizeConfig(input: { baseUrl: string; adminApiKey: string }) {
|
|
return {
|
|
baseUrl: input.baseUrl.trim().replace(/\/$/, ''),
|
|
adminApiKey: input.adminApiKey.trim(),
|
|
};
|
|
}
|
|
|
|
function sortAccounts(accounts: AdminAccountProfile[]) {
|
|
return [...accounts].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
}
|
|
|
|
function normalizeAccount(account: AdminAccountProfile): AdminAccountProfile {
|
|
return {
|
|
...account,
|
|
adminApiKey: account.adminApiKey ?? '',
|
|
enabled: account.enabled ?? true,
|
|
};
|
|
}
|
|
|
|
function sanitizeAccountsForWeb(accounts: AdminAccountProfile[]) {
|
|
if (!IS_WEB) {
|
|
return accounts;
|
|
}
|
|
|
|
return accounts.map((account) => ({
|
|
...account,
|
|
adminApiKey: '',
|
|
}));
|
|
}
|
|
|
|
function persistAdminApiKey(value: string) {
|
|
if (IS_WEB) {
|
|
return deleteItem(ADMIN_KEY_KEY);
|
|
}
|
|
|
|
return setItem(ADMIN_KEY_KEY, value);
|
|
}
|
|
|
|
function persistAccounts(accounts: AdminAccountProfile[]) {
|
|
return setItem(ACCOUNTS_KEY, JSON.stringify(sanitizeAccountsForWeb(accounts)));
|
|
}
|
|
|
|
export function hasAuthenticatedAdminSession(config: { baseUrl: string; adminApiKey: string }) {
|
|
const hasBaseUrl = Boolean(config.baseUrl.trim());
|
|
|
|
if (!hasBaseUrl) {
|
|
return false;
|
|
}
|
|
|
|
if (!IS_WEB) {
|
|
return true;
|
|
}
|
|
|
|
return Boolean(config.adminApiKey.trim());
|
|
}
|
|
|
|
function getNextActiveAccount(accounts: AdminAccountProfile[], activeAccountId?: string) {
|
|
const enabledAccounts = accounts.filter((account) => account.enabled !== false);
|
|
|
|
if (activeAccountId) {
|
|
const preferred = enabledAccounts.find((account) => account.id === activeAccountId);
|
|
if (preferred) {
|
|
return preferred;
|
|
}
|
|
}
|
|
|
|
return enabledAccounts[0];
|
|
}
|
|
|
|
export function getDefaultAdminConfig() {
|
|
return {
|
|
baseUrl: '',
|
|
adminApiKey: '',
|
|
};
|
|
}
|
|
|
|
async function getItem(key: string) {
|
|
try {
|
|
if (Platform.OS === 'web') {
|
|
if (typeof localStorage === 'undefined') {
|
|
return null;
|
|
}
|
|
|
|
return localStorage.getItem(key);
|
|
}
|
|
|
|
return await SecureStore.getItemAsync(key);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function setItem(key: string, value: string) {
|
|
try {
|
|
if (Platform.OS === 'web') {
|
|
if (typeof localStorage !== 'undefined') {
|
|
localStorage.setItem(key, value);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
await SecureStore.setItemAsync(key, value);
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function deleteItem(key: string) {
|
|
try {
|
|
if (Platform.OS === 'web') {
|
|
if (typeof localStorage !== 'undefined') {
|
|
localStorage.removeItem(key);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
await SecureStore.deleteItemAsync(key);
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
|
|
export const adminConfigState = proxy({
|
|
...getDefaultAdminConfig(),
|
|
accounts: [] as AdminAccountProfile[],
|
|
activeAccountId: '',
|
|
hydrated: false,
|
|
saving: false,
|
|
});
|
|
|
|
export async function hydrateAdminConfig() {
|
|
const defaults = getDefaultAdminConfig();
|
|
|
|
try {
|
|
const [baseUrl, adminApiKey, rawAccounts, activeAccountId] = await Promise.all([
|
|
getItem(BASE_URL_KEY),
|
|
getItem(ADMIN_KEY_KEY),
|
|
getItem(ACCOUNTS_KEY),
|
|
getItem(ACTIVE_ACCOUNT_ID_KEY),
|
|
]);
|
|
|
|
let accounts: AdminAccountProfile[] = [];
|
|
|
|
if (rawAccounts) {
|
|
try {
|
|
const parsed = JSON.parse(rawAccounts) as AdminAccountProfile[];
|
|
accounts = Array.isArray(parsed) ? sanitizeAccountsForWeb(parsed.map((account) => normalizeAccount(account))) : [];
|
|
} catch {
|
|
accounts = [];
|
|
}
|
|
}
|
|
|
|
if (accounts.length === 0 && baseUrl) {
|
|
const legacyConfig = normalizeConfig({
|
|
baseUrl,
|
|
adminApiKey: IS_WEB ? defaults.adminApiKey : adminApiKey ?? defaults.adminApiKey,
|
|
});
|
|
|
|
accounts = [
|
|
{
|
|
id: createAccountId(),
|
|
label: getAccountLabel(legacyConfig.baseUrl),
|
|
...legacyConfig,
|
|
updatedAt: new Date().toISOString(),
|
|
enabled: true,
|
|
},
|
|
];
|
|
}
|
|
|
|
const sortedAccounts = sortAccounts(accounts);
|
|
const activeAccount = getNextActiveAccount(sortedAccounts, activeAccountId ?? undefined);
|
|
const nextActiveAccountId = activeAccount?.id || '';
|
|
|
|
adminConfigState.accounts = sortedAccounts;
|
|
adminConfigState.activeAccountId = nextActiveAccountId;
|
|
adminConfigState.baseUrl = activeAccount?.baseUrl ?? defaults.baseUrl;
|
|
adminConfigState.adminApiKey = activeAccount?.adminApiKey ?? defaults.adminApiKey;
|
|
|
|
await Promise.all([
|
|
persistAccounts(sortedAccounts),
|
|
nextActiveAccountId ? setItem(ACTIVE_ACCOUNT_ID_KEY, nextActiveAccountId) : deleteItem(ACTIVE_ACCOUNT_ID_KEY),
|
|
setItem(BASE_URL_KEY, activeAccount?.baseUrl ?? defaults.baseUrl),
|
|
persistAdminApiKey(activeAccount?.adminApiKey ?? defaults.adminApiKey),
|
|
]);
|
|
} finally {
|
|
adminConfigState.hydrated = true;
|
|
}
|
|
}
|
|
|
|
export async function saveAdminConfig(input: { baseUrl: string; adminApiKey: string }) {
|
|
adminConfigState.saving = true;
|
|
|
|
try {
|
|
const normalized = normalizeConfig(input);
|
|
const nextUpdatedAt = new Date().toISOString();
|
|
const existingAccount = adminConfigState.accounts.find(
|
|
(account: AdminAccountProfile) => account.baseUrl === normalized.baseUrl && account.adminApiKey === normalized.adminApiKey
|
|
);
|
|
const nextAccount: AdminAccountProfile = existingAccount
|
|
? {
|
|
...existingAccount,
|
|
label: getAccountLabel(normalized.baseUrl),
|
|
updatedAt: nextUpdatedAt,
|
|
}
|
|
: {
|
|
id: createAccountId(),
|
|
label: getAccountLabel(normalized.baseUrl),
|
|
...normalized,
|
|
updatedAt: nextUpdatedAt,
|
|
enabled: true,
|
|
};
|
|
const nextAccounts = sortAccounts([
|
|
nextAccount,
|
|
...adminConfigState.accounts.filter((account: AdminAccountProfile) => account.id !== nextAccount.id),
|
|
]);
|
|
|
|
await Promise.all([
|
|
setItem(BASE_URL_KEY, normalized.baseUrl),
|
|
persistAdminApiKey(normalized.adminApiKey),
|
|
persistAccounts(nextAccounts),
|
|
setItem(ACTIVE_ACCOUNT_ID_KEY, nextAccount.id),
|
|
]);
|
|
|
|
adminConfigState.accounts = nextAccounts;
|
|
adminConfigState.activeAccountId = nextAccount.id;
|
|
adminConfigState.baseUrl = normalized.baseUrl;
|
|
adminConfigState.adminApiKey = normalized.adminApiKey;
|
|
} finally {
|
|
adminConfigState.saving = false;
|
|
}
|
|
}
|
|
|
|
export async function switchAdminAccount(accountId: string) {
|
|
const account = adminConfigState.accounts.find((item: AdminAccountProfile) => item.id === accountId);
|
|
|
|
if (!account) {
|
|
return;
|
|
}
|
|
|
|
if (account.enabled === false) {
|
|
return;
|
|
}
|
|
|
|
const nextAccount = {
|
|
...account,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
const nextAccounts = sortAccounts([
|
|
nextAccount,
|
|
...adminConfigState.accounts.filter((item: AdminAccountProfile) => item.id !== accountId),
|
|
]);
|
|
|
|
await Promise.all([
|
|
setItem(BASE_URL_KEY, nextAccount.baseUrl),
|
|
persistAdminApiKey(nextAccount.adminApiKey),
|
|
persistAccounts(nextAccounts),
|
|
setItem(ACTIVE_ACCOUNT_ID_KEY, nextAccount.id),
|
|
]);
|
|
|
|
adminConfigState.accounts = nextAccounts;
|
|
adminConfigState.activeAccountId = nextAccount.id;
|
|
adminConfigState.baseUrl = nextAccount.baseUrl;
|
|
adminConfigState.adminApiKey = nextAccount.adminApiKey;
|
|
}
|
|
|
|
export async function removeAdminAccount(accountId: string) {
|
|
const nextAccounts = adminConfigState.accounts.filter((item: AdminAccountProfile) => item.id !== accountId);
|
|
const nextActiveAccount = getNextActiveAccount(nextAccounts, adminConfigState.activeAccountId === accountId ? '' : adminConfigState.activeAccountId);
|
|
|
|
await Promise.all([
|
|
persistAccounts(nextAccounts),
|
|
nextActiveAccount ? setItem(ACTIVE_ACCOUNT_ID_KEY, nextActiveAccount.id) : deleteItem(ACTIVE_ACCOUNT_ID_KEY),
|
|
setItem(BASE_URL_KEY, nextActiveAccount?.baseUrl ?? ''),
|
|
persistAdminApiKey(nextActiveAccount?.adminApiKey ?? ''),
|
|
]);
|
|
|
|
adminConfigState.accounts = nextAccounts;
|
|
adminConfigState.activeAccountId = nextActiveAccount?.id ?? '';
|
|
adminConfigState.baseUrl = nextActiveAccount?.baseUrl ?? '';
|
|
adminConfigState.adminApiKey = nextActiveAccount?.adminApiKey ?? '';
|
|
}
|
|
|
|
export async function logoutAdminAccount() {
|
|
await Promise.all([setItem(BASE_URL_KEY, ''), persistAdminApiKey(''), deleteItem(ACTIVE_ACCOUNT_ID_KEY)]);
|
|
|
|
adminConfigState.activeAccountId = '';
|
|
adminConfigState.baseUrl = '';
|
|
adminConfigState.adminApiKey = '';
|
|
}
|
|
|
|
export async function setAdminAccountEnabled(accountId: string, enabled: boolean) {
|
|
const nextAccounts = sortAccounts(
|
|
adminConfigState.accounts.map((account: AdminAccountProfile) =>
|
|
account.id === accountId ? { ...account, enabled, updatedAt: new Date().toISOString() } : account
|
|
)
|
|
);
|
|
const nextActiveAccount = getNextActiveAccount(nextAccounts, enabled ? accountId : adminConfigState.activeAccountId);
|
|
|
|
await Promise.all([
|
|
persistAccounts(nextAccounts),
|
|
nextActiveAccount ? setItem(ACTIVE_ACCOUNT_ID_KEY, nextActiveAccount.id) : deleteItem(ACTIVE_ACCOUNT_ID_KEY),
|
|
setItem(BASE_URL_KEY, nextActiveAccount?.baseUrl ?? ''),
|
|
persistAdminApiKey(nextActiveAccount?.adminApiKey ?? ''),
|
|
]);
|
|
|
|
adminConfigState.accounts = nextAccounts;
|
|
adminConfigState.activeAccountId = nextActiveAccount?.id ?? '';
|
|
adminConfigState.baseUrl = nextActiveAccount?.baseUrl ?? '';
|
|
adminConfigState.adminApiKey = nextActiveAccount?.adminApiKey ?? '';
|
|
}
|