Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new authentication mechanism (access/refresh token) #665

Merged
merged 11 commits into from
Oct 8, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN apt-get update \

ENV REPO https://github.com/JoinMarket-Org/joinmarket-clientserver
ENV REPO_BRANCH master
ENV REPO_REF v0.9.10
ENV REPO_REF master

WORKDIR /src
RUN git clone "$REPO" . --depth=10 --branch "$REPO_BRANCH" && git checkout "$REPO_REF"
Expand Down
12 changes: 7 additions & 5 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
CurrentWallet,
useCurrentWallet,
useSetCurrentWallet,
useClearCurrentWallet,
useReloadCurrentWalletInfo,
} from '../context/WalletContext'
import { clearSession, setSession } from '../session'
Expand All @@ -44,24 +45,25 @@ export default function App() {
const settings = useSettings()
const currentWallet = useCurrentWallet()
const setCurrentWallet = useSetCurrentWallet()
const clearCurrentWallet = useClearCurrentWallet()
const reloadCurrentWalletInfo = useReloadCurrentWalletInfo()
const serviceInfo = useServiceInfo()
const sessionConnectionError = useSessionConnectionError()
const [reloadingWalletInfoCounter, setReloadingWalletInfoCounter] = useState(0)
const isReloadingWalletInfo = useMemo(() => reloadingWalletInfoCounter > 0, [reloadingWalletInfoCounter])

const startWallet = useCallback(
(name: Api.WalletName, token: Api.ApiToken) => {
setSession({ name, token })
setCurrentWallet({ name, token })
(name: Api.WalletName, auth: Api.ApiAuthContext) => {
setSession({ name, auth })
setCurrentWallet({ name, token: auth.token })
},
[setCurrentWallet],
)

const stopWallet = useCallback(() => {
clearCurrentWallet()
clearSession()
setCurrentWallet(null)
}, [setCurrentWallet])
}, [clearCurrentWallet])

const reloadWalletInfo = useCallback(
(delay: Milliseconds) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/BitcoinQR.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import QRCode from 'qrcode'

import { satsToBtc } from '../utils'
Expand Down
4 changes: 2 additions & 2 deletions src/components/CoinjoinPreconditionViolationAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef } from 'react'
import { Ref, forwardRef } from 'react'
import * as rb from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSettings } from '../context/SettingsContext'
Expand All @@ -14,7 +14,7 @@ interface CoinjoinPreconditionViolationAlertProps {
}

