init commit

This commit is contained in:
Martin Drechsel
2025-04-02 18:54:55 +02:00
commit 2c5ec6b281
126 changed files with 21323 additions and 0 deletions

View File

@ -0,0 +1,165 @@
<template>
<UiDialog
:title="t('title')"
v-model:open="open"
>
<template #trigger="{ id }">
<button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap"
@click="open = true"
>
<Icon name="mdi:plus" />
{{ t('database.create') }}
</button>
</template>
<form
class="flex flex-col gap-4"
@submit="onCreateAsync"
>
<!-- @keyup.enter="onCreateAsync" -->
<UiInput
:check-input="check"
:label="t('database.label')"
:placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name"
autofocus
prepend-icon="mdi:safe"
v-model="database.name"
/>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
</form>
<template #buttons>
<UiButton
class="btn-error"
@click="onClose"
>
{{ t('abort') }}
</UiButton>
<UiButton
class="btn-primary"
@click="onCreateAsync"
>
{{ t('create') }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
import { save } from '@tauri-apps/plugin-dialog';
import { useVaultStore } from '~/stores/vault';
import { vaultDatabaseSchema } from './schema';
const check = ref(false);
const open = ref();
const { t } = useI18n();
const database = reactive<{
name: string;
password: string;
path: string | null;
type: 'password' | 'text';
}>({
name: '',
password: '',
path: '',
type: 'password',
});
const initDatabase = () => {
database.name = t('database.name');
database.password = '';
database.path = '';
database.type = 'password';
};
initDatabase();
const { add } = useSnackbar();
const { createAsync } = useVaultStore();
//const { show } = storeToRefs(useSidebarStore());
const onCreateAsync = async () => {
check.value = true;
const nameCheck = vaultDatabaseSchema.name.safeParse(database.name);
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password
);
console.log(
'checks',
database.name,
nameCheck,
database.password,
passwordCheck
);
if (!nameCheck.success || !passwordCheck.success) return;
open.value = false;
try {
database.path = await save({ defaultPath: `${database.name}.db` });
console.log('data', database);
if (database.path && database.password) {
const vaultId = await createAsync({
path: database.path,
password: database.password,
});
//show.value = true;
await navigateTo(
useLocaleRoute()({ name: 'vault', params: { vaultId } })
);
}
} catch (error) {
console.error(error);
add({ type: 'error', text: JSON.stringify(error) });
}
};
const onClose = () => {
open.value = false;
initDatabase();
};
</script>
<i18n lang="json">
{
"de": {
"database": {
"label": "Datenbankname",
"placeholder": "Passwörter",
"create": "Neue Vault anlegen",
"name": "Passwörter"
},
"title": "Neue Datenbank anlegen",
"create": "Erstellen",
"abort": "Abbrechen",
"description": "Haex Vault für deine geheimsten Geheimnisse"
},
"en": {
"database": {
"label": "Databasename",
"placeholder": "Databasename",
"create": "Create new Vault",
"name": "Passwords"
},
"title": "Create New Database",
"create": "Create",
"abort": "Abort",
"description": "Haex Vault for your most secret secrets"
}
}
</i18n>

View File

@ -0,0 +1,179 @@
<template>
<UiDialog v-model:open="isOpen">
<!-- @close="initDatabase" -->
<template #trigger>
<button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1"
@click="onLoadDatabase"
>
<Icon name="mdi:folder-open-outline" />
{{ t('database.open') }}
</button>
</template>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
@keyup.enter="onOpenDatabase"
autofocus
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
<template #buttons>
<UiButton
class="btn-error"
@click="onClose"
>
{{ t('abort') }}
</UiButton>
<UiButton
type="submit"
class="btn-primary"
@click="onOpenDatabase"
>
{{ t('open') }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
import { open } from '@tauri-apps/plugin-dialog';
import { vaultDatabaseSchema } from './schema';
const { t } = useI18n();
const isOpen = defineModel('isOpen', { type: Boolean });
const props = defineProps({
path: String,
});
const check = ref(false);
const database = reactive<{
name: string;
password: string;
path: string | null;
type: 'password' | 'text';
}>({
name: '',
password: '',
path: '',
type: 'password',
});
const initDatabase = () => {
database.name = '';
database.password = '';
database.path = '';
database.type = 'password';
};
initDatabase();
const { add } = useSnackbar();
const handleError = (error: unknown) => {
isOpen.value = false;
add({ type: 'error', text: JSON.stringify(error) });
//console.error(error);
};
const { openAsync } = useVaultStore();
//const { show } = storeToRefs(useSidebarStore());
const onLoadDatabase = async () => {
try {
database.path = await open({
multiple: false,
directory: false,
filters: [
{
name: 'HaexVault',
extensions: ['db'],
},
],
});
if (!database.path) return;
isOpen.value = true;
} catch (error) {
handleError(error);
}
};
const localePath = useLocalePath();
const onOpenDatabase = async () => {
try {
check.value = true;
const path = database.path || props.path;
const pathCheck = vaultDatabaseSchema.path.safeParse(path);
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password
);
if (!pathCheck.success || !passwordCheck.success || !path) {
add({ type: 'error', text: 'params falsch' });
return;
}
//console.log('try to open', path);
const vaultId = await openAsync({
path,
password: database.password,
});
if (!vaultId) {
add({ type: 'error', text: 'Vault konnte nicht geöffnet werden' });
return;
}
onClose();
/* await navigateTo(
localePath({
name: 'vaultGroup',
params: {
vaultId,
},
query: {
showSidebar: 'true',
},
})
); */
} catch (error) {
console.log(error);
handleError(error);
}
};
const onClose = () => {
initDatabase();
isOpen.value = false;
};
</script>
<i18n lang="json">
{
"de": {
"open": "Öffnen",
"abort": "Abbrechen",
"database": {
"open": "Vault öffnen"
}
},
"en": {
"open": "Open",
"abort": "Abort",
"database": {
"open": "Open Vault"
}
}
}
</i18n>

