1
0
Fork 0

add settings page

This commit is contained in:
ChaotiCryptidz 2022-01-19 13:54:13 +00:00
parent cbc52b7e63
commit d4e9865ada
13 changed files with 273 additions and 83 deletions

View file

@ -9,7 +9,6 @@
"@babel/plugin-transform-runtime": "^7.16.8",
"@babel/preset-env": "^7.16.8",
"@types/file-saver": "^2.0.5",
"@types/hjson": "^2.4.3",
"@types/js-yaml": "^4.0.5",
"@types/prismjs": "^1.16.6",
"@types/uikit": "^3.3.2",
@ -41,7 +40,6 @@
"core-js": "^3.20.3",
"date-fns": "^2.28.0",
"file-saver": "^2.0.5",
"hjson": "^3.2.2",
"i18next": "^21.6.6",
"js-yaml": "^4.1.0",
"json5": "^2.2.0",

View file

@ -29,6 +29,7 @@ import { PolicyView } from "./ui/pages/Policies/PolicyView";
import { Secrets } from "./ui/pages/Secrets/SecretsHome";
import { SetLanguage } from "./ui/pages/SetLanguage";
import { SetVaultURL } from "./ui/pages/SetVaultURL";
import { Settings } from "./ui/pages/Settings/Settings";
import { TOTPDelete } from "./ui/pages/Secrets/TOTP/TOTPDelete";
import { TOTPList } from "./ui/pages/Secrets/TOTP/TOTPList";
import { TOTPNew } from "./ui/pages/Secrets/TOTP/TOTPNew";
@ -59,6 +60,7 @@ export const Main = () => (
<SetVaultURL path="/set_vault_url" settings={settings} api={api} />
<Unseal path="/unseal" settings={settings} api={api} />
<SetLanguage path="/set_language" settings={settings} api={api} />
<Settings path="/settings" settings={settings} api={api} />
<Secrets path="/secrets" settings={settings} api={api} />
<DeleteSecretsEngine path="/secrets/delete_engine/:mount" settings={settings} api={api} />

View file

@ -64,4 +64,24 @@ export class Settings {
this.storage.setItem("theme", value);
this.alertChange("theme");
}
get kvEditorDefaultLanguage(): string {
return this.storage.getItem("kvEditorDefaultLanguage") || "yaml";
}
set kvEditorDefaultLanguage(value: string) {
this.storage.setItem("kvEditorDefaultLanguage", value);
this.alertChange("kvEditorDefaultLanguage");
}
get kvEditorIndent(): number {
const value = this.storage.getItem("kvEditorIndent");
if (value) return parseInt(value);
return 2;
}
set kvEditorIndent(value: number) {
this.storage.setItem("kvEditorIndent", String(value));
this.alertChange("kvEditorIndent");
}
}

View file