export const CoinjoinPreconditionViolationAlert = forwardRef(
({ summary, i18nPrefix = '' }: CoinjoinPreconditionViolationAlertProps, ref: React.Ref<HTMLDivElement>) => {
({ summary, i18nPrefix = '' }: CoinjoinPreconditionViolationAlertProps, ref: Ref<HTMLDivElement>) => {
const { t } = useTranslation()
const settings = useSettings()

Expand Down
9 changes: 5 additions & 4 deletions src/components/CreateWallet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ export default function CreateWallet({ parentRoute, startWallet }) {
const res = await Api.postWalletCreate({}, { walletname: walletName, password })
const body = await (res.ok ? res.json() : Api.Helper.throwError(res))

const { seedphrase, token, walletname: createdWalletFileName } = body
setCreatedWallet({ walletFileName: createdWalletFileName, seedphrase, password, token })
const { seedphrase, walletname: createdWalletFileName } = body
const auth = Api.Helper.parseAuthProps(body)
setCreatedWallet({ walletFileName: createdWalletFileName, seedphrase, password, auth })
} catch (e) {
const message = t('create_wallet.error_creating_failed', {
reason: e.message || 'Unknown reason',
Expand All @@ -112,9 +113,9 @@ export default function CreateWallet({ parentRoute, startWallet }) {
)

const walletConfirmed = useCallback(() => {
if (createdWallet?.walletFileName && createdWallet?.token) {
if (createdWallet?.walletFileName && createdWallet?.auth) {
setAlert(null)
startWallet(createdWallet.walletFileName, createdWallet.token)
startWallet(createdWallet.walletFileName, createdWallet.auth)
navigate(routes.wallet)
} else {
setAlert({ variant: 'danger', message: t('create_wallet.alert_confirmation_failed') })
Expand Down
1 change: 0 additions & 1 deletion src/components/EarnReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useTheme } from '@table-library/react-table-library/theme'
import * as rb from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import * as Api from '../libs/JmWalletApi'
// @ts-ignore
import { useSettings } from '../context/SettingsContext'
import Balance from './Balance'
import Sprite from './Sprite'
Expand Down
20 changes: 11 additions & 9 deletions src/components/ImportWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ enum ImportWalletSteps {

interface ImportWalletProps {
parentRoute: Route
startWallet: (name: Api.WalletName, token: Api.ApiToken) => void
startWallet: (name: Api.WalletName, auth: Api.ApiAuthContext) => void
}

export default function ImportWallet({ parentRoute, startWallet }: ImportWalletProps) {
Expand All @@ -388,9 +388,9 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
const [alert, setAlert] = useState<SimpleAlert>()
const [createWalletFormValues, setCreateWalletFormValues] = useState<CreateWalletFormValues>()
const [importDetailsFormValues, setImportDetailsFormValues] = useState<ImportWalletDetailsFormValues>()
const [recoveredWallet, setRecoveredWallet] = useState<{ walletFileName: Api.WalletName; token: Api.ApiToken }>()
const [recoveredWallet, setRecoveredWallet] = useState<{ walletFileName: Api.WalletName; auth: Api.ApiAuthContext }>()

const isRecovered = useMemo(() => !!recoveredWallet?.walletFileName && recoveredWallet?.token, [recoveredWallet])
const isRecovered = useMemo(() => !!recoveredWallet?.walletFileName && recoveredWallet?.auth, [recoveredWallet])
const canRecover = useMemo(
() => !isRecovered && !serviceInfo?.walletName && !serviceInfo?.rescanning,
[isRecovered, serviceInfo],
Expand Down Expand Up @@ -441,13 +441,14 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
const recoverBody = await (recoverResponse.ok ? recoverResponse.json() : Api.Helper.throwError(recoverResponse))

const { walletname: importedWalletFileName } = recoverBody
setRecoveredWallet({ walletFileName: importedWalletFileName, token: recoverBody.token })
let auth: Api.ApiAuthContext = Api.Helper.parseAuthProps(recoverBody)
setRecoveredWallet({ walletFileName: importedWalletFileName, auth })

// Step #2: update the gaplimit config value if necessary
const originalGaplimit = await refreshConfigValues({
signal,
keys: [JM_GAPLIMIT_CONFIGKEY],
wallet: { name: importedWalletFileName, token: recoverBody.token },
wallet: { name: importedWalletFileName, token: auth.token },
})
.then((it) => it[JM_GAPLIMIT_CONFIGKEY.section] || {})
.then((it) => parseInt(it[JM_GAPLIMIT_CONFIGKEY.field] || String(JM_GAPLIMIT_DEFAULT), 10))
Expand All @@ -465,16 +466,17 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
value: String(gaplimit),
},
],
wallet: { name: importedWalletFileName, token: recoverBody.token },
wallet: { name: importedWalletFileName, token: auth.token },
})
}

// Step #3: lock and unlock the wallet (for new addresses to be imported)
const lockResponse = await Api.getWalletLock({ walletName: importedWalletFileName, token: recoverBody.token })
const lockResponse = await Api.getWalletLock({ walletName: importedWalletFileName, token: auth.token })
if (!lockResponse.ok) await Api.Helper.throwError(lockResponse)

const unlockResponse = await Api.postWalletUnlock({ walletName: importedWalletFileName }, { password })
const unlockBody = await (unlockResponse.ok ? unlockResponse.json() : Api.Helper.throwError(unlockResponse))
auth = Api.Helper.parseAuthProps(unlockBody)

// Step #4: reset `gaplimit´ to previous value if necessary
if (gaplimitUpdateNecessary) {
Expand All @@ -487,7 +489,7 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
value: String(originalGaplimit),
},
],
wallet: { name: importedWalletFileName, token: unlockBody.token },
wallet: { name: importedWalletFileName, token: auth.token },
})
}

Expand All @@ -508,7 +510,7 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
})
}