View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const vaultDatabaseSchema = {
password: z.string().min(6).max(255),
name: z.string().min(1).max(255),
path: z.string().min(4).endsWith('.db'),
};

View File

@ -0,0 +1,250 @@
<template>
<VaultCard
@close="onClose"
@submit="onSubmit"
>
<template #header>
<div class="flex flex-wrap items-center justify-between w-full px-2 py-3">
<div class="w-full flex gap-2 justify-between items-center">
<div class="flex items-center gap-2">
<button
class="btn btn-square btn-primary btn-outline"
@click="onBack"
>
<Icon
name="mdi:chevron-left"
size="32"
/>
</button>
<button
class="btn btn-square btn-error btn-outline"
@click="onBack"
>
<Icon
name="mdi:trash-can-outline"
size="28"
/>
</button>
</div>
<slot name="buttons">
<div
v-if="read_only"
class="h-full"
>
<button
class="btn btn-square btn-primary btn-outline"
@click="read_only = false"
>
<Icon
name="mdi:pencil-outline"
size="24"
/>
</button>
</div>
<div
v-else
class="gap-2 h-full hidden md:flex"
>
<button
class="btn btn-square btn-error btn-outline"
@click="onClose"
>
<Icon name="mdi:close" />
<span class="hidden"> {{ t('abort') }} </span>
</button>
<button
class="btn btn-square btn-success btn-outline"
@click="onSubmit"
>
<Icon name="mdi:check" />
<span class="hidden"> {{ t('create') }} </span>
</button>
</div>
</slot>
</div>
<div
class="flex flex-col items-center w-full min-h-14 gap-2 py-1"
:class="{ '-ml-6': !show }"
:style="{ color }"
>
<Icon
v-if="icon"
:name="icon"
size="28"
/>
<h5
v-show="read_only"
class="overflow-hidden whitespace-nowrap"
>
{{ title }}
</h5>
</div>
</div>
</template>
<div class="h-full">
<slot />
<div
v-show="!read_only"
class="fixed bottom-2 left-0 w-full flex items-center justify-between px-4 md:hidden"
>
<div
class="transition-all duration-500"
:class="{ 'pl-96': show }"
>
<button
class="btn btn-square btn-error btn-outline"
@click="onClose"
>
<Icon name="mdi:close" />
<span class="hidden"> {{ t('abort') }} </span>
</button>
</div>
<div>
<button
class="btn btn-square btn-success"
@click="onSubmit"
>
<Icon name="mdi:check" />
<span class="hidden"> {{ t('create') }} </span>
</button>
</div>
<div></div>
</div>
<!-- <UiButtonAction
class=""
icon="mdi:content-save-outline"
><Icon name="mdi:content-save-outline" />
</UiButtonAction> -->
</div>
</VaultCard>
<VaultModalSaveChanges
v-model="showConfirmation"
@reject="onReject"
@submit="onSubmit"
/>
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
const { t } = useI18n();
const { show } = storeToRefs(useSidebarStore());
const read_only = defineModel<boolean>('read_only', { default: false });
const props = defineProps({
color: String,
hasChanges: Boolean,
icon: String,
title: String,
});
const emit = defineEmits<{
back: [void];
close: [void];
reject: [to?: RouteLocationNormalizedLoadedGeneric];
submit: [to?: RouteLocationNormalizedLoadedGeneric];
}>();
const showConfirmation = ref(false);
const to = ref<RouteLocationNormalizedLoadedGeneric>();
const isApprovedForLeave = ref(false);
const wantToGoBack = ref(false);
const onSubmit = () => {
showConfirmation.value = false;
isApprovedForLeave.value = true;
if (wantToGoBack.value) {
wantToGoBack.value = false;
read_only.value = true;
emit('submit');
} else {
emit('submit', to.value);
}
};
const onReject = () => {
showConfirmation.value = false;
isApprovedForLeave.value = true;
read_only.value = true;
if (wantToGoBack.value) {
wantToGoBack.value = false;
emit('back');
} else emit('reject', to.value);
};
const onBack = () => {
if (props.hasChanges) {
wantToGoBack.value = true;
showConfirmation.value = true;
} else {
emit('back');
}
};
const onClose = () => {
if (props.hasChanges) {
showConfirmation.value = true;
} else {
emit('close'); //read_only.value = true;
}
};
const onDelete = () => {};
onBeforeRouteLeave((_to, _from, next) => {
//console.log('check before leave', _to, _from);
to.value = _to;
if (isApprovedForLeave.value) {
isApprovedForLeave.value = false;
next();
} else if (props.hasChanges) {
showConfirmation.value = true;
} else {
next();
}
});
</script>
<i18n lang="json">
{
"de": {
"create": "Anlegen",
"abort": "Abbrechen",
"entry": {
"title": "Titel",
"username": "Nutzername",
"password": "Passwort",
"url": "Url"
},
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "Verlauf"
}
},
"en": {
"create": "Create",
"abort": "Abort",
"entry": {
"title": "Title",
"username": "Username",
"password": "Password",
"url": "Url"
},
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "History"
}
}
}
</i18n>