@ -35,9 +35,9 @@ module.exports = {
not_implemented: "Not Yet Implemented",
// Copyable Modal
copy_box_download_btn: "Download",
copy_box_copy_btn: "Copy",
copy_box_close_btn: "Close",
copy_modal_download_btn: "Download",
copy_modal_copy_btn: "Copy",
copy_modal_close_btn: "Close",
// Generic Loading Text
content_loading: "Loading..",
@ -54,8 +54,7 @@ module.exports = {
me_seal_vault_btn: "Seal Vault",
me_copy_token_btn: "Copy Token",
me_renew_lease_btn: "Renew Token Lease",
me_change_language_btn: "Change Language",
me_set_vault_url_btn: "Set Vault URL",
me_settings_btn: "Settings",
// Home Page
home_page_title: "Home",
@ -69,6 +68,23 @@ module.exports = {
home_policies_title: "Policies",
home_policies_description: "Manage policies and permissions.",
// Settings Page
settings_title: "Settings",
// General Settings
settings_general_title: "General",
settings_general_theme: "Theme",
settings_general_vault_url: "Vault API URL",
settings_general_language: "Language",
settings_general_page_direction: "Page Direction",
settings_general_page_direction_ltr: "Left to Right",
settings_general_page_direction_rtl: "Right to Left",
// Key/Value Editor Settings
settings_kveditor_title: "Key/Value Editor",
settings_kveditor_default_language: "Default Data Interchange Format",
settings_kveditor_default_indent: "Default Indent",
// Set Vault URL Page
set_vault_url_title: "Set Vault URL",
set_vault_url_placeholder: "Vault URL",

View file

@ -1,6 +1,6 @@
import { Button } from "../elements/Button";
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";
@ -103,30 +103,10 @@ export class Me extends Component<DefaultPageProps, MeState> {
</a>
</li>
)}
<li>
<a href="/set_language">{i18next.t("me_change_language_btn")}</a>
</li>
<li>
<a href="/set_vault_url">{i18next.t("me_set_vault_url_btn")}</a>
</li>
<InputWithTitle title="Theme">
<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"}
/>
</select>
</InputWithTitle>
<br />
<Button text={i18next.t("me_settings_btn")} color="primary" route="/settings" />
</ul>
</>
)

View file

@ -6,38 +6,31 @@ import { InputWithTitle } from "../../../elements/InputWithTitle";
import { SecretTitleElement } from "../SecretTitleElement";
import { setErrorText } from "../../../../pageUtils";
import { sortedObjectMap } from "../../../../utils";
import Hjson from "hjson";
import JSON5 from "json5";
import i18next from "i18next";
import yaml from "js-yaml";
// todo: put this in settings
const defaultIndent = 2;
const defaultSyntax = "json";
export const SupportedEditorLanguages = [
{ name: "json", readable: "JSON" },
{ name: "json5", readable: "JSON5" },
{ name: "yaml", readable: "YAML" },
];
function parseData(data: string, syntax = "json"): Record<string, unknown> {
if (syntax == "json") {
return JSON.parse(data) as Record<string, unknown>;
} else if (syntax == "json5") {
return JSON5.parse(data);
} else if (syntax == "hjson") {
return Hjson.parse(data) as Record<string, unknown>;
} else if (syntax == "yaml") {
return yaml.load(data) as Record<string, unknown>;
}
}
function dumpData(
data: Record<string, unknown>,
space: number = defaultIndent,
syntax = "json",
): string {
function dumpData(data: Record<string, unknown>, space = 4, syntax = "json"): string {
if (syntax == "json") {
return JSON.stringify(data, null, space);
} else if (syntax == "json5") {
return JSON5.stringify(data, null, space);
} else if (syntax == "hjson") {
return Hjson.stringify(data, { space: space });
} else if (syntax == "yaml") {
return yaml.dump(data, { indent: space });
}
@ -77,7 +70,7 @@ export class KVEditor extends Component<KVEditProps, KVEditState> {
super();
this.state = {
dataLoaded: false,
syntax: defaultSyntax,
syntax: "",
};
}
@ -119,13 +112,18 @@ export class KVEditor extends Component<KVEditProps, KVEditState> {
}
async componentDidMount() {
this.setState({ syntax: this.props.settings.kvEditorDefaultLanguage });
if (!this.state.dataLoaded) {
await this.loadData();
}
}
getStringKVData(data: Record<string, unknown>): string {
return dumpData(Object.fromEntries(sortedObjectMap(data)), defaultIndent, this.state.syntax);
return dumpData(
Object.fromEntries(sortedObjectMap(data)),
this.props.settings.kvEditorIndent,
this.state.syntax,
);
}
syntaxSelectRef = createRef<HTMLSelectElement>();
@ -141,8 +139,6 @@ export class KVEditor extends Component<KVEditProps, KVEditState> {
codeEditorLanguage = "json";
} else if (this.state.syntax == "json5") {
codeEditorLanguage = "json5";
} else if (this.state.syntax == "hjson") {
codeEditorLanguage = "js";
} else if (this.state.syntax == "yaml") {
codeEditorLanguage = "yaml";
}
@ -157,16 +153,21 @@ export class KVEditor extends Component<KVEditProps, KVEditState> {
this.setState({ syntax: this.syntaxSelectRef.current.value });
}}
>
<option label="JSON" value="json" />
<option label="JSON5" value="json5" />
<option label="Hjson" value="hjson" />
<option label="Yaml" value="yaml" />
{SupportedEditorLanguages.map((lang) => {
return (
<option
label={lang.readable}
value={lang.name}
selected={this.props.settings.kvEditorDefaultLanguage == lang.name}
/>
);
})}
</select>
</InputWithTitle>
<p class="uk-text-danger" id="errorText" />
<CodeEditor
language={codeEditorLanguage}
tabSize={defaultIndent}
tabSize={this.props.settings.kvEditorIndent}
code={this.getStringKVData(this.state.kvData)}
onUpdate={(code) => this.onCodeUpdate(code)}
/>

View file

@ -25,12 +25,7 @@ function SecretsList(baseMount: string, secretPath: string[], secrets: string[])
<a
onClick={async () => {
if (secret.endsWith("/")) {
route(
kvListURL(
baseMount,
[...secretPath, secret.replace("/", "")]
),
);
route(kvListURL(baseMount, [...secretPath, secret.replace("/", "")]));
} else {
route(kvViewURL(baseMount, secretPath, secret));
}

View file

@ -56,7 +56,7 @@ export class KeyValueNew extends Component<DefaultPageProps> {
const mountInfo = await this.props.api.getMount(baseMount);
if (mountInfo.options.version == "1") {
// Can't have a empty secret on KV V1
keyData = { "placeholder_on_kv1": "placeholder_on_kv1" };
keyData = { placeholder_on_kv1: "placeholder_on_kv1" };
}
try {

View file

@ -71,7 +71,7 @@ export class RefreshingTOTPGridItem extends Component<TOTPGridItemProps, { totpV
type TOTPItem = {
totpKey: string;
canDelete: boolean;
}
};
type TOTPListState = {
capabilities?: CapabilitiesType;
@ -98,19 +98,25 @@ export class TOTPList extends Component<DefaultPageProps, TOTPListState> {
try {
const totpKeys = await api.getTOTPKeys(baseMount);
let totpKeyPermissions = await Promise.all(Array.from(totpKeys.map(async (key) => {
const totpCaps = await api.getCapsPath(removeDoubleSlash(baseMount + "/code/" + key));
return {key: key, caps: totpCaps};
})));
const totpKeyPermissions = await Promise.all(
Array.from(
totpKeys.map(async (key) => {
const totpCaps = await api.getCapsPath(removeDoubleSlash(baseMount + "/code/" + key));
return { key: key, caps: totpCaps };
}),
),
);
totpItems = Array.from(totpKeyPermissions.map((keyData) => {
// Filter out all non-readable totp keys.
if (!keyData.caps.includes("read")) return;
return {
totpKey: keyData.key,
canDelete: keyData.caps.includes("delete"),
};
}));
totpItems = Array.from(
totpKeyPermissions.map((keyData) => {
// Filter out all non-readable totp keys.
if (!keyData.caps.includes("read")) return;
return {
totpKey: keyData.key,
canDelete: keyData.caps.includes("delete"),
};
}),
);
} catch (e: unknown) {
const error = e as Error;
if (error != DoesNotExistError) {
@ -166,11 +172,7 @@ export class TOTPList extends Component<DefaultPageProps, TOTPListState> {
} else {
return this.state.totpItems.map((totpItem) => {
return (
<RefreshingTOTPGridItem
{...this.props}
baseMount={baseMount}
{...totpItem}
/>
<RefreshingTOTPGridItem {...this.props} baseMount={baseMount} {...totpItem} />
);
});
}

View file

@ -9,10 +9,8 @@ import { Form } from "../elements/Form";
import { Margin } from "../elements/Margin";
import { MarginInline } from "../elements/MarginInline";
import { PageTitle } from "../elements/PageTitle";
import i18next from "i18next";
import { route } from "preact-router";
const languageIDs = Object.getOwnPropertyNames(translations);
import i18next from "i18next";
export class SetLanguage extends Component<DefaultPageProps> {
constructor() {
@ -25,8 +23,8 @@ export class SetLanguage extends Component<DefaultPageProps> {
<Form onSubmit={(data) => this.onSubmit(data)}>
<Margin>
<select class="uk-select uk-form-width-large" name="language">
{languageIDs.map((languageID) => (
<option value={languageID}>
{Object.getOwnPropertyNames(translations).map((languageID) => (
<option value={languageID} selected={this.props.settings.language == languageID}>
{i18next.getFixedT(languageID, null)("language_name")}
</option>
))}

View file

@ -0,0 +1,105 @@
import { Component, createRef } from "preact";
import { DefaultPageProps } from "../../../types/DefaultPageProps";
import { InputWithTitle } from "../../elements/InputWithTitle";
import i18next from "i18next";
// @ts-ignore
import translations from "../../../translations/index.mjs";
const Themes = [
{ name: "dark", readable: "Dark" },
{ name: "light", readable: "Light" },
];
export class GeneralSettings extends Component<DefaultPageProps> {
themeSelectRef = createRef<HTMLSelectElement>();
vaultURLInputRef = createRef<HTMLInputElement>();
pageDirectionRef = createRef<HTMLSelectElement>();
languageSelectRef = createRef<HTMLSelectElement>();
render() {
return (
<>
<h4>{i18next.t("settings_general_title")}</h4>
<InputWithTitle title={i18next.t("settings_general_theme")}>
<select
ref={this.themeSelectRef}
class="uk-select uk-form-width-medium"
onChange={() => {
const newTheme = this.themeSelectRef.current.value;
this.props.settings.theme = newTheme;
}}
>
{Themes.map((theme) => {
return (
<option
label={theme.readable}
value={theme.name}
selected={this.props.settings.theme == theme.name}
/>
);
})}
</select>
</InputWithTitle>
<InputWithTitle title={i18next.t("settings_general_vault_url")}>
<input
ref={this.vaultURLInputRef}
class="uk-input uk-form-width-medium"
value={this.props.settings.apiURL}
onChange={() => {
// TODO: check for api health to see if is valid api url.
this.props.settings.apiURL = this.vaultURLInputRef.current.value;
}}
/>
</InputWithTitle>
<InputWithTitle title={i18next.t("settings_general_language")}>
<select
ref={this.languageSelectRef}
class="uk-select uk-form-width-medium"
onChange={async () => {
const language = this.languageSelectRef.current.value;
this.props.settings.language = language;
const t = await i18next.changeLanguage(language);
this.props.settings.pageDirection = t("language_direction");
window.location.reload();
}}
>
{Object.getOwnPropertyNames(translations).map((languageID) => (
<option value={languageID} selected={this.props.settings.language == languageID}>
{i18next.getFixedT(languageID, null)("language_name")}
</option>
))}
</select>
</InputWithTitle>
<InputWithTitle title={i18next.t("settings_general_page_direction")}>
<select
ref={this.pageDirectionRef}
class="uk-select uk-form-width-medium"
onChange={() => {
this.props.settings.pageDirection = this.pageDirectionRef.current.value;
document.documentElement.dir = this.props.settings.pageDirection;
}}
>
{[
{ name: "ltr", readable: i18next.t("settings_general_page_direction_ltr") },
{ name: "rtl", readable: i18next.t("settings_general_page_direction_rtl") },
].map((direction) => {
return (
<option
label={direction.readable}
value={direction.name}
selected={this.props.settings.pageDirection == direction.name}
/>
);
})}
</select>
</InputWithTitle>
</>
);
}
}

View file

@ -0,0 +1,51 @@
import { Component, createRef } from "preact";
import { DefaultPageProps } from "../../../types/DefaultPageProps";
import { InputWithTitle } from "../../elements/InputWithTitle";
import { SupportedEditorLanguages } from "../Secrets/KeyValue/KeyValueEdit";
import i18next from "i18next";
export class KeyValueEditorSettings extends Component<DefaultPageProps> {
syntaxSelectRef = createRef<HTMLSelectElement>();
indentInputRef = createRef<HTMLInputElement>();
render() {
return (
<>
<h4>{i18next.t("settings_kveditor_title")}</h4>
<InputWithTitle title={i18next.t("settings_kveditor_default_language")}>
<select
ref={this.syntaxSelectRef}
class="uk-select uk-form-width-medium"
onChange={() => {
this.props.settings.kvEditorDefaultLanguage = this.syntaxSelectRef.current.value;
}}
>
{SupportedEditorLanguages.map((lang) => {
return (
<option
label={lang.readable}
value={lang.name}
selected={this.props.settings.kvEditorDefaultLanguage == lang.name}
/>
);
})}
</select>
</InputWithTitle>
<InputWithTitle title={i18next.t("settings_kveditor_default_indent")}>
<input
ref={this.indentInputRef}
class="uk-input uk-form-width-medium"
type="number"
value={this.props.settings.kvEditorIndent}
onChange={() => {
const value = this.indentInputRef.current.value;
const indent = parseInt(value);
this.props.settings.kvEditorIndent = indent;
}}
/>
</InputWithTitle>
</>
);
}
}

View file

@ -0,0 +1,22 @@
import { Component } from "preact";
import { DefaultPageProps } from "../../../types/DefaultPageProps";
import { GeneralSettings } from "./GeneralSettings";
import { KeyValueEditorSettings } from "./KeyValueEditorSettings";
import { PageTitle } from "../../elements/PageTitle";
import i18next from "i18next";
export class Settings extends Component<DefaultPageProps> {
render() {
return (
<>
<PageTitle title={i18next.t("settings_title")} />
<div>
<GeneralSettings {...this.props} />
</div>
<div>
<KeyValueEditorSettings {...this.props} />
</div>
</>
);
}
}