🔨 Add form for webdav settings

This commit is contained in:
SebClem 2022-10-24 22:58:19 +02:00
parent f3aebdd4ff
commit 96a6e22072
Signed by: sebclem
GPG Key ID: 5A4308F6A359EA50
17 changed files with 372 additions and 31 deletions

View File

@ -10,7 +10,9 @@ const backupConfigPath = "/data/backupConfigV2.json";
export function validateBackupConfig(config: BackupConfig){
const validator = Joi.object(backupConfigValidation);
return validator.validateAsync(config);
return validator.validateAsync(config, {
abortEarly: false
});
}
export function saveBackupConfig(config: BackupConfig){

View File

@ -14,7 +14,9 @@ const NEXTCLOUD_ENDPOINT = "/remote.php/dav/files/$username";
export function validateWebdavConfig(config: WebdavConfig) {
const validator = Joi.object(WebdavConfigValidation);
return validator.validateAsync(config);
return validator.validateAsync(config, {
abortEarly: false
});
}
export function saveWebdavConfig(config: WebdavConfig) {

View File

@ -3,7 +3,7 @@ import { WebdavEndpointType } from "./webdavConfig.js";
const WebdavConfigValidation = {
url: Joi.string().not().empty().required(),
url: Joi.string().not().empty().uri().required(),
username: Joi.string().not().empty().required(),
password: Joi.string().not().empty().required(),
backupDir: Joi.string().required(),

View File

@ -0,0 +1 @@
VITE_API_URL="http://localhost:3000/"

View File

@ -1 +1,8 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -19,7 +19,7 @@
"pinia": "^2.0.23",
"roboto-fontface": "*",
"vue": "^3.2.40",
"vuetify": "3.0.0-beta.13",
"vuetify": "3.0.0-beta.14",
"webfontloader": "^1.0.0"
},
"devDependencies": {

View File

@ -24,7 +24,7 @@ specifiers:
vite-plugin-vuetify: ^1.0.0-alpha.12
vue: ^3.2.40
vue-tsc: 1.0.7
vuetify: 3.0.0-beta.13
vuetify: 3.0.0-beta.14
webfontloader: ^1.0.0
dependencies:
@ -36,7 +36,7 @@ dependencies:
pinia: 2.0.23_rg374xhldfcyvjtaj3qktyfz5y
roboto-fontface: 0.10.0
vue: 3.2.40
vuetify: 3.0.0-beta.13_vdkwhj2kz5kpysy3rwb432nm3y
vuetify: 3.0.0-beta.14_vdkwhj2kz5kpysy3rwb432nm3y
webfontloader: 1.6.28
devDependencies:
@ -53,7 +53,7 @@ devDependencies:
prettier: 2.7.1
typescript: 4.7.4
vite: 3.1.7
vite-plugin-vuetify: 1.0.0-alpha.17_l4pgesydsz3cvwqxhtnrp3zlwq
vite-plugin-vuetify: 1.0.0-alpha.17_jvcjrjiyr77axku57qj2zhmwla
vue-tsc: 1.0.7_typescript@4.7.4
packages:
@ -482,7 +482,7 @@ packages:
'@types/node': 16.11.65
dev: true
/@vuetify/loader-shared/1.6.0_t3dvazyjkhimlko2vtyi5desum:
/@vuetify/loader-shared/1.6.0_ufv4csfzemzfiqpf5bhlrg5qhm:
resolution: {integrity: sha512-mRvswe5SIecagmKkL1c0UQx5V9ACKLyEGegOOe5vC3ccFH8rMbyI4AVZaAKm/EnGXKirWJ1umL8RQFcGd+C0tw==}
peerDependencies:
vue: ^3.0.0
@ -491,7 +491,7 @@ packages:
find-cache-dir: 3.3.2
upath: 2.0.1
vue: 3.2.40
vuetify: 3.0.0-beta.13_vdkwhj2kz5kpysy3rwb432nm3y
vuetify: 3.0.0-beta.14_vdkwhj2kz5kpysy3rwb432nm3y
/acorn-jsx/5.3.2_acorn@8.8.0:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@ -2150,18 +2150,18 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
/vite-plugin-vuetify/1.0.0-alpha.17_l4pgesydsz3cvwqxhtnrp3zlwq:
/vite-plugin-vuetify/1.0.0-alpha.17_jvcjrjiyr77axku57qj2zhmwla:
resolution: {integrity: sha512-lP4vG+Z3LnIEjI8nSE8/MJwDRQK5OnS2i4zHDu36yyD4E7wmPKyIkWJdkfBIsx/O+PU7dGc/3MeLARgr+RErEQ==}
engines: {node: '>=12'}
peerDependencies:
vite: ^2.7.0 || ^3.0.0
vuetify: ^3.0.0-beta.4
dependencies:
'@vuetify/loader-shared': 1.6.0_t3dvazyjkhimlko2vtyi5desum
'@vuetify/loader-shared': 1.6.0_ufv4csfzemzfiqpf5bhlrg5qhm
debug: 4.3.4
upath: 2.0.1
vite: 3.1.7
vuetify: 3.0.0-beta.13_vdkwhj2kz5kpysy3rwb432nm3y
vuetify: 3.0.0-beta.14_vdkwhj2kz5kpysy3rwb432nm3y
transitivePeerDependencies:
- supports-color
- vue
@ -2252,18 +2252,15 @@ packages:
'@vue/server-renderer': 3.2.40_vue@3.2.40
'@vue/shared': 3.2.40
/vuetify/3.0.0-beta.13_vdkwhj2kz5kpysy3rwb432nm3y:
resolution: {integrity: sha512-CgDyKCIBZ0ogKUxRw3gNswx9GA6eSSp7otZcJapWJCVuon/RooQPQjf3bDuRb5ZpJ55NkX13ugjl2nq+b0U8SA==}
/vuetify/3.0.0-beta.14_vdkwhj2kz5kpysy3rwb432nm3y:
resolution: {integrity: sha512-7UX2rAcMPGdJ0SGAK+wUvlJTHR0KasYILhg7dV9hLyv9SfaTX4tbOQnSQVf4mXzdFB+5jDyaTs+bOYMVeoqXYg==}
engines: {node: ^12.20 || >=14.13}
peerDependencies:
'@formatjs/intl': ^1.0.0 || ^2.0.0
vite-plugin-vuetify: ^1.0.0-alpha.12
vue: ^3.2.0
vue-i18n: ^9.0.0
webpack-plugin-vuetify: ^2.0.0-alpha.11
peerDependenciesMeta:
'@formatjs/intl':
optional: true
vite-plugin-vuetify:
optional: true
vue-i18n:
@ -2271,7 +2268,7 @@ packages:
webpack-plugin-vuetify:
optional: true
dependencies:
vite-plugin-vuetify: 1.0.0-alpha.17_l4pgesydsz3cvwqxhtnrp3zlwq
vite-plugin-vuetify: 1.0.0-alpha.17_jvcjrjiyr77axku57qj2zhmwla
vue: 3.2.40
/webfontloader/1.6.28:

View File

@ -3,6 +3,7 @@
<v-main>
<navbar-component></navbar-component>
<message-bar></message-bar>
<webdav-settings-menu></webdav-settings-menu>
</v-main>
</v-app>
</template>
@ -10,6 +11,7 @@
<script setup lang="ts">
import NavbarComponent from "./components/NavbarComponent.vue";
import MessageBar from "./components/MessageBar.vue";
import WebdavSettingsMenu from "./components/settings/WebdavConfigMenu.vue";
</script>
<style scoped></style>

View File

@ -12,19 +12,33 @@
<v-btn icon id="message-btn">
<v-icon>mdi-bell</v-icon>
</v-btn>
<v-btn icon @click="emit('settingClick')">
<v-icon>mdi-cog</v-icon>
</v-btn>
<v-menu width="210px">
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props">
<v-icon>mdi-cog</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
title="Cloud settings"
@click="dialogStatusStore.webdav = true"
prepend-icon="mdi-cloud"
></v-list-item>
<v-list-item
title="Backup settings"
prepend-icon="mdi-rotate-left"
@click="dialogStatusStore.backup = true"
></v-list-item>
</v-list>
</v-menu>
</v-app-bar>
</template>
<script setup lang="ts">
import { useDialogStatusStore } from "@/stores/dialogStatus";
import logoUrl from "../assets/logo.svg";
const emit = defineEmits<{
(e: "settingClick"): void;
(e: "notificationClick"): void;
}>();
const dialogStatusStore = useDialogStatusStore();
</script>
<style scoped>
.shake {

View File

@ -0,0 +1,208 @@
<template>
<v-form class="mx-4">
<v-row>
<v-col>
<div class="text-subtitle-1 text-medium-emphasis">URL</div>
<v-text-field
placeholder="https://exemple.com"
variant="outlined"
density="compact"
prepend-inner-icon="mdi-web"
hide-details="auto"
v-model="data.url"
:error-messages="errors.url"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<div class="text-subtitle-1 text-medium-emphasis">Endpoint</div>
<v-select
variant="outlined"
density="compact"
:items="items"
hide-details="auto"
v-model="data.webdavEndpoint.type"
:error-messages="errors.type"
>
</v-select>
</v-col>
</v-row>
<v-row v-if="data.webdavEndpoint.type == WebdavEndpointType.CUSTOM">
<v-col>
<div class="text-subtitle-1 text-medium-emphasis">Custom Endpoint</div>
<v-text-field
placeholder="/remote.php/dav/files/$username"
hint="You can use the $username variable"
variant="outlined"
density="compact"
hide-details="auto"
v-model="data.webdavEndpoint.customEndpoint"
:error-messages="errors.customEndpoint"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="d-flex align-content-end">
<v-switch
color="warning"
label="Allow Self Signed Certificate"
v-model="data.allowSelfSignedCerts"
hide-details="auto"
density="compact"
inset
:error-messages="errors.allowSelfSignedCerts"
></v-switch>
</v-col>
</v-row>
<v-row>
<v-col><v-divider></v-divider></v-col>
</v-row>
<v-row>
<v-col>
<v-alert type="info" variant="outlined">
You need to use <b>App password</b>.<br />
More info
<a
target="_blank"
href="https://github.com/Sebclem/hassio-nextcloud-backup/blob/master/nextcloud_backup/DOCS.md#nextcloud-config"
>
here.
</a>
</v-alert>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6">
<div class="text-subtitle-1 text-medium-emphasis">Username</div>
<v-text-field
variant="outlined"
density="compact"
placeholder="Username"
prepend-inner-icon="mdi-account"
hide-details="auto"
v-model="data.username"
:error-messages="errors.username"
>
</v-text-field>
</v-col>
<v-col cols="12" md="6">
<div class="text-subtitle-1 text-medium-emphasis">App password</div>
<v-text-field
placeholder="App password"
type="password"
variant="outlined"
density="compact"
prepend-inner-icon="mdi-key"
hide-details="auto"
v-model="data.password"
:error-messages="errors.password"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col><v-divider></v-divider></v-col>
</v-row>
<v-row>
<v-col>
<div class="text-subtitle-1 text-medium-emphasis">Backup folder</div>
<v-text-field
placeholder="Backup folder"
variant="outlined"
density="compact"
prepend-inner-icon="mdi-folder"
hide-details="auto"
v-model="data.backupDir"
:error-messages="errors.backupDir"
></v-text-field>
</v-col>
</v-row>
</v-form>
</template>
<script setup lang="ts">
import {
WebdavEndpointType,
type WebdavConfig,
} from "../../types/webdavConfig";
import {
getWebdavConfig,
saveWebdavConfig,
} from "../../services/ConfigService";
import { ref } from "vue";
import { HTTPError } from "ky";
const items = [
{
title: "Nextcloud",
value: WebdavEndpointType.NEXTCLOUD,
},
{
title: "Custom",
value: WebdavEndpointType.CUSTOM,
},
];
const errors = ref({
url: [],
username: [],
password: [],
backupDir: [],
allowSelfSignedCerts: [],
type: [],
customEndpoint: [],
});
const data = ref<WebdavConfig>({
url: "",
allowSelfSignedCerts: false,
backupDir: "",
username: "",
password: "",
webdavEndpoint: {
type: WebdavEndpointType.NEXTCLOUD,
},
});
const emit = defineEmits<{
(e: "success"): void;
(e: "fail"): void;
(e: "loaded"): void;
}>();
function save() {
clearErrors();
saveWebdavConfig(data.value)
.then(() => {
emit("success");
})
.catch(async (reason) => {
if (reason instanceof HTTPError) {
const response = await reason.response.json();
if (Array.isArray(response)) {
for (let elem of response) {
errors.value[elem.context.key as keyof typeof errors.value] =
elem.message;
}
}
}
emit("fail");
});
}
function clearErrors() {
for (let elem in errors.value) {
errors.value[elem as keyof typeof errors.value] = [];
}
}
function loadData() {
console.log("ok");
getWebdavConfig().then((value) => {
data.value = value;
emit("loaded");
});
}
loadData();
defineExpose({ save });
</script>

View File

@ -0,0 +1,66 @@
<template>
<v-dialog
v-model="dialogStatusStore.webdav"
persistent
:width="width"
:fullscreen="xs"
scrollable
>
<v-card>
<v-card-title class="text-center">Cloud Settings</v-card-title>
<v-divider></v-divider>
<v-card-text>
<webdav-settings-form
ref="form"
@fail="saving = false"
@success="dialogStatusStore.webdav = false"
@loaded="loading = false"
></webdav-settings-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="justify-end">
<v-btn
color="red"
@click="dialogStatusStore.webdav = false"
:disabled="saving"
>Cancel</v-btn
>
<v-btn color="success" @click="save()" :loading="saveLoading"
>Save</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { useDialogStatusStore } from "@/stores/dialogStatus";
import { computed, ref } from "vue";
import { useDisplay } from "vuetify/lib/framework.mjs";
import WebdavSettingsForm from "./WebdavConfigForm.vue";
const dialogStatusStore = useDialogStatusStore();
const form = ref<InstanceType<typeof WebdavSettingsForm> | null>(null);
const loading = ref(true);
const saving = ref(false);
const { xs, mdAndDown } = useDisplay();
let saveLoading = computed(() => {
return saving.value || loading.value;
});
const width = computed(() => {
if (xs.value) {
return undefined;
} else if (mdAndDown.value) {
return "80%";
} else {
return "50%";
}
});
function save() {
saving.value = true;
form.value?.save();
}
</script>

View File

@ -1,8 +1,9 @@
import { createPinia } from "pinia";
import { createApp } from "vue";
import App from "./App.vue";
import vuetify from "./plugins/vuetify";
import { loadFonts } from "./plugins/webfontloader";
loadFonts();
createApp(App).use(vuetify).mount("#app");
const pinia = createPinia();
createApp(App).use(vuetify).use(pinia).mount("#app");

View File

@ -0,0 +1,14 @@
import type { WebdavConfig } from "@/types/webdavConfig";
import kyClient from "./kyClient";
export function getWebdavConfig() {
return kyClient.get("config/webdav").json<WebdavConfig>();
}
export function saveWebdavConfig(config: WebdavConfig) {
return kyClient
.put("config/webdav", {
json: config,
})
.json();
}

View File

@ -1,7 +1,6 @@
import ky from "ky";
const kyClient = ky.create({
prefixUrl: "http://localhost:3000/v2/api",
prefixUrl: `${import.meta.env.VITE_API_URL}v2/api`,
});
export default kyClient;

View File

@ -0,0 +1,8 @@
import { ref } from "vue";
import { defineStore } from "pinia";
export const useDialogStatusStore = defineStore("dialogStatus", () => {
const webdav = ref(false);
const backup = ref(false);
return { webdav, backup };
});

View File

@ -0,0 +1,16 @@
export enum WebdavEndpointType {
NEXTCLOUD = "NEXTCLOUD",
CUSTOM = "CUSTOM",
}
export interface WebdavConfig {
url: string;
username: string;
password: string;
backupDir: string;
allowSelfSignedCerts: boolean;
webdavEndpoint: {
type: WebdavEndpointType;
customEndpoint?: string;
};
}

View File

@ -2,7 +2,8 @@
"extends": [
"config:base",
":prHourlyLimitNone",
":prConcurrentLimitNone"
":prConcurrentLimitNone",
"group:allNonMajor"
],
"commitMessagePrefix": ":arrow_up:",
"regexManagers": [
@ -15,5 +16,8 @@
"datasourceTemplate": "docker",
"versioningTemplate": "semver"
}
]
],
"vulnerabilityAlerts": {
"enabled": true
}
}