Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored Onboarding #223

Merged
merged 21 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
94102c1
pre-fill browser name
overheadhunter Aug 21, 2023
4136a76
renamed `SetupUserKeys.vue` → `InitialSetup.vue`
overheadhunter Aug 21, 2023
e5812bc
refactor onboarding
overheadhunter Aug 21, 2023
36fcf97
`BrowserKeys.load` can now return `BrowserKeys | undefined`
overheadhunter Aug 23, 2023
b980a52
handle "wrong account key"
overheadhunter Aug 23, 2023
de412b6
Merge branch 'develop' into feature/onboarding
overheadhunter Aug 30, 2023
d5756a6
fine-tuned labels
overheadhunter Aug 30, 2023
dfc957c
add logout link to onboarding wizard
overheadhunter Aug 30, 2023
1c9449c
added simple navigation bar to initial setup, improved browser name e…
tobihagemann Sep 4, 2023
c6c9a82
fixed some edge cases (invalid states) when editing browser name
tobihagemann Sep 4, 2023
d7f6da8
localization
tobihagemann Sep 5, 2023
e3cbc0f
restructered layout
tobihagemann Sep 5, 2023
8e4d172
renamed localization keys
tobihagemann Sep 5, 2023
e2f5b34
renamed other instances of setup code to account key
tobihagemann Sep 5, 2023
20f72bc
Merge branch 'develop' into feature/onboarding
tobihagemann Sep 8, 2023
dd90976
removed unique device name per owner constraint
tobihagemann Sep 8, 2023
bf29d70
Merge branch 'develop' into feature/onboarding
overheadhunter Oct 25, 2023
7959d67
added SetupAlreadyCompleted state in InitialSetup
tobihagemann Oct 30, 2023
b19e2d2
Recover user key when the device is absend in Hub
SailReal Oct 30, 2023
6849858
Wrap long one word device name in initial setup
SailReal Oct 30, 2023
3f5bb7a
Exclude looking for nullable device keys while recovering user key
SailReal Oct 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</div>
</div>

<div v-else-if="state == State.CreateUserKey">
<div v-else-if="state == State.CreateUserKey" class="text-sm text-gray-600">
<form @submit.prevent="createUserKey()">
<div class="flex justify-center">
<div class="bg-white px-4 py-5 shadow sm:rounded-lg sm:p-6 text-center sm:w-full sm:max-w-lg">
Expand All @@ -18,63 +18,25 @@
</div>
<div class="mt-3 sm:mt-5">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ t('setupUserKey.createUserKey.title') }}
{{ t('initialSetup.title') }}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
{{ t('setupUserKey.createUserKey.description') }}
<p>
{{ t('initialSetup.description') }}
</p>
</div>
<div class="mt-5 sm:mt-6 text-left">
<label for="deviceName" class="block text-sm font-medium text-gray-700">{{ t('setupUserKey.deviceName') }}</label>
<input id="deviceName" v-model="deviceName" v-focus type="text" name="deviceName" class="mt-1 focus:ring-primary focus:border-primary block w-full shadow-sm sm:text-sm border-gray-300 rounded-md disabled:bg-gray-200" aria-describedby="deviceNameDescription" />
<p id="deviceNameDescription" class="mt-2 text-sm text-gray-500">{{ t('setupUserKey.deviceName.description') }}</p>
</div>
<div class="mt-5 sm:mt-6">
<button type="submit" :disabled="processing" class="inline-flex w-full justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-primary-d1 focus:outline-none focus:ring-2 focus:primary focus:ring-offset-2 sm:text-sm disabled:opacity-50 disabled:hover:bg-primary disabled:cursor-not-allowed">
{{ t('setupUserKey.createUserKey.submit') }}
</button>
<div v-if="onCreateError != null">
<p class="text-sm text-red-900 mt-2">{{ t('common.unexpectedError', [onCreateError.message]) }}</p>
</div>
</div>
</div>
</div>
</div>
</form>
</div>

<div v-else-if="state == State.SaveSetupCode">
<form @submit.prevent="$router.push('/app/vaults')">
<div class="flex justify-center">
<div class="bg-white px-4 py-5 shadow sm:rounded-lg sm:p-6 text-center sm:w-full sm:max-w-lg">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-emerald-100">
<KeyIcon class="h-6 w-6 text-emerald-600" aria-hidden="true" />
</div>
<div class="mt-3 sm:mt-5">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ t('setupUserKey.saveSetupCode.title') }}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
{{ t('setupUserKey.saveSetupCode.description') }}
</p>
</div>
<div class="relative mt-5 sm:mt-6">
<div class="relative mt-5 sm:mt-6 text-left">
<label for="setupCode" class="block font-medium">{{ t('initialSetup.setupCode') }}</label>
<div class="overflow-hidden rounded-lg border border-gray-300 shadow-sm focus-within:border-primary focus-within:ring-1 focus-within:ring-primary">
<label for="setupCode" class="sr-only">{{ t('setupUserKey.setupCode') }}</label>
<textarea id="setupCode" v-model="setupCode" rows="1" name="setupCode" class="block w-full resize-none border-0 py-3 font-mono text-lg text-center focus:ring-0" readonly />

