1
0
Fork 0

Add tsx syntax to Unseal.

This commit is contained in:
Kitteh 2021-05-24 14:37:37 +01:00
parent ed26eba220
commit 4930b8e727
34 changed files with 260 additions and 332 deletions

View file

@ -42,7 +42,6 @@
"webpack": "^5.37.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2",
"z-makeelement": "^1.0.3",
"z-pagerouter": "^1.0.2"
}
}

View file

@ -1,5 +1,5 @@
import { Component, JSX, createRef } from "preact";
import { MarginInline } from "./ReactMarginInline";
import { MarginInline } from "./MarginInline";
import { addClipboardNotifications } from "../pageUtils";
import ClipboardJS from "clipboard";
import i18next from "i18next";

View file

@ -1,22 +0,0 @@
/* eslint-disable */
import { ElementInfo, makeElement } from "z-makeelement";
/* eslint-enable */
export function Form(
children: Element[],
submit: (form: HTMLFormElement) => unknown,
options: Partial<ElementInfo> = {},
): HTMLFormElement {
const form = makeElement({
tag: "form",
children: children,
...options,
}) as HTMLFormElement;
form.addEventListener("submit", (e: Event) => {
e.preventDefault();
submit(form);
});
return form;
}

View file

@ -1,5 +1,5 @@
import { JSX } from "preact";
import { Margin } from "./ReactMargin";
import { Margin } from "./Margin";
export type InputWithTitleProps = {
title: string;

View file

@ -1,9 +0,0 @@
import { makeElement } from "z-makeelement";
export function Margin(children: Element | Element[]): Element {
return makeElement({
tag: "div",
class: "uk-margin",
children: children,
});
}

View file

@ -1,13 +0,0 @@
import { makeElement } from "z-makeelement";
export function MarginInline(children: Element | Element[]): Element {
return makeElement({
tag: "div",
class: "uk-margin",
children: makeElement({
tag: "div",
class: "uk-inline",
children: children,
}),
});
}

View file

@ -1,49 +0,0 @@
import { Margin } from "./Margin";
import { makeElement } from "z-makeelement";
import QrScanner from "qr-scanner";
/* eslint-disable import/no-unresolved */
// @ts-ignore
import qrScannerWorkerSource from "!!raw-loader!qr-scanner/qr-scanner-worker.min.js";
QrScanner.WORKER_PATH = URL.createObjectURL(new Blob([qrScannerWorkerSource]));
export interface QRScannerType extends HTMLElement {
deinit(): void;
}
export async function QRScanner(onScan: (code: string) => void): Promise<QRScannerType> {
const webcamVideo = makeElement({
tag: "video",
}) as HTMLVideoElement;
const QRInput = makeElement({
tag: "div",
children: [Margin(webcamVideo)],
}) as QRScannerType;
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment",
},
audio: false,
});
webcamVideo.srcObject = stream;
const lastSeenValue = "";
const qrScanner = new QrScanner(webcamVideo, function (value) {
if (lastSeenValue == value) return;
onScan(value);
});
await qrScanner.start();
QRInput.deinit = () => {
try {
stream.getTracks().forEach(function (track) {
track.stop();
});
} catch (_) {
() => {};
}
};
return QRInput;
}

View file

@ -0,0 +1,58 @@
/* eslint-disable import/no-unresolved */
// @ts-ignore
import qrScannerWorkerSource from "!!raw-loader!qr-scanner/qr-scanner-worker.min.js";
QrScanner.WORKER_PATH = URL.createObjectURL(new Blob([qrScannerWorkerSource]));
// end ignore
import { Component, JSX, createRef } from "preact";
import { Margin } from "./Margin";
import QrScanner from "qr-scanner";
export type QRScannerProps = {
onScan: (code: string) => void;
};
export class QRScanner extends Component<QRScannerProps, unknown> {
videoElement = createRef<HTMLVideoElement>();
stream: MediaStream;
componentDidMount(): void {
void navigator.mediaDevices
.getUserMedia({
video: {
facingMode: "environment",
},
audio: false,
})
.then((stream) => {
this.stream = stream;
this.videoElement.current.srcObject = stream;
const lastSeenValue = "";
const qrScanner = new QrScanner(this.videoElement.current, (value) => {
if (lastSeenValue == value) return;
this.props.onScan(value);
});
void qrScanner.start();
});
}
componentWillUnmount(): void {
try {
this.stream.getTracks().forEach(function (track) {
track.stop();
});
} catch (_) {
() => {};
}
}
render(): JSX.Element {
return (
<div>
<Margin>
<video ref={this.videoElement}></video>
</Margin>
</div>
);
}
}

