diff --git a/apps/router/src/api/GatewayApi.ts b/apps/router/src/api/GatewayApi.ts index b97a8b07f..6df05d377 100644 --- a/apps/router/src/api/GatewayApi.ts +++ b/apps/router/src/api/GatewayApi.ts @@ -16,14 +16,14 @@ import { } from '@fedimint/types'; import { GatewayConfig } from '../types/gateway'; -export const SESSION_STORAGE_KEY = 'gateway-ui-key'; - // GatewayApi is an implementation of the ApiInterface export class GatewayApi { private baseUrl: string; + private id: string; constructor(config: GatewayConfig) { this.baseUrl = config.baseUrl; + this.id = config.id; } // Tests a provided password or the one in session storage @@ -35,7 +35,7 @@ export class GatewayApi { } // Replace with temp password to check. - sessionStorage.setItem(SESSION_STORAGE_KEY, tempPassword); + sessionStorage.setItem(this.id, tempPassword); try { await this.fetchInfo(); @@ -49,11 +49,11 @@ export class GatewayApi { }; private getPassword = (): string | null => { - return sessionStorage.getItem(SESSION_STORAGE_KEY); + return sessionStorage.getItem(this.id) || null; }; clearPassword = () => { - sessionStorage.removeItem(SESSION_STORAGE_KEY); + sessionStorage.removeItem(this.id); }; private post = async (api: string, body: unknown): Promise => { @@ -406,7 +406,6 @@ export class GatewayApi { if (res.ok) { const result = await res.json(); - console.log('sendPaymentV2 result:', result); return Promise.resolve(result); } diff --git a/apps/router/src/api/GuardianApi.ts b/apps/router/src/api/GuardianApi.ts index 13e421407..c1ea0d2fd 100644 --- a/apps/router/src/api/GuardianApi.ts +++ b/apps/router/src/api/GuardianApi.ts @@ -21,13 +21,14 @@ import { SharedRpc, } from '../types/guardian'; -export const SESSION_STORAGE_KEY = 'guardian-ui-key'; - export class GuardianApi { private websocket: JsonRpcWebsocket | null = null; private connectPromise: Promise | null = null; + private guardianConfig: GuardianConfig; - constructor(private guardianConfig: GuardianConfig) {} + constructor(guardianConfig: GuardianConfig) { + this.guardianConfig = guardianConfig; + } /*** WebSocket methods ***/ @@ -82,13 +83,8 @@ export class GuardianApi { return true; }; - public getPassword = (): string | null => { - return sessionStorage.getItem(SESSION_STORAGE_KEY); - }; - public testPassword = async (password: string): Promise => { - // Replace with password to check. - sessionStorage.setItem(SESSION_STORAGE_KEY, password); + this.setSessionPassword(password); // Attempt a 'status' rpc call with the temporary password. try { @@ -102,7 +98,7 @@ export class GuardianApi { }; private clearPassword = () => { - sessionStorage.removeItem(SESSION_STORAGE_KEY); + sessionStorage.removeItem(this.guardianConfig.id); }; /*** Shared RPC methods */ @@ -118,10 +114,16 @@ export class GuardianApi { /*** Setup RPC methods ***/ - public setPassword = async (password: string): Promise => { - // Save password to session storage so that it's included in the r[c] call - sessionStorage.setItem(SESSION_STORAGE_KEY, password); + public getPassword = (): string | null => { + return sessionStorage.getItem(this.guardianConfig.id) || null; + }; + public setSessionPassword = (password: string): void => { + sessionStorage.setItem(this.guardianConfig.id, password); + }; + + public setPassword = async (password: string): Promise => { + this.setSessionPassword(password); try { await this.call(SetupRpc.setPassword); } catch (err) { @@ -297,6 +299,7 @@ export class GuardianApi { params: unknown = null ): Promise => { try { + const password = this.getPassword(); if (!this.guardianConfig?.baseUrl) { throw new Error('guardian baseUrl not found in config'); } @@ -304,7 +307,7 @@ export class GuardianApi { // console.log('method', method); const response = await websocket.call(method, [ { - auth: this.getPassword() || null, + auth: password || null, params, }, ]); diff --git a/apps/router/src/api/ServiceCheckApi.ts b/apps/router/src/api/ServiceCheckApi.ts deleted file mode 100644 index 18ae5a89a..000000000 --- a/apps/router/src/api/ServiceCheckApi.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { JsonRpcError, JsonRpcWebsocket } from 'jsonrpc-client-websocket'; -import { StatusResponse, GatewayInfo } from '@fedimint/types'; - -interface ServiceCheckResponse { - serviceType: 'guardian' | 'gateway'; - serviceName: string; - synced: boolean; -} - -export class ServiceCheckApi { - private static readonly REQUEST_TIMEOUT_MS = 30000; - - public async check( - baseUrl: string, - password: string - ): Promise { - const urlType = this.getUrlType(baseUrl); - return urlType === 'websocket' - ? this.checkGuardian(baseUrl, password) - : this.checkGateway(baseUrl, password); - } - - private getUrlType(url: string): 'websocket' | 'http' { - if (url.startsWith('ws://') || url.startsWith('wss://')) return 'websocket'; - if (url.startsWith('http://') || url.startsWith('https://')) return 'http'; - throw new Error( - 'Invalid baseUrl. Must start with ws://, wss://, http://, or https://' - ); - } - - private async checkGuardian( - baseUrl: string, - password: string - ): Promise { - const websocket = new JsonRpcWebsocket( - baseUrl, - ServiceCheckApi.REQUEST_TIMEOUT_MS, - (error: JsonRpcError) => - console.error('Failed to create websocket', error) - ); - - try { - await websocket.open(); - await this.authenticateWebsocket(websocket, password); - const statusResponse = await this.getGuardianStatus(websocket, password); - const result = statusResponse.result as StatusResponse; - return { - serviceType: 'guardian', - serviceName: 'Guardian', // Guardians don't have a specific name in the status response - synced: result.server === 'consensus_running', - }; - } catch (error) { - console.error('Error checking guardian service:', error); - throw error; - } finally { - await websocket.close(); - } - } - - private async authenticateWebsocket( - websocket: JsonRpcWebsocket, - password: string - ): Promise { - const authResponse = await websocket.call('auth', [ - { auth: password, params: null }, - ]); - if (authResponse.error) throw authResponse.error; - } - - private async getGuardianStatus( - websocket: JsonRpcWebsocket, - password: string - ) { - const statusResponse = await websocket.call('status', [ - { auth: password, params: null }, - ]); - if (statusResponse.error) throw statusResponse.error; - return statusResponse; - } - - private async checkGateway( - baseUrl: string, - password: string - ): Promise { - try { - const response = await fetch(`${baseUrl}/info`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${password}`, - }, - }); - - if (!response.ok) { - throw new Error( - `Error fetching gateway info: ${response.status} ${response.statusText}` - ); - } - - const gatewayInfo = (await response.json()) as GatewayInfo; - return { - serviceType: 'gateway', - serviceName: gatewayInfo.lightning_alias, - synced: gatewayInfo.synced_to_chain, - }; - } catch (error) { - console.error('Error checking gateway service:', error); - throw error; - } - } -} diff --git a/apps/router/src/context/AppContext.tsx b/apps/router/src/context/AppContext.tsx index 99bd083a2..cb4a051b4 100644 --- a/apps/router/src/context/AppContext.tsx +++ b/apps/router/src/context/AppContext.tsx @@ -185,16 +185,17 @@ export const AppContextProvider: React.FC = ({ service.baseUrl.startsWith('http'); const addService = async (service: Service) => { const id = await sha256Hash(service.baseUrl); + const newService = { ...service, id }; - if (isGuardian(service)) { + if (isGuardian(newService)) { dispatch({ type: APP_ACTION_TYPE.ADD_GUARDIAN, - payload: { id, guardian: { config: service as GuardianConfig } }, + payload: { id, guardian: { config: newService as GuardianConfig } }, }); - } else if (isGateway(service)) { + } else if (isGateway(newService)) { dispatch({ type: APP_ACTION_TYPE.ADD_GATEWAY, - payload: { id, gateway: { config: service as GatewayConfig } }, + payload: { id, gateway: { config: newService as GatewayConfig } }, }); } else { throw new Error(`Invalid service baseUrl in config.json: ${service}`); @@ -225,7 +226,6 @@ export const AppContextProvider: React.FC = ({ handleConfig(data); } catch (error) { console.error('Error parsing config JSON:', error); - console.log('Raw config content:', text); } }) .catch((error) => { diff --git a/apps/router/src/context/guardian/SetupContext.tsx b/apps/router/src/context/guardian/SetupContext.tsx index 1d23b13a0..9f149b39e 100644 --- a/apps/router/src/context/guardian/SetupContext.tsx +++ b/apps/router/src/context/guardian/SetupContext.tsx @@ -11,7 +11,7 @@ import { randomNames } from '../../guardian-ui/setup/randomNames'; import { FollowerConfigs, HostConfigs, - useGuardianApi, + useGuardianContext, useHandleBackgroundGuardianSetupActions, useHandleSetupServerStatus, useUpdateLocalStorageOnSetupStateChange, @@ -123,7 +123,7 @@ export const SetupContextProvider: React.FC = ({ initServerStatus, children, }: SetupContextProviderProps) => { - const api = useGuardianApi(); + const { api } = useGuardianContext(); const [state, dispatch] = useReducer(reducer, { ...initialState, password: api.getPassword() || initialState.password, diff --git a/apps/router/src/context/hooks.tsx b/apps/router/src/context/hooks.tsx index c98668d89..61c11e0ba 100644 --- a/apps/router/src/context/hooks.tsx +++ b/apps/router/src/context/hooks.tsx @@ -81,21 +81,19 @@ export const useGuardianDispatch = (): Dispatch => { }; export const useLoadGuardian = (): void => { - const guardianApi = useGuardianApi(); - const guardianState = useGuardianState(); - const dispatch = useGuardianDispatch(); + const { api, state, id, dispatch } = useGuardianContext(); useEffect(() => { const load = async () => { try { - await guardianApi.connect(); - const server = (await guardianApi.status()).server; + await api.connect(); + const server = (await api.status()).server; // If they're at a point where a password has been configured, make // sure they have a valid password set. If not, set needsAuth. if (server !== GuardianServerStatus.AwaitingPassword) { - const password = guardianApi.getPassword(); + const password = api.getPassword(); const hasValidPassword = password - ? await guardianApi.testPassword(password) + ? await api.testPassword(password) : false; if (!hasValidPassword) { dispatch({ @@ -129,10 +127,10 @@ export const useLoadGuardian = (): void => { } }; - if (guardianState.status === GuardianStatus.Loading) { + if (state.status === GuardianStatus.Loading) { load().catch((err) => console.error(err)); } - }, [guardianState.status, guardianApi, dispatch]); + }, [state.status, api, dispatch, id]); }; export const useGuardianContext = (): GuardianContextValue => { @@ -441,10 +439,10 @@ export const useGatewayInfo = (): GatewayInfo => { }; export const useLoadGateway = () => { - const state = useGatewayState(); - const dispatch = useGatewayDispatch(); - - const api = useGatewayApi(); + const { state, dispatch, api, id } = useGatewayContext(); + if (sessionStorage.getItem(id)) { + state.needsAuth = false; + } useEffect(() => { if (!state.needsAuth) { diff --git a/apps/router/src/gateway-ui/Gateway.tsx b/apps/router/src/gateway-ui/Gateway.tsx index e96b88a31..e8b9a6c93 100644 --- a/apps/router/src/gateway-ui/Gateway.tsx +++ b/apps/router/src/gateway-ui/Gateway.tsx @@ -17,7 +17,7 @@ import { Login } from '@fedimint/ui'; import { GATEWAY_APP_ACTION_TYPE } from '../types/gateway'; export const Gateway = () => { - const { state, dispatch, api } = useGatewayContext(); + const { state, dispatch, api, id } = useGatewayContext(); const [showConnectFed, setShowConnectFed] = useState(false); const [walletModalState, setWalletModalState] = useState({ isOpen: false, @@ -30,6 +30,7 @@ export const Gateway = () => { if (state.needsAuth) { return ( dispatch({ diff --git a/apps/router/src/guardian-ui/Guardian.tsx b/apps/router/src/guardian-ui/Guardian.tsx index 7005882ba..7861885f1 100644 --- a/apps/router/src/guardian-ui/Guardian.tsx +++ b/apps/router/src/guardian-ui/Guardian.tsx @@ -11,7 +11,7 @@ import { GUARDIAN_APP_ACTION_TYPE, GuardianStatus } from '../types/guardian'; import { formatApiErrorMessage } from './utils/api'; export const Guardian: React.FC = () => { - const { api, state, dispatch } = useGuardianContext(); + const { api, state, dispatch, id } = useGuardianContext(); useLoadGuardian(); const { t } = useTranslation(); @@ -37,6 +37,7 @@ export const Guardian: React.FC = () => { if (state.needsAuth) { return ( api.testPassword(password || '')} setAuthenticated={() => dispatch({ @@ -78,6 +79,7 @@ export const Guardian: React.FC = () => { api, dispatch, t, + id, ]); return ( diff --git a/apps/router/src/guardian-ui/utils/env.ts b/apps/router/src/guardian-ui/utils/env.ts index 5f2bf080b..2942e3c1e 100644 --- a/apps/router/src/guardian-ui/utils/env.ts +++ b/apps/router/src/guardian-ui/utils/env.ts @@ -1,13 +1,11 @@ -import { GuardianConfig } from '../../types/guardian'; - export async function getEnv() { - const response = await fetch('config.json'); - if (!response.ok) { + const baseUrlResponse = await fetch('config.json'); + if (!baseUrlResponse.ok) { throw new Error('Could not find config.json'); } - const config: GuardianConfig = await response.json(); - if (config.baseUrl === 'config api not set' || !config.baseUrl) { + const baseUrl = await baseUrlResponse.json(); + if (baseUrl === 'config api not set' || !baseUrl) { throw new Error('Config API not set in config.json'); } - return config; + return baseUrl; } diff --git a/apps/router/src/home/ConnectServiceModal.tsx b/apps/router/src/home/ConnectServiceModal.tsx index 4bf8c7758..43acacf7c 100644 --- a/apps/router/src/home/ConnectServiceModal.tsx +++ b/apps/router/src/home/ConnectServiceModal.tsx @@ -14,13 +14,11 @@ import { Badge, Flex, Box, - Divider, } from '@chakra-ui/react'; import { sha256Hash, useTranslation } from '@fedimint/utils'; import { useAppContext } from '../context/hooks'; import { APP_ACTION_TYPE } from '../context/AppContext'; -import { checkServiceExists, getServiceType } from './utils'; -import { ServiceCheckApi } from '../api/ServiceCheckApi'; +import { getServiceType } from './utils'; interface ConnectServiceModalProps { isOpen: boolean; @@ -33,54 +31,44 @@ export const ConnectServiceModal: React.FC = ({ }) => { const { t } = useTranslation(); const [configUrl, setConfigUrl] = useState(''); - const [password, setPassword] = useState(''); - const [serviceInfo, setServiceInfo] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const { guardians, gateways, dispatch } = useAppContext(); const resetForm = useCallback(() => { setConfigUrl(''); - setPassword(''); - setServiceInfo(null); setError(null); }, []); - const handleCheck = useCallback(async () => { - const api = new ServiceCheckApi(); - setIsLoading(true); - setError(null); - try { - if (checkServiceExists(configUrl, guardians, gateways)) { - setError('A service with this URL already exists'); - return; - } - const info = await api.check(configUrl, password); - setServiceInfo(JSON.stringify(info, null, 2)); - } catch (error) { - setError( - error instanceof Error ? error.message : 'An unknown error occurred' - ); - } finally { - setIsLoading(false); - } - }, [configUrl, password, guardians, gateways]); + const checkServiceExists = useCallback( + async (url: string) => { + const id = await sha256Hash(url); + return id in guardians || id in gateways; + }, + [guardians, gateways] + ); const handleConfirm = useCallback(async () => { setError(null); + setIsLoading(true); try { + const exists = await checkServiceExists(configUrl); + if (exists) { + throw new Error('Service already exists'); + } + const id = await sha256Hash(configUrl); const serviceType = getServiceType(configUrl); if (serviceType === 'guardian') { dispatch({ type: APP_ACTION_TYPE.ADD_GUARDIAN, - payload: { id, guardian: { config: { baseUrl: configUrl } } }, + payload: { id, guardian: { config: { id, baseUrl: configUrl } } }, }); } else { dispatch({ type: APP_ACTION_TYPE.ADD_GATEWAY, - payload: { id, gateway: { config: { baseUrl: configUrl } } }, + payload: { id, gateway: { config: { id, baseUrl: configUrl } } }, }); } resetForm(); @@ -89,21 +77,10 @@ export const ConnectServiceModal: React.FC = ({ setError( error instanceof Error ? error.message : 'An unknown error occurred' ); + } finally { + setIsLoading(false); } - }, [configUrl, dispatch, resetForm, onClose]); - - const handleKeyPress = useCallback( - (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - if (!serviceInfo) { - handleCheck(); - } else { - handleConfirm(); - } - } - }, - [serviceInfo, handleCheck, handleConfirm] - ); + }, [configUrl, dispatch, resetForm, onClose, checkServiceExists]); return ( = ({ placeholder='wss://fedimintd.domain.com:6000' value={configUrl} onChange={(e) => setConfigUrl(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleConfirm(); + } + }} /> - - {t('common.password')} - setPassword(e.target.value)} - onKeyDown={handleKeyPress} - /> - - {!serviceInfo && ( - - )} - {serviceInfo && ( - <> - - - - - - - )} + diff --git a/apps/router/src/types/gateway.tsx b/apps/router/src/types/gateway.tsx index 030d95add..3443a64bb 100644 --- a/apps/router/src/types/gateway.tsx +++ b/apps/router/src/types/gateway.tsx @@ -2,6 +2,7 @@ import { GatewayBalances, GatewayInfo } from '@fedimint/types'; import { Unit } from './index'; export type GatewayConfig = { + id: string; baseUrl: string; }; diff --git a/apps/router/src/types/guardian.tsx b/apps/router/src/types/guardian.tsx index be07097a3..64237faa2 100644 --- a/apps/router/src/types/guardian.tsx +++ b/apps/router/src/types/guardian.tsx @@ -1,6 +1,7 @@ import { Peer, GuardianServerStatus, ConfigGenParams } from '@fedimint/types'; export type GuardianConfig = { + id: string; baseUrl: string; }; diff --git a/packages/ui/src/KeyValues.tsx b/packages/ui/src/KeyValues.tsx index e3e604f7d..ef8620e59 100644 --- a/packages/ui/src/KeyValues.tsx +++ b/packages/ui/src/KeyValues.tsx @@ -33,9 +33,7 @@ export const KeyValues: React.FC = ({ > {label || key} - - {value} - + {value} ))} diff --git a/packages/ui/src/Login.tsx b/packages/ui/src/Login.tsx index 2661a5c54..c34f0be0d 100644 --- a/packages/ui/src/Login.tsx +++ b/packages/ui/src/Login.tsx @@ -15,12 +15,14 @@ import { IoEyeOutline, IoEyeOffOutline } from 'react-icons/io5'; import { useTranslation } from '@fedimint/utils'; interface LoginProps { + serviceId: string; checkAuth: (password?: string) => Promise; setAuthenticated: () => void; parseError: (err: unknown) => string; } export const Login: React.FC = ({ + serviceId, checkAuth, setAuthenticated, parseError, @@ -63,13 +65,18 @@ export const Login: React.FC = ({ - {t('login.password')} + + {t('login.password')} + setPassword(ev.currentTarget.value)} + autoComplete='current-password' /> setShowPassword(!showPassword)}> {showPassword ? : }