View File

@ -0,0 +1,42 @@
<template>
<div
class="bg-base-100 w-full mx-auto shadow h-full overflow-hidden pt-[7.5rem]"
>
<div
class="fixed top-0 right-0 z-10 transition-all duration-700 w-full font-semibold text-lg h-[7.5rem]"
:class="{ 'pl-96': show }"
>
<div
class="justify-center items-center flex flex-wrap border-b rounded-b border-secondary h-full"
:class="{ 'pl-12': !show }"
>
<slot name="header" />
</div>
</div>
<div class="h-full overflow-scroll bg-base-200">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
const { show } = storeToRefs(useSidebarStore());
const emit = defineEmits(['close', 'submit']);
const { escape, enter } = useMagicKeys();
watchEffect(async () => {
if (escape.value) {
await nextTick();
emit('close');
}
});
watchEffect(async () => {
if (enter.value) {
await nextTick();
emit('submit');
}
});
</script>

View File

@ -0,0 +1,32 @@
<template>
<UiDialog :title="t('title')">
<form class="flex flex-col">
<UiInput v-model="vaultItem.title" />
<UiInput v-model="vaultItem.username" />
<UiInput v-model="vaultItem.password" />
<UiInput v-model="vaultItem.note" />
</form>
</UiDialog>
</template>
<script setup lang="ts">
const { t } = useI18n();
const vaultItem = reactive({
title: '',
username: '',
password: '',
note: '',
});
</script>
<i18n lang="json">
{
"de": {
"title": "Eintrag erstellen"
},
"en": {
"title": "Create Entry"
}
}
</i18n>

View File

