diff --git a/src/frontend/src/lib/api/actors/actor.api.ts b/src/frontend/src/lib/api/actors/actor.api.ts index 41ba2a94f..23ca43434 100644 --- a/src/frontend/src/lib/api/actors/actor.api.ts +++ b/src/frontend/src/lib/api/actors/actor.api.ts @@ -6,26 +6,32 @@ import type { IDL } from '@dfinity/candid'; import { Principal } from '@dfinity/principal'; import { assertNonNullish, isNullish } from '@dfinity/utils'; -type GetActorParams = { +type CreateActorParams = { canisterId: string | Principal; idlFactory: IDL.InterfaceFactory; config?: Pick; } & GetAgentParams; +export interface GetActorParams { + certified?: boolean; + identity: OptionIdentity; +} + export class ActorApi> { #actors: Option>> = undefined; async getActor({ identity, canisterId, + certified = false, ...rest - }: Omit & { identity: OptionIdentity }): Promise> { + }: Omit & GetActorParams): Promise> { assertNonNullish(identity, 'No internet identity to initialize the actor.'); const identityText = identity.getPrincipal().toText(); const canisterIdText = canisterId instanceof Principal ? canisterId.toText() : canisterId; - const key = `${canisterIdText}#${identityText}`; + const key = `${canisterIdText}${certified ? '+' : '#'}${identityText}`; if (isNullish(this.#actors) || isNullish(this.#actors[key])) { const actor = await this.createActor({ identity, canisterId, ...rest }); @@ -46,7 +52,7 @@ export class ActorApi> { idlFactory, config = {}, ...rest - }: GetActorParams): Promise> { + }: CreateActorParams): Promise> { const agent = await getAgent(rest); // Creates an actor with using the candid interface and the HttpAgent diff --git a/src/frontend/src/lib/api/actors/actor.juno.api.ts b/src/frontend/src/lib/api/actors/actor.juno.api.ts index f8ce1a249..cfa2cc1dd 100644 --- a/src/frontend/src/lib/api/actors/actor.juno.api.ts +++ b/src/frontend/src/lib/api/actors/actor.juno.api.ts @@ -1,4 +1,5 @@ import type { _SERVICE as ConsoleActor } from '$declarations/console/console.did'; +import { idlFactory as idlFactoryCertifiedConsole } from '$declarations/console/console.factory.certified.did'; import { idlFactory as idlFactoryConsole } from '$declarations/console/console.factory.did'; import type { _SERVICE as MissionControlActor } from '$declarations/mission_control/mission_control.did'; import { idlFactory as idlFactoryMissionControl } from '$declarations/mission_control/mission_control.factory.did'; @@ -8,7 +9,7 @@ import type { _SERVICE as OrbiterActor } from '$declarations/orbiter/orbiter.did import { idlFactory as idlFactoryOrbiter } from '$declarations/orbiter/orbiter.factory.did'; import type { _SERVICE as SatelliteActor } from '$declarations/satellite/satellite.did'; import { idlFactory as idlFactorySatellite } from '$declarations/satellite/satellite.factory.did'; -import { ActorApi } from '$lib/api/actors/actor.api'; +import { ActorApi, type GetActorParams } from '$lib/api/actors/actor.api'; import { CONSOLE_CANISTER_ID, OBSERVATORY_CANISTER_ID } from '$lib/constants/constants'; import type { OptionIdentity } from '$lib/types/itentity'; import type { MissionControlId } from '$lib/types/mission-control'; @@ -20,10 +21,13 @@ const satelliteActor = new ActorApi(); const orbiterActor = new ActorApi(); const missionControlActor = new ActorApi(); -export const getConsoleActor = async (identity: OptionIdentity): Promise => +export const getConsoleActor = async ({ + identity, + certified +}: GetActorParams): Promise => await consoleActor.getActor({ canisterId: CONSOLE_CANISTER_ID, - idlFactory: idlFactoryConsole, + idlFactory: certified ? idlFactoryCertifiedConsole : idlFactoryConsole, identity }); diff --git a/src/frontend/src/lib/api/console.api.ts b/src/frontend/src/lib/api/console.api.ts index e0c953f97..c03a68ea1 100644 --- a/src/frontend/src/lib/api/console.api.ts +++ b/src/frontend/src/lib/api/console.api.ts @@ -1,25 +1,26 @@ import type { MissionControl } from '$declarations/console/console.did'; +import type { GetActorParams } from '$lib/api/actors/actor.api'; import { getConsoleActor } from '$lib/api/actors/actor.juno.api'; import type { OptionIdentity } from '$lib/types/itentity'; import type { Principal } from '@dfinity/principal'; import { fromNullable, isNullish } from '@dfinity/utils'; export const initMissionControl = async (identity: OptionIdentity): Promise => { - const actor = await getConsoleActor(identity); + const { init_user_mission_control_center } = await getConsoleActor({ identity }); - const existingMissionControl: MissionControl | undefined = fromNullable( - await actor.get_user_mission_control_center() - ); + return await init_user_mission_control_center(); +}; - if (!existingMissionControl) { - return await actor.init_user_mission_control_center(); - } +export const getMissionControl = async ( + actorParams: GetActorParams +): Promise => { + const { get_user_mission_control_center } = await getConsoleActor(actorParams); - return existingMissionControl; + return fromNullable(await get_user_mission_control_center()); }; export const getCredits = async (identity: OptionIdentity): Promise => { - const { get_credits } = await getConsoleActor(identity); + const { get_credits } = await getConsoleActor({ identity }); const { e8s } = await get_credits(); return e8s; }; @@ -31,7 +32,7 @@ export const getSatelliteFee = async ({ user: Principal; identity: OptionIdentity; }): Promise => { - const actor = await getConsoleActor(identity); + const actor = await getConsoleActor({ identity }); const result = await actor.get_create_satellite_fee({ user }); const fee = fromNullable(result); @@ -46,7 +47,7 @@ export const getOrbiterFee = async ({ user: Principal; identity: OptionIdentity; }): Promise => { - const actor = await getConsoleActor(identity); + const actor = await getConsoleActor({ identity }); const result = await actor.get_create_orbiter_fee({ user }); const fee = fromNullable(result); diff --git a/src/frontend/src/lib/derived/mission-control.derived.ts b/src/frontend/src/lib/derived/mission-control.derived.ts index 1e71c8bb2..e44635ac3 100644 --- a/src/frontend/src/lib/derived/mission-control.derived.ts +++ b/src/frontend/src/lib/derived/mission-control.derived.ts @@ -1,8 +1,8 @@ -import { missionControlIdUncertifiedStore } from '$lib/stores/mission-control.store'; +import { missionControlIdCertifiedStore } from '$lib/stores/mission-control.store'; import { derived } from 'svelte/store'; // TODO: find a better name but, I don't want to use missionControlId because it would clashes with the properties called missionControlId export const missionControlIdDerived = derived( - [missionControlIdUncertifiedStore], + [missionControlIdCertifiedStore], ([$missionControlDataStore]) => $missionControlDataStore?.data ); diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index c061a75cf..d89a88b61 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -438,6 +438,7 @@ "errors": { "no_identity": "Unexpected error. No identity provided.", "initializing_mission_control": "Mission control center cannot be initialized.", + "mission_control_sign_out": "You have been signed out because your Mission Control could not be certified.", "no_mission_control": "Mission control center is not initialized.", "cli_missing_params": "Missing URL parameters. Either the redirection URL or principal is not provided.", "cli_missing_selection": "No mission control or satellite(s) selected.", diff --git a/src/frontend/src/lib/i18n/zh-cn.json b/src/frontend/src/lib/i18n/zh-cn.json index f127b7ab0..e31c87a57 100644 --- a/src/frontend/src/lib/i18n/zh-cn.json +++ b/src/frontend/src/lib/i18n/zh-cn.json @@ -438,6 +438,7 @@ "errors": { "no_identity": "异常错误,没有提供 identity ", "initializing_mission_control": "Mission control center cannot be initialized.", + "mission_control_sign_out": "You have been signed out because your Mission Control could not be certified.", "no_mission_control": "Mission 控制中心没有初始化.", "cli_missing_params": "URL参数缺失,重定向URL或者 principal没有提供.", "cli_missing_selection": "没选择 mission control 或 satellite(s) .", diff --git a/src/frontend/src/lib/services/auth.services.ts b/src/frontend/src/lib/services/auth.services.ts index 0b909b27a..d7c3196e8 100644 --- a/src/frontend/src/lib/services/auth.services.ts +++ b/src/frontend/src/lib/services/auth.services.ts @@ -44,6 +44,14 @@ export const signIn = async ( export const signOut = (): Promise => logout({}); +export const missionControlErrorSignOut = async () => + await logout({ + msg: { + text: get(i18n).errors.mission_control_sign_out, + level: 'error' + } + }); + export const idleSignOut = async () => await logout({ msg: { diff --git a/src/frontend/src/lib/services/console.services.ts b/src/frontend/src/lib/services/console.services.ts index f9a94bb50..37216c743 100644 --- a/src/frontend/src/lib/services/console.services.ts +++ b/src/frontend/src/lib/services/console.services.ts @@ -1,12 +1,20 @@ +import type { MissionControl } from '$declarations/console/console.did'; import type { Orbiter } from '$declarations/mission_control/mission_control.did'; -import { initMissionControl as initMissionControlApi } from '$lib/api/console.api'; +import { + getMissionControl as getMissionControlApi, + initMissionControl as initMissionControlApi +} from '$lib/api/console.api'; import { missionControlVersion } from '$lib/api/mission-control.api'; import { orbiterVersion } from '$lib/api/orbiter.api'; import { satelliteBuildVersion, satelliteVersion } from '$lib/api/satellites.api'; import { getNewestReleasesMetadata } from '$lib/rest/cdn.rest'; +import { missionControlErrorSignOut } from '$lib/services/auth.services'; import { authStore } from '$lib/stores/auth.store'; +import { i18n } from '$lib/stores/i18n.store'; +import { missionControlIdCertifiedStore } from '$lib/stores/mission-control.store'; import { toasts } from '$lib/stores/toasts.store'; import { versionStore, type ReleaseVersionSatellite } from '$lib/stores/version.store'; +import type { OptionIdentity } from '$lib/types/itentity'; import type { MissionControlId } from '$lib/types/mission-control'; import type { Option } from '$lib/types/utils'; import { container } from '$lib/utils/juno.utils'; @@ -16,26 +24,75 @@ import { assertNonNullish, fromNullable, isNullish, nonNullish } from '@dfinity/ import { satelliteBuildType } from '@junobuild/admin'; import { get } from 'svelte/store'; +interface Certified { + certified: boolean; +} + +type PollAndInitResult = { + missionControlId: MissionControlId; +} & Certified; + export const initMissionControl = async ({ - identity, - onInitMissionControlSuccess + identity +}: { + identity: OptionIdentity; +}): Promise<{ result: 'skip' | 'success' | 'error' }> => { + // If not signed in, we are not going to init and load a mission control. + if (isNullish(identity)) { + return { result: 'skip' }; + } + + try { + // Poll to init mission control center + const { missionControlId, certified } = await pollAndInitMissionControl({ + identity + }); + + missionControlIdCertifiedStore.set({ + data: missionControlId, + certified + }); + + if (certified) { + return { result: 'success' }; + } + + // We deliberately do not await the promise to avoid blocking the main UX. However, if necessary, we take the required measures if Mission Control cannot be certified. + assertMissionControl({ identity }); + + return { result: 'success' }; + } catch (err: unknown) { + toasts.error({ + text: get(i18n).errors.initializing_mission_control, + detail: err + }); + + // There was an error so, we sign the user out otherwise skeleton and other spinners will be displayed forever + await missionControlErrorSignOut(); + + return { result: 'error' }; + } +}; + +const pollAndInitMissionControl = async ({ + identity }: { identity: Identity; - onInitMissionControlSuccess: (missionControlId: MissionControlId) => void; // eslint-disable-next-line no-async-promise-executor, require-await -}) => +}): Promise => // eslint-disable-next-line no-async-promise-executor, require-await - new Promise(async (resolve, reject) => { + new Promise(async (resolve, reject) => { try { - const { missionControlId } = await getMissionControl({ + const { missionControlId, certified } = await getOrInitMissionControlId({ identity }); + // TODO: we can/should probably add a max time to not retry forever even though the user will probably close their browsers. if (isNullish(missionControlId)) { setTimeout(async () => { try { - await initMissionControl({ identity, onInitMissionControlSuccess }); - resolve(); + const result = await pollAndInitMissionControl({ identity }); + resolve(result); } catch (err: unknown) { reject(err); } @@ -43,36 +100,59 @@ export const initMissionControl = async ({ return; } - onInitMissionControlSuccess(missionControlId); - - resolve(); + resolve({ missionControlId, certified }); } catch (err: unknown) { reject(err); } }); -const getMissionControl = async ({ +const getOrInitMissionControlId = async (params: { + identity: Identity; +}): Promise< + { + missionControlId: MissionControlId | undefined; + } & Certified +> => { + const { missionControl, certified } = await getOrInitMissionControl(params); + + const missionControlId = fromNullable(missionControl.mission_control_id); + + return { + missionControlId, + certified + }; +}; + +export const getOrInitMissionControl = async ({ identity }: { - identity: Identity | undefined; -}): Promise<{ - missionControlId: MissionControlId | undefined; -}> => { - if (isNullish(identity)) { - throw new Error('Invalid identity.'); - } + identity: Identity; +}): Promise<{ missionControl: MissionControl } & Certified> => { + const existingMissionControl = await getMissionControlApi({ identity, certified: false }); - const mission_control = await initMissionControlApi(identity); + if (isNullish(existingMissionControl)) { + const newMissionControl = await initMissionControlApi(identity); - const missionControlId: MissionControlId | undefined = fromNullable( - mission_control.mission_control_id - ); + return { + missionControl: newMissionControl, + certified: true + }; + } return { - missionControlId + missionControl: existingMissionControl, + certified: false }; }; +const assertMissionControl = async ({ identity }: { identity: Identity }) => { + try { + await getMissionControlApi({ identity, certified: true }); + } catch (error: unknown) { + await missionControlErrorSignOut(); + } +}; + export const loadVersion = async ({ satelliteId, missionControlId, diff --git a/src/frontend/src/lib/stores/mission-control.store.ts b/src/frontend/src/lib/stores/mission-control.store.ts index ed251abdf..f097d1224 100644 --- a/src/frontend/src/lib/stores/mission-control.store.ts +++ b/src/frontend/src/lib/stores/mission-control.store.ts @@ -2,10 +2,11 @@ import type { MissionControlSettings, User } from '$declarations/mission_control/mission_control.did'; +import { initCertifiedStore } from '$lib/stores/_certified.store'; import { initUncertifiedStore } from '$lib/stores/_uncertified.store'; -import type { Principal } from '@dfinity/principal'; +import type { MissionControlId } from '$lib/types/mission-control'; -export const missionControlIdUncertifiedStore = initUncertifiedStore(); +export const missionControlIdCertifiedStore = initCertifiedStore(); export const missionControlUncertifiedStore = initUncertifiedStore(); diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index b313d57be..2d54ddd52 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -454,6 +454,7 @@ interface I18nCli { interface I18nErrors { no_identity: string; initializing_mission_control: string; + mission_control_sign_out: string; no_mission_control: string; cli_missing_params: string; cli_missing_selection: string; diff --git a/src/frontend/src/routes/+layout.svelte b/src/frontend/src/routes/+layout.svelte index 52754662b..427dedeb0 100644 --- a/src/frontend/src/routes/+layout.svelte +++ b/src/frontend/src/routes/+layout.svelte @@ -1,20 +1,18 @@