Skip to content

Commit

Permalink
Merge branch 'develop' into feature/hub-cli
Browse files Browse the repository at this point in the history
  • Loading branch information
SailReal committed Nov 13, 2023
2 parents 6ec7dd1 + afa8c63 commit ddc6b27
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 12 deletions.
122 changes: 122 additions & 0 deletions frontend/src/components/RecoverVaultDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<TransitionRoot as="template" :show="open" @after-leave="$emit('close')">
<Dialog as="div" class="fixed z-10 inset-0 overflow-y-auto" @close="open = false">
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
<DialogOverlay class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</TransitionChild>

<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<form ref="form" novalidate @submit.prevent="validateRecoveryKey()">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 grow text-center sm:mt-0 sm:ml-4 sm:text-left">
<DialogTitle as="h3" class="text-lg leading-6 font-medium text-gray-900">
{{ t('recoverVaultDialog.title') }}
</DialogTitle>
<div class="mt-2">
<p class="text-sm text-gray-500">
{{ t('recoverVaultDialog.description') }}
</p>
<textarea id="recoveryKey" v-model="recoveryKey" rows="6" name="recoveryKey" class="mt-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary sm:text-sm" :class="{ 'invalid:border-red-300 invalid:text-red-900 focus:invalid:ring-red-500 focus:invalid:border-red-500': onVaultRecoverError instanceof FormValidationFailedError }" required />
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse items-baseline">
<button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary text-base font-medium text-white hover:bg-primary-d1 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary sm:ml-3 sm:w-auto sm:text-sm">
{{ t('recoverVaultDialog.submit') }}
</button>
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" @click="open = false">
{{ t('common.cancel') }}
</button>
<div v-if="onVaultRecoverError != null">
<p v-if="onVaultRecoverError instanceof FormValidationFailedError" class="text-sm text-red-900">
{{ t('recoverVaultDialog.error.formValidationFailed') }}
</p>
<p v-else class="text-sm text-red-900">
{{ t('recoverVaultDialog.error.invalidRecoveryKey') }}
</p>
</div>
</div>
</form>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>

