file upload

This commit is contained in:
Sebastian 2021-11-10 15:43:15 -03:00
parent f9dedc06d6
commit ea13e19ece
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
5 changed files with 69 additions and 29 deletions

View File

@ -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});
}); });
}} }}
/> />

View File

@ -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) => {

View File

@ -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>
); );

View File

@ -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&nbsp; <div class="block">
<FileInput label="click here" bind={[secretValue, setSecretValue]} /> Or upload a secret file
&nbsp;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>
); );

View File

@ -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.