/*
This file is part of GNU Taler
(C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see
*/
import {
AbsoluteTime,
Codec,
LockedAccount,
OfficerAccount,
OfficerId,
OperationOk,
SigningKey,
buildCodecForObject,
codecForAbsoluteTime,
codecForString,
createNewOfficerAccount,
decodeCrock,
encodeCrock,
opFixedSuccess,
unlockOfficerAccount,
} from "@gnu-taler/taler-util";
import { buildStorageKey, useExchangeApiContext, useLocalStorage } from "@gnu-taler/web-util/browser";
import { useMemo } from "preact/hooks";
import { usePreferences } from "./preferences.js";
export interface Officer {
account: LockedAccount;
when: AbsoluteTime;
}
const codecForLockedAccount = codecForString() as Codec;
type OfficerAccountString = {
id: string;
strKey: string;
};
export const codecForOfficerAccount = (): Codec =>
buildCodecForObject()
.property("id", codecForString()) // FIXME
.property("strKey", codecForString()) // FIXME
.build("OfficerAccount");
export const codecForOfficer = (): Codec =>
buildCodecForObject()
.property("account", codecForLockedAccount) // FIXME
.property("when", codecForAbsoluteTime) // FIXME
.build("Officer");
export type OfficerState = OfficerNotReady | OfficerReady;
export type OfficerNotReady = OfficerNotFound | OfficerLocked;
interface OfficerNotFound {
state: "not-found";
create: (password: string) => Promise>;
}
interface OfficerLocked {
state: "locked";
forget: () => OperationOk;
tryUnlock: (password: string) => Promise>;
}
interface OfficerReady {
state: "ready";
account: OfficerAccount;
forget: () => OperationOk;
lock: () => OperationOk;
}
const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
const DEV_ACCOUNT_KEY = buildStorageKey(
"account-dev",
codecForOfficerAccount(),
);
export function useOfficer(): OfficerState {
const {lib:{exchange: api}} = useExchangeApiContext();
const [pref] = usePreferences();
pref.keepSessionAfterReload;
// dev account, is kept on reloaded.
const accountStorage = useLocalStorage(DEV_ACCOUNT_KEY);
const account = useMemo(() => {
if (!accountStorage.value) return undefined;
return {
id: accountStorage.value.id as OfficerId,
signingKey: decodeCrock(accountStorage.value.strKey) as SigningKey,
};
}, [accountStorage.value?.id, accountStorage.value?.strKey]);
const officerStorage = useLocalStorage(OFFICER_KEY);
const officer = useMemo(() => {
if (!officerStorage.value) return undefined;
return officerStorage.value;
}, [officerStorage.value?.account, officerStorage.value?.when.t_ms]);
if (officer === undefined) {
return {
state: "not-found",
create: async (pwd: string) => {
const resp = await api.getSeed()
const extraEntropy = resp.type === "ok" ? resp.body : new Uint8Array();
const { id, safe, signingKey } = await createNewOfficerAccount(
pwd,
extraEntropy,
);
officerStorage.update({
account: safe,
when: AbsoluteTime.now(),
});
// accountStorage.update({ id, signingKey });
const strKey = encodeCrock(signingKey);
accountStorage.update({ id, strKey });
return opFixedSuccess(id)
},
};
}
if (account === undefined) {
return {
state: "locked",
forget: () => {
officerStorage.reset();
return opFixedSuccess(undefined)
},
tryUnlock: async (pwd: string) => {
const ac = await unlockOfficerAccount(officer.account, pwd);
// accountStorage.update(ac);
accountStorage.update({
id: ac.id,
strKey: encodeCrock(ac.signingKey),
});
return opFixedSuccess(undefined)
},
};
}
return {
state: "ready",
account,
lock: () => {
accountStorage.reset();
return opFixedSuccess(undefined)
},
forget: () => {
officerStorage.reset();
accountStorage.reset();
return opFixedSuccess(undefined)
},
};
}