View file

@ -1,5 +1,5 @@
import { Page } from "../../types/Page";
import { Tile } from "../../elements/ReactTile";
import { Tile } from "../../elements/Tile";
import { notImplemented, prePageChecks } from "../../pageUtils";
import { render } from "preact";
import i18next from "i18next";

View file

@ -1,6 +1,6 @@
import { Form } from "../../../../elements/ReactForm";
import { Form } from "../../../../elements/Form";
import { InputWithTitle } from "../../../../elements/InputWithTitle";
import { MarginInline } from "../../../../elements/ReactMarginInline";
import { MarginInline } from "../../../../elements/MarginInline";
import { Page } from "../../../../types/Page";
import { UserType } from "../../../../api/types/userpass/user";
import { createOrUpdateUserPassUser } from "../../../../api/auth/userpass/createOrUpdateUserPassUser";

View file

@ -1,6 +1,6 @@
import { Form } from "../../../../elements/ReactForm";
import { Margin } from "../../../../elements/ReactMargin";
import { MarginInline } from "../../../../elements/ReactMarginInline";
import { Form } from "../../../../elements/Form";
import { Margin } from "../../../../elements/Margin";
import { MarginInline } from "../../../../elements/MarginInline";
import { Page } from "../../../../types/Page";
import { UserType } from "../../../../api/types/userpass/user";
import { createOrUpdateUserPassUser } from "../../../../api/auth/userpass/createOrUpdateUserPassUser";

View file

@ -1,5 +1,5 @@
import { Page } from "../types/Page";
import { Tile } from "../elements/ReactTile";
import { Tile } from "../elements/Tile";
import { TokenInfo } from "../api/types/token";
import { lookupSelf } from "../api/sys/lookupSelf";
import { prePageChecks, setErrorText } from "../pageUtils";

View file

@ -1,7 +1,7 @@
import { Component, JSX, render } from "preact";
import { Form } from "../elements/ReactForm";
import { Margin } from "../elements/ReactMargin";
import { MarginInline } from "../elements/ReactMarginInline";
import { Form } from "../elements/Form";
import { Margin } from "../elements/Margin";
import { MarginInline } from "../elements/MarginInline";
import { Page } from "../types/Page";
import { lookupSelf } from "../api/sys/lookupSelf";
import { setErrorText } from "../pageUtils";

View file

@ -1,7 +1,7 @@
import { Component, JSX, createRef, render } from "preact";
import { CopyableInputBox } from "../elements/CopyableInputBox";
import { Form } from "../elements/ReactForm";
import { Margin } from "../elements/ReactMargin";
import { Form } from "../elements/Form";
import { Margin } from "../elements/Margin";
import { Page } from "../types/Page";
import i18next from "i18next";

View file

@ -1,5 +1,5 @@
import { Form } from "../../../elements/ReactForm";
import { Margin } from "../../../elements/ReactMargin";
import { Form } from "../../../elements/Form";
import { Margin } from "../../../elements/Margin";
import { Page } from "../../../types/Page";
import { SecretTitleElement } from "../SecretTitleElement";
import { createOrUpdateSecret } from "../../../api/kv/createOrUpdateSecret";

View file

@ -1,6 +1,6 @@
import { Form } from "../../../elements/ReactForm";
import { Margin } from "../../../elements/ReactMargin";
import { MarginInline } from "../../../elements/ReactMarginInline";
import { Form } from "../../../elements/Form";
import { Margin } from "../../../elements/Margin";
import { MarginInline } from "../../../elements/MarginInline";
import { Page } from "../../../types/Page";
import { newMount } from "../../../api/sys/newMount";
import { render } from "preact";

View file

@ -1,6 +1,6 @@
import { Form } from "../../../elements/ReactForm";
import { Margin } from "../../../elements/ReactMargin";
import { MarginInline } from "../../../elements/ReactMarginInline";
import { Form } from "../../../elements/Form";
import { Margin } from "../../../elements/Margin";
import { MarginInline } from "../../../elements/MarginInline";
import { Page } from "../../../types/Page";
import { newMount } from "../../../api/sys/newMount";
import { render } from "preact";

View file

@ -1,6 +1,6 @@
import { Form } from "../../../elements/ReactForm";
import { Margin } from "../../../elements/ReactMargin";
import { MarginInline } from "../../../elements/ReactMarginInline";
import { Form } from "../../../elements/Form";
import { Margin } from "../../../elements/Margin";
import { MarginInline } from "../../../elements/MarginInline";
import { Page } from "../../../types/Page";
import { newMount } from "../../../api/sys/newMount";
import { render } from "preact";