<script setup lang="ts">
import { Dialog, DialogOverlay, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
import { base64 } from 'rfc4648';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { UserDto, VaultDto } from '../common/backend';
import { VaultKeys } from '../common/crypto';
class FormValidationFailedError extends Error {
constructor() {
super('The form is invalid.');
}
}
const { t } = useI18n({ useScope: 'global' });
const form = ref<HTMLFormElement>();
const onVaultRecoverError = ref<Error|null>();
const open = ref(false);
const recoveryKey = ref('');
const processingVaultRecovery = ref(false);
const props = defineProps<{
vault: VaultDto,
me: UserDto
}>();
const emit = defineEmits<{
close: []
recovered: []
}>();
defineExpose({
show
});
function show() {
open.value = true;
}
async function validateRecoveryKey() {
if (!form.value?.checkValidity()) {
throw new FormValidationFailedError();
}
await recoverVault();
}
async function recoverVault() {
onVaultRecoverError.value = null;
try {
processingVaultRecovery.value = true;
const vaultKeys = await VaultKeys.recover(recoveryKey.value);
if (props.me.publicKey && vaultKeys) {
const publicKey = base64.parse(props.me.publicKey);
const jwe = await vaultKeys.encryptForUser(publicKey);
await backend.vaults.grantAccess(props.vault.id, props.me.id, jwe);
emit('recovered');
open.value = false;
}
} catch (error) {
console.error('Recovering vault failed.', error);
onVaultRecoverError.value = error instanceof Error ? error : new Error('Unknown reason');
} finally {
processingVaultRecovery.value = false;
}
}
</script>
69 changes: 57 additions & 12 deletions frontend/src/components/VaultDetails.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div v-if="vault == null">
<div v-if="vault == null || vaultRecoveryRequired == null">
<div v-if="onFetchError == null">
{{ t('common.loading') }}
</div>
Expand Down Expand Up @@ -36,7 +36,7 @@
</dl>
</div>

<div v-if="role == 'OWNER'" class="space-y-6">
<div v-if="role == 'OWNER' && !vaultRecoveryRequired" class="space-y-6">
<div>
<h3 class="font-medium text-gray-900">{{ t('vaultDetails.sharedWith.title') }}</h3>
<ul role="list" class="mt-2 border-t border-b border-gray-200 divide-y divide-gray-200">
Expand Down Expand Up @@ -151,15 +151,31 @@
{{ t('vaultDetails.claimOwnership') }}
</button>
</div>

<div v-else-if="!vault.archived && vaultRecoveryRequired" class="mt-2 flex flex-col gap-2">
<div class="flex">
<div class="flex-shrink-0">
<ExclamationTriangleIcon class="mt-1 h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<h3 class="ml-3 font-medium text-gray-900">{{ t('vaultDetails.recoverVault.title') }}</h3>
</div>
<p class="text-sm text-gray-500">{{ t('vaultDetails.recoverVault.description') }}</p>

<button type="button" class="bg-red-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" @click="showRecoverVaultDialog()">
{{ t('vaultDetails.recoverVault') }}
</button>
</div>

</div>

<ClaimVaultOwnershipDialog v-if="claimingVaultOwnership && vault != null" ref="claimVaultOwnershipDialog" :vault="vault" @action="provedOwnership" @close="claimingVaultOwnership = false" />
<GrantPermissionDialog v-if="grantingPermission && vault != null && vaultKeys != null" ref="grantPermissionDialog" :vault="vault" :users="usersRequiringAccessGrant" :vault-keys="vaultKeys" @close="grantingPermission = false" @permission-granted="permissionGranted()" />
<EditVaultMetadataDialog v-if="editingVaultMetadata && vault != null && vaultKeys != null" ref="editVaultMetadataDialog" :vault="vault" @close="editingVaultMetadata = false" @updated="v => refreshVault(v)" />
<DownloadVaultTemplateDialog v-if="downloadingVaultTemplate && vault != null && vaultKeys != null" ref="downloadVaultTemplateDialog" :vault="vault" :vault-keys="vaultKeys" @close="downloadingVaultTemplate = false" />
<RecoveryKeyDialog v-if="showingRecoveryKey && vault != null && vaultKeys != null" ref="recoveryKeyDialog" :vault="vault" :vault-keys="vaultKeys" @close="showingRecoveryKey = false" />
<ArchiveVaultDialog v-if="archivingVault && vault != null" ref="archiveVaultDialog" :vault="vault" @close="archivingVault = false" @archived="v => refreshVault(v)" />
<GrantPermissionDialog v-if="grantingPermission && vault != null && vaultKeys != null && !vaultRecoveryRequired" ref="grantPermissionDialog" :vault="vault" :users="usersRequiringAccessGrant" :vault-keys="vaultKeys" @close="grantingPermission = false" @permission-granted="permissionGranted()" />
<EditVaultMetadataDialog v-if="editingVaultMetadata && vault != null && vaultKeys != null && !vaultRecoveryRequired" ref="editVaultMetadataDialog" :vault="vault" @close="editingVaultMetadata = false" @updated="v => refreshVault(v)" />
<DownloadVaultTemplateDialog v-if="downloadingVaultTemplate && vault != null && vaultKeys != null && !vaultRecoveryRequired" ref="downloadVaultTemplateDialog" :vault="vault" :vault-keys="vaultKeys" @close="downloadingVaultTemplate = false" />
<RecoveryKeyDialog v-if="showingRecoveryKey && vault != null && vaultKeys != null && !vaultRecoveryRequired" ref="recoveryKeyDialog" :vault="vault" :vault-keys="vaultKeys" @close="showingRecoveryKey = false" />
<ArchiveVaultDialog v-if="archivingVault && vault != null && !vaultRecoveryRequired" ref="archiveVaultDialog" :vault="vault" @close="archivingVault = false" @archived="v => refreshVault(v)" />
<ReactivateVaultDialog v-if="reactivatingVault && vault != null" ref="reactivateVaultDialog" :vault="vault" @close="reactivatingVault = false" @reactivated="v => refreshVault(v)" />
<RecoverVaultDialog v-if="vaultRecoveryRequired && vault != null" ref="recoverVaultDialog" :vault="vault" :me="me!" @close="recoverVault = false" @recovered="reloadView()" />
</template>

<script setup lang="ts">
Expand All @@ -169,7 +185,7 @@ import { PlusSmallIcon } from '@heroicons/vue/24/solid';
import { base64 } from 'rfc4648';
import { computed, nextTick, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { AuthorityDto, ConflictError, MemberDto, NotFoundError, PaymentRequiredError, UserDto, VaultDto, VaultRole } from '../common/backend';
import backend, { AuthorityDto, ConflictError, ForbiddenError, MemberDto, NotFoundError, PaymentRequiredError, UserDto, VaultDto, VaultRole } from '../common/backend';
import { BrowserKeys, UserKeys, VaultKeys } from '../common/crypto';
import { JWT, JWTHeader } from '../common/jwt';
import ArchiveVaultDialog from './ArchiveVaultDialog.vue';
Expand All @@ -179,6 +195,7 @@ import EditVaultMetadataDialog from './EditVaultMetadataDialog.vue';
import FetchError from './FetchError.vue';
import GrantPermissionDialog from './GrantPermissionDialog.vue';
import ReactivateVaultDialog from './ReactivateVaultDialog.vue';
import RecoverVaultDialog from './RecoverVaultDialog.vue';
import RecoveryKeyDialog from './RecoveryKeyDialog.vue';
import SearchInputGroup from './SearchInputGroup.vue';
Expand Down Expand Up @@ -212,6 +229,8 @@ const archivingVault = ref(false);
const archiveVaultDialog = ref<typeof ArchiveVaultDialog>();
const reactivatingVault = ref(false);
const reactivateVaultDialog = ref<typeof ReactivateVaultDialog>();
const recoverVault = ref(false);
const recoverVaultDialog = ref<typeof RecoverVaultDialog>();
const vault = ref<VaultDto>();
const vaultKeys = ref<VaultKeys>();
const members = ref<Map<string, MemberDto>>(new Map());
Expand All @@ -220,6 +239,8 @@ const claimVaultOwnershipDialog = ref<typeof ClaimVaultOwnershipDialog>();
const claimingVaultOwnership = ref(false);
const me = ref<UserDto>();
const vaultRecoveryRequired = ref<boolean | null>(null);
onMounted(fetchData);
async function fetchData() {
Expand All @@ -229,6 +250,8 @@ async function fetchData() {
me.value = await backend.users.me(true);
if (props.role == 'OWNER') {
await fetchOwnerData();
} else {
vaultRecoveryRequired.value = false;
}
} catch (error) {
console.error('Fetching data failed.', error);
Expand All @@ -237,10 +260,21 @@ async function fetchData() {
}
async function fetchOwnerData() {
const vaultKeyJwe = await backend.vaults.accessToken(props.vaultId, true);
vaultKeys.value = await loadVaultKeys(vaultKeyJwe);
(await backend.vaults.getMembers(props.vaultId)).forEach(member => members.value.set(member.id, member));
usersRequiringAccessGrant.value = await backend.vaults.getUsersRequiringAccessGrant(props.vaultId);
try {
const vaultKeyJwe = await backend.vaults.accessToken(props.vaultId, true);
vaultKeys.value = await loadVaultKeys(vaultKeyJwe);
(await backend.vaults.getMembers(props.vaultId)).forEach(member => members.value.set(member.id, member));
usersRequiringAccessGrant.value = await backend.vaults.getUsersRequiringAccessGrant(props.vaultId);
vaultRecoveryRequired.value = false;
} catch(error) {
if (error instanceof ForbiddenError) {
vaultRecoveryRequired.value = true;
} else {
console.error('Retrieving ownership failed.', error);
onFetchError.value = error instanceof Error ? error : new Error('Unknown Error');
}
}
}
async function loadVaultKeys(vaultKeyJwe: string): Promise<VaultKeys> {
Expand Down Expand Up @@ -372,6 +406,11 @@ function showReactivateVaultDialog() {
nextTick(() => reactivateVaultDialog.value?.show());
}
function showRecoverVaultDialog() {
recoverVault.value = true;
nextTick(() => recoverVaultDialog.value?.show());
}
function permissionGranted() {
usersRequiringAccessGrant.value = [];
}
Expand All @@ -381,6 +420,11 @@ function refreshVault(updatedVault: VaultDto) {
emit('vaultUpdated', updatedVault);
}
async function reloadView() {
await fetchOwnerData()
vaultRecoveryRequired.value = false;
}
async function searchAuthority(query: string): Promise<AuthorityDto[]> {
return (await backend.authorities.search(query))
.filter(authority => !members.value.has(authority.id))
Expand Down Expand Up @@ -421,4 +465,5 @@ async function removeMember(memberId: string) {
onUpdateVaultMembershipError.value[memberId] = error instanceof Error ? error : new Error('Unknown Error');
}
}
</script>
9 changes: 9 additions & 0 deletions frontend/src/i18n/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@
"recoveryKeyDialog.description": "Dies ist dein Wiederherstellungsschlüssel für „{0}“. Verwahre ihn gut, er ist im Falle eines Systemausfalls deine einzige Möglichkeit, auf einen Tresor zuzugreifen.",
"recoveryKeyDialog.recoveryKey": "Wiederherstellungsschlüssel für deinen Tresor",

"recoverVaultDialog.title": "Tresor Wiederherstellung",
"recoverVaultDialog.description": "Gib den Wiederherstellungsschlüssel für den Tresor ein, um den Zugriff auf den Tresor wiederherzustellen.",
"recoverVaultDialog.submit": "Tresor wiederherstellen",
"recoverVaultDialog.error.formValidationFailed": "Wiederherstellungsschlüssel darf nicht leer sein.",
"recoverVaultDialog.error.invalidRecoveryKey": "Wiederherstellungsschlüssel ist ungültig.",

"regenerateAccountKeyDialog.confirmRegenerateAccountKey.title": "Neuen Account Key generieren",
"regenerateAccountKeyDialog.confirmRegenerateAccountKey.description": "Wenn du den Verdacht hast, dass dein alter Account Key kompromittiert wurde, kannst du ihn neu generieren. Anschließend können neue Geräte nur mit dem neuen Account Key hinzugefügt werden. Deine bestehenden Geräte bleiben davon unberührt.",
"regenerateAccountKeyDialog.saveAccountKey.title": "Account Key speichern",
Expand Down Expand Up @@ -211,6 +217,9 @@
"vaultDetails.actions.archiveVault": "Tresor archivieren",
"vaultDetails.actions.reactivateVault": "Tresor reaktivieren",
"vaultDetails.error.paymentRequired": "Deine Cryptomator Hub Lizenz hat die Anzahl der verfügbaren Sitze überschritten oder ist abgelaufen. Bitte informiere einen Hub-Administrator, um die Lizenz zu erneuern oder zu erweitern.",
"vaultDetails.recoverVault.title": "Du hast deinen Account zurückgesetzt!",
"vaultDetails.recoverVault.description": "Um wieder Zugriff auf den Tresor zu erhalten, musst du deinen Zugriff mit dem Wiederherstellungsschlüssel wiederherstellen oder ein anderer Eigentümer muss die Zugriffsrechte aktualisieren.",
"vaultDetails.recoverVault": "Tresorzugriff wiederherstellen",

"vaultList.title": "Tresore",
"vaultList.empty.title": "Keine Tresore",
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@
"recoveryKeyDialog.description": "This is your recovery key for \"{0}\". Keep it safe, it is your only chance to regain access to a vault in case of system outage.",
"recoveryKeyDialog.recoveryKey": "Recovery Key of Your Vault",

"recoverVaultDialog.title": "Vault Recovery",
"recoverVaultDialog.description": "Type in the Recover Key of this vault to regain access to it.",
"recoverVaultDialog.submit": "Recover Vault",
"recoverVaultDialog.error.formValidationFailed": "Recover Key must not be empty",
"recoverVaultDialog.error.invalidRecoveryKey": "Invalid Recover Key",

"regenerateAccountKeyDialog.confirmRegenerateAccountKey.title": "Regenerate Account Key",
"regenerateAccountKeyDialog.confirmRegenerateAccountKey.description": "If you suspect that your old Account Key has been compromised, you can regenerate it. You will then only be able to add new devices with the new Account Key. Your existing devices will remain intact.",
"regenerateAccountKeyDialog.saveAccountKey.title": "Save Account Key",
Expand Down Expand Up @@ -212,6 +218,9 @@
"vaultDetails.actions.archiveVault": "Archive Vault",
"vaultDetails.actions.reactivateVault": "Reactivate Vault",
"vaultDetails.error.paymentRequired": "Your Cryptomator Hub license has exceeded the number of available seats or has expired. Please inform a Hub administrator to upgrade or renew the license.",
"vaultDetails.recoverVault.title": "You have Reset Your Account!",
"vaultDetails.recoverVault.description": "To regain access to the vault, you must restore your access using the recovery key or another owner must update the access permissions.",
"vaultDetails.recoverVault": "Recover Vault Access",

"vaultList.title": "Vaults",
"vaultList.empty.title": "No vaults",
Expand Down

0 comments on commit ddc6b27

Please sign in to comment.