aboutsummaryrefslogtreecommitdiff
path: root/packages/aml-backoffice-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-04-29 11:42:52 -0300
committerSebastian <sebasjm@gmail.com>2024-04-29 11:42:52 -0300
commit4ed3a05fdd97b08085e5a390963f9810b93a75fd (patch)
tree79b9a76aa3af65b83bcd4c77b42845b5c32d9af3 /packages/aml-backoffice-ui/src
parent44308fb898372c059cf03a23ad1169d5a6e0c296 (diff)
downloadwallet-core-4ed3a05fdd97b08085e5a390963f9810b93a75fd.tar.xz
create form
Diffstat (limited to 'packages/aml-backoffice-ui/src')
-rw-r--r--packages/aml-backoffice-ui/src/Routing.tsx10
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts22
-rw-r--r--packages/aml-backoffice-ui/src/hooks/officer.ts18
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx42
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx252
5 files changed, 244 insertions, 100 deletions
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx
index 1e32e4f4c..f38fc29c2 100644
--- a/packages/aml-backoffice-ui/src/Routing.tsx
+++ b/packages/aml-backoffice-ui/src/Routing.tsx
@@ -97,18 +97,18 @@ function PublicRounting(): VNode {
export const privatePages = {
account: urlPattern(/\/account/, () => "#/account"),
cases: urlPattern(/\/cases/, () => "#/cases"),
- caseDetails: urlPattern<{ cid: string }>(
- /\/case\/(?<cid>[a-zA-Z0-9]+)/,
- ({ cid }) => `#/case/${cid}`,
- ),
caseUpdate: urlPattern<{ cid: string; type: string }>(
- /\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9]+)/,
+ /\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9_.]+)/,
({ cid, type }) => `#/case/${cid}/new/${type}`,
),
caseNew: urlPattern<{ cid: string }>(
/\/case\/(?<cid>[a-zA-Z0-9]+)\/new/,
({ cid }) => `#/case/${cid}/new`,
),
+ caseDetails: urlPattern<{ cid: string }>(
+ /\/case\/(?<cid>[a-zA-Z0-9]+)/,
+ ({ cid }) => `#/case/${cid}`,
+ ),
};
function PrivateRouting(): VNode {
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
index fae11c05c..e3d97db8c 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -68,11 +68,10 @@ export type FormStatus<T> =
};
function constructFormHandler<T>(
- form: FormValues<T>,
- updateForm: (d: FormValues<T>) => void,
+ form: RecursivePartial<FormValues<T>>,
+ updateForm: (d: RecursivePartial<FormValues<T>>) => void,
errors: FormErrors<T> | undefined,
): FormHandler<T> {
-
const keys = Object.keys(form) as Array<keyof T>;
const handler = keys.reduce((prev, fieldName) => {
@@ -105,17 +104,18 @@ function constructFormHandler<T>(
/**
* FIXME: Consider sending this to web-utils
- *
- *
- * @param defaultValue
- * @param check
- * @returns
+ *
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
*/
export function useFormState<T>(
- defaultValue: FormValues<T>,
- check: (f: FormValues<T>) => FormStatus<T>,
+ defaultValue: RecursivePartial<FormValues<T>>,
+ check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
): [FormHandler<T>, FormStatus<T>] {
- const [form, updateForm] = useState<FormValues<T>>(defaultValue);
+ const [form, updateForm] =
+ useState<RecursivePartial<FormValues<T>>>(defaultValue);
const status = check(form);
const handler = constructFormHandler(form, updateForm, status.errors);
diff --git a/packages/aml-backoffice-ui/src/hooks/officer.ts b/packages/aml-backoffice-ui/src/hooks/officer.ts
index 3ac4c857c..dabe866d3 100644
--- a/packages/aml-backoffice-ui/src/hooks/officer.ts
+++ b/packages/aml-backoffice-ui/src/hooks/officer.ts
@@ -19,6 +19,7 @@ import {
LockedAccount,
OfficerAccount,
OfficerId,
+ OperationOk,
SigningKey,
buildCodecForObject,
codecForAbsoluteTime,
@@ -26,6 +27,7 @@ import {
createNewOfficerAccount,
decodeCrock,
encodeCrock,
+ opFixedSuccess,
unlockOfficerAccount,
} from "@gnu-taler/taler-util";
import { buildStorageKey, useExchangeApiContext, useLocalStorage } from "@gnu-taler/web-util/browser";
@@ -60,7 +62,7 @@ export type OfficerState = OfficerNotReady | OfficerReady;
export type OfficerNotReady = OfficerNotFound | OfficerLocked;
interface OfficerNotFound {
state: "not-found";
- create: (password: string) => Promise<void>;
+ create: (password: string) => Promise<OperationOk<OfficerId>>;
}
interface OfficerLocked {
state: "locked";
@@ -81,7 +83,7 @@ const DEV_ACCOUNT_KEY = buildStorageKey(
);
export function useOfficer(): OfficerState {
- const exchangeContext = useExchangeApiContext();
+ const {lib:{exchange: api}} = useExchangeApiContext();
const [pref] = usePreferences();
pref.keepSessionAfterReload;
// dev account, is kept on reloaded.
@@ -105,16 +107,12 @@ export function useOfficer(): OfficerState {
return {
state: "not-found",
create: async (pwd: string) => {
- const req = await fetch(
- new URL("seed", exchangeContext.lib.exchange.baseUrl).href,
- );
- const b = await req.blob();
- const ar = await b.arrayBuffer();
- const uintar = new Uint8Array(ar);
+ const resp = await api.getSeed()
+ const extraEntropy = resp.type === "ok" ? resp.body : new Uint8Array();
const { id, safe, signingKey } = await createNewOfficerAccount(
pwd,
- uintar,
+ extraEntropy,
);
officerStorage.update({
account: safe,
@@ -124,6 +122,8 @@ export function useOfficer(): OfficerState {
// accountStorage.update({ id, signingKey });
const strKey = encodeCrock(signingKey);
accountStorage.update({ id, strKey });
+
+ return opFixedSuccess(id)
},
};
}
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index e928b831f..6b59b2736 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -20,19 +20,20 @@ import {
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
+ Attention,
ErrorLoading,
Loading,
createNewForm,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { useCases } from "../hooks/useCases.js";
+import { privatePages } from "../Routing.js";
import { amlStateConverter } from "../utils/converter.js";
import { AmlExchangeBackend } from "../utils/types.js";
import { Officer } from "./Officer.js";
-import { privatePages } from "../Routing.js";
export function CasesUI({
records,
@@ -130,7 +131,9 @@ export function CasesUI({
<td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
<div class="text-gray-900">
<a
- href={privatePages.caseDetails.url({ cid: r.h_payto })}
+ href={privatePages.caseDetails.url({
+ cid: r.h_payto,
+ })}
class="text-indigo-600 hover:text-indigo-900"
>
{r.h_payto.substring(0, 16)}...
@@ -187,6 +190,7 @@ export function Cases() {
);
const list = useCases(stateFilter);
+ const { i18n } = useTranslationContext();
if (!list) {
return <Loading />;
@@ -197,8 +201,36 @@ export function Cases() {
if (list.type === "fail") {
switch (list.case) {
- case HttpStatusCode.Unauthorized:
- case HttpStatusCode.Forbidden:
+ case HttpStatusCode.Forbidden: {
+ return (
+ <Fragment>
+ <Attention
+ type="danger"
+ title={i18n.str`Operation denied`}
+ >
+ <i18n.Translate>
+ This account doesnt have access. Request account activation sending your public key.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return (
+ <Fragment>
+ <Attention
+ type="danger"
+ title={i18n.str`Operation denied`}
+ >
+ <i18n.Translate>
+ This account is not allowed to perform list the cases.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
case HttpStatusCode.NotFound:
case HttpStatusCode.Conflict:
return <Officer />;
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 9afd0d212..094e78531 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -13,29 +13,113 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { TranslatedString } from "@gnu-taler/taler-util";
import {
- createNewForm,
- notifyError,
+ Button,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
+import {
+ FormErrors,
+ FormStatus,
+ FormValues,
+ RecursivePartial,
+ useFormState,
+} from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
import { usePreferences } from "../hooks/preferences.js";
+type FormType = {
+ password: string;
+ repeat: string;
+};
+function createFormValidator(
+ i18n: InternationalizationAPI,
+ allowInsecurePassword: boolean,
+) {
+ return function check(
+ state: RecursivePartial<FormValues<FormType>>,
+ ): FormStatus<FormType> {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ password: !state.password
+ ? i18n.str`required`
+ : allowInsecurePassword
+ ? undefined
+ : state.password.length < 8
+ ? i18n.str`should have at least 8 characters`
+ : !state.password.match(/[a-z]/) && state.password.match(/[A-Z]/)
+ ? i18n.str`should have lowercase and uppercase characters`
+ : !state.password.match(/\d/)
+ ? i18n.str`should have numbers`
+ : !state.password.match(/[^a-zA-Z\d]/)
+ ? i18n.str`should have at least one character which is not a number or letter`
+ : undefined,
+
+ repeat: !state.repeat
+ ? i18n.str`required`
+ : state.password !== state.repeat
+ ? i18n.str`doesn't match`
+ : undefined,
+ });
+
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: state as FormType,
+ errors,
+ };
+ }
+ return {
+ status: "fail",
+ result: state,
+ errors,
+ };
+ };
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
+
export function CreateAccount({
onNewAccount,
}: {
- onNewAccount: (password: string) => void;
+ onNewAccount: () => void;
}): VNode {
const { i18n } = useTranslationContext();
- const Form = createNewForm<{
- password: string;
- repeat: string;
- }>();
+ // const Form = createNewForm<FormType>();
const [settings] = usePreferences();
-
+ const officer = useOfficer();
+
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+
+ const [form, status] = useFormState<FormType>(
+ {
+ password: undefined,
+ repeat: undefined,
+ },
+ createFormValidator(i18n, settings.allowInsecurePassword),
+ );
+
+ const createAccountHandler =
+ status.status === "fail" || officer.state !== "not-found"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.create(form.password!.value!),
+ onNewAccount,
+ );
+
return (
<div class="flex min-h-full flex-col ">
+ <LocalNotificationBanner notification={notification} />
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Create account</i18n.Translate>
@@ -44,79 +128,107 @@ export function CreateAccount({
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
- <Form.Provider
- computeFormState={(v) => {
- return {
- password: {
- error: !v.password
- ? i18n.str`required`
- : settings.allowInsecurePassword
- ? undefined
- : v.password.length < 8
- ? i18n.str`should have at least 8 characters`
- : !v.password.match(/[a-z]/) &&
- v.password.match(/[A-Z]/)
- ? i18n.str`should have lowercase and uppercase characters`
- : !v.password.match(/\d/)
- ? i18n.str`should have numbers`
- : !v.password.match(/[^a-zA-Z\d]/)
- ? i18n.str`should have at least one character which is not a number or letter`
- : undefined,
- },
- repeat: {
- error: !v.repeat
- ? i18n.str`required`
- : v.repeat !== v.password
- ? i18n.str`doesn't match`
- : undefined,
- },
- };
- }}
- onSubmit={async (v, s) => {
- const error = s?.password?.error ?? s?.repeat?.error;
- if (error) {
- notifyError(
- i18n.str`Can't create account`,
- error as TranslatedString,
- );
- } else {
- onNewAccount(v.password!);
- }
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
}}
+ autoCapitalize="none"
+ autoCorrect="off"
>
- <div class="mb-4">
- <Form.InputLine
- label={i18n.str`Password`}
- name="password"
- type="password"
- help={
- settings.allowInsecurePassword
- ? i18n.str`short password are insecure, turn off insecure password in settings`
- : i18n.str`lower and upper case letters, number and special character`
- }
- required
- />
+ <div>
+ <label
+ for="password"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Password</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ ref={doAutoFocus}
+ type="text"
+ name="password"
+ id="password"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={form.password?.value ?? ""}
+ enterkeyhint="next"
+ placeholder="strong password"
+ autocomplete="password"
+ title={i18n.str`Password`}
+ required
+ onChange={(e) => {
+ console.log("ASDASD", form.password?.onUpdate);
+ form.password?.onUpdate(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={form.password?.error}
+ isDirty={form.password?.value !== undefined}
+ />
+ </div>
</div>
- <div class="mb-4">
- <Form.InputLine
- label={i18n.str`Repeat password`}
- name="repeat"
- type="password"
- required
- />
+
+ <div>
+ <label
+ for="repeat"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
+ <i18n.Translate>Repeat password</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ name="repeat"
+ id="repeat"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={form.repeat?.value ?? ""}
+ enterkeyhint="next"
+ placeholder="identification"
+ autocomplete="repeat"
+ title={i18n.str`Repeat password`}
+ required
+ onChange={(e): void => {
+ form.repeat?.onUpdate(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={form.repeat?.error}
+ isDirty={form.repeat?.value !== undefined}
+ />
+ </div>
</div>
<div class="mt-8">
- <button
+ <Button
type="submit"
- class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!createAccountHandler}
+ class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={createAccountHandler}
>
<i18n.Translate>Create</i18n.Translate>
- </button>
+ </Button>
</div>
- </Form.Provider>
+ </form>
</div>
</div>
</div>
);
}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}