Add vault-generated totp code support.
This commit is contained in:
parent
5be6596295
commit
7608df51b3
|
@ -2,32 +2,32 @@ import { Component } from "preact";
|
|||
import { settings } from "./globalSettings";
|
||||
|
||||
// @ts-ignore
|
||||
import style_dark from "./scss/main-dark.scss" assert {type: "css"};
|
||||
import style_dark from "./scss/main-dark.scss" assert { type: "css" };
|
||||
// @ts-ignore
|
||||
import style_light from "./scss/main-light.scss" assert {type: "css"};
|
||||
import style_light from "./scss/main-light.scss" assert { type: "css" };
|
||||
|
||||
export const default_theme = "dark";
|
||||
|
||||
const themes: { [key: string]: string } = {
|
||||
"dark": style_dark,
|
||||
"light": style_light,
|
||||
}
|
||||
dark: style_dark as string,
|
||||
light: style_light as string,
|
||||
};
|
||||
|
||||
export class ThemeLoader extends Component<{}, { sheet: string }> {
|
||||
export class ThemeLoader extends Component<unknown, { sheet: string }> {
|
||||
componentDidMount() {
|
||||
this.setCorrectStyle(settings.theme);
|
||||
settings.registerListener((key: string) => {
|
||||
if (key != "theme") return;
|
||||
this.setCorrectStyle(settings.theme);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setCorrectStyle(theme: string) {
|
||||
this.setState({ sheet: themes[theme] })
|
||||
this.setState({ sheet: themes[theme] });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.sheet) return;
|
||||
return <style>{this.state.sheet}</style>
|
||||
return <style>{this.state.sheet}</style>;
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from "./types/transit";
|
||||
import { DoesNotExistError } from "../types/internalErrors";
|
||||
import { MountType, MountsType, NewMountParams } from "./types/mount";
|
||||
import { NewTOTPData, NewTOTPResp } from "./types/totp";
|
||||
import { SealStatusType } from "./types/seal";
|
||||
import { SecretMetadataType } from "./types/secret";
|
||||
import { Settings } from "../settings/Settings";
|
||||
|
@ -461,7 +462,7 @@ export class API {
|
|||
return data.data.keys;
|
||||
}
|
||||
|
||||
async addNewTOTP(baseMount: string, parms: { name: string }): Promise<void> {
|
||||
async addNewTOTP(baseMount: string, parms: NewTOTPData): Promise<NewTOTPResp> {
|
||||
const request = new Request(
|
||||
this.appendAPIURL(removeDoubleSlash(`/v1/${baseMount}/keys/${parms.name}`)),
|
||||
{
|
||||
|
@ -475,6 +476,8 @@ export class API {
|
|||
);
|
||||
const resp = await fetch(request);
|
||||
await checkResponse(resp);
|
||||
const data = (await resp.json()) as { data: NewTOTPResp };
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async deleteTOTP(baseMount: string, name: string): Promise<void> {
|
||||
|
|
18
src/api/types/totp.ts
Normal file
18
src/api/types/totp.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
export type NewTOTPData = {
|
||||
name: string;
|
||||
generate: boolean;
|
||||
exported?: boolean;
|
||||
key_size?: number;
|
||||
url?: string;
|
||||
key?: string;
|
||||
issuer?: string;
|
||||
account_name?: string;
|
||||
period?: number | string;
|
||||
algorithm?: string;
|
||||
digits?: number;
|
||||
};
|
||||
|
||||
export type NewTOTPResp = {
|
||||
url: string;
|
||||
barcode: string;
|
||||
};
|
|
@ -25,13 +25,13 @@ import { formatDistance } from "./formatDistance";
|
|||
//import { pageList } from "./allPages";
|
||||
import { Main } from "./pages";
|
||||
import { NavBar } from "./ui/elements/NavBar";
|
||||
import { ThemeLoader } from "./ThemeLoader";
|
||||
import { api } from "./globalAPI";
|
||||
import { getCurrentUrl, route } from "preact-router";
|
||||
import { playground } from "./playground";
|
||||
import { render } from "preact";
|
||||
import { settings } from "./globalSettings";
|
||||
import i18next from "i18next";
|
||||
import { ThemeLoader } from "./ThemeLoader";
|
||||
|
||||
async function onLoad(): Promise<void> {
|
||||
document.documentElement.dir = settings.pageDirection;
|
||||
|
|
|
@ -30,6 +30,7 @@ import { SetVaultURL } from "./ui/pages/SetVaultURL";
|
|||
import { TOTPDelete } from "./ui/pages/Secrets/TOTP/TOTPDelete";
|
||||
import { TOTPList } from "./ui/pages/Secrets/TOTP/TOTPList";
|
||||
import { TOTPNew } from "./ui/pages/Secrets/TOTP/TOTPNew";
|
||||
import { TOTPNewGenerated } from "./ui/pages/Secrets/TOTP/TOTPNewGenerated";
|
||||
import { TransitDecrypt } from "./ui/pages/Secrets/Transit/TransitDecrypt";
|
||||
import { TransitEncrypt } from "./ui/pages/Secrets/Transit/TransitEncrypt";
|
||||
import { TransitList } from "./ui/pages/Secrets/Transit/TransitList";
|
||||
|
@ -81,6 +82,7 @@ export const Main = () => (
|
|||
|
||||
<TOTPList path="/secrets/totp/list/:baseMount" settings={settings} api={api} />
|
||||
<TOTPNew path="/secrets/totp/new/:baseMount" settings={settings} api={api} />
|
||||
<TOTPNewGenerated path="/secrets/totp/new_generated/:baseMount" settings={settings} api={api} />
|
||||
<TOTPDelete path="/secrets/totp/delete/:baseMount/:item" settings={settings} api={api} />
|
||||
|
||||
<TransitNew path="/secrets/transit/new/:baseMount" settings={settings} api={api} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { settings } from "./globalSettings";
|
||||
import { Settings } from "./settings/Settings";
|
||||
import { settings } from "./globalSettings";
|
||||
|
||||
// Playground is a way to debug and test things.
|
||||
// Anything you put in here is gonna be run on page initial load
|
||||
|
@ -12,7 +12,6 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Please empty this function before committing.
|
||||
export async function playground(): Promise<void> {
|
||||
console.log("Welcome to Playground!");
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { default_theme } from "../ThemeLoader";
|
||||
import { StorageType } from "./storage/StorageType";
|
||||
import { default_theme } from "../ThemeLoader";
|
||||
|
||||
type OnChangeListener = (key: string) => void;
|
||||
|
||||
|
@ -17,7 +17,7 @@ export class Settings {
|
|||
}
|
||||
|
||||
alertChange(key: string) {
|
||||
for (let listener of this.listeners) {
|
||||
for (const listener of this.listeners) {
|
||||
listener(key);
|
||||
}
|
||||
}
|
||||
|
|
13
src/translations/en.js
vendored
13
src/translations/en.js
vendored
|
@ -13,6 +13,7 @@ module.exports = {
|
|||
// Common buttons / placeholders
|
||||
common_new: "New",
|
||||
common_view: "View",
|
||||
common_back: "Back",
|
||||
common_edit: "Edit",
|
||||
common_create: "Create",
|
||||
common_delete: "Delete",
|
||||
|
@ -176,6 +177,18 @@ module.exports = {
|
|||
totp_new_switch_to_qr_btn: "Switch to QR Input",
|
||||
totp_new_switch_back_to_manual_input_btn: "Switch back to manual input",
|
||||
|
||||
totp_new_generated: "New Generated",
|
||||
totp_new_generated_suffix: "(new generated)",
|
||||
totp_new_generated_warning:
|
||||
"Make sure to save this information somewhere safe as there is no way to get it back short of Vault's raw api.",
|
||||
totp_new_generated_issuer: "Issuer",
|
||||
totp_new_generated_account_name: "Account Name",
|
||||
totp_new_generated_algorithm: "Algorithm",
|
||||
totp_new_generated_key_size: "Key Size (bytes)",
|
||||
totp_new_generated_period: "Period (Refresh Time)",
|
||||
totp_new_generated_digits: "Digits (Length 6/8)",
|
||||
totp_new_generated_export: "Show QR & URI (Recommended)",
|
||||
|
||||
// TOTP Delete Page
|
||||
totp_delete_title: "Delete TOTP Key",
|
||||
totp_delete_suffix: " (delete)",
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Component, JSX, createRef } from "preact";
|
||||
import { DefaultPageProps } from "../../types/DefaultPageProps";
|
||||
import { InputWithTitle } from "../elements/InputWithTitle";
|
||||
import { PageTitle } from "../elements/PageTitle";
|
||||
import { addClipboardNotifications, setErrorText } from "../../pageUtils";
|
||||
import { route } from "preact-router";
|
||||
import ClipboardJS from "clipboard";
|
||||
import i18next from "i18next";
|
||||
import { InputWithTitle } from "../elements/InputWithTitle";
|
||||
|
||||
export class CopyLink extends Component<{ text: string; data: string }, unknown> {
|
||||
linkRef = createRef<HTMLAnchorElement>();
|
||||
|
@ -55,7 +55,7 @@ export class Me extends Component<DefaultPageProps, MeState> {
|
|||
});
|
||||
}
|
||||
|
||||
themeSelectRef = createRef<HTMLSelectElement>()
|
||||
themeSelectRef = createRef<HTMLSelectElement>();
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
|
@ -111,12 +111,20 @@ export class Me extends Component<DefaultPageProps, MeState> {
|
|||
</li>
|
||||
|
||||
<InputWithTitle title="Theme">
|
||||
<select ref={this.themeSelectRef} class="uk-select uk-form-width-medium" onChange={() => {
|
||||
let newTheme = this.themeSelectRef.current.value;
|
||||
<select
|
||||
ref={this.themeSelectRef}
|
||||
class="uk-select uk-form-width-medium"
|
||||
onChange={() => {
|
||||
const newTheme = this.themeSelectRef.current.value;
|
||||
this.props.settings.theme = newTheme;
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<option label="Dark" value="dark" selected={this.props.settings.theme == "dark"} />
|
||||
<option label="Light" value="light" selected={this.props.settings.theme == "light"} />
|
||||
<option
|
||||
label="Light"
|
||||
value="light"
|
||||
selected={this.props.settings.theme == "light"}
|
||||
/>
|
||||
</select>
|
||||
</InputWithTitle>
|
||||
</ul>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { DoesNotExistError } from "../../../../types/internalErrors";
|
|||
import { Grid, GridSizes } from "../../../elements/Grid";
|
||||
import { MarginInline } from "../../../elements/MarginInline";
|
||||
import { SecretTitleElement } from "../SecretTitleElement";
|
||||
import { delSecretsEngineURL, totpNewURL } from "../../pageLinks";
|
||||
import { delSecretsEngineURL, totpNewGeneratedURL, totpNewURL } from "../../pageLinks";
|
||||
import { removeDoubleSlash } from "../../../../utils";
|
||||
import { route } from "preact-router";
|
||||
import { setErrorText } from "../../../../pageUtils";
|
||||
|
@ -138,6 +138,16 @@ export class TOTPList extends Component<DefaultPageProps, TOTPListState> {
|
|||
{i18next.t("common_new")}
|
||||
</button>
|
||||
)}
|
||||
{totpCaps.includes("create") && (
|
||||
<button
|
||||
class="uk-button uk-button-primary"
|
||||
onClick={async () => {
|
||||
route(totpNewGeneratedURL(baseMount));
|
||||
}}
|
||||
>
|
||||
{i18next.t("totp_new_generated")}
|
||||
</button>
|
||||
)}
|
||||
{mountCaps.includes("delete") && (
|
||||
<button
|
||||
class="uk-button uk-button-danger"
|
||||
|
|
170
src/ui/pages/Secrets/TOTP/TOTPNewGenerated.tsx
Normal file
170
src/ui/pages/Secrets/TOTP/TOTPNewGenerated.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
import { Component, JSX, createRef } from "preact";
|
||||
import { CopyableInputBox } from "../../../elements/CopyableInputBox";
|
||||
import { DefaultPageProps } from "../../../../types/DefaultPageProps";
|
||||
import { Form } from "../../../elements/Form";
|
||||
import { InputWithTitle } from "../../../elements/InputWithTitle";
|
||||
import { Margin } from "../../../elements/Margin";
|
||||
import { MarginInline } from "../../../elements/MarginInline";
|
||||
import { NewTOTPResp } from "../../../../api/types/totp";
|
||||
import { SecretTitleElement } from "../SecretTitleElement";
|
||||
import { route } from "preact-router";
|
||||
import { setErrorText } from "../../../../pageUtils";
|
||||
import i18next from "i18next";
|
||||
|
||||
export class TOTPNewGeneratedForm extends Component<
|
||||
{ baseMount: string } & DefaultPageProps,
|
||||
{ exportedData: NewTOTPResp }
|
||||
> {
|
||||
uriInputRef = createRef<HTMLInputElement>();
|
||||
|
||||
render(): JSX.Element {
|
||||
if (!this.state.exportedData) {
|
||||
return (
|
||||
<Form onSubmit={(data) => this.onSubmit(data)}>
|
||||
<Margin>
|
||||
<InputWithTitle title={i18next.t("common_name")}>
|
||||
<input
|
||||
class="uk-input uk-form-width-medium"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder={i18next.t("common_name")}
|
||||
required
|
||||
/>
|
||||
</InputWithTitle>
|
||||
</Margin>
|
||||
|
||||
<Margin>
|
||||
<InputWithTitle title={i18next.t("totp_new_generated_issuer")}>
|
||||
<input class="uk-input uk-form-width-medium" name="issuer" type="text" required />
|
||||
</InputWithTitle>
|
||||
</Margin>
|
||||
|
||||
<Margin>
|
||||
<InputWithTitle title={i18next.t("totp_new_generated_account_name")}>
|
||||
<input
|
||||
class="uk-input uk-form-width-medium"
|
||||
name="account_name"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
</InputWithTitle>
|
||||
</Margin>
|
||||
|
||||
<Margin>
|
||||
<InputWithTitle title={i18next.t("totp_new_generated_algorithm")}>
|
||||
<select class="uk-select uk-form-width-medium" name="algorithm">
|
||||
{["SHA512", "SHA256", "SHA1"].map((type) => (
|
||||
<option label={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</InputWithTitle>
|
||||
</Margin>
|
||||
|
||||
<Margin>
|
||||
<InputWithTitle title={i18next.t("totp_new_generated_key_size")}>
|
||||
<input
|
||||
class="uk-input uk-form-width-medium"
|
||||
name="key_size"
|
||||
type="number"
|
||||
value="20"
|
||||
/>
|
||||
</InputWithTitle>
|
||||
</Margin>
|
||||
|
||||
<Margin>
|
||||
<InputWithTitle title={i18next.t("totp_new_generated_period")}>
|
||||
<input class="uk-input uk-form-width-medium" name="period" type="text" value="30s" />
|
||||
</InputWithTitle>
|
||||
</Margin>
|
||||
|
||||
<Margin>
|
||||
<InputWithTitle title={i18next.t("totp_new_generated_digits")}>
|
||||
<input class="uk-input uk-form-width-medium" name="digits" type="number" value="6" />
|
||||
</InputWithTitle>
|
||||
</Margin>
|
||||
|
||||
<Margin>
|
||||
<InputWithTitle title={i18next.t("totp_new_generated_export")}>
|
||||
<input class="uk-checkbox" name="exported" type="checkbox" value="yes" checked />
|
||||
</InputWithTitle>
|
||||
</Margin>
|
||||
|
||||
<p id="errorText" class="uk-text-danger" />
|
||||
|
||||
<MarginInline>
|
||||
<button class="uk-button uk-button-primary" type="submit">
|
||||
{i18next.t("common_create")}
|
||||
</button>
|
||||
</MarginInline>
|
||||
</Form>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<p>{i18next.t("totp_new_generated_warning")}</p>
|
||||
<img src={"data:image/png;base64," + this.state.exportedData.barcode} />
|
||||
<CopyableInputBox copyable text={this.state.exportedData.url} />
|
||||
<button
|
||||
class="uk-button uk-button-primary"
|
||||
onClick={async () => {
|
||||
route("/secrets/totp/list/" + this.props.baseMount);
|
||||
}}
|
||||
>
|
||||
{i18next.t("common_back")}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async onSubmit(data: FormData): Promise<void> {
|
||||
const isExported = data.get("exported") == "yes" ? true : false;
|
||||
|
||||
const parms = {
|
||||
generate: true,
|
||||
name: data.get("name") as string,
|
||||
issuer: data.get("issuer") as string,
|
||||
account_name: data.get("account_name") as string,
|
||||
exported: isExported,
|
||||
key_size: parseInt(data.get("key_size") as string),
|
||||
period: data.get("period") as string,
|
||||
algorithm: data.get("algorithm") as string,
|
||||
digits: parseInt(data.get("digits") as string),
|
||||
};
|
||||
|
||||
console.log(parms);
|
||||
try {
|
||||
const ret = await this.props.api.addNewTOTP(this.props.baseMount, parms);
|
||||
if (!isExported) {
|
||||
route("/secrets/totp/list/" + this.props.baseMount);
|
||||
} else {
|
||||
this.setState({ exportedData: ret });
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const error = e as Error;
|
||||
setErrorText(`API Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TOTPNewGenerated extends Component<DefaultPageProps> {
|
||||
render() {
|
||||
const baseMount = this.props.matches["baseMount"];
|
||||
return (
|
||||
<>
|
||||
<SecretTitleElement
|
||||
type="totp"
|
||||
baseMount={baseMount}
|
||||
suffix={i18next.t("totp_new_generated_suffix")}
|
||||
/>
|
||||
<TOTPNewGeneratedForm
|
||||
settings={this.props.settings}
|
||||
api={this.props.api}
|
||||
baseMount={baseMount}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -33,6 +33,10 @@ export function totpNewURL(baseMount: string): string {
|
|||
return `/secrets/totp/new/${baseMount}`;
|
||||
}
|
||||
|
||||
export function totpNewGeneratedURL(baseMount: string): string {
|
||||
return `/secrets/totp/new_generated/${baseMount}`;
|
||||
}
|
||||
|
||||
export function totpListURL(baseMount: string): string {
|
||||
return `/secrets/totp/list/${baseMount}`;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue