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

[Draft] feat: add guardian recovery #52

Draft
wants to merge 43 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
880f3d2
feat: add initial guardian recovery views
aon Jan 9, 2025
e6efc33
feat: add confirm guardian view
aon Jan 14, 2025
d2f4d8e
feat: improve guardian view with edge cases
aon Jan 15, 2025
eb03f6f
fix: remove commented code
aon Jan 15, 2025
213fff5
Merge pull request #1 from Moonsong-Labs/feat/initial-recovery-views
aon Jan 16, 2025
fee0c9f
Merge pull request #2 from Moonsong-Labs/feat/confirm-guardian-view
aon Jan 16, 2025
5fc1724
feat: improve component imports
aon Jan 17, 2025
774d978
feat: add logout icon in desktop breakpoint
aon Jan 17, 2025
7259168
fix: naming
aon Jan 17, 2025
14c454b
Merge pull request #3 from Moonsong-Labs/feat/add-recovery-flow
aon Jan 17, 2025
72e653e
Merge remote-tracking branch 'upstream/main' into update-upstream
aon Jan 17, 2025
ed8c70c
fix: pnpm lock
aon Jan 17, 2025
9fd9cc8
Merge pull request #4 from Moonsong-Labs/update-upstream
aon Jan 17, 2025
407db29
fix: wrong nav component import
aon Jan 17, 2025
2c2ee47
fix: add missing package to cspell
aon Jan 17, 2025
6864fc0
feat: add recover account views
aon Jan 20, 2025
b4213e0
feat: add unknown account page
aon Jan 20, 2025
f05f3cd
feat: improve account init recovery start
aon Jan 21, 2025
b6d13d9
feat: reorganize routes with typed routes
aon Jan 21, 2025
27d66e9
feat: add recovery process warning when logged in
aon Jan 21, 2025
96e841e
feat: add account not ready page
aon Jan 21, 2025
9ad3286
fix: confirm-guardian page
aon Jan 21, 2025
5bb9e60
feat: add base guardian recovery module
MiniRoman Jan 22, 2025
565cf05
chore: update contracts submodule
MiniRoman Jan 22, 2025
47c4c4f
chore: update contracts submodule
MiniRoman Jan 23, 2025
71df9a9
chore: update contracts submodule
MiniRoman Jan 23, 2025
2885ef9
Merge pull request #5 from Moonsong-Labs/feat/add-recover-account-views
aon Jan 23, 2025
ba6a841
Merge pull request #6 from Moonsong-Labs/feat/add-contracts-integration
aon Jan 23, 2025
c9c065b
feat: add sso account validation
aon Jan 23, 2025
b289130
Update packages/auth-server/pages/recovery/guardian/index.vue
aon Jan 24, 2025
3d9ddd8
Merge pull request #7 from Moonsong-Labs/feat/add-recovery-guardian-r…
aon Jan 24, 2025
acc3fd3
feat: add integration with /recovery/guardian/find-account
aon Jan 27, 2025
cb1ce10
Merge pull request #8 from Moonsong-Labs/feat/integrate-route-recover…
aon Jan 27, 2025
da9dc3b
feat: integrate contracts in guardians settings page
MiniRoman Jan 28, 2025
602987e
feat: set proper path to confirm-guardian page
MiniRoman Jan 28, 2025
56f4eb9
feat: add guardian confirmation integration
MiniRoman Jan 28, 2025
9f1b187
feat: add ui improvements
aon Jan 28, 2025
05ea4b7
feat: address pr comments
MiniRoman Jan 29, 2025
859e9d8
Merge pull request #9 from Moonsong-Labs/feat/integrate-contract-in-g…
aon Jan 29, 2025
bf8656c
feat: merge from dev
aon Jan 30, 2025
af08b5a
feat: update contracts submodule
aon Jan 30, 2025
af569f7
Merge pull request #10 from Moonsong-Labs/feat/ui-improvements
aon Jan 30, 2025
8224b10
chore: update contract submodule
aon Jan 30, 2025
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
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
26 changes: 26 additions & 0 deletions packages/auth-server/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,35 @@
</template>

<script lang="ts" setup>
import type { AppKitNetwork } from "@reown/appkit/networks";
import { createAppKit } from "@reown/appkit/vue";
import { WagmiAdapter } from "@reown/appkit-adapter-wagmi";

import { supportedChains } from "./stores/client";

// BigInt polyfill
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};

// AppKit Configuration
const projectId = "9bc5059f6eed355858cc56a3388e9b50";
const metadata = {
name: "ZKsync SSO Auth Server",
description: "ZKsync SSO Auth Server",
url: "https://auth-test.zksync.dev",
icons: ["https://auth-test.zksync.dev/icon-512.png"],
};
const wagmiAdapter = new WagmiAdapter({
networks: supportedChains,
projectId,
});

createAppKit({
adapters: [wagmiAdapter],
networks: supportedChains as unknown as [AppKitNetwork, ...AppKitNetwork[]],
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,105 @@
<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/flows/GuardianFlow.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>>();

function closeModal() {
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
Loading