startWallet(importedWalletFileName, unlockBody.token)
startWallet(importedWalletFileName, auth)
navigate(routes.wallet)
} catch (e: any) {
if (signal.aborted) return
Expand Down
3 changes: 1 addition & 2 deletions src/components/LogOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import * as rb from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Helper as ApiHelper } from '../libs/JmWalletApi'
import { fetchLog } from '../libs/JamApi'
// @ts-ignore
import { useSettings } from '../context/SettingsContext'
import { CurrentWallet } from '../context/WalletContext'
import Sprite from './Sprite'
Expand Down
4 changes: 2 additions & 2 deletions src/components/ToggleSwitch.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import { ChangeEvent } from 'react'
import styles from './ToggleSwitch.module.css'

interface ToggleSwitchProps {
Expand All @@ -16,7 +16,7 @@ export default function ToggleSwitch({
toggledOn,
disabled = false,
}: ToggleSwitchProps) {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
e.stopPropagation()
onToggle(e.currentTarget.checked)
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Wallets.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ export default function Wallets({ currentWallet, startWallet, stopWallet }) {

setUnlockWalletName(undefined)

const { walletname: unlockedWalletName, token } = body
startWallet(unlockedWalletName, token)
const auth = Api.Helper.parseAuthProps(body)

startWallet(body.walletname, auth)
navigate(routes.wallet)
} catch (e) {
const message = e.message.replace('Wallet', walletName)
Expand Down
12 changes: 10 additions & 2 deletions src/components/Wallets.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,12 @@ describe('<Wallets />', () => {
})
apiMock.postWalletUnlock.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ walletname: dummyWalletName, token: dummyToken }),
json: () =>
Promise.resolve({
walletname: dummyWalletName,
token: dummyToken,
refresh_token: dummyToken,
}),
})

await act(async () => setup({}))
Expand All @@ -223,7 +228,10 @@ describe('<Wallets />', () => {
await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock'))
})

expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, dummyToken)
expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, {
token: dummyToken,
refresh_token: dummyToken,
})
expect(mockedNavigate).toHaveBeenCalledWith('/wallet')
})

Expand Down
6 changes: 2 additions & 4 deletions src/components/jar_details/DisplayBranch.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from 'react'
import { ReactNode } from 'react'
import * as rb from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
// @ts-ignore
import Balance from '../Balance'
// @ts-ignore
import { useSettings } from '../../context/SettingsContext'
import { Branch, BranchEntry } from '../../context/WalletContext'
import styles from './DisplayBranch.module.css'
Expand All @@ -28,7 +26,7 @@ const toSimpleStatus = (value: string) => {
return value.substring(0, indexOfBracket).trim()
}

const toLabelNode = (simpleStatus: string): React.ReactNode => {
const toLabelNode = (simpleStatus: string): ReactNode => {
if (simpleStatus === 'new') return <rb.Badge bg="success">{simpleStatus}</rb.Badge>
if (simpleStatus === 'used') return <rb.Badge bg="secondary">{simpleStatus}</rb.Badge>

Expand Down
2 changes: 2 additions & 0 deletions src/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const JM_MINIMUM_MAKERS_DEFAULT = 4
export const CJ_STATE_TAKER_RUNNING = 0
export const CJ_STATE_MAKER_RUNNING = 1
export const CJ_STATE_NONE_RUNNING = 2

export const JM_API_AUTH_TOKEN_EXPIRY: Milliseconds = Math.round(0.5 * 60 * 60 * 1_000)
2 changes: 1 addition & 1 deletion src/constants/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ const __isFeatureEnabled = (name: Feature, version: SemVer): boolean => {
}

export const isFeatureEnabled = (name: Feature, serviceInfo: ServiceInfo): boolean => {
return !!serviceInfo.server?.version && __isFeatureEnabled(name, serviceInfo.server.version)
return !!serviceInfo.server && __isFeatureEnabled(name, serviceInfo.server.version)
}
5 changes: 2 additions & 3 deletions src/context/ServiceConfigContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react'
// @ts-ignore
import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useRef } from 'react'
import { CurrentWallet, useCurrentWallet } from './WalletContext'

import * as Api from '../libs/JmWalletApi'
Expand Down Expand Up @@ -105,7 +104,7 @@ export interface ServiceConfigContextEntry {

const ServiceConfigContext = createContext<ServiceConfigContextEntry | undefined>(undefined)

const ServiceConfigProvider = ({ children }: React.PropsWithChildren<{}>) => {
const ServiceConfigProvider = ({ children }: PropsWithChildren<{}>) => {
const currentWallet = useCurrentWallet()
const serviceConfig = useRef<ServiceConfig | null>(null)

Expand Down
Loading