diff --git a/jccm/src/Frontend/Common/StateStore.js b/jccm/src/Frontend/Common/StateStore.js index 7cf08cf..1c92342 100644 --- a/jccm/src/Frontend/Common/StateStore.js +++ b/jccm/src/Frontend/Common/StateStore.js @@ -59,6 +59,9 @@ const useStore = create((set, get) => ({ isUserLoggedIn: false, setIsUserLoggedIn: (isUserLoggedIn) => set(() => ({ isUserLoggedIn })), + isInventoryLoading: false, + setIsInventoryLoading: (isInventoryLoading) => set(() => ({ isInventoryLoading })), + user: null, orgs: {}, setUser: (user) => diff --git a/jccm/src/Frontend/Components/Login.js b/jccm/src/Frontend/Components/Login.js index e7f624d..a609d53 100644 --- a/jccm/src/Frontend/Components/Login.js +++ b/jccm/src/Frontend/Components/Login.js @@ -205,10 +205,14 @@ export const Login = ({ isOpen, onClose }) => { return { status: 'two_factor' }; } else { console.log('Login successful!'); + await eventBus.emit('cloud-inventory-refresh'); + return { status: 'success', message: 'Login successful!', data: data }; } } else { console.log('Login failed!'); + await eventBus.emit('cloud-inventory-refresh'); + return { status: 'error', message: 'Login failed!' }; } } catch (error) { @@ -234,8 +238,6 @@ export const Login = ({ isOpen, onClose }) => { setIsUserLoggedIn(true); setCurrentActiveThemeName(Constants.getActiveThemeName(data?.user?.theme)); - setCloudInventory(data.inventory); - setCloudInventoryFilterApplied(data.isFilterApplied); onClose(); } else if (response.status === 'two_factor') { @@ -315,8 +317,6 @@ export const Login = ({ isOpen, onClose }) => { setIsUserLoggedIn(true); setCurrentActiveThemeName(Constants.getActiveThemeName(data?.user?.theme)); - setCloudInventory(data.inventory); - setCloudInventoryFilterApplied(data.isFilterApplied); onClose(); } else { diff --git a/jccm/src/Frontend/Components/UserAvatar.js b/jccm/src/Frontend/Components/UserAvatar.js index f95ad5b..a7af02d 100644 --- a/jccm/src/Frontend/Components/UserAvatar.js +++ b/jccm/src/Frontend/Components/UserAvatar.js @@ -22,7 +22,7 @@ const UserAvatar = () => { useEffect(() => { const intervalId = setInterval(async () => { - await eventBus.emit('user-session-check', { message: ':Periodic user session aliveness check' }); + await eventBus.emit('user-session-check', { message: 'Periodic user session aliveness check' }); }, 30000); return () => clearInterval(intervalId); }, []); diff --git a/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js b/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js index 7fe658a..c5d5f47 100644 --- a/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js +++ b/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js @@ -909,6 +909,12 @@ const InventoryTreeMenuLocal = () => { return siteExists && device.path.startsWith(node.value) && !!!cloudDevices[serialNumber]; }); + const targetOrgs = new Set(); + + targetDevices.forEach((device) => { + targetOrgs.add(device.organization); + }); + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const rateLimit = 1000 / rate; // Rate in calls per second const maxConcurrentCalls = 100; // Maximum number of concurrent async calls @@ -946,7 +952,7 @@ const InventoryTreeMenuLocal = () => { await adoptDeviceFactsWithRateLimit(); setTimeout(async () => { - await eventBus.emit('cloud-inventory-refresh', { notification: false }); + await eventBus.emit('cloud-inventory-refresh', { targetOrgs: Array.from(targetOrgs), notification: false }); }, 3000); }; @@ -1006,6 +1012,12 @@ const InventoryTreeMenuLocal = () => { return device.path.startsWith(node.value) && !!cloudDevices[serialNumber]; }); + const targetOrgs = new Set(); + + targetDevices.forEach((device) => { + targetOrgs.add(device.organization); + }); + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const rateLimit = 1000 / rate; // Rate in calls per second const maxConcurrentCalls = 100; // Maximum number of concurrent async calls @@ -1042,7 +1054,7 @@ const InventoryTreeMenuLocal = () => { await releaseDeviceFactsWithRateLimit(); setTimeout(async () => { - await eventBus.emit('cloud-inventory-refresh', { notification: false }); + await eventBus.emit('cloud-inventory-refresh', { targetOrgs: Array.from(targetOrgs), notification: false }); }, 3000); }; diff --git a/jccm/src/Frontend/Layout/LeftSide.js b/jccm/src/Frontend/Layout/LeftSide.js index 7f90695..e0cae6f 100644 --- a/jccm/src/Frontend/Layout/LeftSide.js +++ b/jccm/src/Frontend/Layout/LeftSide.js @@ -51,6 +51,7 @@ import InventoryTreeMenuCloud from './InventoryTreeMenuCloud'; import OrgFilterMenu from './OrgFilterMenu'; import InventoryTreeMenuLocal from './InventoryTreeMenuLocal'; import eventBus from '../Common/eventBus'; +import { RotatingIcon } from './ChangeIcon'; const StackIcon = bundleIcon(StackFilled, StackRegular); const CloudIcon = bundleIcon(CloudFilled, CloudRegular); @@ -75,6 +76,7 @@ const LeftSide = () => { cloudInventoryFilterApplied, setCloudInventoryFilterApplied, currentActiveThemeName, + isInventoryLoading, } = useStore(); const [selectedTree, setSelectedTree] = useState('local'); @@ -142,13 +144,40 @@ const LeftSide = () => { Local {isUserLoggedIn ? ( - } +
- Cloud - + } + > + Cloud + + {isInventoryLoading && ( +
+ + Loading large cloud inventory... +
+ )} +
) : ( { const { importSettings, settings } = useStore(); - const { isUserLoggedIn, setIsUserLoggedIn, user, setUser } = useStore(); + const { isUserLoggedIn, setIsUserLoggedIn, user, setUser, setIsInventoryLoading } = useStore(); const { inventory, setInventory } = useStore(); const { cloudInventory, setCloudInventory } = useStore(); const { deviceFacts, setDeviceFactsAll, cleanUpDeviceFacts, zeroDeviceFacts } = useStore(); @@ -62,14 +62,22 @@ export const MainEventProcessor = () => { } }; - const handleCloudInventoryRefresh = async ({ notification = false } = {}) => { + const handleCloudInventoryRefresh = async ({ targetOrgs = null, notification = false } = {}) => { console.log('Event: "cloud-inventory-refresh"'); - const response = await electronAPI.saGetCloudInventory(); + // Initialize a timeout for setting the loading state + const loadingTimeout = setTimeout(() => { + setIsInventoryLoading(true); + }, 3000); + + const response = await electronAPI.saGetCloudInventory({targetOrgs}); + + clearTimeout(loadingTimeout); + setIsInventoryLoading(false); if (response.cloudInventory) { if (!_.isEqual(cloudInventoryRef.current, response.inventory)) { - console.log('>>>cloudInventory', response.inventory); + // console.log('>>>cloudInventory', response.inventory); setCloudInventory(response.inventory); setCloudInventoryFilterApplied(response.isFilterApplied); } @@ -119,11 +127,10 @@ export const MainEventProcessor = () => { }; const handleUserSessionCheck = async ({ message = '' } = {}) => { - console.log(`Event: "user-session-check" ${message}`); + console.log(`Event: "user-session-check" ${message.length > 0 ? `-> "${message}"` : ''}`); try { const data = await electronAPI.saWhoamiUser(); if (data.sessionValid) { - // console.log('user-session-check: data', data); if (!_.isEqual(userRef.current, data.user)) { setUser(data.user); setIsUserLoggedIn(true); @@ -131,26 +138,24 @@ export const MainEventProcessor = () => { if (currentActiveThemeNameRef.current !== data?.user?.theme) { setCurrentActiveThemeName(data.user.theme); } - if (!_.isEqual(cloudInventoryRef.current, data?.inventory)) { - setCloudInventory(data.inventory); - setCloudInventoryFilterApplied(data.isFilterApplied); - } + + await handleCloudInventoryRefresh(); } else { setUser(null); - setCloudInventory([]) + setCloudInventory([]); setIsUserLoggedIn(false); setCurrentActiveThemeName(data.theme); } } catch (error) { setUser(null); - setCloudInventory([]) + setCloudInventory([]); setIsUserLoggedIn(false); console.error('Session check error:', error); } }; const handleDeviceFactsRefresh = async () => { - console.log('handleDeviceFactsRefresh'); + console.log('Event: "device-facts-refresh"'); const data = await electronAPI.saLoadDeviceFacts(); if (data.deviceFacts) { @@ -161,7 +166,7 @@ export const MainEventProcessor = () => { }; const handleDeviceFactsCleanup = async () => { - console.log('handleDeviceFactsCleanup'); + console.log('Event: "device-facts-cleanup"'); cleanUpDeviceFacts(); }; diff --git a/jccm/src/Services/ApiServer copy.js b/jccm/src/Services/ApiServer copy.js new file mode 100644 index 0000000..e13cbc8 --- /dev/null +++ b/jccm/src/Services/ApiServer copy.js @@ -0,0 +1,645 @@ +import { ipcMain, BrowserWindow, session, screen } from 'electron'; +import { getCloudInfoMinVersion } from '../config'; +import { Client } from 'ssh2'; + +import { mainWindow } from '../main.js'; + +import { + msGetActiveCloud, + msGetActiveRegionName, + msGetTheme, + msSetTheme, + msSetCloudInventory, + msGetCloudInventory, + msSetActiveCloud, + msSetActiveRegionName, + msSetOrgFilter, + msGetOrgFilter, + msGetUserEmail, + msSetLocalInventory, + msGetLocalInventory, + msGetCloudOrgs, + msSetCloudOrgs, + msLoadDeviceFacts, + msSaveDeviceFacts, + msLoadSubnets, + msSaveSubnets, + msLoadSettings, + msSaveSettings, +} from './mainStore'; +import { + acLookupRegions, + acUserLogin, + acUserMFA, + acUserLogout, + acUserSelf, + acGetCloudSites, + acGetCloudInventory, + acGetDeviceStatsType, + acRequest, + acGetGoogleSSOAuthorizationUrl, + acLoginUserGoogleSSO, +} from './ApiCalls'; + +import { CloudInfo } from '../config'; +import { commitJunosSetConfig, executeJunosCommand, getDeviceFacts } from './Device'; +const sshSessions = {}; + +const serverGetCloudInventory = async (targetOrgs = null) => { + console.log('main: serverGetCloudInventory'); + + const orgFilters = await msGetOrgFilter(); + const selfData = await acUserSelf(); + + if (selfData.status === 'error') { + console.error('main: msGetCloudInventory failed', selfData.error); + return { inventory: [], isFilterApplied: false }; + } + + const inventory = []; + const orgs = {}; + + for (const v of selfData.data.privileges) { + if (v.scope === 'org') { + const orgId = v.org_id; + const orgName = v.name; + + if (!!orgFilters[orgId]) continue; + + const item = { name: orgName, id: orgId }; + + const sitesData = await acGetCloudSites(orgId); + if (sitesData.status === 'error') { + console.error(`serverGetCloudInventory: acGetCloudSites error on org ${orgId}`); + continue; + } + + const sites = Object.fromEntries( + Object.entries(sitesData.data).map(([key, value]) => [value.name, { id: value.id }]) + ); + + orgs[orgName] = { id: v.org_id, sites }; + + if (sitesData.status === 'success') { + item.sites = sitesData.data; + } + const inventoryData = await acGetCloudInventory(orgId); + + if (inventoryData.status === 'success') { + const devices = inventoryData.data; + const siteIdHavingVMAC = new Set(); + + try { + for (const device of devices) { + if (device.site_id) { + for (const site of item['sites'] || []) { + if (site.id === device.site_id) { + device.site_name = site.name; + } + } + } + device.org_name = item.name; + if (device?.mac.toUpperCase() === device.serial.toUpperCase() && device.type === 'switch') { + siteIdHavingVMAC.add(device.site_id); + } + } + } catch (error) { + console.error('serverGetCloudInventory: ', error); + } + + if (siteIdHavingVMAC.size > 0) { + const SN2VSN = {}; + for (const siteId of siteIdHavingVMAC) { + console.log(`Get device stats for site(${siteId})`); + const response = await acGetDeviceStatsType(siteId, 'switch'); + + if (response.status === 'success') { + const cloudDeviceStates = response.data; + for (const cloudDevice of cloudDeviceStates) { + SN2VSN[cloudDevice.serial] = cloudDevice?.module_stat[0]; + } + } + } + + for (const device of devices) { + if (device?.mac.toUpperCase() === device.serial.toUpperCase() && device.type === 'switch') { + module = SN2VSN[device.serial]; + device.original_mac = module?.mac; + device.original_serial = module?.serial; + device.is_vmac_enabled = true; + console.log( + `switch device: ${device.hostname}, ${device.original_serial} -> ${device.serial}` + ); + } + } + } + + item.inventory = devices; + } + inventory.push(item); + } + } + + // Print out the cloud inventory + // console.log(JSON.stringify(inventory, null, 2)); + // console.log(JSON.stringify(orgs, null, 2)); + + await msSetCloudInventory(inventory); + await msSetCloudOrgs(orgs); + + const isFilterApplied = Object.keys(orgFilters).length > 0; + return { inventory, isFilterApplied }; +}; + +export const setupApiHandlers = () => { + ipcMain.handle('saFetchAvailableClouds', async (event) => { + console.log('main: saFetchAvailableClouds'); + const clouds = getCloudInfoMinVersion(); + return { clouds }; + }); + + ipcMain.handle('saLookupApiEndpoint', async (event, args) => { + console.log('main: saLookupApiEndpoint'); + const cloudId = args.cloud; + const email = args.email; + try { + const response = await acLookupRegions(cloudId, email); + return response; + } catch (error) { + console.error('User lookup failed!', error); + return { status: 'error', error }; + } + }); + + ipcMain.handle('saLoginUser', async (event, args) => { + console.log('main: saLoginUser'); + + const cloudId = args.cloud; + const regionName = args.region; + const email = args.email; + const password = args.password || null; + const passcode = args.passcode || null; + + if (!regionName || !email) return { login: 'error', error: 'Missing required fields' }; + if (!password && !passcode) return { login: 'error', error: 'Missing authentication credentials' }; + + if (password && !passcode) { + const response = await acUserLogin(cloudId, regionName, email, password); + + if (response.status === 'success') { + if (response.data.two_factor_required && !response.data.two_factor_passed) { + return { login: true, two_factor: true }; + } else { + const cloudDescription = CloudInfo[cloudId].description; + const service = `${cloudDescription}/${regionName}`; + const theme = await msGetTheme(); + return { + login: true, + user: { ...response.data, service, theme, cloudDescription, regionName }, + }; + } + } + } else if (passcode) { + const response = await acUserMFA(passcode); + + if (response.status === 'success') { + if (response.data.two_factor_verified) { + const cloudDescription = CloudInfo[cloudId].description; + const service = `${cloudDescription}/${regionName}`; + const theme = await msGetTheme(); + + return { + login: true, + user: { ...response.data, service, theme, cloudDescription, regionName }, + }; + } else { + return { login: false, error: 'two factor auth failed' }; + } + } + } else { + if (!password && !passcode) return { login: 'error', error: 'Missing authentication credentials.' }; + } + }); + + ipcMain.handle('saLogoutUser', async (event) => { + console.log('main: saLogoutUser'); + try { + await acUserLogout(); + return { logout: true }; + } catch (error) { + return { logout: false, error }; + } + }); + + ipcMain.handle('saWhoamiUser', async (event) => { + console.log('main: saWhoamiUser'); + try { + const response = await acUserSelf(); + + if (response.status === 'success') { + const cloudId = await msGetActiveCloud(); + const cloudDescription = CloudInfo[cloudId].description; + const regionName = await msGetActiveRegionName(); + const service = `${cloudDescription}/${regionName}`; + const theme = await msGetTheme(); + + // const { inventory, isFilterApplied } = await serverGetCloudInventory(); + + return { + sessionValid: true, + user: { ...response.data, service, theme, cloudDescription, regionName }, + // inventory, + // isFilterApplied, + }; + } else { + const theme = await msGetTheme(); + return { sessionValid: false, error: 'whoami call failed', theme }; + } + } catch (error) { + console.log('main: saWhoamiUser: error:', error); + return { sessionValid: false, error }; + } + }); + + ipcMain.handle('saSetThemeUser', async (event, args) => { + console.log('main: saSetThemeUser', args); + const theme = args.theme; + await msSetTheme(theme); + }); + + ipcMain.handle('saGetCloudInventory', async (event, args = {}) => { + console.log('main: saGetCloudInventory'); + const { targetOrgs = null } = args; + + const { inventory, isFilterApplied } = await serverGetCloudInventory(targetOrgs); + + return { cloudInventory: true, inventory, isFilterApplied }; + }); + + ipcMain.handle('saProxyCall', async (event, args) => { + console.log('main: saProxyCall'); + const { method, api, body } = args; + + try { + const response = await acRequest(api, method, body); + return { proxy: true, response }; + } catch (error) { + return { proxy: false, error }; + } + }); + + ipcMain.handle('saOrgFilter', async (event, args) => { + console.log('main: saOrgFilter'); + const { method, body } = args; + + if (method === 'GET') { + const selfData = await acUserSelf(); + if (selfData.status === 'error') { + return []; + } + + const orgs = []; + const filters = await msGetOrgFilter(); + + const cloudId = await msGetActiveCloud(); + const cloudDescription = CloudInfo[cloudId].description; + const regionName = await msGetActiveRegionName(); + const userEmail = await msGetUserEmail(); + const path = `${userEmail}/${cloudDescription}/${regionName}`; + + for (const v of selfData.data.privileges) { + if (v.scope === 'org') { + const orgId = v.org_id; + const item = { name: v.name, id: orgId, path }; + orgs.push(item); + } + } + + return { orgFilter: true, orgs, filters }; + } else if (method === 'SET') { + const filters = body.filters; + msSetOrgFilter(filters); + return { orgFilter: true }; + } else { + return { orgFilter: false, error: `Unknown method: "${method}"` }; + } + }); + + ipcMain.handle('saGetLocalInventory', async (event) => { + console.log('main: saGetLocalInventory'); + const inventory = await msGetLocalInventory(); + + return { localInventory: true, inventory }; + }); + + ipcMain.handle('saSetLocalInventory', async (event, args) => { + console.log('main: saSetLocalInventory'); + const inventory = args.inventory; + await msSetLocalInventory(inventory); + + return { localInventory: true }; + }); + + // SSH handling + ipcMain.on('startSSHConnection', async (event, { id, cols, rows }) => { + console.log('main: startSSHConnection: id: ' + id); + const inventory = await msGetLocalInventory(); + + const found = inventory.filter( + ({ organization, site, address, port }) => id === `/Inventory/${organization}/${site}/${address}/${port}` + ); + if (found.length === 0) { + console.error('No device found: path id: ', id); + return; + } + const device = found[0]; + const { address, port, username, password } = device; + + const conn = new Client(); + sshSessions[id] = conn; + + conn.on('ready', () => { + console.log(`SSH session successfully opened for id: ${id}`); + event.reply('sshSessionOpened', { id }); // Notify renderer that the session is open + + conn.shell({ cols, rows }, (err, stream) => { + if (err) { + event.reply('sshErrorOccurred', { id, message: err.message }); + return; + } + + stream.on('data', (data) => { + event.reply('sshDataReceived', { id, data: data.toString() }); + }); + + stream.on('close', () => { + event.reply('sshDataReceived', { id, data: 'The SSH session has been closed.\r\n' }); + + conn.end(); + delete sshSessions[id]; + event.reply('sshSessionClosed', { id }); + // Clean up listeners + ipcMain.removeAllListeners(`sendSSHInput-${id}`); + ipcMain.removeAllListeners(`resizeSSHSession-${id}`); + }); + + ipcMain.on(`sendSSHInput-${id}`, (_, data) => { + stream.write(data); + }); + + ipcMain.on(`resizeSSHSession-${id}`, (_, { cols, rows }) => { + stream.setWindow(rows, cols, 0, 0); + }); + }); + }).connect({ + host: address, + port, + username, + password, + poll: 10, // Adjust the polling interval to 10 milliseconds + keepaliveInterval: 10000, // Send keepalive every 10 seconds + keepaliveCountMax: 3, // Close the connection after 3 failed keepalives + }); + + conn.on('error', (err) => { + event.reply('sshErrorOccurred', { id, message: err.message }); + }); + + conn.on('end', () => { + delete sshSessions[id]; + }); + }); + + ipcMain.on('disconnectSSHSession', (event, { id }) => { + console.log('main: disconnectSSHSession'); + + if (sshSessions[id]) { + sshSessions[id].end(); + delete sshSessions[id]; + } + }); + + // IPC main handler to adopt the device + ipcMain.handle('saAdoptDevice', async (event, args) => { + console.log('main: saAdoptDevice'); + + const { organization, site, address, port, username, password, jsiTerm, deleteOutboundSSHTerm, ...others } = + args; + + const cloudOrgs = await msGetCloudOrgs(); + const orgId = cloudOrgs[organization]?.id; + const siteId = cloudOrgs[organization]?.sites?.[site]?.id || null; + + try { + let endpoint = 'ocdevices'; + if (jsiTerm) { + endpoint = 'jsi/devices'; + } + + const api = `orgs/${orgId}/${endpoint}/outbound_ssh_cmd${siteId ? `?site_id=${siteId}` : ''}`; + const response = await acRequest(api, 'GET', null); + + const configCommand = deleteOutboundSSHTerm + ? `delete system services outbound-ssh\n${response.cmd}\n` + : `${response.cmd}\n`; + + const reply = await commitJunosSetConfig(address, port, username, password, configCommand); + + if (reply.status === 'success' && reply.data.includes('')) { + return { adopt: true, reply }; + } else { + return { adopt: false, reply }; + } + } catch (error) { + console.error('Configuration failed!', error); + return { adopt: false, reply: error }; + } + }); + + ipcMain.handle('saReleaseDevice', async (event, args) => { + console.log('main: saReleaseDevice'); + + const { organization, serial } = args; + + const cloudOrgs = await msGetCloudOrgs(); + const orgId = cloudOrgs[organization]?.id; + + try { + console.log('device releasing!'); + + const serialsPayload = typeof serial === 'string' ? [serial] : serial; + + const response = await acRequest(`orgs/${orgId}/inventory`, 'PUT', { + op: 'delete', + serials: serialsPayload, + }); + + return { release: true, reply: response }; + } catch (error) { + console.error('Configuration failed!', error); + return { release: false, reply: error }; + } + }); + + ipcMain.handle('saExecuteJunosCommand', async (event, args) => { + console.log('main: saExecuteJunosCommand'); + + try { + const { address, port, username, password, command, timeout } = args; + const reply = await executeJunosCommand(address, port, username, password, command, timeout); + return { command: true, reply }; + } catch (error) { + console.error('Junos command execution failed!', error); + return { command: false, reply }; + } + }); + + ipcMain.handle('saLoadDeviceFacts', async (event) => { + console.log('main: saLoadDeviceFacts'); + const facts = await msLoadDeviceFacts(); + + return { deviceFacts: true, facts }; + }); + + ipcMain.handle('saSaveDeviceFacts', async (event, args) => { + console.log('main: saSaveDeviceFacts'); + const facts = args.facts; + await msSaveDeviceFacts(facts); + + return { deviceFacts: true }; + }); + + ipcMain.handle('saLoadSubnets', async (event) => { + console.log('main: saLoadSubnets'); + const subnets = await msLoadSubnets(); + + return { status: true, subnets }; + }); + + ipcMain.handle('saSaveSubnets', async (event, args) => { + console.log('main: saSaveSubnets'); + const subnets = args.subnets; + await msSaveSubnets(subnets); + console.log('main: saSaveSubnets:', subnets); + + return { status: true }; + }); + + ipcMain.handle('saLoadSettings', async (event) => { + console.log('main: saLoadSettings'); + const settings = await msLoadSettings(); + + return { status: true, settings }; + }); + + ipcMain.handle('saSaveSettings', async (event, args) => { + console.log('main: saSaveSettings'); + const settings = args.settings; + await msSaveSettings(settings); + console.log('main: saSaveSettings:', settings); + + return { status: true }; + }); + + ipcMain.handle('saGetDeviceFacts', async (event, args) => { + console.log('main: saGetDeviceFacts'); + + try { + const { address, port, username, password, timeout, upperSerialNumber } = args; + const reply = await getDeviceFacts(address, port, username, password, timeout, upperSerialNumber); + + return { facts: true, reply }; + } catch (error) { + // console.error('saGetDeviceFacts: Junos command execution failed!', args, error); + return { facts: false, reply: error }; + } + }); + + ipcMain.handle('saGetGoogleSSOAuthCode', async (event, args) => { + console.log('main: saGetGoogleSSOAuthCode'); + + const cloudId = args.cloud; + const regionName = args.region; + + if (!cloudId || !regionName) { + return { login: 'error', error: 'Missing required fields' }; + } + + const response = await acGetGoogleSSOAuthorizationUrl(cloudId, regionName); + + if (response.status === 'success') { + const authorizationUrl = response.authorizationUrl; + console.log('saGetGoogleSSOAuthCode gets authorization url successfully'); + + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + + // Open the authorization URL in a new BrowserWindow + const authWindow = new BrowserWindow({ + width: Math.floor(width * 0.8), // 80% of the screen width + height: Math.floor(height * 0.7), // 70% of the screen height + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + enableRemoteModule: false, + }, + }); + + authWindow.loadURL(authorizationUrl); + + // Intercept the HTTP request to the callback URL + session.defaultSession.webRequest.onBeforeRequest( + { urls: ['http://localhost/callback*'] }, + (details, callback) => { + const url = new URL(details.url); + const authCode = url.searchParams.get('code'); + + if (authCode) { + // Complete the request processing + callback({ cancel: false }); + + // Load a success message and close the auth window after the request processing is done + setImmediate(() => { + // a built-in function in Node.js that schedules a callback to be executed immediately after I/O events + authWindow.loadURL('data:text/html,Authentication successful! You can close this window.'); + authWindow.close(); + + // Send the authorization code to the renderer process + mainWindow.webContents.send('saGoogleSSOAuthCodeReceived', authCode); + }); + } else { + callback({ cancel: false }); + } + } + ); + + return { login: true }; + } else { + console.log('saGetGoogleSSOAuthCode failed to get authorization url: ', response); + return { login: false, error: response.error }; + } + }); + + ipcMain.handle('saLoginUserGoogleSSO', async (event, args) => { + console.log('main: saLoginUserGoogleSSO'); + + const authCode = args.authCode; + + if (!authCode) return { login: 'error', error: 'Missing required fields' }; + + const response = await acLoginUserGoogleSSO(authCode); + + if (response.status === 'success') { + const cloudId = await msGetActiveCloud(); + const regionName = await msGetActiveRegionName(); + const cloudDescription = CloudInfo[cloudId].description; + + const service = `${cloudDescription}/${regionName}`; + const theme = await msGetTheme(); + + return { + login: true, + user: { ...response.data, service, theme, cloudDescription, regionName }, + }; + } + }); +}; diff --git a/jccm/src/Services/ApiServer.js b/jccm/src/Services/ApiServer.js index 4bcf6dd..4156e0f 100644 --- a/jccm/src/Services/ApiServer.js +++ b/jccm/src/Services/ApiServer.js @@ -45,7 +45,7 @@ import { CloudInfo } from '../config'; import { commitJunosSetConfig, executeJunosCommand, getDeviceFacts } from './Device'; const sshSessions = {}; -const serverGetCloudInventory = async () => { +const serverGetCloudInventory = async (targetOrgs = null) => { console.log('main: serverGetCloudInventory'); const orgFilters = await msGetOrgFilter(); @@ -56,19 +56,32 @@ const serverGetCloudInventory = async () => { return { inventory: [], isFilterApplied: false }; } + const currentInventory = await msGetCloudInventory(); const inventory = []; const orgs = {}; for (const v of selfData.data.privileges) { if (v.scope === 'org') { const orgId = v.org_id; + const orgName = v.name; + if (!!orgFilters[orgId]) continue; - const item = { name: v.name, id: orgId }; + if (targetOrgs !== null && !targetOrgs.includes(orgName)) { + // Find the organization in the current inventory with the same orgId + const existingOrg = currentInventory.find((org) => org.id === orgId); + if (existingOrg) { + // console.log('>>>> Reuse existing org:', JSON.stringify(existingOrg, null, 2)); + inventory.push(existingOrg); + } + continue; // Skip further processing for this organization + } + + const item = { name: orgName, id: orgId }; const sitesData = await acGetCloudSites(orgId); if (sitesData.status === 'error') { - console.error(`serverGetCloudInventory: acGetCloudSites error on org ${orgId}`) + console.error(`serverGetCloudInventory: acGetCloudSites error on org ${orgId}`); continue; } @@ -76,12 +89,12 @@ const serverGetCloudInventory = async () => { Object.entries(sitesData.data).map(([key, value]) => [value.name, { id: value.id }]) ); - orgs[v.name] = { id: v.org_id, sites }; - + orgs[orgName] = { id: orgId, sites }; if (sitesData.status === 'success') { item.sites = sitesData.data; } + const inventoryData = await acGetCloudInventory(orgId); if (inventoryData.status === 'success') { @@ -89,27 +102,27 @@ const serverGetCloudInventory = async () => { const siteIdHavingVMAC = new Set(); try { - for (const device of devices) { - if (device.site_id) { - for (const site of item['sites'] || []) { - if (site.id === device.site_id) { - device.site_name = site.name; + for (const device of devices) { + if (device.site_id) { + for (const site of item['sites'] || []) { + if (site.id === device.site_id) { + device.site_name = site.name; + } } } + device.org_name = item.name; + if (device?.mac.toUpperCase() === device.serial.toUpperCase() && device.type === 'switch') { + siteIdHavingVMAC.add(device.site_id); + } } - device.org_name = item.name; - if (device?.mac.toUpperCase() === device.serial.toUpperCase() && device.type === 'switch') { - siteIdHavingVMAC.add(device.site_id); - } + } catch (error) { + console.error('serverGetCloudInventory: ', error); } - } catch (error) { - console.error('serverGetCloudInventory: ', error); - } if (siteIdHavingVMAC.size > 0) { const SN2VSN = {}; for (const siteId of siteIdHavingVMAC) { - console.log(`Get device stats for site(${siteId})`) + console.log(`Get device stats for site(${siteId})`); const response = await acGetDeviceStatsType(siteId, 'switch'); if (response.status === 'success') { @@ -135,6 +148,7 @@ const serverGetCloudInventory = async () => { item.inventory = devices; } + inventory.push(item); } } @@ -192,12 +206,9 @@ export const setupApiHandlers = () => { const cloudDescription = CloudInfo[cloudId].description; const service = `${cloudDescription}/${regionName}`; const theme = await msGetTheme(); - const { inventory, isFilterApplied } = await serverGetCloudInventory(); return { login: true, user: { ...response.data, service, theme, cloudDescription, regionName }, - inventory, - isFilterApplied, }; } } @@ -209,13 +220,10 @@ export const setupApiHandlers = () => { const cloudDescription = CloudInfo[cloudId].description; const service = `${cloudDescription}/${regionName}`; const theme = await msGetTheme(); - const { inventory, isFilterApplied } = await serverGetCloudInventory(); return { login: true, user: { ...response.data, service, theme, cloudDescription, regionName }, - inventory, - isFilterApplied, }; } else { return { login: false, error: 'two factor auth failed' }; @@ -248,13 +256,13 @@ export const setupApiHandlers = () => { const service = `${cloudDescription}/${regionName}`; const theme = await msGetTheme(); - const { inventory, isFilterApplied } = await serverGetCloudInventory(); + // const { inventory, isFilterApplied } = await serverGetCloudInventory(); return { sessionValid: true, user: { ...response.data, service, theme, cloudDescription, regionName }, - inventory, - isFilterApplied, + // inventory, + // isFilterApplied, }; } else { const theme = await msGetTheme(); @@ -272,9 +280,11 @@ export const setupApiHandlers = () => { await msSetTheme(theme); }); - ipcMain.handle('saGetCloudInventory', async (event) => { + ipcMain.handle('saGetCloudInventory', async (event, args = {}) => { console.log('main: saGetCloudInventory'); - const { inventory, isFilterApplied } = await serverGetCloudInventory(); + const { targetOrgs = null } = args; + + const { inventory, isFilterApplied } = await serverGetCloudInventory(targetOrgs); return { cloudInventory: true, inventory, isFilterApplied }; }); @@ -638,13 +648,10 @@ export const setupApiHandlers = () => { const service = `${cloudDescription}/${regionName}`; const theme = await msGetTheme(); - const { inventory, isFilterApplied } = await serverGetCloudInventory(); return { login: true, user: { ...response.data, service, theme, cloudDescription, regionName }, - inventory, - isFilterApplied, }; } }); diff --git a/jccm/src/preload.js b/jccm/src/preload.js index b31f191..ea27e17 100644 --- a/jccm/src/preload.js +++ b/jccm/src/preload.js @@ -14,7 +14,7 @@ contextBridge.exposeInMainWorld('electronAPI', { saWhoamiUser: () => ipcRenderer.invoke('saWhoamiUser'), saSetThemeUser: (args) => ipcRenderer.invoke('saSetThemeUser', args), - saGetCloudInventory: () => ipcRenderer.invoke('saGetCloudInventory'), + saGetCloudInventory: (args) => ipcRenderer.invoke('saGetCloudInventory', args), saProxyCall: (args) => ipcRenderer.invoke('saProxyCall', args), saOrgFilter: (args) => ipcRenderer.invoke('saOrgFilter', args), saGetLocalInventory: () => ipcRenderer.invoke('saGetLocalInventory'),