Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions packages/admin-portal/src/components/keys-ceremony/DownloadStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import {useMutation} from "@apollo/client"
import React, {useContext, useState} from "react"
import {FormControlLabel, FormGroup, Typography, Checkbox} from "@mui/material"
import {Alert, FormControlLabel, FormGroup, Typography, Checkbox} from "@mui/material"
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"
import DownloadIcon from "@mui/icons-material/Download"
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos"
Expand All @@ -18,8 +18,14 @@ import {AuthContext} from "@/providers/AuthContextProvider"
import {WizardStyles} from "@/components/styles/WizardStyles"
import {GET_PRIVATE_KEY} from "@/queries/GetPrivateKey"
import {Dialog} from "@sequentech/ui-essentials"
import {useNotify} from "react-admin"
import {useGetOne, useNotify} from "react-admin"
import {useAliasRenderer} from "@/hooks/useAliasRenderer"
import {SettingsContext} from "@/providers/SettingsContextProvider"
import {
IKeysCeremonyExecutionStatus as EStatus,
IKeysCeremonyTrusteeStatus as TStatus,
} from "@/services/KeyCeremony"
import {isPrivateKeyDownloadUnavailableError} from "@/services/privateKeyDownloadError"

export interface DownloadStepProps {
electionEvent: Sequent_Backend_Election_Event
Expand All @@ -36,10 +42,12 @@ export const DownloadStep: React.FC<DownloadStepProps> = ({
}) => {
const {t} = useTranslation()
const authContext = useContext(AuthContext)
const {globalSettings} = useContext(SettingsContext)
const [downloaded, setDownloaded] = useState<boolean>(false)
const [downloading, setDownloading] = useState<boolean>(false)
const [openConfirmationModal, setOpenConfirmationModal] = useState(false)
const [errors, setErrors] = useState<String | null>(null)
const [downloadUnavailable, setDownloadUnavailable] = useState(false)
const notify = useNotify()
const aliasRenderer = useAliasRenderer()

Expand All @@ -57,10 +65,30 @@ export const DownloadStep: React.FC<DownloadStepProps> = ({
}
const {firstCheckbox, secondCheckbox} = checkboxState

const {data: latestCeremony} = useGetOne<Sequent_Backend_Keys_Ceremony>(
"sequent_backend_keys_ceremony",
{id: currentCeremony.id},
{refetchInterval: globalSettings.QUERY_FAST_POLL_INTERVAL_MS}
)
const isDownloadUnavailable =
downloadUnavailable ||
(latestCeremony?.execution_status ?? currentCeremony.execution_status) !==
EStatus.IN_PROGRESS
const trusteeStatus = (latestCeremony?.status ?? currentCeremony.status)?.trustees?.find(
(trustee: {name: string}) => trustee.name === authContext.trustee
)?.status
const downloadUnavailableMessage =
trusteeStatus === TStatus.KEY_CHECKED
? "keysGeneration.downloadStep.alreadyVerified"
: "keysGeneration.downloadStep.unavailable"

const [getPrivateKeysMutation] = useMutation<GetPrivateKeyMutation>(GET_PRIVATE_KEY)
const download = async () => {
setErrors(null)
setDownloaded(false)
if (isDownloadUnavailable) {
return
}
setDownloading(true)
try {
const {data, errors} = await getPrivateKeysMutation({
Expand All @@ -71,9 +99,11 @@ export const DownloadStep: React.FC<DownloadStepProps> = ({
})
setDownloading(false)
if (errors) {
setErrors(
t("keysGeneration.downloadStep.errorDownloading", {error: errors.toString()})
)
if (isPrivateKeyDownloadUnavailableError({graphQLErrors: errors})) {
setDownloadUnavailable(true)
} else {
setErrors(t("keysGeneration.downloadStep.unexpectedError"))
}
return null
} else {
const privateKey = data?.get_private_key?.private_key_base64
Expand All @@ -92,11 +122,13 @@ export const DownloadStep: React.FC<DownloadStepProps> = ({
tempLink.click()
setDownloaded(true)
}
} catch (exception: any) {
} catch (exception: unknown) {
setDownloading(false)
setErrors(
t("keysGeneration.downloadStep.errorDownloading", {error: exception.toString()})
)
if (isPrivateKeyDownloadUnavailableError(exception)) {
setDownloadUnavailable(true)
} else {
setErrors(t("keysGeneration.downloadStep.unexpectedError"))
}
return null
}
}
Expand All @@ -116,19 +148,23 @@ export const DownloadStep: React.FC<DownloadStepProps> = ({
<WizardStyles.DownloadButton
color="primary"
onClick={download}
disabled={downloading || isDownloadUnavailable}
className="keys-download-download-button"
>
<DownloadIcon />
{t("keysGeneration.downloadStep.downloadButton")}
</WizardStyles.DownloadButton>
<WizardStyles.StatusBox>
{downloading ? <WizardStyles.DownloadProgress /> : null}
{isDownloadUnavailable ? (
<Alert severity="info">{t(downloadUnavailableMessage)}</Alert>
) : null}
{downloaded ? (
<WizardStyles.SucessMessage
variant="body1"
className="keys-download-success"
>
{t("keysGeneration.checkStep.downloaded")}
{t("keysGeneration.downloadStep.downloaded")}
</WizardStyles.SucessMessage>
) : null}
{errors ? (
Expand Down
52 changes: 52 additions & 0 deletions packages/admin-portal/src/services/privateKeyDownloadError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2026 Sequent Tech Inc <legal@sequentech.io>
//
// SPDX-License-Identifier: AGPL-3.0-only

import {
isPrivateKeyDownloadUnavailableError,
PRIVATE_KEY_DOWNLOAD_UNAVAILABLE_ERROR_CODE,
} from "./privateKeyDownloadError"

describe("isPrivateKeyDownloadUnavailableError", () => {
it("recognizes a direct Hasura error code", () => {
expect(
isPrivateKeyDownloadUnavailableError({
graphQLErrors: [{extensions: {code: PRIVATE_KEY_DOWNLOAD_UNAVAILABLE_ERROR_CODE}}],
})
).toBe(true)
})

it("recognizes a code in the action response body", () => {
expect(
isPrivateKeyDownloadUnavailableError({
graphQLErrors: [
{
extensions: {
internal: {
response: {
body: JSON.stringify({
extensions: {
code: PRIVATE_KEY_DOWNLOAD_UNAVAILABLE_ERROR_CODE,
},
}),
},
},
},
},
],
})
).toBe(true)
})

it("rejects unrelated and malformed errors", () => {
expect(
isPrivateKeyDownloadUnavailableError({
graphQLErrors: [
{extensions: {code: "InternalServerError"}},
{extensions: {internal: {response: {body: "not json"}}}},
],
})
).toBe(false)
expect(isPrivateKeyDownloadUnavailableError(new Error("Network error"))).toBe(false)
})
})
26 changes: 26 additions & 0 deletions packages/admin-portal/src/services/privateKeyDownloadError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2026 Sequent Tech Inc <legal@sequentech.io>
//
// SPDX-License-Identifier: AGPL-3.0-only

import {IGraphQLActionError} from "@sequentech/ui-core"
import {parseActionResponseBody} from "./graphqlActionError"

export const PRIVATE_KEY_DOWNLOAD_UNAVAILABLE_ERROR_CODE = "PrivateKeyDownloadUnavailable"

export const isPrivateKeyDownloadUnavailableError = (error: unknown): boolean => {
const actionError = error as IGraphQLActionError | undefined

return (
actionError?.graphQLErrors?.some((graphQLError) => {
if (graphQLError.extensions?.code === PRIVATE_KEY_DOWNLOAD_UNAVAILABLE_ERROR_CODE) {
return true
}

const responseBody = parseActionResponseBody(
graphQLError.extensions?.internal?.response?.body
) as {extensions?: {code?: unknown}} | undefined

return responseBody?.extensions?.code === PRIVATE_KEY_DOWNLOAD_UNAVAILABLE_ERROR_CODE
}) === true
)
}
7 changes: 5 additions & 2 deletions packages/admin-portal/src/translations/cat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2026,8 +2026,12 @@ const catalanTranslation: TranslationType = {
subtitle:
"Per continuar, si us plau descarrega i guarda la teva Clau Privada Encriptada en almenys dos dispositius diferents:",
downloadButton: "Descarregar la teva Clau Privada Encriptada",
errorDownloading: "Error de desc脿rrega: {{error}}",
downloaded: "Clau Privada Encriptada descarregada correctament.",
errorEmptyKey: "Error de desc脿rrega, fitxer buit",
unexpectedError: "No s'ha pogut descarregar la clau privada. Torna-ho a provar.",
alreadyVerified: "La teva clau privada ja s'havia descarregat i verificat.",
unavailable:
"La desc脿rrega de la clau privada ja no est脿 disponible perqu猫 la cerim貌nia ha avan莽at.",
confirmdDialog: {
ok: "Confirmar c貌pies de seguretat i Continuar",
cancel: "Tornar",
Expand All @@ -2048,7 +2052,6 @@ const catalanTranslation: TranslationType = {
"C貌pia de Seguretat de la Clau Encriptada Privada inv脿lida, si us plau intenta-ho de nou",
errorEmptyFile: "Fitxer buit o no trobat",
verified: "C貌pia de seguretat verificada correctament.",
downloaded: "Clau Encriptada Privada generada amb 猫xit.",
},
},
miruExport: {
Expand Down
7 changes: 5 additions & 2 deletions packages/admin-portal/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1993,8 +1993,12 @@ const englishTranslation = {
subtitle:
"To continue, please download and store your Encrypted Private Key at least into two different devices:",
downloadButton: "Download your Encrypted Private Key",
errorDownloading: "Download error: {{error}}",
downloaded: "Encrypted Private Key downloaded successfully.",
errorEmptyKey: "Download error, empty file",
unexpectedError: "The private key could not be downloaded. Please try again.",
alreadyVerified: "Your private key was already downloaded and verified.",
unavailable:
"Private key download is no longer available because the ceremony has moved on.",
confirmdDialog: {
ok: "Confirm Backups and Continue",
cancel: "Go Back",
Expand All @@ -2014,7 +2018,6 @@ const englishTranslation = {
errorUploading: "Invalid Encrypted Private Key Backup, please try again",
errorEmptyFile: "File empty or not found",
verified: "Backup verified successfully.",
downloaded: "Encrypted Private Key generated successfully.",
},
},
miruExport: {
Expand Down
7 changes: 5 additions & 2 deletions packages/admin-portal/src/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2016,8 +2016,12 @@ const spanishTranslation: TranslationType = {
subtitle:
"Para continuar, por favor descarga y guarda tu Clave Privada Encriptada en al menos dos dispositivos diferentes:",
downloadButton: "Descargar tu Clave Privada Encriptada",
errorDownloading: "Error de descarga: {{error}}",
downloaded: "Clave Privada Encriptada descargada correctamente.",
errorEmptyKey: "Error de descarga, fichero vac铆o",
unexpectedError: "No se pudo descargar la clave privada. Int茅ntalo de nuevo.",
alreadyVerified: "Tu clave privada ya se hab铆a descargado y verificado.",
unavailable:
"La descarga de la clave privada ya no est谩 disponible porque la ceremonia ha avanzado.",
confirmdDialog: {
ok: "Confirmar copias de seguridad y Continuar",
cancel: "Volver",
Expand All @@ -2038,7 +2042,6 @@ const spanishTranslation: TranslationType = {
"Copa de Seguridad de la Clave Encriptada Privada inv谩lida, por favor int茅ntalo de nuevo",
errorEmptyFile: "Fichero vac铆o o no encontrado",
verified: "Copia de seguridad verificada correctamente.",
downloaded: "Clave Encriptada Privada generada exitosamente.",
},
},
miruExport: {
Expand Down
7 changes: 5 additions & 2 deletions packages/admin-portal/src/translations/eu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2009,8 +2009,12 @@ const basqueTranslation: TranslationType = {
subtitle:
"Jarraitzeko, mesedez deskargatu eta gorde zure Zifratutako Giltza Pribatua gutxienez bi gailu desberdinetan:",
downloadButton: "Deskargatu zure Zifratutako Giltza Pribatua",
errorDownloading: "Deskarga errorea: {{error}}",
downloaded: "Zifratutako Giltza Pribatua behar bezala deskargatu da.",
errorEmptyKey: "Deskarga errorea, fitxategi hutsa",
unexpectedError: "Ezin izan da gako pribatua deskargatu. Saiatu berriro.",
alreadyVerified: "Zure gako pribatua deskargatuta eta egiaztatuta zegoen.",
unavailable:
"Gako pribatuaren deskarga jada ez dago erabilgarri, zeremoniak aurrera egin duelako.",
confirmdDialog: {
ok: "Berretsi Babeskopiak eta Jarraitu",
cancel: "Itzuli",
Expand All @@ -2031,7 +2035,6 @@ const basqueTranslation: TranslationType = {
"Zifratutako Giltza Pribatu Babeskopia baliogabea, mesedez saiatu berriro",
errorEmptyFile: "Fitxategia hutsa edo ez da aurkitu",
verified: "Babeskopia arrakastaz egiaztatua.",
downloaded: "Zifratutako Giltza Pribatua arrakastaz sortua.",
},
},
miruExport: {
Expand Down
7 changes: 5 additions & 2 deletions packages/admin-portal/src/translations/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2029,8 +2029,12 @@ const frenchTranslation: TranslationType = {
subtitle:
"Pour continuer, veuillez t茅l茅charger et sauvegarder votre Cl茅 Priv茅e Crypt茅e sur au moins deux appareils diff茅rents :",
downloadButton: "T茅l茅charger votre Cl茅 Priv茅e Crypt茅e",
errorDownloading: "Erreur de t茅l茅chargement : {{error}}",
downloaded: "Cl茅 Priv茅e Crypt茅e t茅l茅charg茅e avec succ猫s.",
errorEmptyKey: "Erreur de t茅l茅chargement, fichier vide",
unexpectedError: "La cl茅 priv茅e n'a pas pu 锚tre t茅l茅charg茅e. Veuillez r茅essayer.",
alreadyVerified: "Votre cl茅 priv茅e a d茅j脿 茅t茅 t茅l茅charg茅e et v茅rifi茅e.",
unavailable:
"Le t茅l茅chargement de la cl茅 priv茅e n'est plus disponible car la c茅r茅monie a progress茅.",
confirmdDialog: {
ok: "Confirmer les copies de sauvegarde et Continuer",
cancel: "Revenir",
Expand All @@ -2051,7 +2055,6 @@ const frenchTranslation: TranslationType = {
"Copa de Sauvegarde de la Cl茅 Priv茅e Crypt茅e invalide, veuillez r茅essayer",
errorEmptyFile: "Fichier vide ou non trouv茅",
verified: "Copie de sauvegarde v茅rifi茅e avec succ猫s.",
downloaded: "Cl茅 Priv茅e Crypt茅e g茅n茅r茅e avec succ猫s.",
},
},
miruExport: {
Expand Down
7 changes: 5 additions & 2 deletions packages/admin-portal/src/translations/gl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2015,8 +2015,12 @@ const galegoTranslation: TranslationType = {
subtitle:
"Para continuar, por favor descarga e almacena a t煤a Chave Privada Cifrada en polo menos dous dispositivos diferentes:",
downloadButton: "Descargar a t煤a Chave Privada Cifrada",
errorDownloading: "Erro de descarga: {{error}}",
downloaded: "Chave Privada Cifrada descargada correctamente.",
errorEmptyKey: "Erro de descarga, ficheiro baleiro",
unexpectedError: "Non se puido descargar a clave privada. T茅ntao de novo.",
alreadyVerified: "A t煤a chave privada xa se descargara e verificara.",
unavailable:
"A descarga da clave privada xa non est谩 dispo帽ible porque a cerimonia avanzou.",
confirmdDialog: {
ok: "Confirmar Copias de Seguridade e Continuar",
cancel: "Volver Atr谩s",
Expand All @@ -2037,7 +2041,6 @@ const galegoTranslation: TranslationType = {
"Copia de Seguridade da Chave Privada Cifrada inv谩lida, por favor intenta de novo",
errorEmptyFile: "Ficheiro baleiro ou non atopado",
verified: "Copia de seguridade verificada correctamente.",
downloaded: "Chave Privada Cifrada xerada correctamente.",
},
},
miruExport: {
Expand Down
7 changes: 5 additions & 2 deletions packages/admin-portal/src/translations/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2014,8 +2014,12 @@ const dutchTranslation: TranslationType = {
subtitle:
"Om door te gaan, download en bewaar uw Versleutelde Priv茅sleutel op minstens twee verschillende apparaten:",
downloadButton: "Download uw Versleutelde Priv茅sleutel",
errorDownloading: "Downloadfout: {{error}}",
downloaded: "Versleutelde priv茅sleutel succesvol gedownload.",
errorEmptyKey: "Downloadfout, leeg bestand",
unexpectedError: "De priv茅sleutel kon niet worden gedownload. Probeer het opnieuw.",
alreadyVerified: "Uw priv茅sleutel was al gedownload en geverifieerd.",
unavailable:
"De priv茅sleutel kan niet meer worden gedownload omdat de ceremonie is gevorderd.",
confirmdDialog: {
ok: "Back-ups Bevestigen en Doorgaan",
cancel: "Terug",
Expand All @@ -2035,7 +2039,6 @@ const dutchTranslation: TranslationType = {
errorUploading: "Ongeldige Back-up van Versleutelde Priv茅sleutel, probeer opnieuw",
errorEmptyFile: "Bestand leeg of niet gevonden",
verified: "Back-up succesvol geverifieerd.",
downloaded: "Versleutelde Priv茅sleutel succesvol gegenereerd.",
},
},
miruExport: {
Expand Down
7 changes: 5 additions & 2 deletions packages/admin-portal/src/translations/tl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2020,8 +2020,12 @@ const tagalogTranslation: TranslationType = {
subtitle:
"Upang magpatuloy, mangyaring i-download at itago ang iyong Encrypted Private Key sa hindi bababa sa dalawang magkaibang device:",
downloadButton: "I-download ang iyong Encrypted Private Key",
errorDownloading: "Error sa pag-download: {{error}}",
downloaded: "Matagumpay na na-download ang Encrypted Private Key.",
errorEmptyKey: "Error sa pag-download, walang laman na file",
unexpectedError: "Hindi ma-download ang pribadong key. Pakisubukang muli.",
alreadyVerified: "Na-download at na-verify na ang iyong pribadong key.",
unavailable:
"Hindi na maaaring i-download ang pribadong key dahil nagpatuloy na ang seremonya.",
confirmdDialog: {
ok: "Kumpirmahin ang mga Backup at Magpatuloy",
cancel: "Bumalik",
Expand All @@ -2041,7 +2045,6 @@ const tagalogTranslation: TranslationType = {
errorUploading: "Di-wastong Encrypted Private Key Backup, mangyaring subukan muli",
errorEmptyFile: "Walang laman na file o hindi natagpuan",
verified: "Backup ay matagumpay na nasuri.",
downloaded: "Encrypted Private Key ay matagumpay na nabuo.",
},
},
miruExport: {
Expand Down
Loading
Loading