file upload
This commit is contained in:
parent
f9dedc06d6
commit
ea13e19ece
@ -20,11 +20,26 @@
|
|||||||
*/
|
*/
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||||
import { TextInputProps } from "./TextInput";
|
|
||||||
|
|
||||||
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
|
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
export function FileInput(props: TextInputProps): VNode {
|
export interface FileTypeContent {
|
||||||
|
content: string;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileInputProps {
|
||||||
|
label: string;
|
||||||
|
grabFocus?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
error?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
onChange: (v: FileTypeContent | undefined) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileInput(props: FileInputProps): VNode {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (props.grabFocus) {
|
if (props.grabFocus) {
|
||||||
@ -32,18 +47,19 @@ export function FileInput(props: TextInputProps): VNode {
|
|||||||
}
|
}
|
||||||
}, [props.grabFocus]);
|
}, [props.grabFocus]);
|
||||||
|
|
||||||
const value = props.bind[0];
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
// const [dirty, setDirty] = useState(false)
|
|
||||||
const image = useRef<HTMLInputElement>(null);
|
|
||||||
const [sizeError, setSizeError] = useState(false);
|
const [sizeError, setSizeError] = useState(false);
|
||||||
function onChange(v: string): void {
|
|
||||||
// setDirty(true);
|
|
||||||
props.bind[1](v);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<a onClick={() => image.current?.click()}>{props.label}</a>
|
<a class="button" onClick={(e) => fileInputRef.current?.click()}>
|
||||||
|
<div class="icon is-small ">
|
||||||
|
<i class="mdi mdi-folder" />
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
{props.label}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
{props.tooltip && (
|
{props.tooltip && (
|
||||||
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
|
||||||
<i class="mdi mdi-information" />
|
<i class="mdi mdi-information" />
|
||||||
@ -52,18 +68,19 @@ export function FileInput(props: TextInputProps): VNode {
|
|||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
ref={image}
|
ref={fileInputRef}
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
type="file"
|
type="file"
|
||||||
name={String(name)}
|
// name={String(name)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const f: FileList | null = e.currentTarget.files;
|
const f: FileList | null = e.currentTarget.files;
|
||||||
if (!f || f.length != 1) {
|
if (!f || f.length != 1) {
|
||||||
return onChange("");
|
return props.onChange(undefined);
|
||||||
}
|
}
|
||||||
|
console.log(f)
|
||||||
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
|
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
|
||||||
setSizeError(true);
|
setSizeError(true);
|
||||||
return onChange("");
|
return props.onChange(undefined);
|
||||||
}
|
}
|
||||||
setSizeError(false);
|
setSizeError(false);
|
||||||
return f[0].arrayBuffer().then((b) => {
|
return f[0].arrayBuffer().then((b) => {
|
||||||
@ -73,7 +90,7 @@ export function FileInput(props: TextInputProps): VNode {
|
|||||||
"",
|
"",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return onChange(`data:${f[0].type};base64,${b64}` as any);
|
return props.onChange({content: `data:${f[0].type};base64,${b64}`, name: f[0].name, type: f[0].type});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -4,6 +4,7 @@ import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
|||||||
export interface TextInputProps {
|
export interface TextInputProps {
|
||||||
label: string;
|
label: string;
|
||||||
grabFocus?: boolean;
|
grabFocus?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
@ -33,6 +34,7 @@ export function TextInput(props: TextInputProps): VNode {
|
|||||||
<div class="control has-icons-right">
|
<div class="control has-icons-right">
|
||||||
<input
|
<input
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={props.disabled}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
class={showError ? "input is-danger" : "input"}
|
class={showError ? "input is-danger" : "input"}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AuthenticationProviderStatusOk } from "anastasis-core";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { useAnastasisContext } from "../../context/anastasis";
|
import { useAnastasisContext } from "../../context/anastasis";
|
||||||
@ -22,6 +23,7 @@ export function ReviewPoliciesScreen(): VNode {
|
|||||||
reducer.currentReducerState.authentication_methods ?? [];
|
reducer.currentReducerState.authentication_methods ?? [];
|
||||||
const policies = reducer.currentReducerState.policies ?? [];
|
const policies = reducer.currentReducerState.policies ?? [];
|
||||||
|
|
||||||
|
const providers = reducer.currentReducerState.authentication_providers ?? {}
|
||||||
|
|
||||||
if (editingPolicy !== undefined) {
|
if (editingPolicy !== undefined) {
|
||||||
return (
|
return (
|
||||||
@ -96,6 +98,7 @@ export function ReviewPoliciesScreen(): VNode {
|
|||||||
</h3>
|
</h3>
|
||||||
{!methods.length && <p>No auth method found</p>}
|
{!methods.length && <p>No auth method found</p>}
|
||||||
{methods.map((m, i) => {
|
{methods.map((m, i) => {
|
||||||
|
const p = providers[m.provider] as AuthenticationProviderStatusOk
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
key={i}
|
key={i}
|
||||||
@ -107,7 +110,7 @@ export function ReviewPoliciesScreen(): VNode {
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{m.instructions} recovery provided by{" "}
|
{m.instructions} recovery provided by{" "}
|
||||||
<a href={m.provider}>{m.provider}</a>
|
<a href={m.provider} target="_blank" rel="noreferrer" >{p.business_name}</a>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
@ -5,11 +5,16 @@ import { useState } from "preact/hooks";
|
|||||||
import { useAnastasisContext } from "../../context/anastasis";
|
import { useAnastasisContext } from "../../context/anastasis";
|
||||||
import { AnastasisClientFrame } from "./index";
|
import { AnastasisClientFrame } from "./index";
|
||||||
import { TextInput } from "../../components/fields/TextInput";
|
import { TextInput } from "../../components/fields/TextInput";
|
||||||
import { FileInput } from "../../components/fields/FileInput";
|
import { FileInput, FileTypeContent } from "../../components/fields/FileInput";
|
||||||
|
|
||||||
export function SecretEditorScreen(): VNode {
|
export function SecretEditorScreen(): VNode {
|
||||||
const reducer = useAnastasisContext();
|
const reducer = useAnastasisContext();
|
||||||
const [secretValue, setSecretValue] = useState("");
|
const [secretValue, setSecretValue] = useState("");
|
||||||
|
const [secretFile, _setSecretFile] = useState<FileTypeContent | undefined>(undefined);
|
||||||
|
function setSecretFile(v) {
|
||||||
|
setSecretValue("") // reset secret value when uploading a file
|
||||||
|
_setSecretFile(v)
|
||||||
|
}
|
||||||
|
|
||||||
const currentSecretName =
|
const currentSecretName =
|
||||||
reducer?.currentReducerState &&
|
reducer?.currentReducerState &&
|
||||||
@ -29,15 +34,20 @@ export function SecretEditorScreen(): VNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const secretNext = async (): Promise<void> => {
|
const secretNext = async (): Promise<void> => {
|
||||||
|
const secret = secretFile ? {
|
||||||
|
value: encodeCrock(stringToBytes(secretValue)),
|
||||||
|
filename: secretFile.name,
|
||||||
|
mime: secretFile.type,
|
||||||
|
} : {
|
||||||
|
value: encodeCrock(stringToBytes(secretValue)),
|
||||||
|
mime: "text/plain",
|
||||||
|
}
|
||||||
return reducer.runTransaction(async (tx) => {
|
return reducer.runTransaction(async (tx) => {
|
||||||
await tx.transition("enter_secret_name", {
|
await tx.transition("enter_secret_name", {
|
||||||
name: secretName,
|
name: secretName,
|
||||||
});
|
});
|
||||||
await tx.transition("enter_secret", {
|
await tx.transition("enter_secret", {
|
||||||
secret: {
|
secret,
|
||||||
value: encodeCrock(stringToBytes(secretValue)),
|
|
||||||
mime: "text/plain",
|
|
||||||
},
|
|
||||||
expiration: {
|
expiration: {
|
||||||
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
|
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
|
||||||
},
|
},
|
||||||
@ -45,12 +55,16 @@ export function SecretEditorScreen(): VNode {
|
|||||||
await tx.transition("next", {});
|
await tx.transition("next", {});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const errors = !secretName ? 'Add a secret name' : (
|
||||||
|
(!secretValue && !secretFile) ? 'Add a secret value or a choose a file to upload' : undefined
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame
|
<AnastasisClientFrame
|
||||||
|
hideNext={errors}
|
||||||
title="Backup: Provide secret to backup"
|
title="Backup: Provide secret to backup"
|
||||||
onNext={() => secretNext()}
|
onNext={() => secretNext()}
|
||||||
>
|
>
|
||||||
<div>
|
<div class="block">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Secret name:"
|
label="Secret name:"
|
||||||
tooltip="The secret name allows you to identify your secret when restoring it. It is a label that you can choose freely."
|
tooltip="The secret name allows you to identify your secret when restoring it. It is a label that you can choose freely."
|
||||||
@ -58,16 +72,20 @@ export function SecretEditorScreen(): VNode {
|
|||||||
bind={[secretName, setSecretName]}
|
bind={[secretName, setSecretName]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="block">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
disabled={!!secretFile}
|
||||||
label="Enter the secret as text:"
|
label="Enter the secret as text:"
|
||||||
bind={[secretValue, setSecretValue]}
|
bind={[secretValue, setSecretValue]}
|
||||||
/>
|
/>
|
||||||
{/* <div style={{ display: "flex" }}>
|
</div>
|
||||||
or
|
<div class="block">
|
||||||
<FileInput label="click here" bind={[secretValue, setSecretValue]} />
|
Or upload a secret file
|
||||||
to import a file
|
<FileInput label="Choose file" onChange={setSecretFile} />
|
||||||
</div> */}
|
{secretFile && <div>
|
||||||
|
Uploading secret file <b>{secretFile.name}</b> <a onClick={() => setSecretFile(undefined)}>cancel</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
|
@ -30,7 +30,7 @@ export function AuthMethodQuestionSetup({
|
|||||||
<AnastasisClientFrame hideNav title="Add Security Question">
|
<AnastasisClientFrame hideNav title="Add Security Question">
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
For2 security question authentication, you need to provide a question
|
For security question authentication, you need to provide a question
|
||||||
and its answer. When recovering your secret, you will be shown the
|
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
|
question and you will need to type the answer exactly as you typed it
|
||||||
here.
|
here.
|
||||||
|
Loading…
Reference in New Issue
Block a user