View file

@ -1,5 +1,5 @@
import { Page } from "../../types/Page";
import { Tile } from "../../elements/ReactTile";
import { Tile } from "../../elements/Tile";
import { render } from "preact";
import i18next from "i18next";

View file

@ -1,6 +1,6 @@
import { Form } from "../../../elements/ReactForm";
import { Margin } from "../../../elements/ReactMargin";
import { MarginInline } from "../../../elements/ReactMarginInline";
import { Form } from "../../../elements/Form";
import { Margin } from "../../../elements/Margin";
import { MarginInline } from "../../../elements/MarginInline";
import { Page } from "../../../types/Page";
import { SecretTitleElement } from "../SecretTitleElement";
import { addNewTOTP } from "../../../api/totp/addNewTOTP";

View file

@ -1,7 +1,7 @@
import { Component, JSX, render } from "preact";
import { CopyableInputBox } from "../../../elements/CopyableInputBox";
import { DoesNotExistError } from "../../../types/internalErrors";
import { MarginInline } from "../../../elements/ReactMarginInline";
import { MarginInline } from "../../../elements/MarginInline";
import { Page } from "../../../types/Page";
import { PageRouter } from "z-pagerouter";
import { PageState } from "../../../PageState";

View file

@ -1,6 +1,6 @@
import { Form } from "../../../elements/ReactForm";
import { Margin } from "../../../elements/ReactMargin";
import { MarginInline } from "../../../elements/ReactMarginInline";
import { Form } from "../../../elements/Form";
import { Margin } from "../../../elements/Margin";
import { MarginInline } from "../../../elements/MarginInline";
import { Page } from "../../../types/Page";
import { SecretTitleElement } from "../SecretTitleElement";
import { newTransitKey } from "../../../api/transit/newTransitKey";

View file

@ -1,8 +1,8 @@
import { CopyableModal } from "../../../elements/CopyableModal";
import { FileUploadInput } from "../../../elements/FileUploadInput";
import { Form } from "../../../elements/ReactForm";
import { Form } from "../../../elements/Form";
import { InputWithTitle } from "../../../elements/InputWithTitle";
import { Margin } from "../../../elements/ReactMargin";
import { Margin } from "../../../elements/Margin";
import { Page } from "../../../types/Page";
import { SecretTitleElement } from "../SecretTitleElement";
import { fileToBase64 } from "../../../htmlUtils";

View file

@ -1,8 +1,8 @@
import { CopyableModal } from "../../../elements/CopyableModal";
import { FileUploadInput } from "../../../elements/FileUploadInput";
import { Form } from "../../../elements/ReactForm";
import { Form } from "../../../elements/Form";
import { InputWithTitle } from "../../../elements/InputWithTitle";
import { Margin } from "../../../elements/ReactMargin";
import { Margin } from "../../../elements/Margin";
import { Page } from "../../../types/Page";
import { SecretTitleElement } from "../SecretTitleElement";
import { fileToBase64 } from "../../../htmlUtils";

View file

@ -1,6 +1,6 @@
import { CopyableModal } from "../../../elements/CopyableModal";
import { Form } from "../../../elements/ReactForm";
import { Margin } from "../../../elements/ReactMargin";
import { Form } from "../../../elements/Form";
import { Margin } from "../../../elements/Margin";
import { Page } from "../../../types/Page";
import { SecretTitleElement } from "../SecretTitleElement";
import { getTransitKey } from "../../../api/transit/getTransitKey";

View file

@ -1,6 +1,6 @@
import { Page } from "../../../types/Page";
import { SecretTitleElement } from "../SecretTitleElement";
import { Tile } from "../../../elements/ReactTile";
import { Tile } from "../../../elements/Tile";
import { getTransitKey } from "../../../api/transit/getTransitKey";
import { render } from "preact";
import i18next from "i18next";

View file

@ -2,9 +2,9 @@
import translations from "../translations/index.mjs";
// ts-unignore
import { Form } from "../elements/ReactForm";
import { Margin } from "../elements/ReactMargin";
import { MarginInline } from "../elements/ReactMarginInline";
import { Form } from "../elements/Form";
import { Margin } from "../elements/Margin";
import { MarginInline } from "../elements/MarginInline";
import { Page } from "../types/Page";
import { reloadNavBar } from "../elements/NavBar";
import { render } from "preact";

View file

