diff --git a/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/api/ZenithConnectPersistenceException.java b/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/api/ZenithConnectPersistenceException.java index d2459e62d26e..1f8c2f881b21 100644 --- a/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/api/ZenithConnectPersistenceException.java +++ b/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/api/ZenithConnectPersistenceException.java @@ -22,10 +22,18 @@ package org.opennms.features.zenithconnect.persistence.api; public class ZenithConnectPersistenceException extends Exception { + private boolean attemptedToAddDuplicate; + public ZenithConnectPersistenceException() { super(); } + public ZenithConnectPersistenceException(boolean attemptedToAddDuplicate) { + super(); + + this.attemptedToAddDuplicate = attemptedToAddDuplicate; + } + public ZenithConnectPersistenceException(String message) { super(message); } @@ -33,4 +41,8 @@ public ZenithConnectPersistenceException(String message) { public ZenithConnectPersistenceException(String message, Throwable e) { super(message, e); } + + public boolean isAttemptedToAddDuplicate() { + return attemptedToAddDuplicate; + } } diff --git a/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/api/ZenithConnectPersistenceService.java b/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/api/ZenithConnectPersistenceService.java index 24ed0fe0275e..373147f2bdaf 100644 --- a/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/api/ZenithConnectPersistenceService.java +++ b/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/api/ZenithConnectPersistenceService.java @@ -32,10 +32,22 @@ public interface ZenithConnectPersistenceService { /** * Add a new registration. - * Currently we only support a single registration. + * Currently we only support a single registration, so this will replace any registration that + * already exists. * Returns the added registration, including an id and createTimeMs. + * @param preventDuplicates If true, will check to see if the given registration appears to be + * a duplicate of an existing registration (same systemId and same accessToken or refreshToken). + * If so, will throw an exception. This is to prevent e.g. the UI from sending multiple + * duplicate add requests. */ - ZenithConnectRegistration addRegistration(ZenithConnectRegistration registration) throws ZenithConnectPersistenceException; + ZenithConnectRegistration addRegistration(ZenithConnectRegistration registration, boolean preventDuplicates) + throws ZenithConnectPersistenceException; + + /** + * Add a new registration, ignoring preventDuplicates. + */ + ZenithConnectRegistration addRegistration(ZenithConnectRegistration registration) + throws ZenithConnectPersistenceException; /** * Update an existing registration. The given id and registration.id must match an existing registration. diff --git a/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/impl/ZenithConnectPersistenceServiceImpl.java b/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/impl/ZenithConnectPersistenceServiceImpl.java index 22c838a5c52e..371ba0b05b1b 100644 --- a/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/impl/ZenithConnectPersistenceServiceImpl.java +++ b/features/zenith-connect/persistence/src/main/java/org/opennms/features/zenithconnect/persistence/impl/ZenithConnectPersistenceServiceImpl.java @@ -59,7 +59,12 @@ public ZenithConnectRegistrations getRegistrations() throws ZenithConnectPersist } /** {@inheritDoc} */ - public ZenithConnectRegistration addRegistration(ZenithConnectRegistration registration) throws ZenithConnectPersistenceException { + public ZenithConnectRegistration addRegistration(ZenithConnectRegistration registration, boolean preventDuplicates) + throws ZenithConnectPersistenceException { + if (preventDuplicates && isDuplicateRegistration(registration)) { + throw new ZenithConnectPersistenceException(true); + } + registration.id = UUID.randomUUID().toString(); registration.createTimeMs = Instant.now().toEpochMilli(); @@ -69,6 +74,11 @@ public ZenithConnectRegistration addRegistration(ZenithConnectRegistration regis return registration; } + public ZenithConnectRegistration addRegistration(ZenithConnectRegistration registration) + throws ZenithConnectPersistenceException { + return addRegistration(registration, false); + } + /** {@inheritDoc} */ public void updateRegistration(String id, ZenithConnectRegistration registration) throws ZenithConnectPersistenceException { if (Strings.isNullOrEmpty(id) || @@ -136,4 +146,31 @@ private void setRegistrations(ZenithConnectRegistrations registrations) throws Z throw new ZenithConnectPersistenceException("Could not serialize Zenith Connect registrations", e); } } + + private boolean isDuplicateRegistration(ZenithConnectRegistration registration) + throws ZenithConnectPersistenceException { + ZenithConnectRegistrations existingRegistrations = getRegistrationsImpl(); + ZenithConnectRegistration existingRegistration = existingRegistrations.first(); + + if (existingRegistration == null) { + return false; + } + + boolean systemIdsMatch = registration.systemId != null && existingRegistration.systemId != null && + registration.systemId.equals(existingRegistration.systemId); + + if (systemIdsMatch) { + boolean accessTokenMatches = registration.accessToken != null && + existingRegistration.accessToken != null && + registration.accessToken.equals(existingRegistration.accessToken); + + boolean refreshTokenMatches = registration.refreshToken != null && + existingRegistration.refreshToken != null && + registration.refreshToken.equals(existingRegistration.refreshToken); + + return accessTokenMatches || refreshTokenMatches; + } + + return false; + } } diff --git a/features/zenith-connect/persistence/src/test/java/org/opennms/features/zenithconnect/persistence/impl/ZenithConnectPersistenceServiceImplIT.java b/features/zenith-connect/persistence/src/test/java/org/opennms/features/zenithconnect/persistence/impl/ZenithConnectPersistenceServiceImplIT.java index 2f0bf5a6e6da..e4e862da8f34 100644 --- a/features/zenith-connect/persistence/src/test/java/org/opennms/features/zenithconnect/persistence/impl/ZenithConnectPersistenceServiceImplIT.java +++ b/features/zenith-connect/persistence/src/test/java/org/opennms/features/zenithconnect/persistence/impl/ZenithConnectPersistenceServiceImplIT.java @@ -83,6 +83,38 @@ public void testAddAndGetRegistrations() throws ZenithConnectPersistenceExceptio assertThat(createdRegistration.createTimeMs, greaterThanOrEqualTo(currentTime)); } + @Test + public void testAddDuplicateRegistrations() throws ZenithConnectPersistenceException { + // add a registration + var registration = createDefaultRegistration(); + long currentTime = Instant.now().toEpochMilli(); + persistenceService.addRegistration(registration); + + // make sure it was added correctly + ZenithConnectRegistrations registrations = persistenceService.getRegistrations(); + assertNotNull(registrations); + + ZenithConnectRegistration createdRegistration = registrations.first(); + assertRegistrationFieldsEqual(registration, createdRegistration); + + assertThat(createdRegistration.id, not(emptyString())); + assertThat(createdRegistration.id, matchesPattern(UUID_REGEX)); + assertThat(createdRegistration.createTimeMs, greaterThanOrEqualTo(currentTime)); + + // Now add it again, should fail with specific exception and flag + var duplicate = createDefaultRegistration(); + ZenithConnectPersistenceException exception = null; + + try { + persistenceService.addRegistration(duplicate, true); + } catch (ZenithConnectPersistenceException e) { + exception = e; + } + + assertNotNull("Adding duplicate to persistenceService.addRegistration should throw a ZenithConnectPersistenceException.", exception); + assertTrue("Adding duplicate to persistenceService.addRegistration should set attemptedToAddDuplicate.", exception.isAttemptedToAddDuplicate()); + } + @Test public void testUpdateRegistration() throws ZenithConnectPersistenceException { // add a registration diff --git a/features/zenith-connect/rest/src/main/java/org/opennms/features/zenithconnect/rest/impl/DefaultZenithConnectRestService.java b/features/zenith-connect/rest/src/main/java/org/opennms/features/zenithconnect/rest/impl/DefaultZenithConnectRestService.java index b7df17a0fcb5..52de3dd69c4b 100644 --- a/features/zenith-connect/rest/src/main/java/org/opennms/features/zenithconnect/rest/impl/DefaultZenithConnectRestService.java +++ b/features/zenith-connect/rest/src/main/java/org/opennms/features/zenithconnect/rest/impl/DefaultZenithConnectRestService.java @@ -46,7 +46,11 @@ public DefaultZenithConnectRestService(ZenithConnectPersistenceService persisten this.persistenceService = persistenceService; } - /** {@inheritDoc} */ + /** + * Get all registrations. + * Currently, there is only one registration at a time, so this will return a + * ZenithConnectRegistrations object with a single object in the registrations list. + */ @Override public Response getRegistrations() { try { @@ -58,19 +62,31 @@ public Response getRegistrations() { } } - /** {@inheritDoc} */ + /** + * Add the given registration. + * This will throw a 400 Bad Request if the request is a duplicate of an existing registration. + * Throws a 500 Server Error if the request is malformed, or there was an error updating the database. + */ @Override public Response addRegistration(ZenithConnectRegistration registration) { try { - var newRegistration = persistenceService.addRegistration(registration); + var newRegistration = persistenceService.addRegistration(registration, true); return Response.status(Response.Status.CREATED).entity(newRegistration).build(); } catch (ZenithConnectPersistenceException e) { - LOG.error("Could not add registration: {}.", e.getMessage(), e); + LOG.error("Could not add registration: {}. Attempted to add duplicate: {}", + e.getMessage(), e.isAttemptedToAddDuplicate(), e); + + if (e.isAttemptedToAddDuplicate()) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + return Response.serverError().build(); } } - /** {@inheritDoc} */ + /** + * Update an existing registration. The id and registration.id must match an existing registration. + */ @Override public Response updateRegistration(String id, ZenithConnectRegistration registration) { try { diff --git a/ui/src/components/ZenithConnect/ZenithConnectRegister.vue b/ui/src/components/ZenithConnect/ZenithConnectRegister.vue index f5ca5a3bbfa0..816f8ecb1591 100644 --- a/ui/src/components/ZenithConnect/ZenithConnectRegister.vue +++ b/ui/src/components/ZenithConnect/ZenithConnectRegister.vue @@ -11,31 +11,39 @@
Zenith Connect
-
- Register your Meridian instance with Zenith in order to send data. -
-

Steps

-
- + Register your OpenNMS instance with Zenith in order to send data.
+
+ + + + +
-
-
-
- You will be redirected to Zenith's Zenith Connect where you will need to enter your Zenith password - for the Meridian user associated with Zenith. -
-
+
Register with Zenith + + + View Registrations +
@@ -80,20 +89,24 @@ @@ -138,6 +194,10 @@ onMounted(async () => { .zc-container { display: flex; + .zc-register-steps-expansion-panel { + width: 50%; + } + .content-container { width: 35rem; flex: auto; @@ -154,7 +214,7 @@ onMounted(async () => { } .instructions { - width: 70%; + width: 95%; } .input { @@ -164,6 +224,11 @@ onMounted(async () => { .spacer { margin-bottom: 1rem; } + + .btns { + display: flex; + flex-direction: row; + } } } } diff --git a/ui/src/components/ZenithConnect/ZenithConnectRegisterResult.vue b/ui/src/components/ZenithConnect/ZenithConnectRegisterResult.vue index c70879f02a9e..202e215b0436 100644 --- a/ui/src/components/ZenithConnect/ZenithConnectRegisterResult.vue +++ b/ui/src/components/ZenithConnect/ZenithConnectRegisterResult.vue @@ -9,60 +9,65 @@
-
-

Registration Results

+
+
{{ processingStatus }}
-
- - - - - - - - - - - - - - - - - -
ResultSystem IDDisplay NameAccess TokenRefresh Token
-
-
Success
-
-
-
Failed
-
-
{{ zenithConnectStore.registerResponse?.nmsSystemId }}{{ zenithConnectStore.registerResponse?.nmsDisplayName }} -
- {{ ellipsify(zenithConnectStore.registerResponse?.accessToken ?? '', 30) }} - - - -
-
-
- {{ ellipsify(zenithConnectStore.registerResponse?.refreshToken ?? '', 30) }} - - - -
-
+
+
+

Registration Results

+
+
+ + + + + + + + + + + + + + + + + +
ResultSystem IDDisplay NameAccess TokenRefresh Token
+
+
Success
+
+
+
Failed
+
+
{{ zenithConnectStore.currentRegistration?.systemId }}{{ zenithConnectStore.currentRegistration?.displayName }} +
+ {{ ellipsify(zenithConnectStore.currentRegistration?.accessToken ?? '', 30) }} + + + +
+
+
+ {{ ellipsify(zenithConnectStore.currentRegistration?.refreshToken ?? '', 30) }} + + + +
+
+
@@ -87,17 +92,21 @@ import { v4 as uuidv4 } from 'uuid' import { useRoute } from 'vue-router' import BreadCrumbs from '@/components/Layout/BreadCrumbs.vue' import useSnackbar from '@/composables/useSnackbar' +import useSpinner from '@/composables/useSpinner' import { ellipsify } from '@/lib/utils' import { useMenuStore } from '@/stores/menuStore' import { useZenithConnectStore } from '@/stores/zenithConnectStore' import { BreadCrumb } from '@/types' -import { ZenithConnectRegisterResponse, ZenithConnectRegistration } from '@/types/zenithConnect' +import { ZenithConnectRegistration, ZenithConnectRegistrationResponse } from '@/types/zenithConnect' const menuStore = useMenuStore() const zenithConnectStore = useZenithConnectStore() const route = useRoute() const router = useRouter() +const { startSpinner, stopSpinner } = useSpinner() +const isProcessing = ref(false) +const processingStatus = ref('') const homeUrl = computed(() => menuStore.mainMenu.homeUrl) const { showSnackBar } = useSnackbar() @@ -105,7 +114,7 @@ const breadcrumbs = computed(() => { return [ { label: 'Home', to: homeUrl.value, isAbsoluteLink: true }, { label: 'Zenith Connect', to: '/zenith-connect' }, - { label: 'Zenith Connect Register Result', to: '#', position: 'last' } + { label: 'Zenith Connect Registration Result', to: '#', position: 'last' } ] }) @@ -117,45 +126,113 @@ const gotoView = () => { router.push('/zenith-connect') } -const onCopyToken = (isAccessToken: boolean) => { +const onCopyToken = async (isAccessToken: boolean) => { const token = (isAccessToken ? zenithConnectStore.registerResponse?.accessToken : zenithConnectStore.registerResponse?.refreshToken) ?? '' - navigator.clipboard - .writeText(token) - .then(() => { - showSnackBar({ - msg: `${isAccessToken ? 'Access' : 'Refresh'} token copied.` - }) - }) - .catch(() => { - showSnackBar({ - msg: 'Failed to copy token.' - }) + try { + await navigator.clipboard.writeText(token) + + showSnackBar({ + msg: `${isAccessToken ? 'Access' : 'Refresh'} token copied.` }) + } catch { + displayError('Failed to copy token.') + } } -onMounted(() => { - // TODO: This is all mocked up, needs real implementation once we actually save registrations +// parse the registration response from Zenith +const parseRegistrationResponse = () => { const response = { success: route.query.success && route.query.success === 'true' ? true : false, nmsSystemId: route.query.nmsSystemId ?? '', nmsDisplayName: route.query.nmsDisplayName ?? '', accessToken: route.query.accessToken ?? '', refreshToken: route.query.refreshToken ?? '' - } as ZenithConnectRegisterResponse + } as ZenithConnectRegistrationResponse + + return response +} + +const displayError = (msg?: string) => { + showSnackBar({ + msg: msg ?? 'Error registering with Zenith.', + error: true + }) +} - zenithConnectStore.setRegisterResponse(response) +const processRegistrationResponse = async () => { + let status = false + isProcessing.value = true + processingStatus.value = 'Processing Zenith Registration response...' + startSpinner() + + try { + const response: ZenithConnectRegistrationResponse = parseRegistrationResponse() + zenithConnectStore.setRegistrationResponse(response) + + if (!response.success || !response.refreshToken) { + displayError() + return false + } - // add to registrations - const registration = { - ...response, - id: uuidv4(), - registrationDate: new Date(), - lastConnected: new Date(), - connected: false - } as ZenithConnectRegistration + const registration = { + systemId: response.nmsSystemId, + displayName: response.nmsDisplayName, + zenithHost: menuStore.mainMenu.zenithConnectBaseUrl, + zenithRelativeUrl: menuStore.mainMenu.zenithConnectRelativeUrl, + accessToken: response.accessToken, + refreshToken: response.refreshToken, + registered: true, + active: false + } as ZenithConnectRegistration + + processingStatus.value = 'Saving registration...' + + const addResponse = await zenithConnectStore.addRegistration(registration) + + if (!addResponse) { + displayError() + return false + } - zenithConnectStore.addRegistration(registration) + processingStatus.value = 'Getting registrations...' + + const fetchResponse = await zenithConnectStore.fetchRegistrations() + + if (!fetchResponse) { + displayError() + return false + } + + status = true + } catch (e) { + showSnackBar({ + msg: 'Error registering with Zenith.', + error: true + }) + } finally { + stopSpinner() + isProcessing.value = false + processingStatus.value = '' + } + + return status +} + +onMounted(async () => { + const status = await processRegistrationResponse() + + // if registration was successful, redirect to view page + if (status) { + showSnackBar({ + msg: 'Registration was successful. Redirecting to View page...', + timeout: 4000 + }) + + window.setTimeout(() => { + router.push('/zenith-connect') + }, 5000); + } }) @@ -183,6 +260,10 @@ onMounted(() => { } } + .processing-status { + font-weight: bold; + } + .register-success { background-color: var($success); color: white; diff --git a/ui/src/components/ZenithConnect/ZenithConnectView.vue b/ui/src/components/ZenithConnect/ZenithConnectView.vue index bdd18c4a649c..65bd3e56d97c 100644 --- a/ui/src/components/ZenithConnect/ZenithConnectView.vue +++ b/ui/src/components/ZenithConnect/ZenithConnectView.vue @@ -19,8 +19,7 @@ Registration Status Registered On - Last Connected On - Connected Status + Active System ID Display Name Access Token @@ -28,34 +27,33 @@ Actions - + -
-
Success
+
+
Registered
-
Failed
+
Unregistered
- {{ reg.registrationDate ? fnsFormat(reg.registrationDate, 'yyyy-MM-dd HH:mm:ss') : '--' }} - {{ reg.lastConnected ? fnsFormat(reg.lastConnected, 'yyyy-MM-dd HH:mm:ss') : '--' }} + {{ formatRegistrationDate(currentRegistration) }} -
-
Connected
+
+
Active
-
Not Connected
+
Inactive
- {{ reg.nmsSystemId }} - {{ reg.nmsDisplayName }} + {{ currentRegistration?.systemId }} + {{ currentRegistration?.displayName }}
- {{ ellipsify(reg.accessToken ?? '', 30) }} + {{ ellipsify(currentRegistration?.accessToken ?? '', 30) }} @@ -63,11 +61,11 @@
- {{ ellipsify(reg.refreshToken ?? '', 30) }} + {{ ellipsify(currentRegistration?.refreshToken ?? '', 30) }} @@ -77,8 +75,8 @@
Send Data @@ -125,6 +123,7 @@ const router = useRouter() const { showSnackBar } = useSnackbar() const homeUrl = computed(() => menuStore.mainMenu.homeUrl) +const currentRegistration = computed(() => zenithConnectStore.currentRegistration) const breadcrumbs = computed(() => { return [ @@ -137,36 +136,48 @@ const icons = markRaw({ ContentCopy }) -const onCopyToken = (token: string) => { - navigator.clipboard - .writeText(token) - .then(() => { - showSnackBar({ - msg: 'Token copied' - }) +const formatRegistrationDate = (reg?: ZenithConnectRegistration) => { + if (reg?.createTimeMs) { + const date = new Date(reg.createTimeMs) + + return fnsFormat(date, 'yyyy-MM-dd HH:mm:ss') + } + + return '--' +} + +const onCopyToken = async (token: string) => { + try { + await navigator.clipboard.writeText(token) + + showSnackBar({ + msg: 'Token copied' }) - .catch(() => { - showSnackBar({ - msg: 'Failed to copy token.' - }) + } catch { + showSnackBar({ + msg: 'Failed to copy token.' }) + } } const onSendData = (reg?: ZenithConnectRegistration) => { - if (reg && reg.nmsSystemId) { - // fake for now - reg.connected = true - reg.lastConnected = new Date() + if (reg && reg.registered && reg.systemId) { + // TODO: fake for now, should set 'active' in DB and possibly notify exporter process + reg.active = true showSnackBar({ - msg: `Sending data for system: ${reg.nmsSystemId}` + msg: `Sending data for ${reg.displayName} (${reg.systemId})` }) } } const gotoRegister = () => { - router.push('zenith-connect/register') + router.push('/zenith-connect/register') } + +onMounted(async () => { + await zenithConnectStore.fetchRegistrations() +})