245 lines
11 KiB
JavaScript
245 lines
11 KiB
JavaScript
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
|
||
};
|
||
};
|