anastasis-webui: finish backup flow
This commit is contained in:
parent
b8d03b6b2a
commit
0f1ef7eca1
@ -21,6 +21,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gnu-taler/taler-util": "workspace:^0.8.3",
|
||||||
"preact": "^10.3.1",
|
"preact": "^10.3.1",
|
||||||
"preact-render-to-string": "^5.1.4",
|
"preact-render-to-string": "^5.1.4",
|
||||||
"preact-router": "^3.2.1"
|
"preact-router": "^3.2.1"
|
||||||
|
@ -1,6 +1,58 @@
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
type ReducerState = any;
|
export type ReducerState =
|
||||||
|
| ReducerStateBackup
|
||||||
|
| ReducerStateRecovery
|
||||||
|
| ReducerStateError;
|
||||||
|
|
||||||
|
export interface ReducerStateBackup {
|
||||||
|
recovery_state: undefined;
|
||||||
|
backup_state: BackupStates;
|
||||||
|
code: undefined;
|
||||||
|
continents: any;
|
||||||
|
countries: any;
|
||||||
|
authentication_providers: any;
|
||||||
|
authentication_methods?: AuthMethod[];
|
||||||
|
required_attributes: any;
|
||||||
|
secret_name?: string;
|
||||||
|
policies?: {
|
||||||
|
methods: {
|
||||||
|
authentication_method: number;
|
||||||
|
provider: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
success_details: {
|
||||||
|
[provider_url: string]: {
|
||||||
|
policy_version: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
payments?: string[];
|
||||||
|
policy_payment_requests?: {
|
||||||
|
payto: string;
|
||||||
|
provider: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthMethod {
|
||||||
|
type: string;
|
||||||
|
instructions: string;
|
||||||
|
challenge: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReducerStateRecovery {
|
||||||
|
backup_state: undefined;
|
||||||
|
recovery_state: RecoveryStates;
|
||||||
|
code: undefined;
|
||||||
|
|
||||||
|
continents: any;
|
||||||
|
countries: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReducerStateError {
|
||||||
|
backup_state: undefined;
|
||||||
|
recovery_state: undefined;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface AnastasisState {
|
interface AnastasisState {
|
||||||
reducerState: ReducerState | undefined;
|
reducerState: ReducerState | undefined;
|
||||||
@ -10,6 +62,13 @@ interface AnastasisState {
|
|||||||
export enum BackupStates {
|
export enum BackupStates {
|
||||||
ContinentSelecting = "CONTINENT_SELECTING",
|
ContinentSelecting = "CONTINENT_SELECTING",
|
||||||
CountrySelecting = "COUNTRY_SELECTING",
|
CountrySelecting = "COUNTRY_SELECTING",
|
||||||
|
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
|
||||||
|
AuthenticationsEditing = "AUTHENTICATIONS_EDITING",
|
||||||
|
PoliciesReviewing = "POLICIES_REVIEWING",
|
||||||
|
SecretEditing = "SECRET_EDITING",
|
||||||
|
TruthsPaying = "TRUTHS_PAYING",
|
||||||
|
PoliciesPaying = "POLICIES_PAYING",
|
||||||
|
BackupFinished = "BACKUP_FINISHED",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RecoveryStates {
|
export enum RecoveryStates {
|
||||||
@ -49,20 +108,62 @@ async function reduceState(
|
|||||||
return resp.json();
|
return resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReducerTransactionHandle {
|
||||||
|
transactionState: ReducerState;
|
||||||
|
transition(action: string, args: any): Promise<ReducerState>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AnastasisReducerApi {
|
export interface AnastasisReducerApi {
|
||||||
currentReducerState: ReducerState;
|
currentReducerState: ReducerState | undefined;
|
||||||
currentError: any;
|
currentError: any;
|
||||||
|
dismissError: () => void;
|
||||||
startBackup: () => void;
|
startBackup: () => void;
|
||||||
startRecover: () => void;
|
startRecover: () => void;
|
||||||
|
reset: () => void;
|
||||||
back: () => void;
|
back: () => void;
|
||||||
transition(action: string, args: any): void;
|
transition(action: string, args: any): void;
|
||||||
|
/**
|
||||||
|
* Run multiple reducer steps in a transaction without
|
||||||
|
* affecting the UI-visible transition state in-between.
|
||||||
|
*/
|
||||||
|
runTransaction(f: (h: ReducerTransactionHandle) => Promise<void>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreState(): any {
|
||||||
|
let state: any;
|
||||||
|
try {
|
||||||
|
let s = localStorage.getItem("anastasisReducerState");
|
||||||
|
if (s === "undefined") {
|
||||||
|
state = undefined;
|
||||||
|
} else if (s) {
|
||||||
|
console.log("restoring state from", s);
|
||||||
|
state = JSON.parse(s);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
return state ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAnastasisReducer(): AnastasisReducerApi {
|
export function useAnastasisReducer(): AnastasisReducerApi {
|
||||||
const [anastasisState, setAnastasisState] = useState<AnastasisState>({
|
const [anastasisState, setAnastasisStateInternal] = useState<AnastasisState>(
|
||||||
reducerState: undefined,
|
() => ({
|
||||||
currentError: undefined,
|
reducerState: restoreState(),
|
||||||
});
|
currentError: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAnastasisState = (newState: AnastasisState) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
"anastasisReducerState",
|
||||||
|
JSON.stringify(newState.reducerState),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
setAnastasisStateInternal(newState);
|
||||||
|
};
|
||||||
|
|
||||||
async function doTransition(action: string, args: any) {
|
async function doTransition(action: string, args: any) {
|
||||||
console.log("reducing with", action, args);
|
console.log("reducing with", action, args);
|
||||||
@ -102,30 +203,74 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
doTransition(action, args);
|
doTransition(action, args);
|
||||||
},
|
},
|
||||||
back() {
|
back() {
|
||||||
|
const reducerState = anastasisState.reducerState;
|
||||||
|
if (!reducerState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
anastasisState.reducerState.backup_state ===
|
reducerState.backup_state === BackupStates.ContinentSelecting ||
|
||||||
BackupStates.ContinentSelecting ||
|
reducerState.recovery_state === RecoveryStates.ContinentSelecting
|
||||||
anastasisState.reducerState.recovery_state ===
|
|
||||||
RecoveryStates.ContinentSelecting
|
|
||||||
) {
|
) {
|
||||||
setAnastasisState({
|
setAnastasisState({
|
||||||
...anastasisState,
|
...anastasisState,
|
||||||
currentError: undefined,
|
currentError: undefined,
|
||||||
reducerState: undefined,
|
reducerState: undefined,
|
||||||
});
|
});
|
||||||
} else if (
|
|
||||||
anastasisState.reducerState.backup_state ===
|
|
||||||
BackupStates.CountrySelecting
|
|
||||||
) {
|
|
||||||
doTransition("unselect_continent", {});
|
|
||||||
} else if (
|
|
||||||
anastasisState.reducerState.recovery_state ===
|
|
||||||
RecoveryStates.CountrySelecting
|
|
||||||
) {
|
|
||||||
doTransition("unselect_continent", {});
|
|
||||||
} else {
|
} else {
|
||||||
doTransition("back", {});
|
doTransition("back", {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dismissError() {
|
||||||
|
setAnastasisState({ ...anastasisState, currentError: undefined });
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
setAnastasisState({
|
||||||
|
...anastasisState,
|
||||||
|
currentError: undefined,
|
||||||
|
reducerState: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
runTransaction(f) {
|
||||||
|
async function run() {
|
||||||
|
const txHandle = new ReducerTxImpl(anastasisState.reducerState!);
|
||||||
|
try {
|
||||||
|
await f(txHandle);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("exception during reducer transaction", e);
|
||||||
|
}
|
||||||
|
const s = txHandle.transactionState;
|
||||||
|
console.log("transaction finished, new state", s);
|
||||||
|
if (s.code !== undefined) {
|
||||||
|
setAnastasisState({
|
||||||
|
...anastasisState,
|
||||||
|
currentError: txHandle.transactionState,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAnastasisState({
|
||||||
|
...anastasisState,
|
||||||
|
reducerState: txHandle.transactionState,
|
||||||
|
currentError: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ReducerTxImpl implements ReducerTransactionHandle {
|
||||||
|
constructor(public transactionState: ReducerState) {}
|
||||||
|
async transition(action: string, args: any): Promise<ReducerState> {
|
||||||
|
console.log("making transition in transaction", action);
|
||||||
|
this.transactionState = await reduceState(
|
||||||
|
this.transactionState,
|
||||||
|
action,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
// Abort transaction as soon as we transition into an error state.
|
||||||
|
if (this.transactionState.code !== undefined) {
|
||||||
|
throw Error("transition resulted in error");
|
||||||
|
}
|
||||||
|
return this.transactionState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,80 +1,290 @@
|
|||||||
|
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
|
||||||
import { FunctionalComponent, h } from "preact";
|
import { FunctionalComponent, h } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import {
|
import {
|
||||||
AnastasisReducerApi,
|
AnastasisReducerApi,
|
||||||
|
AuthMethod,
|
||||||
|
BackupStates,
|
||||||
|
ReducerStateBackup,
|
||||||
|
ReducerStateRecovery,
|
||||||
useAnastasisReducer,
|
useAnastasisReducer,
|
||||||
} from "../../hooks/use-anastasis-reducer";
|
} from "../../hooks/use-anastasis-reducer";
|
||||||
import style from "./style.css";
|
import style from "./style.css";
|
||||||
|
|
||||||
|
interface ContinentSelectionProps {
|
||||||
|
reducer: AnastasisReducerApi;
|
||||||
|
reducerState: ReducerStateBackup | ReducerStateRecovery;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBackup(reducer: AnastasisReducerApi) {
|
||||||
|
return !!reducer.currentReducerState?.backup_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContinentSelection(props: ContinentSelectionProps) {
|
||||||
|
const { reducer, reducerState } = props;
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>{isBackup(reducer) ? "Backup" : "Recovery"}: Select Continent</h1>
|
||||||
|
<ErrorBanner reducer={reducer} />
|
||||||
|
<div>
|
||||||
|
{reducerState.continents.map((x: any) => {
|
||||||
|
const sel = (x: string) =>
|
||||||
|
reducer.transition("select_continent", { continent: x });
|
||||||
|
return (
|
||||||
|
<button onClick={() => sel(x.name)} key={x.name}>
|
||||||
|
{x.name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CountrySelectionProps {
|
||||||
|
reducer: AnastasisReducerApi;
|
||||||
|
reducerState: ReducerStateBackup | ReducerStateRecovery;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CountrySelection(props: CountrySelectionProps) {
|
||||||
|
const { reducer, reducerState } = props;
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Backup: Select Country</h1>
|
||||||
|
<ErrorBanner reducer={reducer} />
|
||||||
|
<div>
|
||||||
|
{reducerState.countries.map((x: any) => {
|
||||||
|
const sel = (x: any) =>
|
||||||
|
reducer.transition("select_country", {
|
||||||
|
country_code: x.code,
|
||||||
|
currencies: [x.currency],
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<button onClick={() => sel(x)} key={x.name}>
|
||||||
|
{x.name} ({x.currency})
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const Home: FunctionalComponent = () => {
|
const Home: FunctionalComponent = () => {
|
||||||
const reducer = useAnastasisReducer();
|
const reducer = useAnastasisReducer();
|
||||||
if (!reducer.currentReducerState) {
|
const reducerState = reducer.currentReducerState;
|
||||||
|
if (!reducerState) {
|
||||||
return (
|
return (
|
||||||
<div class={style.home}>
|
<div class={style.home}>
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
<p>
|
<p>
|
||||||
<button onClick={() => reducer.startBackup()}>Backup</button>
|
<button autoFocus onClick={() => reducer.startBackup()}>
|
||||||
<button>Recover</button>
|
Backup
|
||||||
|
</button>
|
||||||
|
<button onClick={() => reducer.startRecover()}>Recover</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log("state", reducer.currentReducerState);
|
console.log("state", reducer.currentReducerState);
|
||||||
if (reducer.currentReducerState.backup_state === "CONTINENT_SELECTING") {
|
|
||||||
return (
|
if (reducerState.backup_state === BackupStates.ContinentSelecting) {
|
||||||
<div class={style.home}>
|
return <ContinentSelection reducer={reducer} reducerState={reducerState} />;
|
||||||
<h1>Backup: Select Continent</h1>
|
|
||||||
<ErrorBanner reducer={reducer} />
|
|
||||||
<div>
|
|
||||||
{reducer.currentReducerState.continents.map((x: any) => {
|
|
||||||
const sel = (x: string) =>
|
|
||||||
reducer.transition("select_continent", { continent: x });
|
|
||||||
return (
|
|
||||||
<button onClick={() => sel(x.name)} key={x.name}>
|
|
||||||
{x.name}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button onClick={() => reducer.back()}>Back</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (reducer.currentReducerState.backup_state === "COUNTRY_SELECTING") {
|
if (reducerState.backup_state === BackupStates.CountrySelecting) {
|
||||||
return (
|
return <CountrySelection reducer={reducer} reducerState={reducerState} />;
|
||||||
<div class={style.home}>
|
|
||||||
<h1>Backup: Select Continent</h1>
|
|
||||||
<ErrorBanner reducer={reducer} />
|
|
||||||
<div>
|
|
||||||
{reducer.currentReducerState.countries.map((x: any) => {
|
|
||||||
const sel = (x: any) =>
|
|
||||||
reducer.transition("select_country", {
|
|
||||||
country_code: x.code,
|
|
||||||
currencies: [x.currency],
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<button onClick={() => sel(x)} key={x.name}>
|
|
||||||
{x.name} ({x.currency})
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button onClick={() => reducer.back()}>Back</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (
|
if (reducerState.backup_state === BackupStates.UserAttributesCollecting) {
|
||||||
reducer.currentReducerState.backup_state === "USER_ATTRIBUTES_COLLECTING"
|
return <AttributeEntry reducer={reducer} backupState={reducerState} />;
|
||||||
) {
|
}
|
||||||
return <AttributeEntry reducer={reducer} />;
|
if (reducerState.backup_state === BackupStates.AuthenticationsEditing) {
|
||||||
|
return (
|
||||||
|
<AuthenticationEditor backupState={reducerState} reducer={reducer} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reducer.currentReducerState.backup_state === "AUTHENTICATIONS_EDITING") {
|
if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
|
||||||
return <AuthenticationEditor reducer={reducer} />;
|
const backupState: ReducerStateBackup = reducerState;
|
||||||
|
const authMethods = backupState.authentication_methods!;
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Backup: Review Recovery Policies</h1>
|
||||||
|
<ErrorBanner reducer={reducer} />
|
||||||
|
<div>
|
||||||
|
{backupState.policies?.map((p, i) => {
|
||||||
|
const policyName = p.methods
|
||||||
|
.map((x) => authMethods[x.authentication_method].type)
|
||||||
|
.join(" + ");
|
||||||
|
return (
|
||||||
|
<div class={style.policy}>
|
||||||
|
<h3>
|
||||||
|
Policy #{i + 1}: {policyName}
|
||||||
|
</h3>
|
||||||
|
Required Authentications:
|
||||||
|
<ul>
|
||||||
|
{p.methods.map((x) => {
|
||||||
|
const m = authMethods[x.authentication_method];
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{m.type} ({m.instructions}) at provider {x.provider}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
reducer.transition("delete_policy", { policy_index: i })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete Policy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
|
<button onClick={() => reducer.transition("next", {})}>Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reducerState.backup_state === BackupStates.SecretEditing) {
|
||||||
|
const [secretName, setSecretName] = useState("");
|
||||||
|
const [secretValue, setSecretValue] = useState("");
|
||||||
|
const secretNext = () => {
|
||||||
|
reducer.runTransaction(async (tx) => {
|
||||||
|
await tx.transition("enter_secret_name", {
|
||||||
|
name: secretName,
|
||||||
|
});
|
||||||
|
await tx.transition("enter_secret", {
|
||||||
|
secret: {
|
||||||
|
value: "EDJP6WK5EG50",
|
||||||
|
mime: "text/plain",
|
||||||
|
},
|
||||||
|
expiration: {
|
||||||
|
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await tx.transition("next", {});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Backup: Provide secret</h1>
|
||||||
|
<ErrorBanner reducer={reducer} />
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Secret name: <input type="text" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Secret value: <input type="text" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
or:
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
File Upload: <input type="file" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
|
<button onClick={() => secretNext()}>Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reducerState.backup_state === BackupStates.BackupFinished) {
|
||||||
|
const backupState: ReducerStateBackup = reducerState;
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Backup finished</h1>
|
||||||
|
<p>
|
||||||
|
Your backup of secret "{backupState.secret_name ?? "??"}" was
|
||||||
|
successful.
|
||||||
|
</p>
|
||||||
|
<p>The backup is stored by the following providers:</p>
|
||||||
|
<ul>
|
||||||
|
{Object.keys(backupState.success_details).map((x, i) => {
|
||||||
|
const sd = backupState.success_details[x];
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{x} (Policy version {sd.policy_version})
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<button onClick={() => reducer.reset()}>
|
||||||
|
Start a new backup/recovery
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reducerState.backup_state === BackupStates.TruthsPaying) {
|
||||||
|
const backupState: ReducerStateBackup = reducerState;
|
||||||
|
const payments = backupState.payments ?? [];
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Backup: Authentication Storage Payments</h1>
|
||||||
|
<p>
|
||||||
|
Some of the providers require a payment to store the encrypted
|
||||||
|
authentication information.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{payments.map((x) => {
|
||||||
|
return <li>{x}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
|
<button onClick={() => reducer.transition("pay", {})}>
|
||||||
|
Check payment(s)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reducerState.backup_state === BackupStates.PoliciesPaying) {
|
||||||
|
const backupState: ReducerStateBackup = reducerState;
|
||||||
|
const payments = backupState.policy_payment_requests ?? [];
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Backup: Recovery Document Payments</h1>
|
||||||
|
<p>
|
||||||
|
Some of the providers require a payment to store the encrypted
|
||||||
|
recovery document.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{payments.map((x) => {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{x.provider}: {x.payto}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
|
<button onClick={() => reducer.transition("pay", {})}>
|
||||||
|
Check payment(s)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("unknown state", reducer.currentReducerState);
|
console.log("unknown state", reducer.currentReducerState);
|
||||||
@ -82,31 +292,232 @@ const Home: FunctionalComponent = () => {
|
|||||||
<div class={style.home}>
|
<div class={style.home}>
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
<p>Bug: Unknown state.</p>
|
<p>Bug: Unknown state.</p>
|
||||||
|
<button onClick={() => reducer.reset()}>Reset</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface AuthMethodSetupProps {
|
||||||
|
method: string;
|
||||||
|
addAuthMethod: (x: any) => void;
|
||||||
|
cancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
|
||||||
|
const [mobileNumber, setMobileNumber] = useState("");
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Add {props.method} authentication</h1>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
For SMS authentication, you need to provide a mobile number. When
|
||||||
|
recovering your secret, you will be asked to enter the code you
|
||||||
|
receive via SMS.
|
||||||
|
</p>
|
||||||
|
<label>
|
||||||
|
Mobile number{" "}
|
||||||
|
<input
|
||||||
|
value={mobileNumber}
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setMobileNumber((e.target as any).value)}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => props.cancel()}>Cancel</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
props.addAuthMethod({
|
||||||
|
authentication_method: {
|
||||||
|
type: "sms",
|
||||||
|
instructions: `SMS to ${mobileNumber}`,
|
||||||
|
challenge: "E1QPPS8A",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
|
||||||
|
const [questionText, setQuestionText] = useState("");
|
||||||
|
const [answerText, setAnswerText] = useState("");
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Add {props.method} authentication</h1>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
For security question authentication, you need to provide a question
|
||||||
|
and its answer. When recovering your secret, you will be shown the
|
||||||
|
question and you will need to type the answer exactly as you typed it
|
||||||
|
here.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Security question
|
||||||
|
<input
|
||||||
|
value={questionText}
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setQuestionText((e.target as any).value)}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Answer
|
||||||
|
<input
|
||||||
|
value={answerText}
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setAnswerText((e.target as any).value)}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => props.cancel()}>Cancel</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
props.addAuthMethod({
|
||||||
|
authentication_method: {
|
||||||
|
type: "question",
|
||||||
|
instructions: questionText,
|
||||||
|
challenge: encodeCrock(stringToBytes(answerText)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthMethodNotImplemented(props: AuthMethodSetupProps) {
|
||||||
|
return (
|
||||||
|
<div class={style.home}>
|
||||||
|
<h1>Add {props.method} authentication</h1>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
This auth method is not implemented yet, please choose another one.
|
||||||
|
</p>
|
||||||
|
<button onClick={() => props.cancel()}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuthenticationEditorProps {
|
export interface AuthenticationEditorProps {
|
||||||
reducer: AnastasisReducerApi;
|
reducer: AnastasisReducerApi;
|
||||||
|
backupState: ReducerStateBackup;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthenticationEditor(props: AuthenticationEditorProps) {
|
function AuthenticationEditor(props: AuthenticationEditorProps) {
|
||||||
const { reducer } = props;
|
const [selectedMethod, setSelectedMethod] = useState<string | undefined>(
|
||||||
const providers = reducer.currentReducerState.authentication_providers;
|
undefined,
|
||||||
const authAvailable = new Set<string>();
|
);
|
||||||
|
const { reducer, backupState } = props;
|
||||||
|
const providers = backupState.authentication_providers;
|
||||||
|
const authAvailableSet = new Set<string>();
|
||||||
for (const provKey of Object.keys(providers)) {
|
for (const provKey of Object.keys(providers)) {
|
||||||
const p = providers[provKey];
|
const p = providers[provKey];
|
||||||
for (const meth of p.methods) {
|
for (const meth of p.methods) {
|
||||||
authAvailable.add(meth.type);
|
authAvailableSet.add(meth.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (selectedMethod) {
|
||||||
|
const cancel = () => setSelectedMethod(undefined);
|
||||||
|
const addMethod = (args: any) => {
|
||||||
|
reducer.transition("add_authentication", args);
|
||||||
|
setSelectedMethod(undefined);
|
||||||
|
};
|
||||||
|
switch (selectedMethod) {
|
||||||
|
case "sms":
|
||||||
|
return (
|
||||||
|
<AuthMethodSmsSetup
|
||||||
|
cancel={cancel}
|
||||||
|
addAuthMethod={addMethod}
|
||||||
|
method="sms"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "question":
|
||||||
|
return (
|
||||||
|
<AuthMethodQuestionSetup
|
||||||
|
cancel={cancel}
|
||||||
|
addAuthMethod={addMethod}
|
||||||
|
method="sms"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<AuthMethodNotImplemented
|
||||||
|
cancel={cancel}
|
||||||
|
addAuthMethod={addMethod}
|
||||||
|
method={selectedMethod}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function MethodButton(props: { method: string; label: String }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={!authAvailableSet.has(props.method)}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedMethod(props.method);
|
||||||
|
reducer.dismissError();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const configuredAuthMethods: AuthMethod[] =
|
||||||
|
backupState.authentication_methods ?? [];
|
||||||
|
const haveMethodsConfigured = configuredAuthMethods.length;
|
||||||
return (
|
return (
|
||||||
<div class={style.home}>
|
<div class={style.home}>
|
||||||
<h1>Backup: Configure Authentication Methods</h1>
|
<h1>Backup: Configure Authentication Methods</h1>
|
||||||
<p>Auths available: {JSON.stringify(Array.from(authAvailable))}</p>
|
<ErrorBanner reducer={reducer} />
|
||||||
<button>Next</button>
|
<h2>Add authentication method</h2>
|
||||||
|
<div>
|
||||||
|
<MethodButton method="sms" label="SMS" />
|
||||||
|
<MethodButton method="email" label="Email" />
|
||||||
|
<MethodButton method="question" label="Question" />
|
||||||
|
<MethodButton method="post" label="Physical Mail" />
|
||||||
|
<MethodButton method="totp" label="TOTP" />
|
||||||
|
<MethodButton method="iban" label="IBAN" />
|
||||||
|
</div>
|
||||||
|
<h2>Configured authentication methods</h2>
|
||||||
|
{haveMethodsConfigured ? (
|
||||||
|
configuredAuthMethods.map((x, i) => {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
{x.type} ({x.instructions}){" "}
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
reducer.transition("delete_authentication", {
|
||||||
|
authentication_method: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p>No authentication methods configured yet.</p>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => reducer.back()}>Back</button>
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
|
<button onClick={() => reducer.transition("next", {})}>Next</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -114,19 +525,21 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
|
|||||||
|
|
||||||
export interface AttributeEntryProps {
|
export interface AttributeEntryProps {
|
||||||
reducer: AnastasisReducerApi;
|
reducer: AnastasisReducerApi;
|
||||||
|
backupState: ReducerStateBackup;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AttributeEntry(props: AttributeEntryProps) {
|
function AttributeEntry(props: AttributeEntryProps) {
|
||||||
const reducer = props.reducer;
|
const { reducer, backupState } = props;
|
||||||
const [attrs, setAttrs] = useState<Record<string, string>>({});
|
const [attrs, setAttrs] = useState<Record<string, string>>({});
|
||||||
return (
|
return (
|
||||||
<div class={style.home}>
|
<div class={style.home}>
|
||||||
<h1>Backup: Enter Basic User Attributes</h1>
|
<h1>Backup: Enter Basic User Attributes</h1>
|
||||||
<ErrorBanner reducer={reducer} />
|
<ErrorBanner reducer={reducer} />
|
||||||
<div>
|
<div>
|
||||||
{reducer.currentReducerState.required_attributes.map((x: any) => {
|
{backupState.required_attributes.map((x: any, i: number) => {
|
||||||
return (
|
return (
|
||||||
<AttributeEntryField
|
<AttributeEntryField
|
||||||
|
isFirst={i == 0}
|
||||||
setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
|
setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
|
||||||
spec={x}
|
spec={x}
|
||||||
value={attrs[x.name]}
|
value={attrs[x.name]}
|
||||||
@ -134,23 +547,24 @@ function AttributeEntry(props: AttributeEntryProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
reducer.transition("enter_user_attributes", {
|
|
||||||
identity_attributes: attrs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => reducer.back()}>Back</button>
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
reducer.transition("enter_user_attributes", {
|
||||||
|
identity_attributes: attrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttributeEntryFieldProps {
|
export interface AttributeEntryFieldProps {
|
||||||
|
isFirst: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
setValue: (newValue: string) => void;
|
setValue: (newValue: string) => void;
|
||||||
spec: any;
|
spec: any;
|
||||||
@ -161,6 +575,7 @@ function AttributeEntryField(props: AttributeEntryFieldProps) {
|
|||||||
<div>
|
<div>
|
||||||
<label>{props.spec.label}</label>
|
<label>{props.spec.label}</label>
|
||||||
<input
|
<input
|
||||||
|
autoFocus={props.isFirst}
|
||||||
type="text"
|
type="text"
|
||||||
value={props.value}
|
value={props.value}
|
||||||
onChange={(e) => props.setValue((e as any).target.value)}
|
onChange={(e) => props.setValue((e as any).target.value)}
|
||||||
@ -179,7 +594,14 @@ interface ErrorBannerProps {
|
|||||||
function ErrorBanner(props: ErrorBannerProps) {
|
function ErrorBanner(props: ErrorBannerProps) {
|
||||||
const currentError = props.reducer.currentError;
|
const currentError = props.reducer.currentError;
|
||||||
if (currentError) {
|
if (currentError) {
|
||||||
return <div>Error: {JSON.stringify(currentError)}</div>;
|
return (
|
||||||
|
<div id={style.error}>
|
||||||
|
<p>Error: {JSON.stringify(currentError)}</p>
|
||||||
|
<button onClick={() => props.reducer.dismissError()}>
|
||||||
|
Dismiss Error
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,24 @@
|
|||||||
.home {
|
.home {
|
||||||
padding: 56px 20px;
|
padding: 56px 20px;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home div {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy {
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home > #error {
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: rgb(228, 189, 197);
|
||||||
|
border-radius: 0.5em;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ importers:
|
|||||||
|
|
||||||
packages/anastasis-webui:
|
packages/anastasis-webui:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@gnu-taler/taler-util': workspace:^0.8.3
|
||||||
'@types/enzyme': ^3.10.5
|
'@types/enzyme': ^3.10.5
|
||||||
'@types/jest': ^26.0.8
|
'@types/jest': ^26.0.8
|
||||||
'@typescript-eslint/eslint-plugin': ^2.25.0
|
'@typescript-eslint/eslint-plugin': ^2.25.0
|
||||||
@ -44,6 +45,7 @@ importers:
|
|||||||
sirv-cli: ^1.0.0-next.3
|
sirv-cli: ^1.0.0-next.3
|
||||||
typescript: ^3.7.5
|
typescript: ^3.7.5
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@gnu-taler/taler-util': link:../taler-util
|
||||||
preact: 10.5.14
|
preact: 10.5.14
|
||||||
preact-render-to-string: 5.1.19_preact@10.5.14
|
preact-render-to-string: 5.1.19_preact@10.5.14
|
||||||
preact-router: 3.2.1_preact@10.5.14
|
preact-router: 3.2.1_preact@10.5.14
|
||||||
@ -4599,7 +4601,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 0.0.39
|
'@types/estree': 0.0.39
|
||||||
estree-walker: 1.0.1
|
estree-walker: 1.0.1
|
||||||
picomatch: 2.2.2
|
picomatch: 2.3.0
|
||||||
rollup: 2.56.2
|
rollup: 2.56.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -7681,7 +7683,7 @@ packages:
|
|||||||
/axios/0.21.1:
|
/axios/0.21.1:
|
||||||
resolution: {integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==}
|
resolution: {integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.14.2
|
follow-redirects: 1.14.2_debug@4.3.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
@ -10771,18 +10773,18 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7
|
eslint: ^3 || ^4 || ^5 || ^6 || ^7
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.2
|
array-includes: 3.1.3
|
||||||
array.prototype.flatmap: 1.2.4
|
array.prototype.flatmap: 1.2.4
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 6.8.0
|
eslint: 6.8.0
|
||||||
has: 1.0.3
|
has: 1.0.3
|
||||||
jsx-ast-utils: 3.2.0
|
jsx-ast-utils: 3.2.0
|
||||||
object.entries: 1.1.3
|
object.entries: 1.1.4
|
||||||
object.fromentries: 2.0.3
|
object.fromentries: 2.0.4
|
||||||
object.values: 1.1.2
|
object.values: 1.1.4
|
||||||
prop-types: 15.7.2
|
prop-types: 15.7.2
|
||||||
resolve: 1.19.0
|
resolve: 1.20.0
|
||||||
string.prototype.matchall: 4.0.3
|
string.prototype.matchall: 4.0.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-react/7.22.0_eslint@7.18.0:
|
/eslint-plugin-react/7.22.0_eslint@7.18.0:
|
||||||
@ -11444,7 +11446,7 @@ packages:
|
|||||||
readable-stream: 2.3.7
|
readable-stream: 2.3.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/follow-redirects/1.14.2:
|
/follow-redirects/1.14.2_debug@4.3.2:
|
||||||
resolution: {integrity: sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==}
|
resolution: {integrity: sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -11452,6 +11454,8 @@ packages:
|
|||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
debug:
|
debug:
|
||||||
optional: true
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
debug: 4.3.2_supports-color@6.1.0
|
||||||
|
|
||||||
/for-each/0.3.3:
|
/for-each/0.3.3:
|
||||||
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||||
@ -12485,7 +12489,7 @@ packages:
|
|||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
eventemitter3: 4.0.7
|
eventemitter3: 4.0.7
|
||||||
follow-redirects: 1.14.2
|
follow-redirects: 1.14.2_debug@4.3.2
|
||||||
requires-port: 1.0.0
|
requires-port: 1.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
@ -14131,7 +14135,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==}
|
resolution: {integrity: sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.2
|
array-includes: 3.1.3
|
||||||
object.assign: 4.1.2
|
object.assign: 4.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -15962,11 +15966,11 @@ packages:
|
|||||||
- typescript
|
- typescript
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/pnp-webpack-plugin/1.7.0_typescript@4.3.5:
|
/pnp-webpack-plugin/1.7.0_typescript@4.4.3:
|
||||||
resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==}
|
resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
ts-pnp: 1.2.0_typescript@4.3.5
|
ts-pnp: 1.2.0_typescript@4.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
dev: true
|
dev: true
|
||||||
@ -16770,7 +16774,7 @@ packages:
|
|||||||
native-url: 0.3.4
|
native-url: 0.3.4
|
||||||
optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0
|
optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0
|
||||||
ora: 5.4.1
|
ora: 5.4.1
|
||||||
pnp-webpack-plugin: 1.7.0_typescript@4.3.5
|
pnp-webpack-plugin: 1.7.0_typescript@4.4.3
|
||||||
postcss: 8.3.6
|
postcss: 8.3.6
|
||||||
postcss-load-config: 3.1.0
|
postcss-load-config: 3.1.0
|
||||||
postcss-loader: 4.3.0_postcss@8.3.6+webpack@4.46.0
|
postcss-loader: 4.3.0_postcss@8.3.6+webpack@4.46.0
|
||||||
@ -16788,7 +16792,7 @@ packages:
|
|||||||
stack-trace: 0.0.10
|
stack-trace: 0.0.10
|
||||||
style-loader: 2.0.0_webpack@4.46.0
|
style-loader: 2.0.0_webpack@4.46.0
|
||||||
terser-webpack-plugin: 4.2.3_webpack@4.46.0
|
terser-webpack-plugin: 4.2.3_webpack@4.46.0
|
||||||
typescript: 4.3.5
|
typescript: 4.4.3
|
||||||
update-notifier: 5.1.0
|
update-notifier: 5.1.0
|
||||||
url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0
|
url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0
|
||||||
validate-npm-package-name: 3.0.0
|
validate-npm-package-name: 3.0.0
|
||||||
@ -18039,11 +18043,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
rollup: ^2.0.0
|
rollup: ^2.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.12.13
|
'@babel/code-frame': 7.14.5
|
||||||
jest-worker: 26.6.2
|
jest-worker: 26.6.2
|
||||||
rollup: 2.56.2
|
rollup: 2.56.2
|
||||||
serialize-javascript: 4.0.0
|
serialize-javascript: 4.0.0
|
||||||
terser: 5.4.0
|
terser: 5.7.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/rollup/2.37.1:
|
/rollup/2.37.1:
|
||||||
@ -19167,6 +19171,7 @@ packages:
|
|||||||
/svgo/1.3.2:
|
/svgo/1.3.2:
|
||||||
resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==}
|
resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 2.4.2
|
chalk: 2.4.2
|
||||||
@ -19588,6 +19593,18 @@ packages:
|
|||||||
typescript: 4.3.5
|
typescript: 4.3.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ts-pnp/1.2.0_typescript@4.4.3:
|
||||||
|
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
typescript: 4.4.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tsconfig-paths/3.9.0:
|
/tsconfig-paths/3.9.0:
|
||||||
resolution: {integrity: sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==}
|
resolution: {integrity: sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user