@ -1,5 +1,5 @@
import { Form } from "../elements/ReactForm";
import { Margin } from "../elements/ReactMargin";
import { Form } from "../elements/Form";
import { Margin } from "../elements/Margin";
import { Page } from "../types/Page";
import { render } from "preact";

View file

@ -1,193 +0,0 @@
import { Form } from "../elements/Form";
import { MarginInline } from "../elements/MarginInline";
import { Page } from "../types/Page";
import { QRScanner, QRScannerType } from "../elements/QRScanner";
import { SealStatusType, getSealStatus } from "../api/sys/getSealStatus";
import { makeElement } from "z-makeelement";
import { setErrorText } from "../pageUtils";
import { submitUnsealKey } from "../api/sys/submitUnsealKey";
import i18next from "i18next";
const UnsealInputModes = {
FORM_INPUT: "FORM_INPUT",
QR_INPUT: "QR_INPUT",
};
export class UnsealPage extends Page {
constructor() {
super();
this.mode = UnsealInputModes.FORM_INPUT;
}
mode: string;
refresher: number;
qrScanner: QRScannerType;
unsealProgress: HTMLProgressElement;
unsealProgressText: HTMLParagraphElement;
unsealInputContent: HTMLElement;
unsealKeyForm: HTMLFormElement;
async cleanup(): Promise<void> {
this.deinitWebcam();
clearInterval(this.refresher);
}
deinitWebcam(): void {
try {
this.qrScanner.deinit();
} catch (_) {
// Do Nothing
}
}
makeRefresher(): void {
const id = setInterval(async () => {
await this.doRefresh();
return;
}, 1000);
this.refresher = id as unknown as number;
}
async doRefresh(): Promise<void> {
const status = await getSealStatus();
await this.updateSealProgress(status);
}
async render(): Promise<void> {
this.unsealProgress = makeElement({
tag: "progress",
class: "uk-progress",
attributes: { value: "0", max: "0" },
}) as HTMLProgressElement;
this.unsealProgressText = makeElement({
tag: "p",
text: i18next.t("unseal_keys_progress", {
progress: "0",
keys_needed: "0",
}),
}) as HTMLParagraphElement;
this.unsealInputContent = makeElement({
tag: "div",
});
await this.router.setPageContent(
makeElement({
tag: "div",
children: [
this.unsealProgress,
makeElement({
tag: "p",
id: "errorText",
class: ["uk-text-danger", "uk-margin-top"],
}),
this.unsealProgressText,
this.unsealInputContent,
],
}),
);
await this.switchInputMode(this.mode);
await this.updateSealProgress(await getSealStatus());
this.makeRefresher();
}
setButtons(method: string): void {
const newMethod: string =
method == UnsealInputModes.FORM_INPUT
? UnsealInputModes.QR_INPUT
: UnsealInputModes.FORM_INPUT;
const buttonText: string =
newMethod == UnsealInputModes.FORM_INPUT
? i18next.t("unseal_input_btn")
: i18next.t("unseal_qr_btn");
this.unsealInputContent.appendChild(
makeElement({
tag: "button",
class: ["uk-button", "uk-button-primary"],
text: buttonText,
onclick: async () => {
await this.switchInputMode(newMethod);
},
}),
);
}
async switchInputMode(method: string): Promise<void> {
this.deinitWebcam();
this.unsealInputContent.querySelectorAll("*").forEach((n) => n.remove());
if (method == UnsealInputModes.FORM_INPUT) this.makeUnsealForm();
if (method == UnsealInputModes.QR_INPUT) await this.makeQRInput();
this.setButtons(method);
}
makeUnsealForm(): void {
this.unsealKeyForm = Form(
[
MarginInline(
makeElement({
tag: "input",
class: ["uk-input", "uk-form-width-medium"],
attributes: {
required: "true",
type: "password",
placeholder: i18next.t("key_input_placeholder"),
name: "key",
},
}),
),
MarginInline(
makeElement({
tag: "button",
class: ["uk-button", "uk-button-primary"],
text: i18next.t("submit_key_btn"),
}),
),
],
async (_) => {
await this.handleKeySubmit();
},
);
this.unsealInputContent.appendChild(this.unsealKeyForm);
}
async makeQRInput(): Promise<void> {
this.qrScanner = await QRScanner(async (code: string) => {
await this.submitKey(code);
});
this.unsealInputContent.appendChild(this.qrScanner);
}
async updateSealProgress(data: SealStatusType): Promise<void> {
const progress = data.progress;
const keysNeeded = data.t;
const text = this.unsealProgressText;
text.innerText = i18next.t("unseal_keys_progress", {
progress: String(progress),
keys_needed: String(keysNeeded),
});
const progressBar = this.unsealProgress;
progressBar.value = progress;
progressBar.max = keysNeeded;
if (!data.sealed) {
progressBar.value = keysNeeded;
await this.router.changePage("HOME");
}
}
async submitKey(key: string): Promise<void> {
try {
await submitUnsealKey(key);
await this.updateSealProgress(await getSealStatus());
} catch (e: unknown) {
const error = e as Error;
setErrorText(error.message);
}
}
async handleKeySubmit(): Promise<void> {
const formData = new FormData(this.unsealKeyForm);
await this.submitKey(formData.get("key") as string);
}
get name(): string {
return i18next.t("unseal_vault_text");
}
}