@ -0,0 +1,117 @@
<template>
<VaultCardEdit
v-if="vaultGroup"
:color="vaultGroup.color ?? 'text-base-content'"
:has-changes="hasChanges"
:icon="vaultGroup.icon ?? 'mdi:folder-outline'"
:title="vaultGroup.name ?? ''"
@back="$emit('back')"
@close="$emit('close')"
@reject="(to) => $emit('reject', to)"
@submit="(to) => $emit('submit', to)"
v-model:read_only="read_only"
>
<div class="flex flex-col gap-4 w-full p-4">
<UiInput
v-show="!read_only"
:label="t('vaultGroup.name')"
:placeholder="t('vaultGroup.name')"
:rules="vaultGroupSchema.name"
:with-copy-button="read_only"
:read_only
autofocus
v-model.trim="vaultGroup.name"
/>
<UiInput
v-show="!read_only || vaultGroup.description?.length"
:read_only
:label="t('vaultGroup.description')"
:placeholder="t('vaultGroup.description')"
:rules="vaultGroupSchema.description"
:with-copy-button="read_only"
v-model.trim="vaultGroup.description"
/>
<UiColorPicker
:read_only
:label="t('vaultGroup.color')"
:placeholder="t('vaultGroup.color')"
v-model="vaultGroup.color"
/>
<UiIconPicker
:read_only
:label="t('vaultGroup.icon')"
:placeholder="t('vaultGroup.icon')"
v-model="vaultGroup.icon"
/>
</div>
</VaultCardEdit>
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
import {
vaultGroupSchema,
type SelectVaultGroup,
} from '~/database/schemas/vault';
const { t } = useI18n();
const showConfirmation = ref(false);
const vaultGroup = defineModel<SelectVaultGroup>({ required: true });
const read_only = defineModel<boolean>('read_only');
const props = defineProps({
originally: Object as PropType<SelectVaultGroup>,
});
defineEmits<{
submit: [to?: RouteLocationNormalizedLoadedGeneric];
close: [void];
back: [void];
reject: [to?: RouteLocationNormalizedLoadedGeneric];
}>();
const hasChanges = computed(() => {
console.log('group has changes', props.originally, vaultGroup.value);
if (!props.originally) {
if (
vaultGroup.value.color?.length ||
vaultGroup.value.description?.length ||
vaultGroup.value.icon?.length ||
vaultGroup.value.name?.length
) {
return true;
} else {
return false;
}
}
return JSON.stringify(props.originally) !== JSON.stringify(vaultGroup.value);
});
/* const onClose = () => {
if (props.originally) vaultGroup.value = { ...props.originally };
emit('close');
}; */
</script>
<i18n lang="json">
{
"de": {
"vaultGroup": {
"name": "Name",
"description": "Beschreibung",
"icon": "Icon",
"color": "Farbe"
}
},
"en": {
"vaultGroup": {
"name": "Name",
"description": "Description",
"icon": "Icon",
"color": "Color"
}
}
}
</i18n>

View File

@ -0,0 +1,42 @@
<template>
<UiListButton
v-if="entry"
:key="entry.id"
@click="navigateToEntryAsync(entry.id)"
class="text-base-content"
>
<div class="flex items-center gap-3">
<div class="w-8">
<Icon
v-if="entry.icon || groupIcon"
:name="entry.icon || groupIcon!"
/>
</div>
<div class="flex flex-col items-start">
<div v-if="!entry.title && !entry.username && !entry.url">
{{ entry.id }}
</div>
<div class="font-semibold">
{{ entry.title }}
</div>
<span class="text-sm">
{{ entry.username }}
</span>
<span class="text-sm">
{{ entry.url }}
</span>
</div>
</div>
</UiListButton>
</template>
<script setup lang="ts">
import type { SelectVaultEntry } from '~/database/schemas/vault';
defineProps({
entry: Object as PropType<SelectVaultEntry>,
groupIcon: [String, null],
});
const { navigateToEntryAsync } = useVaultEntryStore();
</script>

View File

@ -0,0 +1,7 @@
<template>
<UiList>
<slot />
</UiList>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,78 @@
<template>
<UiDialog
v-model:open="showConfirmation"
:title="t('dialog.title')"
>
{{ t('dialog.question') }}
<template #buttons>
<UiButton
class="btn-outline btn-error focus:bg-primary"
tabindex="10"
@click="$emit('reject')"
>
<Icon name="mdi:cancel" />
<span class="hidden sm:block">
{{ t('dialog.reject') }}
</span>
</UiButton>
<UiButton
class="btn-outline focus:bg-primary"
tabindex="11"
ref="abortButtonRef"
@click="showConfirmation = false"
>
<Icon name="mdi:close" />
<span class="hidden sm:block">
{{ t('dialog.abort') }}
</span>
</UiButton>
<UiButton
class="btn-outline btn-success"
tabindex="12"
@click="$emit('submit')"
>
<Icon name="mdi:check" />
<span class="hidden sm:block">
{{ t('dialog.save') }}
</span>
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
const showConfirmation = defineModel<boolean>();
const abortButtonRef = useTemplateRef('abortButtonRef');
const { t } = useI18n();
const { currentScreenSize } = storeToRefs(useUiStore());
onUpdated(() => {
abortButtonRef.value?.$el.focus();
});
defineEmits(['submit', 'reject']);
</script>
<i18n lang="json">
{
"de": {
"dialog": {
"title": "Ungespeicherte Änderungen",
"question": "Möchten Sie die Änderungen speichern?",
"reject": "Verwerfen",
"abort": "Abbrechen",
"save": "Speichern"
}
},
"en": {
"dialog": {
"title": "Unsaved Changes",
"question": "Would you like to save the changes?",
"reject": "Reject",
"abort": "Abort",
"save": "Save"
}
}
}
</i18n>