<!-- Spacer element to match the height of the toolbar -->
<div class="py-2" aria-hidden="true">
<div class="h-9" />
</div>
</div>

<div class="absolute inset-x-0 bottom-0">
<div class="flex flex-nowrap justify-end space-x-2 py-2 px-2 sm:px-3">
<div class="flex-shrink-0">
<button type="button" class="relative inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-2 px-2 text-sm font-medium text-gray-500 hover:bg-gray-100 sm:px-3" @click="copySetupCode()">
<button type="button" class="relative inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-2 px-2 font-medium hover:bg-gray-100 sm:px-3" @click="copySetupCode()">
<ClipboardIcon class="h-5 w-5 flex-shrink-0 text-gray-300 sm:-ml-1" aria-hidden="true" />
<span v-if="!copiedSetupCode" class="hidden truncate sm:ml-2 sm:block text-gray-900">{{ t('common.copy') }}</span>
<span v-else class="hidden truncate sm:ml-2 sm:block text-gray-900">{{ t('common.copied') }}</span>
Expand All @@ -83,13 +45,34 @@
</div>
</div>
</div>
<div class="mt-2">
<p class="text-sm text-gray-500">{{ t('setupUserKey.saveSetupCode.setupCodeHint') }}</p>
<div class="mt-5 flex items-start">
<div class="mx-4">
<ExclamationTriangleIcon class="h-8 w-8" aria-hidden="true" />
</div>
<p class="text-left">
{{ t('initialSetup.dontLooseYourSetupCode') }}
</p>
</div>

<p class="mt-5 text-left">
<span>{{ t('initialSetup.devicesName.0') }}</span>
<span ref="deviceNameField" contenteditable class="focus:ring-primary select-all font-mono" @click="editBrowserName()" @keydown.enter.prevent="deviceNameField?.blur()" @blur="changeBrowserName" v-text="deviceName" />
<PencilIcon class="inline-block h-4 w-4 ml-1 -mt-1 cursor-pointer" aria-hidden="true" @click="editBrowserName()" />
<span>{{ t('initialSetup.devicesName.2') }}</span>
</p>

<div class="text-center mt-5 sm:mt-6">
<input id="confirmSetupKey" v-model="confirmSetupKey" name="confirmSetupKey" type="checkbox" class="h-4 w-4 mx-2 rounded border-gray-300 text-primary focus:ring-primary" required>
<label for="confirmSetupKey" class="font-medium cursor-pointer">{{ t('initialSetup.confirmSetupKey') }}</label>
</div>

