diff --git a/api/src/__test__/mothership/api-key/api-key-check-jobs.test.ts b/api/src/__test__/mothership/api-key/api-key-check-jobs.test.ts deleted file mode 100644 index 2eed4d255..000000000 --- a/api/src/__test__/mothership/api-key/api-key-check-jobs.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types'; -import * as apiKeyCheckJobs from '@app/mothership/jobs/api-key-check-jobs'; -import * as apiKeyValidator from '@app/mothership/api-key/validate-api-key-with-keyserver'; -import { describe, expect, it, vi } from 'vitest'; -import { type RecursivePartial } from '@app/types/index'; -import { type RootState } from '@app/store/index'; - -describe('apiKeyCheckJob Tests', () => { - it('API Check Job (with success)', async () => { - const getState = vi.fn<[], RecursivePartial>().mockReturnValue({ - apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION }, - config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } }, - emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } }, - }); - - const dispatch = vi.fn(); - - const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer').mockResolvedValue(API_KEY_STATUS.API_KEY_VALID); - - await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(true); - - expect(validationSpy).toHaveBeenCalledOnce(); - - expect(dispatch).toHaveBeenLastCalledWith({ - payload: API_KEY_STATUS.API_KEY_VALID, - type: 'apiKey/setApiKeyState', - }); - }); - - it('API Check Job (with invalid length key)', async () => { - // Setup state - const getState = vi.fn<[], RecursivePartial>().mockReturnValue({ - apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION }, - config: { remote: { apikey: 'too-short-key' } }, - emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } }, - }); - - const dispatch = vi.fn(); - - const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer').mockResolvedValue(API_KEY_STATUS.API_KEY_VALID); - - await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(false); - expect(dispatch).toHaveBeenCalledWith(expect.any(Function)); - - expect(validationSpy).not.toHaveBeenCalled(); - }); - - it('API Check Job (with a failure that throws an error - NETWORK_ERROR)', async () => { - const getState = vi.fn<[], RecursivePartial>().mockReturnValue({ - apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION }, - config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } }, - emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } }, - }); - - const dispatch = vi.fn(); - - const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer') - .mockResolvedValueOnce(API_KEY_STATUS.NETWORK_ERROR); - - await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Keyserver Failure, must retry]`); - - expect(validationSpy).toHaveBeenCalledOnce(); - - expect(dispatch).toHaveBeenCalledWith({ - payload: API_KEY_STATUS.NETWORK_ERROR, - type: 'apiKey/setApiKeyState', - }); - }); - - it('API Check Job (with a failure that throws an error - INVALID_RESPONSE)', async () => { - const getState = vi.fn<[], RecursivePartial>().mockReturnValue({ - apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION }, - config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } }, - emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } }, - }); - - const dispatch = vi.fn(); - - const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer') - .mockResolvedValueOnce(API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE); - - await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Keyserver Failure, must retry]`); - - expect(validationSpy).toHaveBeenCalledOnce(); - - expect(dispatch).toHaveBeenCalledWith({ - payload: API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE, - type: 'apiKey/setApiKeyState', - }); - }, 10_000); - - it('API Check Job (with failure that results in a log out)', async () => { - const getState = vi.fn<[], RecursivePartial>().mockReturnValue({ - apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION }, - config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } }, - emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } }, - }); - - const dispatch = vi.fn(); - - const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer') - .mockResolvedValue(API_KEY_STATUS.API_KEY_INVALID); - - await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(false); - - expect(validationSpy).toHaveBeenCalledOnce(); - expect(dispatch).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith(expect.any(Function)); - }, 10_000); -}); diff --git a/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts b/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts index 781954fe5..bbf4340a6 100644 --- a/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts +++ b/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts @@ -1,10 +1,7 @@ import { decodeJwt } from 'jose'; import type { ConnectSignInInput } from '@app/graphql/generated/api/types'; -import { NODE_ENV } from '@app/environment'; import { Role } from '@app/graphql/generated/api/types'; -import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types'; -import { validateApiKeyWithKeyServer } from '@app/mothership/api-key/validate-api-key-with-keyserver'; import { getters, store } from '@app/store/index'; import { loginUser } from '@app/store/modules/config'; import { FileLoadStatus } from '@app/store/types'; @@ -12,17 +9,6 @@ import { ApiKeyService } from '@app/unraid-api/auth/api-key.service'; export const connectSignIn = async (input: ConnectSignInInput): Promise => { if (getters.emhttp().status === FileLoadStatus.LOADED) { - const result = - NODE_ENV === 'development' - ? API_KEY_STATUS.API_KEY_VALID - : await validateApiKeyWithKeyServer({ - apiKey: input.apiKey, - flashGuid: getters.emhttp().var.flashGuid, - }); - if (result !== API_KEY_STATUS.API_KEY_VALID) { - throw new Error(`Validating API Key Failed with Error: ${result}`); - } - const userInfo = input.idToken ? decodeJwt(input.idToken) : (input.userInfo ?? null); if ( diff --git a/api/src/graphql/resolvers/query/cloud/check-api.ts b/api/src/graphql/resolvers/query/cloud/check-api.ts index d7cc48703..3d0ac3fd4 100644 --- a/api/src/graphql/resolvers/query/cloud/check-api.ts +++ b/api/src/graphql/resolvers/query/cloud/check-api.ts @@ -1,12 +1,7 @@ import { logger } from '@app/core/log'; -import { getters } from '@app/store'; import { type ApiKeyResponse } from '@app/graphql/generated/api/types'; -import { isApiKeyValid } from '@app/store/getters/index'; export const checkApi = async (): Promise => { logger.trace('Cloud endpoint: Checking API'); - const valid = isApiKeyValid(); - const error = valid ? null : getters.apiKey().status; - - return { valid, error }; + return { valid: true }; }; diff --git a/api/src/index.ts b/api/src/index.ts index d48b3cc64..20ca9a6f8 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -21,7 +21,6 @@ import { PingTimeoutJobs } from '@app/mothership/jobs/ping-timeout-jobs'; import { store } from '@app/store'; import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file'; import { shutdownApiEvent } from '@app/store/actions/shutdown-api-event'; -import { validateApiKeyIfPresent } from '@app/store/listeners/api-key-listener'; import { startMiddlewareListeners } from '@app/store/listeners/listener-middleware'; import { loadConfigFile } from '@app/store/modules/config'; import { loadStateFiles } from '@app/store/modules/emhttp'; @@ -33,6 +32,8 @@ import { StateManager } from '@app/store/watch/state-watch'; import { setupVarRunWatch } from '@app/store/watch/var-run-watch'; import { bootstrapNestServer } from '@app/unraid-api/main'; +import { setupNewMothershipSubscription } from './mothership/subscribe-to-mothership'; + let server: NestFastifyApplication | null = null; const unlinkUnixPort = () => { @@ -72,6 +73,8 @@ try { // Load my dynamix config file into store await store.dispatch(loadDynamixConfigFile()); + await setupNewMothershipSubscription(); + // Start listening to file updates StateManager.getInstance(); @@ -92,15 +95,12 @@ try { // Start webserver server = await bootstrapNestServer(); - PingTimeoutJobs.init(); startMiddlewareListeners(); - await validateApiKeyIfPresent(); - // On process exit stop HTTP server - exitHook(() => { - console.log('exithook'); + exitHook((signal) => { + console.log('exithook', signal); server?.close?.(); // If port is unix socket, delete socket before exiting unlinkUnixPort(); @@ -113,7 +113,9 @@ try { await new Promise(() => {}); } catch (error: unknown) { if (error instanceof Error) { - logger.error('API-ERROR %s %s', error.message, error.stack); + logger.error(error, 'API-ERROR'); + } else { + logger.error(error, 'Encountered unexpected error'); } if (server) { await server?.close?.(); diff --git a/api/src/mothership/api-key/retry-validate-api-key.ts b/api/src/mothership/api-key/retry-validate-api-key.ts deleted file mode 100644 index 119ba9b1e..000000000 --- a/api/src/mothership/api-key/retry-validate-api-key.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { THIRTY_MINUTES_MS } from '@app/consts'; -import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types'; -import { apiKeyCheckJob } from '@app/mothership/jobs/api-key-check-jobs'; -import { type AppDispatch, type RootState } from '@app/store/index'; -import { isApiKeyLoading } from '@app/store/getters/index'; -import pRetry from 'p-retry'; -import { setApiKeyState } from '@app/store/modules/apikey'; -import { keyServerLogger } from '@app/core/log'; - -export const retryValidateApiKey = async (getState: () => RootState, dispatch: AppDispatch): Promise => { - // Start job here - if (isApiKeyLoading(getState())) { - keyServerLogger.warn('Already running API Key validation, not starting another job'); - } else { - keyServerLogger.info('Starting API Key Validation Job'); - dispatch(setApiKeyState(API_KEY_STATUS.PENDING_VALIDATION)); - await pRetry(async count => apiKeyCheckJob(getState, dispatch, count), { - retries: 20_000, - minTimeout: 2_000, - maxTimeout: THIRTY_MINUTES_MS, - randomize: true, - factor: 2, - }); - } -}; diff --git a/api/src/mothership/api-key/validate-api-key-with-keyserver.ts b/api/src/mothership/api-key/validate-api-key-with-keyserver.ts deleted file mode 100644 index 383cfa360..000000000 --- a/api/src/mothership/api-key/validate-api-key-with-keyserver.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { KEYSERVER_VALIDATION_ENDPOINT } from '@app/consts'; -import { keyServerLogger as ksLog } from '@app/core/log'; -import { sendFormToKeyServer } from '@app/core/utils/misc/send-form-to-keyserver'; -import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types'; -import { type Response } from 'got'; - -/** - * Perform a web validation of the API Key - * @param state - * @returns - */ -export const validateApiKeyWithKeyServer = async ({ flashGuid, apiKey }: { flashGuid: string; apiKey: string }): Promise => { - // If we're still loading config state, just return the config is loading - - ksLog.info('Validating API Key with KeyServer'); - - // Send apiKey, etc. to key-server for verification - let response: Response; - try { - response = await sendFormToKeyServer(KEYSERVER_VALIDATION_ENDPOINT, { - guid: flashGuid, - apikey: apiKey, - }); - } catch (error: unknown) { - ksLog.error({ error }, 'Caught error reaching Key Server'); - - return API_KEY_STATUS.NETWORK_ERROR; - } - - ksLog.trace('Got response back from key-server while validating API key'); - - if (response.statusCode !== 200) { - ksLog.warn('Error while validating API key with key-server', response); - return API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE; - } - - // Get response data - let data: { valid: boolean }; - try { - data = JSON.parse(response.body); - } catch (error: unknown) { - ksLog.warn('Failed to parse Keyserver response body', error); - return API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE; - } - - const { valid } = data; - - if (typeof valid === 'boolean') { - if (valid) { - return API_KEY_STATUS.API_KEY_VALID; - } - - return API_KEY_STATUS.API_KEY_INVALID; - } - - ksLog.warn('Returned data from keyserver appears to be invalid', data); - return API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE; -}; diff --git a/api/src/mothership/graphql-client.ts b/api/src/mothership/graphql-client.ts index 47ed2d889..0dbfecd15 100644 --- a/api/src/mothership/graphql-client.ts +++ b/api/src/mothership/graphql-client.ts @@ -1,30 +1,25 @@ +import type { NormalizedCacheObject } from '@apollo/client/core/index.js'; +import type { Client, Event as ClientEvent } from 'graphql-ws'; +import { ApolloClient, ApolloLink, InMemoryCache, Observable } from '@apollo/client/core/index.js'; +import { ErrorLink } from '@apollo/client/link/error/index.js'; +import { RetryLink } from '@apollo/client/link/retry/index.js'; +import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js'; +import { createClient } from 'graphql-ws'; +import { WebSocket } from 'ws'; + import { FIVE_MINUTES_MS } from '@app/consts'; import { minigraphLogger } from '@app/core/log'; +import { API_VERSION, MOTHERSHIP_GRAPHQL_LINK } from '@app/environment'; +import { MinigraphStatus } from '@app/graphql/generated/api/types'; +import { buildDelayFunction } from '@app/mothership/utils/delay-function'; import { getMothershipConnectionParams, getMothershipWebsocketHeaders, } from '@app/mothership/utils/get-mothership-websocket-headers'; import { getters, store } from '@app/store'; -import { type Client, createClient } from 'graphql-ws'; import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status'; -import { - ApolloClient, - InMemoryCache, - type NormalizedCacheObject, -} from '@apollo/client/core/index.js'; -import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js'; -import { MinigraphStatus } from '@app/graphql/generated/api/types'; -import { API_VERSION, MOTHERSHIP_GRAPHQL_LINK } from '@app/environment'; -import { - receivedMothershipPing, - setMothershipTimeout, -} from '@app/store/modules/minigraph'; import { logoutUser } from '@app/store/modules/config'; -import { RetryLink } from '@apollo/client/link/retry/index.js'; -import { ErrorLink } from '@apollo/client/link/error/index.js'; -import { isApiKeyValid } from '@app/store/getters/index'; -import { buildDelayFunction } from '@app/mothership/utils/delay-function'; -import { WebSocket } from 'ws'; +import { receivedMothershipPing, setMothershipTimeout } from '@app/store/modules/minigraph'; const getWebsocketWithMothershipHeaders = () => { return class WebsocketWithMothershipHeaders extends WebSocket { @@ -56,11 +51,14 @@ export const isAPIStateDataFullyLoaded = (state = store.getState()) => { Boolean(emhttp.var.version) ); }; - + +const isInvalidApiKeyError = (error: unknown) => + error instanceof Error && error.message.includes('API Key Invalid'); + export class GraphQLClient { public static instance: ApolloClient | null = null; public static client: Client | null = null; - + private constructor() {} /** @@ -68,11 +66,9 @@ export class GraphQLClient { * @returns ApolloClient instance or null, if state is not valid */ public static getInstance(): ApolloClient | null { - const isStateValid = isAPIStateDataFullyLoaded() && isApiKeyValid(); + const isStateValid = isAPIStateDataFullyLoaded(); if (!isStateValid) { - minigraphLogger.error( - 'GraphQL Client is not valid. Returning null for instance' - ); + minigraphLogger.error('GraphQL Client is not valid. Returning null for instance'); return null; } @@ -85,7 +81,7 @@ export class GraphQLClient { * @returns Apollo Instance (if creation was possible) */ public static createSingletonInstance = () => { - const isStateValid = isAPIStateDataFullyLoaded() && isApiKeyValid(); + const isStateValid = isAPIStateDataFullyLoaded(); if (!GraphQLClient.instance && isStateValid) { minigraphLogger.debug('Creating a new Apollo Client Instance'); @@ -97,71 +93,34 @@ export class GraphQLClient { public static clearInstance = async () => { if (this.instance) { + await this.instance.clearStore(); this.instance?.stop(); } if (GraphQLClient.client) { + GraphQLClient.clearClientEventHandlers(); + GraphQLClient.client.terminate(); await GraphQLClient.client.dispose(); GraphQLClient.client = null; } GraphQLClient.instance = null; GraphQLClient.client = null; + minigraphLogger.trace('Cleared GraphQl client & instance'); }; static createGraphqlClient() { + /** a graphql-ws client to communicate with mothership if user opts-in */ GraphQLClient.client = createClient({ url: MOTHERSHIP_GRAPHQL_LINK.replace('http', 'ws'), webSocketImpl: getWebsocketWithMothershipHeaders(), connectionParams: () => getMothershipConnectionParams(), }); const wsLink = new GraphQLWsLink(GraphQLClient.client); + const { appErrorLink, retryLink, errorLink } = GraphQLClient.createApolloLinks(); - const retryLink = new RetryLink({ - delay(count, operation, error) { - if ( - error instanceof Error && - error.message.includes('API Key Invalid') - ) { - void store.dispatch( - logoutUser({ reason: 'Invalid API Key on Mothership' }) - ); - } - - const getDelay = delayFn(count); - store.dispatch(setMothershipTimeout(getDelay)); - minigraphLogger.info('Delay currently is: %i', getDelay); - return getDelay; - }, - attempts: { max: Infinity }, - }); - const errorLink = new ErrorLink((handler) => { - if (handler.graphQLErrors) { - // GQL Error Occurred, we should log and move on - minigraphLogger.info( - 'GQL Error Encountered %o', - handler.graphQLErrors - ); - } else if (handler.networkError) { - minigraphLogger.error( - 'Network Error Encountered %s', - handler.networkError.message - ); - if ( - getters.minigraph().status !== - MinigraphStatus.ERROR_RETRYING - ) { - store.dispatch( - setGraphqlConnectionStatus({ - status: MinigraphStatus.ERROR_RETRYING, - error: handler.networkError.message, - }) - ); - } - } - }); const apolloClient = new ApolloClient({ - link: retryLink.concat(errorLink).concat(wsLink), + link: ApolloLink.from([appErrorLink, retryLink, errorLink, wsLink]), cache: new InMemoryCache(), defaultOptions: { watchQuery: { @@ -174,40 +133,149 @@ export class GraphQLClient { }, }, }); + GraphQLClient.initEventHandlers(); + return apolloClient; + } + + /** + * Creates and configures Apollo links for error handling and retries + * + * @returns Object containing configured Apollo links: + * - appErrorLink: Prevents errors from bubbling "up" & potentially crashing the API + * - retryLink: Handles retrying failed operations with exponential backoff + * - errorLink: Handles GraphQL and network errors, including API key validation and connection status updates + */ + static createApolloLinks() { + /** prevents errors from bubbling beyond this link/apollo instance & potentially crashing the api */ + const appErrorLink = new ApolloLink((operation, forward) => { + return new Observable((observer) => { + forward(operation).subscribe({ + next: (result) => observer.next(result), + error: (error) => { + minigraphLogger.warn('Apollo error, will not retry: %s', error?.message); + observer.complete(); + }, + complete: () => observer.complete(), + }); + }); + }); + + const retryLink = new RetryLink({ + delay(count, operation, error) { + const getDelay = delayFn(count); + store.dispatch(setMothershipTimeout(getDelay)); + minigraphLogger.info('Delay currently is: %i', getDelay); + return getDelay; + }, + attempts: { + max: Infinity, + retryIf: (error) => !isInvalidApiKeyError(error), + }, + }); + + const errorLink = new ErrorLink((handler) => { + if (handler.graphQLErrors) { + // GQL Error Occurred, we should log and move on + minigraphLogger.info('GQL Error Encountered %o', handler.graphQLErrors); + } else if (handler.networkError) { + /**---------------------------------------------- + * Handling of Network Errors + * + * When the handler has a void return, + * the network error will bubble up + * (i.e. left in the `ApolloLink.from` array). + * + * The underlying operation/request + * may be retried per the retry link. + * + * If the error is not retried, it will bubble + * into the appErrorLink and terminate there. + *---------------------------------------------**/ + minigraphLogger.error(handler.networkError, 'Network Error'); + const error = handler.networkError; + + if (error?.message?.includes('to be an array of GraphQL errors, but got')) { + minigraphLogger.warn('detected malformed graphql error in websocket message'); + } + + if (isInvalidApiKeyError(error)) { + store + .dispatch(logoutUser({ reason: 'Invalid API Key on Mothership' })) + .catch((err) => { + minigraphLogger.error(err, 'Error during logout'); + }); + } else if (getters.minigraph().status !== MinigraphStatus.ERROR_RETRYING) { + store.dispatch( + setGraphqlConnectionStatus({ + status: MinigraphStatus.ERROR_RETRYING, + error: handler.networkError.message, + }) + ); + } + } + }); + return { appErrorLink, retryLink, errorLink } as const; + } + + /** + * Initialize event handlers for the GraphQL client websocket connection + * + * Sets up handlers for: + * - 'connecting': Updates store with connecting status and logs connection attempt + * - 'error': Logs any GraphQL client errors + * - 'connected': Updates store with connected status and logs successful connection + * - 'ping': Handles ping messages from mothership to track connection health + * + * @param client - The GraphQL client instance to attach handlers to. Defaults to GraphQLClient.client + * @returns void + */ + private static initEventHandlers(client = GraphQLClient.client): void { + if (!client) return; // Maybe a listener to initiate this - GraphQLClient.client.on('connecting', () => { + client.on('connecting', () => { store.dispatch( setGraphqlConnectionStatus({ status: MinigraphStatus.CONNECTING, error: null, }) ); - minigraphLogger.info( - 'Connecting to %s', - MOTHERSHIP_GRAPHQL_LINK.replace('http', 'ws') - ); + minigraphLogger.info('Connecting to %s', MOTHERSHIP_GRAPHQL_LINK.replace('http', 'ws')); }); - GraphQLClient.client.on('error', (err) => { + client.on('error', (err) => { minigraphLogger.error('GraphQL Client Error: %o', err); - }) - GraphQLClient.client.on('connected', () => { + }); + client.on('connected', () => { store.dispatch( setGraphqlConnectionStatus({ status: MinigraphStatus.CONNECTED, error: null, }) ); - minigraphLogger.info( - 'Connected to %s', - MOTHERSHIP_GRAPHQL_LINK.replace('http', 'ws') - ); + minigraphLogger.info('Connected to %s', MOTHERSHIP_GRAPHQL_LINK.replace('http', 'ws')); }); - GraphQLClient.client.on('ping', () => { + client.on('ping', () => { // Received ping from mothership minigraphLogger.trace('ping'); store.dispatch(receivedMothershipPing()); }); - return apolloClient; + } + + /** + * Clears event handlers from the GraphQL client websocket connection + * + * Removes handlers for the specified events by replacing them with empty functions. + * This ensures no lingering event handlers remain when disposing of a client. + * + * @param client - The GraphQL client instance to clear handlers from. Defaults to GraphQLClient.client + * @param events - Array of event types to clear handlers for. Defaults to ['connected', 'connecting', 'error', 'ping'] + * @returns void + */ + private static clearClientEventHandlers( + client = GraphQLClient.client, + events: ClientEvent[] = ['connected', 'connecting', 'error', 'ping'] + ): void { + if (!client) return; + events.forEach((eventName) => client.on(eventName, () => {})); } } diff --git a/api/src/mothership/jobs/api-key-check-jobs.ts b/api/src/mothership/jobs/api-key-check-jobs.ts deleted file mode 100644 index 6bbacaf7d..000000000 --- a/api/src/mothership/jobs/api-key-check-jobs.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { isAPIStateDataFullyLoaded } from '@app/mothership/graphql-client'; -import { keyServerLogger } from '@app/core/log'; -import { validateApiKeyWithKeyServer } from '@app/mothership/api-key/validate-api-key-with-keyserver'; -import { type RootState, type AppDispatch } from '@app/store/index'; -import { setApiKeyState } from '@app/store/modules/apikey'; -import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types'; -import { logoutUser } from '@app/store/modules/config'; -import { isApiKeyValid } from '@app/store/getters/index'; -import { isApiKeyCorrectLength } from '@app/mothership/api-key/is-api-key-correct-length'; -import { NODE_ENV } from '@app/environment'; - -export const apiKeyCheckJob = async ( - getState: () => RootState, - dispatch: AppDispatch, - count?: number -): Promise => { - keyServerLogger.debug('Running keyserver validation number: %s', count); - const state = getState(); - if (state.apiKey.status === API_KEY_STATUS.NO_API_KEY) { - // Stop Job - return false; - } - - if (isAPIStateDataFullyLoaded(state)) { - if (isApiKeyValid(state)) { - return true; - } - - if (!isApiKeyCorrectLength(state.config.remote.apikey)) { - keyServerLogger.error('API Key has invalid length, logging you out.'); - await dispatch(logoutUser({ reason: 'API Key has invalid length' })); - return false; - } - - if (['development'].includes(NODE_ENV)) { - keyServerLogger.debug('In dev environment, marking API Key as Valid'); - dispatch(setApiKeyState(API_KEY_STATUS.API_KEY_VALID)); - return true; - } - - const validationResponse = await validateApiKeyWithKeyServer({ - flashGuid: state.emhttp.var.flashGuid, - apiKey: state.config.remote.apikey, - }); - switch (validationResponse) { - case API_KEY_STATUS.API_KEY_VALID: - keyServerLogger.info('Stopping API Key Job as Keyserver Marked API Key Valid'); - dispatch(setApiKeyState(validationResponse)); - return true; - case API_KEY_STATUS.API_KEY_INVALID: - await dispatch(logoutUser({ reason: 'Invalid API Key' })); - return false; - default: - keyServerLogger.info('Request failed with status:', validationResponse); - dispatch(setApiKeyState(validationResponse)); - throw new Error('Keyserver Failure, must retry'); - } - } else { - keyServerLogger.warn('State Data Has Not Fully Loaded, this should not be possible'); - dispatch(setApiKeyState(API_KEY_STATUS.NO_API_KEY)); - return false; - } -}; diff --git a/api/src/mothership/jobs/ping-timeout-jobs.ts b/api/src/mothership/jobs/ping-timeout-jobs.ts index c197350a1..ff95836c4 100644 --- a/api/src/mothership/jobs/ping-timeout-jobs.ts +++ b/api/src/mothership/jobs/ping-timeout-jobs.ts @@ -1,3 +1,5 @@ +import { Cron, Expression, Initializer } from '@reflet/cron'; + import { KEEP_ALIVE_INTERVAL_MS, ONE_MINUTE_MS } from '@app/consts'; import { minigraphLogger, mothershipLogger, remoteAccessLogger } from '@app/core/log'; import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types'; @@ -6,25 +8,21 @@ import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-sta import { store } from '@app/store/index'; import { setRemoteAccessRunningType } from '@app/store/modules/dynamic-remote-access'; import { clearSubscription } from '@app/store/modules/remote-graphql'; -import { Cron, Expression, Initializer } from '@reflet/cron'; export class PingTimeoutJobs extends Initializer { @Cron.PreventOverlap @Cron(Expression.EVERY_MINUTE) @Cron.Start async checkForPingTimeouts() { - const state = store.getState() + const state = store.getState(); if (!isAPIStateDataFullyLoaded(state)) { - mothershipLogger.warn( - 'State data not fully loaded, but job has been started' - ); + mothershipLogger.warn('State data not fully loaded, but job has been started'); return; } // Check for ping timeouts in remote graphql events const subscriptionsToClear = state.remoteGraphQL.subscriptions.filter( - (subscription) => - Date.now() - subscription.lastPing > KEEP_ALIVE_INTERVAL_MS + (subscription) => Date.now() - subscription.lastPing > KEEP_ALIVE_INTERVAL_MS ); if (subscriptionsToClear.length > 0) { mothershipLogger.debug( @@ -36,9 +34,7 @@ export class PingTimeoutJobs extends Initializer { ); } - subscriptionsToClear.forEach((sub) => - store.dispatch(clearSubscription(sub.sha256)) - ); + subscriptionsToClear.forEach((sub) => store.dispatch(clearSubscription(sub.sha256))); // Check for ping timeouts in mothership if ( @@ -61,13 +57,10 @@ export class PingTimeoutJobs extends Initializer { // Check for ping timeouts from mothership events if ( state.minigraph.selfDisconnectedSince && - Date.now() - state.minigraph.selfDisconnectedSince > - KEEP_ALIVE_INTERVAL_MS && + Date.now() - state.minigraph.selfDisconnectedSince > KEEP_ALIVE_INTERVAL_MS && state.minigraph.status === MinigraphStatus.CONNECTED ) { - minigraphLogger.error( - `SELF DISCONNECTION EVENT NEVER CLEARED, SOCKET MUST BE RECONNECTED` - ); + minigraphLogger.error(`SELF DISCONNECTION EVENT NEVER CLEARED, SOCKET MUST BE RECONNECTED`); store.dispatch( setGraphqlConnectionStatus({ status: MinigraphStatus.PING_FAILURE, @@ -77,12 +70,33 @@ export class PingTimeoutJobs extends Initializer { } // Check for ping timeouts in remote access - if (state.dynamicRemoteAccess.lastPing && Date.now() - state.dynamicRemoteAccess.lastPing > ONE_MINUTE_MS) { - remoteAccessLogger.error( - `NO PINGS RECEIVED IN 1 MINUTE, REMOTE ACCESS MUST BE DISABLED` - ); + if ( + state.dynamicRemoteAccess.lastPing && + Date.now() - state.dynamicRemoteAccess.lastPing > ONE_MINUTE_MS + ) { + remoteAccessLogger.error(`NO PINGS RECEIVED IN 1 MINUTE, REMOTE ACCESS MUST BE DISABLED`); store.dispatch(setRemoteAccessRunningType(DynamicRemoteAccessType.DISABLED)); - } } } + +let pingTimeoutJobs: ReturnType> | null = null; + +export const initPingTimeoutJobs = (): boolean => { + if (!pingTimeoutJobs) { + pingTimeoutJobs = PingTimeoutJobs.init(); + } + pingTimeoutJobs.startAll(); + return pingTimeoutJobs.get('checkForPingTimeouts').running ?? false; +}; + +export const stopPingTimeoutJobs = () => { + minigraphLogger.trace('Stopping Ping Timeout Jobs'); + if (!pingTimeoutJobs) { + minigraphLogger.warn('PingTimeoutJobs Handler not found.'); + return; + } + pingTimeoutJobs.stopAll(); + pingTimeoutJobs.clear(); + pingTimeoutJobs = null; +}; diff --git a/api/src/mothership/subscribe-to-mothership.ts b/api/src/mothership/subscribe-to-mothership.ts index 779cf6214..4048c77cb 100644 --- a/api/src/mothership/subscribe-to-mothership.ts +++ b/api/src/mothership/subscribe-to-mothership.ts @@ -1,20 +1,15 @@ import { minigraphLogger, mothershipLogger } from '@app/core/log'; -import { GraphQLClient } from './graphql-client'; -import { store } from '@app/store'; - -import { - EVENTS_SUBSCRIPTION, - RemoteGraphQL_Fragment, -} from '@app/graphql/mothership/subscriptions'; - -import { ClientType } from '@app/graphql/generated/client/graphql'; -import { notNull } from '@app/utils'; import { useFragment } from '@app/graphql/generated/client/fragment-masking'; +import { ClientType } from '@app/graphql/generated/client/graphql'; +import { EVENTS_SUBSCRIPTION, RemoteGraphQL_Fragment } from '@app/graphql/mothership/subscriptions'; +import { store } from '@app/store'; import { handleRemoteGraphQLEvent } from '@app/store/actions/handle-remote-graphql-event'; -import { - setSelfDisconnected, - setSelfReconnected, -} from '@app/store/modules/minigraph'; +import { setSelfDisconnected, setSelfReconnected } from '@app/store/modules/minigraph'; +import { notNull } from '@app/utils'; + +import { GraphQLClient } from './graphql-client'; +import { initPingTimeoutJobs, PingTimeoutJobs } from './jobs/ping-timeout-jobs'; +import { getMothershipConnectionParams } from './utils/get-mothership-websocket-headers'; export const subscribeToEvents = async (apiKey: string) => { minigraphLogger.info('Subscribing to Events'); @@ -29,15 +24,9 @@ export const subscribeToEvents = async (apiKey: string) => { }); eventsSub.subscribe(async ({ data, errors }) => { if (errors) { - mothershipLogger.error( - 'GraphQL Error with events subscription: %s', - errors.join(',') - ); + mothershipLogger.error('GraphQL Error with events subscription: %s', errors.join(',')); } else if (data) { - mothershipLogger.trace( - { events: data.events }, - 'Got events from mothership' - ); + mothershipLogger.trace({ events: data.events }, 'Got events from mothership'); for (const event of data.events?.filter(notNull) ?? []) { switch (event.__typename) { @@ -71,15 +60,10 @@ export const subscribeToEvents = async (apiKey: string) => { } case 'RemoteGraphQLEvent': { - const eventAsRemoteGraphQLEvent = useFragment( - RemoteGraphQL_Fragment, - event - ); + const eventAsRemoteGraphQLEvent = useFragment(RemoteGraphQL_Fragment, event); // No need to check API key here anymore - void store.dispatch( - handleRemoteGraphQLEvent(eventAsRemoteGraphQLEvent) - ); + void store.dispatch(handleRemoteGraphQLEvent(eventAsRemoteGraphQLEvent)); break; } @@ -90,3 +74,16 @@ export const subscribeToEvents = async (apiKey: string) => { } }); }; + +export const setupNewMothershipSubscription = async (state = store.getState()) => { + await GraphQLClient.clearInstance(); + if (getMothershipConnectionParams(state)?.apiKey) { + minigraphLogger.trace('Creating Graphql client'); + const client = GraphQLClient.createSingletonInstance(); + if (client) { + minigraphLogger.trace('Connecting to mothership'); + await subscribeToEvents(state.config.remote.apikey); + initPingTimeoutJobs(); + } + } +}; diff --git a/api/src/mothership/utils/get-mothership-websocket-headers.ts b/api/src/mothership/utils/get-mothership-websocket-headers.ts index 20b408b4a..98a4da4b2 100644 --- a/api/src/mothership/utils/get-mothership-websocket-headers.ts +++ b/api/src/mothership/utils/get-mothership-websocket-headers.ts @@ -3,7 +3,6 @@ import { API_VERSION } from '@app/environment'; import { ClientType } from '@app/graphql/generated/client/graphql'; import { isAPIStateDataFullyLoaded } from '@app/mothership/graphql-client'; import { store } from '@app/store'; -import { isApiKeyValid } from '@app/store/getters/index'; import { type OutgoingHttpHeaders } from 'node:http2'; @@ -17,8 +16,7 @@ interface MothershipWebsocketHeaders extends OutgoingHttpHeaders { export const getMothershipWebsocketHeaders = (state = store.getState()): MothershipWebsocketHeaders | OutgoingHttpHeaders => { const { config, emhttp } = state; - - if (isAPIStateDataFullyLoaded(state) && isApiKeyValid(state)) { + if (isAPIStateDataFullyLoaded(state)) { const headers: MothershipWebsocketHeaders = { 'x-api-key': config.remote.apikey, 'x-flash-guid': emhttp.var.flashGuid, @@ -29,7 +27,6 @@ export const getMothershipWebsocketHeaders = (state = store.getState()): Mothers logger.debug('Mothership websocket headers: %o', headers); return headers; } - return {}; }; @@ -43,7 +40,7 @@ interface MothershipConnectionParams extends Record { export const getMothershipConnectionParams = (state = store.getState()): MothershipConnectionParams | Record => { const { config, emhttp } = state; - if (isAPIStateDataFullyLoaded(state) && isApiKeyValid(state)) { + if (isAPIStateDataFullyLoaded(state)) { return { clientType: ClientType.API, apiKey: config.remote.apikey, diff --git a/api/src/store/getters/index.ts b/api/src/store/getters/index.ts new file mode 100644 index 000000000..170b10831 --- /dev/null +++ b/api/src/store/getters/index.ts @@ -0,0 +1,17 @@ +import { getters, store } from '@app/store'; +import { CacheKeys, type DNSCheck } from '@app/store/types'; +import { type CloudResponse } from '@app/graphql/generated/api/types'; + +export const getCloudCache = (): CloudResponse | undefined => { + const { nodeCache } = getters.cache(); + return nodeCache.get(CacheKeys.checkCloud); +}; + +export const getDnsCache = (): DNSCheck | undefined => { + const { nodeCache } = getters.cache(); + return nodeCache.get(CacheKeys.checkDns); +}; + +export const hasRemoteSubscription = (sha256: string, state = store.getState()): boolean => { + return state.remoteGraphQL.subscriptions.some(sub => sub.sha256 === sha256); +} \ No newline at end of file diff --git a/api/src/store/index.ts b/api/src/store/index.ts index b37f7ae2d..d91e238db 100644 --- a/api/src/store/index.ts +++ b/api/src/store/index.ts @@ -8,7 +8,6 @@ import { cache } from '@app/store/modules/cache'; import { docker } from '@app/store/modules/docker'; import { upnp } from '@app/store/modules/upnp'; import { listenerMiddleware } from '@app/store/listeners/listener-middleware'; -import { apiKeyReducer } from '@app/store/modules/apikey'; import { dynamicRemoteAccessReducer } from '@app/store/modules/dynamic-remote-access'; import { remoteGraphQLReducer } from '@app/store/modules/remote-graphql'; import { dynamix } from '@app/store/modules/dynamix'; @@ -16,7 +15,6 @@ import { notificationReducer } from '@app/store/modules/notifications'; export const store = configureStore({ reducer: { - apiKey: apiKeyReducer, config: configReducer, dynamicRemoteAccess: dynamicRemoteAccessReducer, minigraph: mothership.reducer, @@ -40,7 +38,6 @@ export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; export const getters = { - apiKey: () => store.getState().apiKey, cache: () => store.getState().cache, config: () => store.getState().config, docker: () => store.getState().docker, diff --git a/api/src/store/listeners/api-key-listener.ts b/api/src/store/listeners/api-key-listener.ts deleted file mode 100644 index fa9ab3052..000000000 --- a/api/src/store/listeners/api-key-listener.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { retryValidateApiKey } from '@app/mothership/api-key/retry-validate-api-key'; -import { isAPIStateDataFullyLoaded } from '@app/mothership/graphql-client'; -import { isApiKeyLoading } from '@app/store/getters/index'; -import { store } from '@app/store/index'; - -export const validateApiKeyIfPresent = async () => { - const currentState = store.getState(); - if ( - currentState.config.remote?.apikey && - currentState.emhttp.var.flashGuid && - isAPIStateDataFullyLoaded(currentState) && - !isApiKeyLoading(currentState) - ) { - await retryValidateApiKey(store.getState, store.dispatch); - } -}; diff --git a/api/src/store/listeners/listener-middleware.ts b/api/src/store/listeners/listener-middleware.ts index a14210ac4..a4bdc3ac1 100644 --- a/api/src/store/listeners/listener-middleware.ts +++ b/api/src/store/listeners/listener-middleware.ts @@ -30,11 +30,11 @@ export const addAppListener = addListener as TypedAddListener { // Begin listening for events enableLocalApiKeyListener(); + enableMothershipJobsListener(); enableConfigFileListener('flash')(); enableConfigFileListener('memory')(); enableUpnpListener(); enableVersionListener(); - enableMothershipJobsListener(); enableDynamicRemoteAccessListener(); enableArrayEventListener(); enableWanAccessChangeListener(); diff --git a/api/src/store/listeners/local-api-key-listener.ts b/api/src/store/listeners/local-api-key-listener.ts index 27cb736ad..9349f72d4 100644 --- a/api/src/store/listeners/local-api-key-listener.ts +++ b/api/src/store/listeners/local-api-key-listener.ts @@ -1,8 +1,5 @@ import { logger } from '@app/core/log'; -import { NODE_ENV } from '@app/environment'; import { Role } from '@app/graphql/generated/api/types'; -import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types'; -import { validateApiKeyWithKeyServer } from '@app/mothership/api-key/validate-api-key-with-keyserver'; import { getters } from '@app/store/index'; import { startAppListening } from '@app/store/listeners/listener-middleware'; import { updateUserConfig } from '@app/store/modules/config'; @@ -23,18 +20,6 @@ export const enableLocalApiKeyListener = () => const { remote } = getters.config(); const { apikey, username } = remote; // Validate the API key with the key server - const validationResult = - NODE_ENV === 'development' - ? API_KEY_STATUS.API_KEY_VALID - : await validateApiKeyWithKeyServer({ - apiKey: apikey as string, - flashGuid: getters.emhttp().var.flashGuid, - }); - - if (validationResult !== API_KEY_STATUS.API_KEY_VALID) { - throw new Error('API key validation failed'); - } - const apiKeyService = new ApiKeyService(); // Create local API key const localApiKey = await apiKeyService.create( diff --git a/api/src/store/listeners/mothership-subscription-listener.ts b/api/src/store/listeners/mothership-subscription-listener.ts index 9c89c732c..71859ff1a 100644 --- a/api/src/store/listeners/mothership-subscription-listener.ts +++ b/api/src/store/listeners/mothership-subscription-listener.ts @@ -1,34 +1,41 @@ -import { startAppListening } from '@app/store/listeners/listener-middleware'; -import { subscribeToEvents } from '@app/mothership/subscribe-to-mothership'; -import { getMothershipConnectionParams } from '@app/mothership/utils/get-mothership-websocket-headers'; import isEqual from 'lodash/isEqual'; -import { GraphQLClient } from '@app/mothership/graphql-client'; -import { MinigraphStatus } from '@app/graphql/generated/api/types'; -import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status'; + import { minigraphLogger } from '@app/core/log'; +import { MinigraphStatus } from '@app/graphql/generated/api/types'; +import { setupNewMothershipSubscription } from '@app/mothership/subscribe-to-mothership'; +import { getMothershipConnectionParams } from '@app/mothership/utils/get-mothership-websocket-headers'; +import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status'; +import { startAppListening } from '@app/store/listeners/listener-middleware'; -export const enableMothershipJobsListener = () => startAppListening({ - predicate(action, currentState, previousState) { - // This event happens on first app load, or if a user signs out and signs back in, etc - if (!isEqual(getMothershipConnectionParams(currentState), getMothershipConnectionParams(previousState)) && getMothershipConnectionParams(currentState)?.apiKey) { - minigraphLogger.info('Connecting / Reconnecting Mothership Due to Changed Config File or First Load') - return true; - } +export const enableMothershipJobsListener = () => + startAppListening({ + predicate(action, currentState, previousState) { + const newConnectionParams = !isEqual( + getMothershipConnectionParams(currentState), + getMothershipConnectionParams(previousState) + ); + const apiKey = getMothershipConnectionParams(currentState)?.apiKey; - if (setGraphqlConnectionStatus.match(action) && [MinigraphStatus.PING_FAILURE, MinigraphStatus.PRE_INIT].includes(action.payload.status)) { - minigraphLogger.info('Reconnecting Mothership - PING_FAILURE / PRE_INIT - SetGraphQLConnectionStatus Event') - return true; - } + // This event happens on first app load, or if a user signs out and signs back in, etc + if (newConnectionParams && apiKey) { + minigraphLogger.info('Connecting / Reconnecting Mothership Due to Changed Config File'); + return true; + } - return false; - }, async effect(_, { getState }) { - await GraphQLClient.clearInstance(); - if (getMothershipConnectionParams(getState())?.apiKey) { - const client = GraphQLClient.createSingletonInstance(); - if (client) { - await subscribeToEvents(getState().config.remote.apikey); - } + if ( + setGraphqlConnectionStatus.match(action) && + [MinigraphStatus.PING_FAILURE, MinigraphStatus.PRE_INIT].includes(action.payload.status) + ) { + minigraphLogger.info( + 'Reconnecting Mothership - PING_FAILURE / PRE_INIT - SetGraphQLConnectionStatus Event' + ); + return true; + } - } - }, -}); + return false; + }, + async effect(_, { getState }) { + minigraphLogger.trace('Renewing mothership subscription'); + await setupNewMothershipSubscription(getState()); + }, + }); diff --git a/api/src/store/modules/apikey.ts b/api/src/store/modules/apikey.ts deleted file mode 100644 index e37b00dbd..000000000 --- a/api/src/store/modules/apikey.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types'; -import { loginUser, logoutUser } from '@app/store/modules/config'; -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; - -interface ApiKeyInitialState { - status: API_KEY_STATUS; -} - -const initialState: ApiKeyInitialState = { - status: API_KEY_STATUS.NO_API_KEY, -}; - -const apiKey = createSlice({ - name: 'apiKey', - initialState, - reducers: { - setApiKeyState(state, action: PayloadAction) { - state.status = action.payload; - }, - }, - extraReducers(builder) { - builder.addCase(loginUser.fulfilled, (state) => { - state.status = API_KEY_STATUS.API_KEY_VALID; - }); - builder.addCase(logoutUser.fulfilled, (state) => { - state.status = API_KEY_STATUS.NO_API_KEY; - }); - }, -}); - -const { actions, reducer } = apiKey; - -export const { setApiKeyState } = actions; -export const apiKeyReducer = reducer; diff --git a/api/src/store/modules/config.ts b/api/src/store/modules/config.ts index 6163297ad..8f875bd00 100644 --- a/api/src/store/modules/config.ts +++ b/api/src/store/modules/config.ts @@ -25,6 +25,8 @@ import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub'; import { isEqual } from 'lodash-es'; import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access'; import { NODE_ENV } from '@app/environment'; +import { GraphQLClient } from '@app/mothership/graphql-client'; +import { stopPingTimeoutJobs } from '@app/mothership/jobs/ping-timeout-jobs'; export type SliceState = { status: FileLoadStatus; @@ -105,6 +107,8 @@ export const logoutUser = createAsyncThunk< }; // Publish to owner endpoint await pubsub.publish(PUBSUB_CHANNEL.OWNER, { owner }); + stopPingTimeoutJobs(); + await GraphQLClient.clearInstance(); }); /** diff --git a/api/src/store/modules/minigraph.ts b/api/src/store/modules/minigraph.ts index 8addcde25..3e035186a 100644 --- a/api/src/store/modules/minigraph.ts +++ b/api/src/store/modules/minigraph.ts @@ -52,7 +52,7 @@ export const mothership = createSlice({ extraReducers(builder) { builder.addCase(setGraphqlConnectionStatus, (state, action) => { minigraphLogger.debug( - 'GraphQL Connection Status: ', + 'GraphQL Connection Status: %o', action.payload ); state.status = action.payload.status; diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts index 2bd20039b..f61169749 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts @@ -98,7 +98,7 @@ export class NotificationsService { private async handleNotificationAdd(path: string) { // The path looks like /{notification base path}/{type}/{notification id} const type = path.includes('/unread/') ? NotificationType.UNREAD : NotificationType.ARCHIVE; - this.logger.debug(`Adding ${type} Notification: ${path}`); + // this.logger.debug(`Adding ${type} Notification: ${path}`); const notification = await this.loadNotificationFile(path, NotificationType[type]); this.increment(notification.importance, NotificationsService.overview[type.toLowerCase()]); @@ -632,7 +632,7 @@ export class NotificationsService { type: 'ini', }); - this.logger.verbose(`Loaded notification ini file from ${path}}`); + // this.logger.verbose(`Loaded notification ini file from ${path}}`); const notification: Notification = this.notificationFileToGqlNotification( { id: this.getIdFromPath(path), type }, @@ -722,7 +722,7 @@ export class NotificationsService { this.logger.warn(`[formatTimestamp] Could not parse date from timestamp: ${date}`); return timestamp; } - this.logger.debug(`[formatTimestamp] ${settings.date} :: ${settings.time} :: ${date}`); + // this.logger.debug(`[formatTimestamp] ${settings.date} :: ${settings.time} :: ${date}`); return formatDatetime(date, { dateFormat: settings.date, timeFormat: settings.time,