Skip to content

Commit

Permalink
Merge Guardian recovery (#67)
Browse files Browse the repository at this point in the history
* feat: add initial guardian recovery views

* feat: add confirm guardian view

* feat: improve guardian view with edge cases

* fix: remove commented code

* feat: improve component imports

* feat: add logout icon in desktop breakpoint

* fix: naming

* fix: pnpm lock

* fix: wrong nav component import

* fix: add missing package to cspell

* feat: add recover account views

* feat: add unknown account page

* feat: improve account init recovery start

* feat: reorganize routes with typed routes

* feat: add recovery process warning when logged in

* feat: add account not ready page

* fix: confirm-guardian page

* feat: add base guardian recovery module

* chore: update contracts submodule

* chore: update contracts submodule

* chore: update contracts submodule

* feat: add sso account validation

* Update packages/auth-server/pages/recovery/guardian/index.vue

Co-authored-by: Lukasz Romanowski <[email protected]>

* feat: add integration with /recovery/guardian/find-account

* feat: integrate contracts in guardians settings page

* feat: set proper path to confirm-guardian page

* feat: add guardian confirmation integration

* feat: add ui improvements

* feat: address pr comments

* feat: update contracts submodule

* chore: update contract submodule

* feat: add integration to confirm recovery view

* feat: add integration with cancel recovery (#53)

* feat: add integration with cancel recovery

* feat: update contracts submodule and abi

* feat: add verify recovery view on the main page

* chore: update contracts

* feat: add missing nuxt config

* feat: improve confirm guardian flow

* chore: update contracts package

* feat: execute pending recovery on login (#57)

* feat: execute pending recovery on login

* feat: move recovery client to sdk

* feat: add account-not-ready view

* chore: fix pnpm lock

---------

Co-authored-by: aon <[email protected]>

---------

Co-authored-by: aon <[email protected]>
Co-authored-by: Lukasz Romanowski <[email protected]>
  • Loading branch information
3 people authored Feb 13, 2025
1 parent ba0fcdf commit 36dc3c1
Show file tree
Hide file tree
Showing 66 changed files with 7,082 additions and 632 deletions.
1 change: 1 addition & 0 deletions cspell-config/cspell-packages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ wagmi
cbor
levischuck
ofetch
reown
12 changes: 12 additions & 0 deletions packages/auth-server/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@
</template>

<script lang="ts" setup>
import { createAppKit } from "@reown/appkit/vue";
const { defaultChain } = useClientStore();
const { metadata, projectId, wagmiAdapter } = useAppKit();
// BigInt polyfill
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};
createAppKit({
adapters: [wagmiAdapter],
networks: [defaultChain],
projectId,
metadata,
});
</script>
68 changes: 68 additions & 0 deletions packages/auth-server/components/account-recovery/AccountSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<template>
<div class="relative">
<select
:id="id"
v-model="selectedValue"
class="w-full px-4 py-3 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-zk text-neutral-900 dark:text-neutral-100 appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed truncate pr-8"
:class="{
'border-error-500 dark:border-error-400': error,
}"
:disabled="disabled || !accounts.length"
>
<option
value=""
disabled
>
{{ accounts.length ? 'Select an account' : 'No accounts found' }}
</option>
<option
v-for="account in accounts"
:key="account"
:value="account"
>
{{ account }}
</option>
</select>

<div class="absolute inset-y-0 right-0 flex items-center px-4 pointer-events-none">
<ZkIcon icon="arrow_drop_down" />
</div>

<!-- Error messages -->
<div
v-if="error && messages?.length"
class="mt-2 space-y-1"
>
<p
v-for="(message, index) in messages"
:key="index"
class="text-sm text-error-500 dark:text-error-400"
>
{{ message }}
</p>
</div>
</div>
</template>

<script setup lang="ts">
import type { Address } from "viem";
import { computed } from "vue";
const props = defineProps<{
id?: string;
modelValue: string;
accounts: Address[];
error?: boolean;
messages?: string[];
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
}>();
const selectedValue = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<template>
<Dialog
ref="modalRef"
content-class="min-w-[700px] min-h-[500px]"
description-class="flex-1 mb-0 flex text-base"
close-class="h-8 max-h-8"
:title="title"
>
<template #trigger>
<slot name="trigger">
<Button
class="w-full lg:w-auto"
type="primary"
>
Add Recovery Method
</Button>
</slot>
</template>

<template #submit>
<div />
</template>

<template #cancel>
<div />
</template>

<!-- Method Selection Step -->
<div
v-if="currentStep === 'select-method'"
class="space-y-6 text-left flex-1 flex flex-col"
>
<div class="flex flex-col gap-6 items-center flex-1 justify-center max-w-md mx-auto w-full">
<div class="text-center">
<p class="text-xl font-medium mb-2">
Choose a Recovery Method
</p>
<p class="text-base text-gray-600 dark:text-gray-400">
Select how you'd like to recover your account if you lose access
</p>
</div>

<div class="flex flex-col gap-5 w-full max-w-xs">
<Button
class="w-full"
@click="selectMethod('guardian')"
>
Recover with Guardian
</Button>

<div class="flex w-full flex-col gap-2">
<Button
disabled
class="w-full"
>
Recover with Email
</Button>
<span class="text-sm text-gray-500 text-center">
Coming soon...
</span>
</div>
</div>
</div>
</div>

<GuardianFlow
v-if="currentStep === 'guardian'"
:close-modal="closeModal"
@back="currentStep = 'select-method'"
/>
</Dialog>
</template>

<script setup lang="ts">
import { ref } from "vue";
import GuardianFlow from "~/components/account-recovery/guardian-flow/Root.vue";
import Button from "~/components/zk/button.vue";
import Dialog from "~/components/zk/dialog.vue";
type Step = "select-method" | "guardian" | "email";
const currentStep = ref<Step>("select-method");
const modalRef = ref<InstanceType<typeof Dialog>>();
const emit = defineEmits<{
(e: "closed"): void;
}>();
function closeModal() {
emit("closed");
modalRef.value?.close();
}
const title = computed(() => {
switch (currentStep.value) {
case "select-method":
return "Add Recovery Method";
case "guardian":
return "Guardian Recovery Setup";
case "email":
return "Email Recovery Setup";
default:
throw new Error("Invalid step");
}
});
function selectMethod(method: "guardian" | "email") {
currentStep.value = method;
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<template>
<div class="w-full max-w-md flex flex-col gap-6">
<!-- Generate Passkeys Step -->
<div
v-if="currentStep === generatePasskeysStep"
class="w-full max-w-md flex flex-col gap-6"
>
<p class="text-center text-neutral-700 dark:text-neutral-300">
Generate new passkeys to secure your account
</p>

<ZkButton
class="w-full"
:loading="registerInProgress"
@click="handleGeneratePasskeys"
>
Generate Passkeys
</ZkButton>

<ZkButton
type="secondary"
class="w-full"
@click="$emit('back')"
>
Back
</ZkButton>
</div>

<!-- Confirmation Step -->
<div
v-if="currentStep === confirmationStep"
class="w-full max-w-md flex flex-col gap-6"
>
<div class="flex flex-col gap-4 text-center text-neutral-700 dark:text-neutral-300">
<p>
Your passkeys have been generated successfully.
</p>
<p>
Please share the following url with your guardian to complete the recovery process:
</p>
</div>

<div class="w-full items-center gap-2 p-4 bg-neutral-100 dark:bg-neutral-900 rounded-zk">
<a
:href="recoveryUrl"
target="_blank"
class="text-sm text-neutral-800 dark:text-neutral-100 break-all hover:text-neutral-900 dark:hover:text-neutral-400 leading-relaxed underline underline-offset-4 decoration-neutral-400 hover:decoration-neutral-900 dark:decoration-neutral-600 dark:hover:decoration-neutral-400"
>
{{ recoveryUrl }}
</a>
<common-copy-to-clipboard
:text="recoveryUrl ?? ''"
class="!inline-flex ml-1"
/>
</div>

<p class="text-sm text-center text-neutral-600 dark:text-neutral-400">
You'll be able to access your account once your guardian confirms the recovery.
</p>

<ZkLink
type="primary"
href="/"
class="w-full"
>
Back to Home
</ZkLink>
</div>
</div>
</template>

<script setup lang="ts">
import type { RegisterNewPasskeyReturnType } from "zksync-sso/client/passkey";
const props = defineProps<{
currentStep: number;
generatePasskeysStep: number;
confirmationStep: number;
address: string;
newPasskey: RegisterNewPasskeyReturnType | null;
registerInProgress: boolean;
}>();
const emit = defineEmits<{
(e: "back"): void;
(e: "update:newPasskey", value: RegisterNewPasskeyReturnType): void;
(e: "update:currentStep", value: number): void;
}>();
const runtimeConfig = useRuntimeConfig();
const appUrl = runtimeConfig.public.appUrl;
const { registerPasskey } = usePasskeyRegister();
const recoveryUrl = computedAsync(async () => {
const queryParams = new URLSearchParams();
const credentialId = props.newPasskey?.credentialId ?? "";
const credentialPublicKey = uint8ArrayToHex(props.newPasskey?.credentialPublicKey ?? new Uint8Array()) ?? "";
queryParams.set("credentialId", credentialId);
queryParams.set("credentialPublicKey", credentialPublicKey);
queryParams.set("accountAddress", props.address);
// Create checksum from concatenated credential data
const dataToHash = `${props.address}:${credentialId}:${credentialPublicKey}`;
const fullHash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(dataToHash)));
const shortHash = fullHash.slice(0, 8); // Take first 8 bytes of the hash
const checksum = uint8ArrayToHex(shortHash);
queryParams.set("checksum", checksum);
return `${appUrl}/recovery/guardian/confirm-recovery?${queryParams.toString()}`;
});
const handleGeneratePasskeys = async () => {
const result = await registerPasskey();
if (!result) {
throw new Error("Failed to register passkey");
}
emit("update:newPasskey", result);
emit("update:currentStep", props.confirmationStep);
};
</script>
Loading

0 comments on commit 36dc3c1

Please sign in to comment.