<div class="mt-5 sm:mt-6">
<button type="submit" class="inline-flex w-full justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-primary-d1 focus:outline-none focus:ring-2 focus:primary focus:ring-offset-2 sm:text-sm">
{{ t('setupUserKey.saveSetupCode.submit') }}
<button type="submit" :disabled="!confirmSetupKey || processing" class="inline-flex w-full justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-primary-d1 focus:outline-none focus:ring-2 focus:primary focus:ring-offset-2 disabled:opacity-50 disabled:hover:bg-primary disabled:cursor-not-allowed">
{{ t('initialSetup.submit') }}
</button>
<div v-if="onCreateError != null">
<p class="text-red-900 mt-2">{{ t('common.unexpectedError', [onCreateError.message]) }}</p>
</div>
</div>
</div>
</div>
Expand All @@ -106,26 +89,26 @@
</div>
<div class="mt-3 sm:mt-5">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ t('setupUserKey.enterSetupCode.title') }}
{{ t('registerDevice.title') }}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
{{ t('setupUserKey.enterSetupCode.description') }}
{{ t('registerDevice.description') }}
</p>
</div>
<div class="mt-5 sm:mt-6 text-left">
<label for="setupCode" class="block text-sm font-medium text-gray-700">{{ t('setupUserKey.setupCode') }}</label>
<label for="setupCode" class="block text-sm font-medium text-gray-700">{{ t('registerDevice.setupCode') }}</label>
<input id="setupCode" v-model="setupCode" v-focus type="text" name="setupCode" class="mt-1 focus:ring-primary focus:border-primary block w-full shadow-sm sm:text-sm border-gray-300 rounded-md disabled:bg-gray-200" aria-describedby="setupCodeDescription" />
<p id="setupCodeDescription" class="mt-2 text-sm text-gray-500">{{ t('setupUserKey.setupCode.description') }}</p>
<p id="setupCodeDescription" class="mt-2 text-sm text-gray-500">{{ t('registerDevice.setupCode.description') }}</p>
</div>
<div class="mt-5 sm:mt-6 text-left">
<label for="deviceName" class="block text-sm font-medium text-gray-700">{{ t('setupUserKey.deviceName') }}</label>
<label for="deviceName" class="block text-sm font-medium text-gray-700">{{ t('registerDevice.deviceName') }}</label>
<input id="deviceName" v-model="deviceName" type="text" name="deviceName" class="mt-1 focus:ring-primary focus:border-primary block w-full shadow-sm sm:text-sm border-gray-300 rounded-md disabled:bg-gray-200" aria-describedby="deviceNameDescription" />
<p id="deviceNameDescription" class="mt-2 text-sm text-gray-500">{{ t('setupUserKey.deviceName.description') }}</p>
<p id="deviceNameDescription" class="mt-2 text-sm text-gray-500">{{ t('registerDevice.deviceName.description') }}</p>
</div>
<div class="mt-5 sm:mt-6">
<button type="submit" :disabled="processing" class="inline-flex w-full justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-primary-d1 focus:outline-none focus:ring-2 focus:primary focus:ring-offset-2 sm:text-sm disabled:opacity-50 disabled:hover:bg-primary disabled:cursor-not-allowed">
{{ t('setupUserKey.enterSetupCode.submit') }}
{{ t('registerDevice.submit') }}
</button>
<div v-if="onRecoverError != null">
<p class="text-sm text-red-900 mt-2">{{ t('common.unexpectedError', [onRecoverError.message]) }}</p>
Expand All @@ -134,10 +117,10 @@
</div>
</div>
<p class="mt-10 text-center text-sm text-gray-500">
{{ t('setupUserKey.lostSetupCode.title') }}
{{ t('registerDevice.lostSetupCode.title') }}
{{ ' ' }}
<a role="button" tabindex="0" class="font-medium leading-6 text-red-600 hover:text-red-900" @click="showResetUserAccountDialog()">
{{ t('setupUserKey.lostSetupCode.resetUserAccount') }}
{{ t('registerDevice.lostSetupCode.resetUserAccount') }}
</a>
</p>
</div>
Expand All @@ -150,7 +133,7 @@

