Files

245 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
window.INP2P = window.INP2P || {};
window.INP2P.createState = function createState(Vue) {
const { ref, computed } = Vue;
const buildVersion = ref('20260307-2246');
const tab = ref('dashboard');
const tabs = [
{id:'dashboard',name:'仪表盘'},{id:'nodes',name:'设备管理'},{id:'sdwan',name:'组网'},{id:'p2p',name:'连接调试'},
{id:'apikeys',name:'API 密钥'},{id:'users',name:'用户'},{id:'enroll',name:'注册码'},
{id:'settings',name:'高级设置'},{id:'audit',name:'审计日志'}
];
const loggedIn = ref(false), msg = ref(''), msgType = ref('ok');
const opSummary = ref('');
const debugMode = ref(new URLSearchParams(location.search).get('debug') === '1');
const debugEvents = ref([]);
const logEvent = (type, payload={}) => {
const item = { id: Date.now() + Math.random(), ts: new Date().toISOString(), type, ...payload };
debugEvents.value = [item, ...debugEvents.value].slice(0, 20);
if (debugMode.value) {
if (type === 'error' || type === 'api_error') console.error('[INP2P]', item);
else console.log('[INP2P]', item);
}
};
const clearDebugEvents = () => { debugEvents.value = []; };
const loginBusy = ref(false), globalBusy = ref(false);
const tabBusy = ref({ sdwan:false, p2p:false, tenants:false, settings:false, audit:false });
const tabState = ref({});
const setTabBusy = (k, v) => { tabBusy.value = { ...tabBusy.value, [k]: !!v }; };
const beginTab = (k) => { setTabBusy(k, true); tabState.value = { ...tabState.value, [k]: 'loading' }; };
const endTabOk = (k, isEmpty=false) => { setTabBusy(k, false); tabState.value = { ...tabState.value, [k]: (isEmpty ? 'empty' : 'success') }; };
const endTabErr = (k) => { setTabBusy(k, false); tabState.value = { ...tabState.value, [k]: 'error' }; };
const role = ref(''), status = ref(1);
const loginUser = ref(''), loginPass = ref(''), loginErr = ref('');
const theme = ref(localStorage.getItem('inp2p_theme') || 'b');
const applyTheme = () => {
document.documentElement.setAttribute('data-theme', theme.value === 'a' ? 'a' : 'b');
};
const toggleTheme = () => {
theme.value = theme.value === 'a' ? 'b' : 'a';
localStorage.setItem('inp2p_theme', theme.value);
applyTheme();
};
const refreshSec = ref(15), timer = ref(null);
const health = ref({}), stats = ref({}), nodes = ref([]), nodeKeyword = ref('');
const sd = ref({ enabled:false, name:'sdwan-main', gatewayCIDR:'10.10.0.0/24', mode:'mesh', hubNode:'', mtu:1420, nodes:[], routes:['10.10.0.0/24'], subnetProxies:[] });
const connectForm = ref({ from:'', to:'', srcPort:80, dstPort:80, appName:'manual-connect' });
const nodeEditVisible = ref(false);
const nodeEditMode = ref('alias');
const nodeEditTitle = ref('编辑设备');
const nodeEditNode = ref(null);
const nodeEditValue = ref('');
const tenants = ref([]), activeTenant = ref(1), keys = ref([]), users = ref([]), enrolls = ref([]);
const tenantKeyword = ref(localStorage.getItem('inp2p_kw_tenant') || '');
const keyKeyword = ref(localStorage.getItem('inp2p_kw_key') || '');
const userKeyword = ref(localStorage.getItem('inp2p_kw_user') || '');
const enrollKeyword = ref(localStorage.getItem('inp2p_kw_enroll') || '');
const tenantForm = ref({ name:'', admin_password:'', operator_password:'' });
const keyForm = ref({ scope:'all', ttl:0 });
const userForm = ref({ role:'operator', email:'', password:'' });
const selectedKeyIds = ref([]);
const selectedUserIds = ref([]);
const selectedEnrollIds = ref([]);
const bulkFailedKeyIds = ref([]);
const bulkFailedUserIds = ref([]);
const bulkFailedEnrollIds = ref([]);
const keepFailedPool = ref(localStorage.getItem('inp2p_keep_failed_pool') !== '0');
const auditPreset = ref(localStorage.getItem('inp2p_audit_preset') || 'all');
const tokenType = ref('');
const tenantId = ref(0);
const subnet = ref('');
const settings = ref({
advanced_impersonate:'0', advanced_force_network:'0', advanced_cross_tenant:'0',
advanced_override_enabled:'0', allow_apikey_mutate_sdwan:'0', allow_operator_kick_cross_node:'0', allow_connect_cross_tenant:'0'
});
const audit = ref([]);
const auditTenant = ref('');
const auditFilters = ref({
action:'', actor_type:'', actor_id:'', target_type:'', target_id:'', from:'', to:'',
limit: 100, offset: 0
});
const auditActionOptions = [
'auth_login_success','auth_login_failed','tenant_create','apikey_create',
'user_create','enroll_create','enroll_status','enroll_consume','enroll_consume_denied',
'node_alias_update','node_ip_update','node_kick','connect_request_denied'
];
const isAdmin = computed(() => role.value === 'admin' && tenantId.value === 1);
const roleLabel = computed(() => isAdmin.value ? '系统管理' : '子网管理');
const filteredTabs = computed(() => isAdmin.value ? tabs : tabs.filter(t => !['apikeys','users','enroll','settings','audit'].includes(t.id)));
const filteredNodes = computed(() => {
const k = (nodeKeyword.value || '').trim().toLowerCase();
if (!k) return nodes.value;
return nodes.value.filter(n => (n.name||'').toLowerCase().includes(k) || (n.publicIP||'').toLowerCase().includes(k));
});
const filteredTenants = computed(() => {
const k = (tenantKeyword.value || '').trim().toLowerCase();
if (!k) return tenants.value;
return (tenants.value || []).filter(t =>
String(t.id||'').includes(k) || String(t.name||'').toLowerCase().includes(k) || String(t.subnet||'').toLowerCase().includes(k)
);
});
const filteredKeys = computed(() => {
const k = (keyKeyword.value || '').trim().toLowerCase();
if (!k) return keys.value;
return (keys.value || []).filter(x =>
String(x.id||'').includes(k) || String(x.scope||'').toLowerCase().includes(k)
);
});
const filteredUsers = computed(() => {
const k = (userKeyword.value || '').trim().toLowerCase();
if (!k) return users.value;
return (users.value || []).filter(u =>
String(u.id||'').includes(k) || String(u.role||'').toLowerCase().includes(k) || String(u.email||'').toLowerCase().includes(k)
);
});
const filteredEnrolls = computed(() => {
const k = (enrollKeyword.value || '').trim().toLowerCase();
if (!k) return enrolls.value;
return (enrolls.value || []).filter(e =>
String(e.id||'').includes(k) || String(e.code||'').toLowerCase().includes(k)
);
});
const onlineMap = computed(() => new Set((nodes.value || []).map(n => n.name)));
const nodeStatus = (name) => (name && onlineMap.value.has(name)) ? '在线' : '离线';
const isOnline = (name) => (name && onlineMap.value.has(name));
const nodeOnline = (n) => {
if (!n) return false;
if (typeof n.online === 'boolean') return n.online;
if (n.status === 1 || String(n.status).toLowerCase() === 'online') return true;
if (n.status === 0 || String(n.status).toLowerCase() === 'offline') return false;
if (n.lastSeen && (Date.now() - new Date(n.lastSeen).getTime()) > 120000) return false;
return !!(n.name && onlineMap.value.has(n.name));
};
const nodeStatusTextOf = (n) => nodeOnline(n) ? '在线' : '离线';
const nodeStatusClassOf = (n) => nodeOnline(n) ? 'status-online' : 'status-offline';
const humanizeError = (msg) => {
const t = String(msg || '').trim();
if (!t) return '请求失败,请稍后重试';
if (/unauthorized|invalid credentials|token/i.test(t)) return '认证已失效,请重新登录';
if (/forbidden|permission|denied/i.test(t)) return '权限不足,当前角色不可执行此操作';
if (/timeout|timed out/i.test(t)) return '请求超时,请检查网络后重试';
if (/network|failed to fetch|load failed/i.test(t)) return '网络异常,请稍后再试';
if (/bad request|invalid/i.test(t)) return '参数不正确,请检查输入项';
return t;
};
const toast = (text, t='ok') => {
msg.value = text;
msgType.value = t;
setTimeout(() => { if (msg.value === text) msg.value = ''; }, 2500);
};
const validateUsername = (v) => {
const x = (v || '').trim();
if (!/^[A-Za-z]{6,}$/.test(x)) return '用户名需仅字母且≥6位';
return '';
};
const validateRequired = (v, label='字段') => {
if (v === null || v === undefined || String(v).trim() === '') return `${label}不能为空`;
return '';
};
const validateSDWAN = (sdwan) => {
const x = sdwan || {};
if (!x.gatewayCIDR || !String(x.gatewayCIDR).includes('/')) return '网段格式不正确(如 10.10.0.0/24)';
const dup = new Set();
for (const n of (x.nodes || [])) {
if (!n || !n.node) continue;
if (dup.has(n.node)) return `SDWAN 节点重复:${n.node}`;
dup.add(n.node);
if (n.ip && !/^\d+\.\d+\.\d+\.\d+$/.test(String(n.ip))) return `节点IP格式错误:${n.ip}`;
}
return '';
};
const tenantFormErr = computed(() => {
const n = (tenantForm.value.name || '').trim();
return n ? '' : '租户名不能为空';
});
const keyFormErr = computed(() => {
const sc = (keyForm.value.scope || '').trim();
return sc ? '' : 'scope 不能为空';
});
const userFormErr = computed(() => {
if (!(userForm.value.email || '').trim()) return 'email/username 不能为空';
if (!(userForm.value.password || '').trim()) return 'password 不能为空';
if (String(userForm.value.password || '').length < 6) return 'password 至少 6 位';
return '';
});
const sdwanFormErr = computed(() => validateSDWAN(sd.value));
const natText = (v) => {
const x = String(v || '').toLowerCase();
if (!x) return 'Unknown';
if (x.includes('cone')) return 'Cone';
if (x.includes('symmetric')) return 'Symmetric';
if (x.includes('easy')) return 'Easy';
return x;
};
const uptime = ts => {
if(!ts) return '-';
const sec = Math.max(0, Math.floor((Date.now() - new Date(ts).getTime()) / 1000));
if(sec < 60) return sec + 's';
if(sec < 3600) return Math.floor(sec/60) + 'm';
if(sec < 86400) return Math.floor(sec/3600) + 'h';
return Math.floor(sec/86400) + 'd';
};
const fmtTime = t => t ? new Date(t).toLocaleString() : '-';
return {
buildVersion, tab, tabs,
loggedIn, msg, msgType, opSummary, loginBusy, globalBusy, tabBusy, tabState,
setTabBusy, beginTab, endTabOk, endTabErr,
role, status, loginUser, loginPass, loginErr, refreshSec, timer,
theme, applyTheme, toggleTheme,
health, stats, nodes, nodeKeyword, sd, connectForm,
nodeEditVisible, nodeEditMode, nodeEditTitle, nodeEditNode, nodeEditValue,
tenants, activeTenant, keys, users, enrolls, tenantForm, keyForm, userForm,
selectedKeyIds, selectedUserIds, selectedEnrollIds,
bulkFailedKeyIds, bulkFailedUserIds, bulkFailedEnrollIds, keepFailedPool, auditPreset,
tenantKeyword, keyKeyword, userKeyword, enrollKeyword,
tokenType, tenantId, subnet, settings, audit, auditTenant,
auditFilters, auditActionOptions,
isAdmin, roleLabel, filteredTabs, filteredNodes, filteredTenants, filteredKeys, filteredUsers, filteredEnrolls,
onlineMap, nodeStatus, isOnline, nodeOnline, nodeStatusTextOf, nodeStatusClassOf,
debugMode, debugEvents, logEvent, clearDebugEvents,
validateUsername, validateRequired, validateSDWAN,
tenantFormErr, keyFormErr, userFormErr, sdwanFormErr,
humanizeError, toast, natText, uptime, fmtTime
};
};