157
src/pages/Unseal.tsx Normal file
View file

@ -0,0 +1,157 @@
import { Component, JSX } from "preact";
import { Form } from "../elements/Form";
import { MarginInline } from "../elements/MarginInline";
import { Page } from "../types/Page";
import { QRScanner } from "../elements/QRScanner";
import { getSealStatus } from "../api/sys/getSealStatus";
import { render } from "preact/compat";
import { setErrorText } from "../pageUtils";
import { submitUnsealKey } from "../api/sys/submitUnsealKey";
import { toStr } from "../utils";
import i18next from "i18next";
const UnsealInputModes = {
FORM_INPUT: "FORM_INPUT",
QR_INPUT: "QR_INPUT",
};
export type UnsealFormInputProps = {
onSubmit: (code: string) => void;
};
export function UnsealFormInput(props: UnsealFormInputProps): JSX.Element {
return (
<Form
onSubmit={(data: FormData) => {
props.onSubmit(data.get("unsealKey") as string);
}}
>
<MarginInline>
<input
class="uk-input uk-form-width-medium"
name="unsealKey"
type="password"
placeholder={i18next.t("key_input_placeholder")}
required
/>
</MarginInline>
<MarginInline>
<button class="uk-button uk-button-primary" type="submit">
{i18next.t("submit_key_btn")}
</button>
</MarginInline>
</Form>
);
}
type UnsealPageState = {
mode: string;
keys_submitted: number;
keys_needed: number;
};
export class UnsealPageElement extends Component<{ page: Page }, UnsealPageState> {
constructor() {
super();
this.state = {
mode: UnsealInputModes.FORM_INPUT,
keys_submitted: 0,
keys_needed: 0,
};
}
timer: number;
async submitKey(key: string): Promise<void> {
try {
await submitUnsealKey(key);
this.updateStateWithSealStatus();
} catch (e: unknown) {
const error = e as Error;
setErrorText(error.message);
}
}
updateStateWithSealStatus(): void {
void getSealStatus().then((data) => {
this.setState({
keys_submitted: data.progress,
keys_needed: data.t,
});
if (!data.sealed) {
void this.props.page.router.changePage("HOME");
}
});
}
componentWillUnmount(): void {
clearInterval(this.timer);
}
componentDidMount(): void {
this.updateStateWithSealStatus();
this.timer = setInterval(() => {
this.updateStateWithSealStatus();
}, 500) as unknown as number;
}
render(): JSX.Element {
return (
<div>
<progress
class="uk-progress"
value={this.state.keys_submitted}
max={this.state.keys_needed}
/>
<p id="errorText" class="uk-text-danger uk-margin-top" />
<p>
{i18next.t("unseal_keys_progress", {
progress: toStr(this.state.keys_submitted),
keys_needed: toStr(this.state.keys_needed),
})}
</p>
{this.state.mode == UnsealInputModes.FORM_INPUT && (
<UnsealFormInput onSubmit={(code) => this.submitKey(code)} />
)}
{this.state.mode == UnsealInputModes.QR_INPUT && (
<QRScanner onScan={(code) => this.submitKey(code)} />
)}
<button
class="uk-button uk-button-primary"
onClick={async () => {
let newMethod: string;
if (this.state.mode == UnsealInputModes.FORM_INPUT) {
newMethod = UnsealInputModes.QR_INPUT;
} else {
newMethod = UnsealInputModes.FORM_INPUT;
}
this.setState({ mode: newMethod });
}}
>
{this.state.mode == UnsealInputModes.QR_INPUT
? i18next.t("unseal_input_btn")
: i18next.t("unseal_qr_btn")}
</button>
</div>
);
}
}
export class UnsealPage extends Page {
constructor() {
super();
}
async render(): Promise<void> {
render(<UnsealPageElement page={this} />, this.router.pageContentElement);
}
get name(): string {
return i18next.t("unseal_vault_text");
}
}