<script setup lang="ts">
import { ClipboardIcon } from '@heroicons/vue/20/solid';
import { KeyIcon } from '@heroicons/vue/24/outline';
import { ExclamationTriangleIcon, PencilIcon } from '@heroicons/vue/24/outline';
import { nextTick, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import backend, { UserDto } from '../common/backend';
Expand All @@ -164,7 +147,6 @@ import ResetUserAccountDialog from './ResetUserAccountDialog.vue';
enum State {
Preparing,
CreateUserKey,
SaveSetupCode,
EnterSetupCode
}

Expand All @@ -184,23 +166,63 @@ const state = ref(State.Preparing);
const processing = ref(false);

const user = ref<UserDto>();
const setupCode = ref('');
const deviceName = ref('');
const setupCode = ref<string>('');
const deviceName = ref(guessBrowserName());
const deviceNameField = ref<HTMLSpanElement>();
const copiedSetupCode = ref(false);
const debouncedCopyFinish = debounce(() => copiedSetupCode.value = false, 2000);
const confirmSetupKey = ref(false);
const resettingUserAccount = ref(false);
const resetUserAccountDialog = ref<typeof ResetUserAccountDialog>();

onMounted(fetchData);

function guessBrowserName(): string {
var match = navigator.userAgent.toLowerCase().match(/(android|iphone|opr|edge|chrome|safari|firefox)/) || [''];
switch (match[0]) {
case 'android': return 'Android';
case 'iphone': return 'iPhone';
case 'opr': return 'Opera';
case 'edge': return 'Edge';
case 'chrome': return 'Chrome';
case 'safari': return 'Safari';
case 'firefox': return 'Firefox';
default: return 'Browser';
}
}

function editBrowserName() {
const span = deviceNameField.value!;
span.focus();
const range = document.createRange();
range.selectNodeContents(span);
const sel = window.getSelection() as Selection;
sel.removeAllRanges();
sel.addRange(range);
}

function changeBrowserName(e: Event) {
const span = e.target as HTMLElement;
let val = span.innerText.trim();
if (val == '') {
val = guessBrowserName(); // reset to default TODO: or previous value?
span.innerText = val;
editBrowserName(); // keep editing
} else {
deviceName.value = val;
}
}

async function fetchData() {
onFetchError.value = null;
try {
user.value = await backend.users.me();
const browserKeys = await BrowserKeys.load(user.value.id);
const browserId = await browserKeys.id();
if (!user.value.publicKey) {
setupCode.value = crypto.randomUUID();
state.value = State.CreateUserKey;
} else if (!browserKeys.keyPair) {
} else if (!browserKeys.keyPair || user.value.devices.find(d => d.id == browserId) == null) {
state.value = State.EnterSetupCode;
} else {
throw new Error('Invalid state');
Expand All @@ -221,14 +243,13 @@ async function createUserKey() {
processing.value = true;

const userKeys = await UserKeys.create();
setupCode.value = crypto.randomUUID();
me.publicKey = await userKeys.encodedPublicKey();
me.privateKey = await userKeys.encryptedPrivateKey(setupCode.value);
me.setupCode = await JWEBuilder.ecdhEs(userKeys.keyPair.publicKey).encrypt({ setupCode: setupCode.value });
const browserKeys = await createBrowserKeys(me.id);
await submitBrowserKeys(browserKeys, me, userKeys);

state.value = State.SaveSetupCode;
await router.push('/app/vaults');
} catch (error) {
console.error('Creating user key failed.', error);
onCreateError.value = error instanceof Error ? error : new Error('Unknown reason');
Expand Down
35 changes: 19 additions & 16 deletions frontend/src/i18n/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,22 +167,25 @@
"resetUserAccountDialog.description": "Setze deinen Nutzeraccount nur zurück, wenn du deinen Einrichtungscode verloren hast. Du wirst Zugriff auf alle Tresore verlieren, so dass deren Besitzer dir erneut Zugriff gewähren müssen.",
"resetUserAccountDialog.submit": "Account zurücksetzen",

"setupUserKey.setupCode": "Einrichtungscode",
"setupUserKey.setupCode.description": "Dein Einrichtungscode wird benötigt, um diesen Browser zu autorisieren.",
"setupUserKey.deviceName": "Browser-Name",
"setupUserKey.deviceName.description": "Benenne diesen Browser, um ihn in deinem Profil wiedererkennen zu können.",
"setupUserKey.createUserKey.title": "Profil einrichten",
"setupUserKey.createUserKey.description": "Dies ist der erste Login mit diesem Browser.",
"setupUserKey.createUserKey.submit": "Weiter",
"setupUserKey.saveSetupCode.title": "Speichere deinen Einrichtungscode",
"setupUserKey.saveSetupCode.description": "Du benötigst ihn, um weitere Apps und Geräte zu deinem Profil hinzuzufügen.",
"setupUserKey.saveSetupCode.setupCodeHint": "Deinen Einrichtungscode kannst du jederzeit in deinem Profil abrufen.",
"setupUserKey.saveSetupCode.submit": "Einrichtung abschließen",
"setupUserKey.enterSetupCode.title": "Autorisierung erforderlich",
"setupUserKey.enterSetupCode.description": "Dies ist der erste Login mit diesem Browser.",
"setupUserKey.enterSetupCode.submit": "Einrichtung abschließen",
"setupUserKey.lostSetupCode.title": "Hast du deinen Einrichtungscode verloren?",
"setupUserKey.lostSetupCode.resetUserAccount": "Account zurücksetzen",
"initialSetup.title": "Welcome",
"initialSetup.description": "This is your first login in Cryptomator Hub. Every new Hub user has a unique Account Key. Please make sure to store it safely:",
"initialSetup.setupCode": "Account Key",
"initialSetup.dontLooseYourSetupCode": "Your Account Key is required to log in from other apps or browsers.",
"initialSetup.devicesName.0": "This browser will be added to your account as ",
"initialSetup.devicesName.1": "Browser Name",
"initialSetup.devicesName.2": ". You can manage your devices and change your account key on your profile page at any time.",
"initialSetup.confirmSetupKey": "I understand the importance of my Account Key",
"initialSetup.submit": "Finish Setup",

"registerDevice.title": "Authorization Required",
"registerDevice.description": "This is your first login on this browser.",
"registerDevice.setupCode": "Account Key",
"registerDevice.setupCode.description": "Dein Account Key wird benötigt, um diesen Browser mit deinem Account zu verknüpfen.",
"registerDevice.deviceName": "Browser-Name",
"registerDevice.deviceName.description": "Benenne diesen Browser, um ihn in deinem Profil wiedererkennen zu können.",
"registerDevice.submit": "Finish Setup",
"registerDevice.lostSetupCode.title": "Lost your Account Key?",
"registerDevice.lostSetupCode.resetUserAccount": "Reset my account",

"userProfile.title": "Profil",
"userProfile.actions.manageAccount": "Account verwalten",
Expand Down
Loading
Loading