diff --git a/src/backend/common/AbstractComponent.ts b/src/backend/common/AbstractComponent.ts index d672b20d..7e1f9ad2 100644 --- a/src/backend/common/AbstractComponent.ts +++ b/src/backend/common/AbstractComponent.ts @@ -581,7 +581,7 @@ export default abstract class AbstractComponent extends AbstractInitializable { }); } - protected emitComponentUpdate = >>(payload: T) => { + public emitComponentUpdate = >>(payload: T) => { if('errors' in payload) { if(payload.errors.length > 0) { payload.errors = payload.errors.map(x => x instanceof Error ? serializeError(x) : x); @@ -610,7 +610,7 @@ export default abstract class AbstractComponent extends AbstractInitializable { this.setStatus(payload.title); } - protected setStatus = (status: string) => { + public setStatus = (status: string) => { this.status = status; this.emitComponentUpdate({status}); } diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index 9ab3eb41..c2bfb910 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -35,7 +35,7 @@ import { setupWebscrobblerRoutes } from "./webscrobblerRoutes.ts"; import type ScrobbleSources from "../sources/ScrobbleSources.ts"; import type ScrobbleClients from "../scrobblers/ScrobbleClients.ts"; import prom from 'prom-client'; -import { SimpleError } from "../common/errors/MSErrors.ts"; +import { findAuthIssue, SimpleError } from "../common/errors/MSErrors.ts"; import { DrizzlePlayRepository, type QueryPlaysOpts, type QueryPlaysOptsJson } from "../common/database/drizzle/repositories/PlayRepository.ts"; import { playSelectToDeadScrobble } from "../common/database/drizzle/entityUtils.ts"; import AbstractHistoricalScrobbleClient from "../scrobblers/AbstractHistoricalScrobbleClient.ts"; @@ -352,6 +352,31 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro return res.sendStatus(200); }); + app.post('/api/components/:componentVal/auth', componentAwareMiddle, async (req: ComponentAwareRequest, res, next) => { + const { + component, + } = req; + let didAuth = false; + try { + logger.verbose('User requested auth test'); + await component.testAuth(true); + component.errors = component.errors.filter(x => !findAuthIssue(x)); + didAuth = true; + return res.sendStatus(200); + } catch (e) { + component.errors = component.errors.filter(x => !findAuthIssue(x)); + component.errors.push(e); + return res.status(500).json({error: serializeError(e)}); + } finally { + const data = component.getApiData(); + component.emitComponentUpdate({ + errors: data.errors, + state: data.state, + status: didAuth ? 'Authenticated successfully' : data.status + }); + } + }); + app.get('/api/components/:componentVal/plays', componentAwareMiddle, async (req: ComponentAwareRequest, res, next) => { const { component, diff --git a/src/client/components/msComponent/MSComponentDetailed.tsx b/src/client/components/msComponent/MSComponentDetailed.tsx index f5da3219..0c4ac1e4 100644 --- a/src/client/components/msComponent/MSComponentDetailed.tsx +++ b/src/client/components/msComponent/MSComponentDetailed.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, type ComponentProps } from "react" +import React, { useCallback, useMemo, type ComponentProps, useEffect } from "react" import { Portal, Group, Span, Menu, Box, Heading, Skeleton, Wrap, HStack, Stack, Flex, Text, Card, Button, CloseButton, SkeletonText, type BadgeProps, type MenuItemProps, createOverlay, Dialog, type MenuSelectionDetails } from '@chakra-ui/react'; import { COMPONENT_STATE, type ComponentClientApiJson, type ComponentCommonApiJson, type ComponentsApiJson, type ComponentState, type ComponentStateBody, isComponentClientApiJson, isComponentSourceApiJson, type MsSseEvent, type MsSseEventPayload } from "../../../core/Api.js"; import { capitalize } from "../../../core/StringUtils.js"; @@ -76,13 +76,24 @@ export const MSComponentStats = (props: { data?: ComponentCommonApiJson, live?: type AuthDialogProps = {data: Pick}; const dialog = createOverlay((props) => { - const { data, ...rest } = props + const { data, ...rest } = props const { isPending, isError, data: url, error } = useQuery({ enabled: data.authType === COMPONENT_AUTH_TYPE.interactive, staleTime: Infinity, ...tanQueries.components.authUrl(data.id), }); + const {mutate, isPending: mutateIsPending, isSuccess} = useMutation({ + mutationKey: ['authChange', data.id], + mutationFn: () => ky.post(`/api/components/${data.id}/auth`) + }); + + useEffect(() => { + if(isSuccess) { + dialog.close('auth'); + } + },[isSuccess, dialog]) + const authFailure = useMemo(() => { for(const e of data.errors) { const authState = findAnyAuthError(e); @@ -139,7 +150,7 @@ const dialog = createOverlay((props) => { {COMPONENT_AUTH_TYPE.interactive === data.authType && } - +