From 71342e9b655a1dd410f0a92b9aa86253a8c4df7e Mon Sep 17 00:00:00 2001 From: Erion Spahija Date: Tue, 24 Jun 2025 15:41:15 +0200 Subject: [PATCH 01/24] default Vault after Signup --- app/actions/_userActions.ts | 34 +++++++++++++++++++---------- components/auth/onboarding-form.tsx | 3 ++- lib/crypto.ts | 7 ++++-- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/app/actions/_userActions.ts b/app/actions/_userActions.ts index 68a87d8..db9560b 100644 --- a/app/actions/_userActions.ts +++ b/app/actions/_userActions.ts @@ -9,6 +9,11 @@ import { NotFoundError, } from "@/lib/errors"; import { prisma } from "@/lib/prisma"; +import { createVault } from "./_vaultActions"; + + + + export const finishOnboarding = withErrorHandling( withAuth( @@ -18,6 +23,7 @@ export const finishOnboarding = withErrorHandling( salt: string; publicKey: string; wrappedPrivateKey: string; + generateAndWrapVaultKey:string; } ) => { const client = await clerkClient(); @@ -26,7 +32,7 @@ export const finishOnboarding = withErrorHandling( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const email = user.primaryEmailAddress!.emailAddress; - const { salt, publicKey, wrappedPrivateKey } = data; + const { salt, publicKey, wrappedPrivateKey, generateAndWrapVaultKey } = data; try { // Check if user already exists @@ -43,16 +49,22 @@ export const finishOnboarding = withErrorHandling( return; } - // Persist the new user - await prisma.user.create({ - data: { - id: user.id, - email, - salt, - publicKey, - wrappedPrivateKey, - }, - }); + // Persist the new user + await prisma.user.create({ + data: { + id: user.id, + email, + salt, + publicKey, + wrappedPrivateKey, + }, + }); + + await createVault({ + name: "Private", + wrappedKey: generateAndWrapVaultKey, + }); + await client.users.updateUser(user.id, { publicMetadata: { diff --git a/components/auth/onboarding-form.tsx b/components/auth/onboarding-form.tsx index f7ad8fe..d553e1b 100644 --- a/components/auth/onboarding-form.tsx +++ b/components/auth/onboarding-form.tsx @@ -44,13 +44,14 @@ export function SignUpForm({ } try { - const { publicKey, wrappedPrivateKey, salt } = + const { publicKey, wrappedPrivateKey, salt, generateAndWrapVaultKey } = await cryptoService.onboarding(password); const response = await finishOnboarding({ salt, publicKey, wrappedPrivateKey, + generateAndWrapVaultKey, }); // Handle error responses diff --git a/lib/crypto.ts b/lib/crypto.ts index 86d7c5e..1c9b696 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -1,17 +1,20 @@ export class CryptoService { public async onboarding( password: string - ): Promise<{ publicKey: string; wrappedPrivateKey: string; salt: string }> { + ): Promise<{ publicKey: string; wrappedPrivateKey: string; salt: string; generateAndWrapVaultKey: string }> { const { publicKey, privateKey } = await this.generateKeyPair(); const salt = crypto.getRandomValues(new Uint8Array(16)); const kek = await this.deriveKek(password, salt); const wrappedPrivateKey = await this.wrapPrivateKey(privateKey, kek); const publicKeyBuffer = await crypto.subtle.exportKey("spki", publicKey); + const generateAndWrapVaultKey = await this.generateAndWrapVaultKey(publicKey); + return { publicKey: BufferTransformer.arrayBufferToBase64(publicKeyBuffer), wrappedPrivateKey: BufferTransformer.arrayBufferToBase64(wrappedPrivateKey), salt: BufferTransformer.arrayBufferToBase64(salt.buffer), + generateAndWrapVaultKey: generateAndWrapVaultKey.wrappedKey, }; } @@ -184,7 +187,7 @@ export class CryptoService { hash: "SHA-256", }, true, - ["encrypt", "decrypt"] + ["encrypt", "decrypt", "wrapKey"] ); } From 483cb53811d6ebf0fc137e25e0a75b8f89567e4f Mon Sep 17 00:00:00 2001 From: Erion Spahija Date: Tue, 1 Jul 2025 14:52:47 +0200 Subject: [PATCH 02/24] Formatierung --- app/actions/_userActions.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/actions/_userActions.ts b/app/actions/_userActions.ts index db9560b..8fade09 100644 --- a/app/actions/_userActions.ts +++ b/app/actions/_userActions.ts @@ -11,10 +11,6 @@ import { import { prisma } from "@/lib/prisma"; import { createVault } from "./_vaultActions"; - - - - export const finishOnboarding = withErrorHandling( withAuth( async ( From 3b307bfa74be518c3896fb2f1582aa7f4330cbdb Mon Sep 17 00:00:00 2001 From: Erion Spahija Date: Tue, 1 Jul 2025 17:48:01 +0200 Subject: [PATCH 03/24] fix --- app/actions/_userActions.ts | 34 +++++++++++++++++----------------- lib/crypto.ts | 12 ++++++++---- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/actions/_userActions.ts b/app/actions/_userActions.ts index 8fade09..306d7e1 100644 --- a/app/actions/_userActions.ts +++ b/app/actions/_userActions.ts @@ -19,7 +19,7 @@ export const finishOnboarding = withErrorHandling( salt: string; publicKey: string; wrappedPrivateKey: string; - generateAndWrapVaultKey:string; + generateAndWrapVaultKey: string; } ) => { const client = await clerkClient(); @@ -28,7 +28,8 @@ export const finishOnboarding = withErrorHandling( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const email = user.primaryEmailAddress!.emailAddress; - const { salt, publicKey, wrappedPrivateKey, generateAndWrapVaultKey } = data; + const { salt, publicKey, wrappedPrivateKey, generateAndWrapVaultKey } = + data; try { // Check if user already exists @@ -45,22 +46,21 @@ export const finishOnboarding = withErrorHandling( return; } - // Persist the new user - await prisma.user.create({ - data: { - id: user.id, - email, - salt, - publicKey, - wrappedPrivateKey, - }, - }); - - await createVault({ - name: "Private", - wrappedKey: generateAndWrapVaultKey, - }); + // Persist the new user + await prisma.user.create({ + data: { + id: user.id, + email, + salt, + publicKey, + wrappedPrivateKey, + }, + }); + await createVault({ + name: "Private", + wrappedKey: generateAndWrapVaultKey, + }); await client.users.updateUser(user.id, { publicMetadata: { diff --git a/lib/crypto.ts b/lib/crypto.ts index 1c9b696..76ab431 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -1,13 +1,17 @@ export class CryptoService { - public async onboarding( - password: string - ): Promise<{ publicKey: string; wrappedPrivateKey: string; salt: string; generateAndWrapVaultKey: string }> { + public async onboarding(password: string): Promise<{ + publicKey: string; + wrappedPrivateKey: string; + salt: string; + generateAndWrapVaultKey: string; + }> { const { publicKey, privateKey } = await this.generateKeyPair(); const salt = crypto.getRandomValues(new Uint8Array(16)); const kek = await this.deriveKek(password, salt); const wrappedPrivateKey = await this.wrapPrivateKey(privateKey, kek); const publicKeyBuffer = await crypto.subtle.exportKey("spki", publicKey); - const generateAndWrapVaultKey = await this.generateAndWrapVaultKey(publicKey); + const generateAndWrapVaultKey = + await this.generateAndWrapVaultKey(publicKey); return { publicKey: BufferTransformer.arrayBufferToBase64(publicKeyBuffer), From dc70834435c5891db9009793cf0d4e7d4d3de84e Mon Sep 17 00:00:00 2001 From: Erion Spahija Date: Sat, 5 Jul 2025 20:19:17 +0200 Subject: [PATCH 04/24] generateandwrappedkey -> wrapedDefaultVaultKey --- app/actions/_userActions.ts | 6 +++--- components/auth/onboarding-form.tsx | 2 +- lib/crypto.ts | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/actions/_userActions.ts b/app/actions/_userActions.ts index 306d7e1..5a47bdd 100644 --- a/app/actions/_userActions.ts +++ b/app/actions/_userActions.ts @@ -19,7 +19,7 @@ export const finishOnboarding = withErrorHandling( salt: string; publicKey: string; wrappedPrivateKey: string; - generateAndWrapVaultKey: string; + wrappedDefaultVaultKey: string; } ) => { const client = await clerkClient(); @@ -28,7 +28,7 @@ export const finishOnboarding = withErrorHandling( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const email = user.primaryEmailAddress!.emailAddress; - const { salt, publicKey, wrappedPrivateKey, generateAndWrapVaultKey } = + const { salt, publicKey, wrappedPrivateKey, wrappedDefaultVaultKey } = data; try { @@ -59,7 +59,7 @@ export const finishOnboarding = withErrorHandling( await createVault({ name: "Private", - wrappedKey: generateAndWrapVaultKey, + wrappedKey: wrappedDefaultVaultKey, }); await client.users.updateUser(user.id, { diff --git a/components/auth/onboarding-form.tsx b/components/auth/onboarding-form.tsx index d553e1b..8e54c78 100644 --- a/components/auth/onboarding-form.tsx +++ b/components/auth/onboarding-form.tsx @@ -51,7 +51,7 @@ export function SignUpForm({ salt, publicKey, wrappedPrivateKey, - generateAndWrapVaultKey, + wrappedDefaultVaultKey: generateAndWrapVaultKey, }); // Handle error responses diff --git a/lib/crypto.ts b/lib/crypto.ts index 76ab431..0fb79c6 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -10,15 +10,14 @@ export class CryptoService { const kek = await this.deriveKek(password, salt); const wrappedPrivateKey = await this.wrapPrivateKey(privateKey, kek); const publicKeyBuffer = await crypto.subtle.exportKey("spki", publicKey); - const generateAndWrapVaultKey = - await this.generateAndWrapVaultKey(publicKey); + const {wrappedKey: wrappedDefaultVaultKey } = await this.generateAndWrapVaultKey(publicKey); return { publicKey: BufferTransformer.arrayBufferToBase64(publicKeyBuffer), wrappedPrivateKey: BufferTransformer.arrayBufferToBase64(wrappedPrivateKey), salt: BufferTransformer.arrayBufferToBase64(salt.buffer), - generateAndWrapVaultKey: generateAndWrapVaultKey.wrappedKey, + generateAndWrapVaultKey: wrappedDefaultVaultKey, }; } From 13f80df5ac470b876bc9a603c08535d8cd55c266 Mon Sep 17 00:00:00 2001 From: Erion Spahija Date: Sat, 5 Jul 2025 20:27:00 +0200 Subject: [PATCH 05/24] formatting with prettier --- lib/crypto.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/crypto.ts b/lib/crypto.ts index 0fb79c6..f35d36c 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -10,7 +10,8 @@ export class CryptoService { const kek = await this.deriveKek(password, salt); const wrappedPrivateKey = await this.wrapPrivateKey(privateKey, kek); const publicKeyBuffer = await crypto.subtle.exportKey("spki", publicKey); - const {wrappedKey: wrappedDefaultVaultKey } = await this.generateAndWrapVaultKey(publicKey); + const { wrappedKey: wrappedDefaultVaultKey } = + await this.generateAndWrapVaultKey(publicKey); return { publicKey: BufferTransformer.arrayBufferToBase64(publicKeyBuffer), From 4547ef09a443a00760e88845c91663064337c079 Mon Sep 17 00:00:00 2001 From: Marvin <129607867+knivram@users.noreply.github.com> Date: Sun, 6 Jul 2025 00:14:34 +0200 Subject: [PATCH 06/24] Update crypto.ts --- lib/crypto.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/crypto.ts b/lib/crypto.ts index f35d36c..15413fc 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -3,7 +3,7 @@ export class CryptoService { publicKey: string; wrappedPrivateKey: string; salt: string; - generateAndWrapVaultKey: string; + wrappedDefaultVaultKey: string; }> { const { publicKey, privateKey } = await this.generateKeyPair(); const salt = crypto.getRandomValues(new Uint8Array(16)); @@ -18,7 +18,7 @@ export class CryptoService { wrappedPrivateKey: BufferTransformer.arrayBufferToBase64(wrappedPrivateKey), salt: BufferTransformer.arrayBufferToBase64(salt.buffer), - generateAndWrapVaultKey: wrappedDefaultVaultKey, + wrappedDefaultVaultKey, }; } From 88d43d9219c524e2b377b24aa31ef8a86722189a Mon Sep 17 00:00:00 2001 From: Marvin <129607867+knivram@users.noreply.github.com> Date: Sun, 6 Jul 2025 00:15:21 +0200 Subject: [PATCH 07/24] Update onboarding-form.tsx --- components/auth/onboarding-form.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/auth/onboarding-form.tsx b/components/auth/onboarding-form.tsx index 8e54c78..d9b7eda 100644 --- a/components/auth/onboarding-form.tsx +++ b/components/auth/onboarding-form.tsx @@ -44,14 +44,14 @@ export function SignUpForm({ } try { - const { publicKey, wrappedPrivateKey, salt, generateAndWrapVaultKey } = + const { publicKey, wrappedPrivateKey, salt, wrappedDefaultVaultKey } = await cryptoService.onboarding(password); const response = await finishOnboarding({ salt, publicKey, wrappedPrivateKey, - wrappedDefaultVaultKey: generateAndWrapVaultKey, + wrappedDefaultVaultKey, }); // Handle error responses From 4b401a3377634c355670dc4f77a3ffdcc0f076bf Mon Sep 17 00:00:00 2001 From: knivram <129607867+knivram@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:58:15 +0200 Subject: [PATCH 08/24] fixed security issues and removed unused code --- components/secrets-list.tsx | 8 +- lib/crypto.ts | 25 +++-- lib/secrets-client.ts | 179 +++--------------------------------- 3 files changed, 33 insertions(+), 179 deletions(-) diff --git a/components/secrets-list.tsx b/components/secrets-list.tsx index 907e971..012bb49 100644 --- a/components/secrets-list.tsx +++ b/components/secrets-list.tsx @@ -11,6 +11,7 @@ import type { SecretWithDecryptedData, SecretWithDecryptedDataAndVault, } from "@/types/secret"; +import type { VaultWithAccess } from "@/types/vault"; import { SecretFormDialog } from "./secret-form-dialog"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; @@ -28,10 +29,7 @@ const ALL_SECRETS_QUERY_KEY = "all-secrets-list"; interface SecretsListProps { vaultId: string; - vault?: { - isOwner?: boolean; - role?: string; - }; + vault: VaultWithAccess; } type SecretWithOptionalVault = @@ -374,7 +372,7 @@ function SecretsList({ vaultId, vault }: SecretsListProps) { - secretsClient.getSecretsWithDecryptedData(vaultId, privateKey) + secretsClient.getSecretsWithDecryptedData(vault, privateKey) } emptyStateMessage={{ primary: "No secrets in this vault yet.", diff --git a/lib/crypto.ts b/lib/crypto.ts index 15413fc..e91d918 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -64,36 +64,41 @@ export class CryptoService { public async encryptSecret({ secret, - publicKey, + vaultKey, }: { secret: string; - publicKey: CryptoKey; + vaultKey: CryptoKey; }): Promise { const data = new TextEncoder().encode(secret); + const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { - name: "RSA-OAEP", + name: "AES-GCM", + iv, }, - publicKey, + vaultKey, data ); - return BufferTransformer.arrayBufferToBase64(encrypted); + const packed = this.pack(iv, encrypted); + return BufferTransformer.arrayBufferToBase64(packed); } public async decryptSecret({ encryptedSecret, - privateKey, + vaultKey, }: { encryptedSecret: string; - privateKey: CryptoKey; + vaultKey: CryptoKey; }): Promise { const data = BufferTransformer.base64ToArrayBuffer(encryptedSecret); + const { iv, data: encryptedData } = this.unpack(data); const decrypted = await crypto.subtle.decrypt( { - name: "RSA-OAEP", + name: "AES-GCM", + iv, }, - privateKey, - data + vaultKey, + encryptedData ); return new TextDecoder().decode(decrypted); } diff --git a/lib/secrets-client.ts b/lib/secrets-client.ts index af584fc..d528a48 100644 --- a/lib/secrets-client.ts +++ b/lib/secrets-client.ts @@ -19,6 +19,7 @@ import type { SecretWithDecryptedData, SecretWithDecryptedDataAndVault, } from "@/types/secret"; +import type { VaultWithAccess } from "@/types/vault"; import { CryptoService } from "./crypto"; export class SecretsClient { @@ -45,21 +46,10 @@ export class SecretsClient { // Encrypt the secret data with the vault key const dataString = JSON.stringify(input.data); - const data = new TextEncoder().encode(dataString); - const iv = crypto.getRandomValues(new Uint8Array(12)); - - const encrypted = await crypto.subtle.encrypt( - { - name: "AES-GCM", - iv, - }, + const encryptedData = await this.cryptoService.encryptSecret({ + secret: dataString, vaultKey, - data - ); - - // Pack IV and encrypted data - const packedData = this.pack(iv, encrypted); - const encryptedData = this.arrayBufferToBase64(packedData); + }); const response = await createSecret({ vaultId: input.vaultId, @@ -95,39 +85,24 @@ export class SecretsClient { } async getSecretsWithDecryptedData( - vaultId: string, + vault: VaultWithAccess, privateKey: CryptoKey ): Promise { try { - // Get vault to access the wrapped vault key - const vaultResponse = await getVault({ vaultId }); - const vault = handleActionResponse(vaultResponse); - - // Unwrap the vault key const vaultKey = await this.cryptoService.unwrapVaultKey({ wrappedKey: vault.wrappedKey, privateKey, }); - const encryptedSecrets = await this.getSecrets(vaultId); + const encryptedSecrets = await this.getSecrets(vault.id); const decryptedSecrets: SecretWithDecryptedData[] = []; for (const secret of encryptedSecrets) { try { - // Decrypt with vault key - const encryptedData = this.base64ToArrayBuffer(secret.encryptedData); - const { iv, data } = this.unpack(encryptedData); - - const decrypted = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv, - }, + const decryptedDataString = await this.cryptoService.decryptSecret({ + encryptedSecret: secret.encryptedData, vaultKey, - data - ); - - const decryptedDataString = new TextDecoder().decode(decrypted); + }); const secretData: SecretData = JSON.parse(decryptedDataString); decryptedSecrets.push({ @@ -153,63 +128,6 @@ export class SecretsClient { } } - async getSecretWithDecryptedData( - secretId: string, - privateKey: CryptoKey - ): Promise { - try { - const encryptedSecret = await this.getSecret(secretId); - - if (!encryptedSecret) { - return null; - } - - // Get vault to access the wrapped vault key - const vaultResponse = await getVault({ - vaultId: encryptedSecret.vaultId, - }); - const vault = handleActionResponse(vaultResponse); - - // Unwrap the vault key - const vaultKey = await this.cryptoService.unwrapVaultKey({ - wrappedKey: vault.wrappedKey, - privateKey, - }); - - // Decrypt with vault key - const encryptedData = this.base64ToArrayBuffer( - encryptedSecret.encryptedData - ); - const { iv, data } = this.unpack(encryptedData); - - const decrypted = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv, - }, - vaultKey, - data - ); - - const decryptedDataString = new TextDecoder().decode(decrypted); - const secretData: SecretData = JSON.parse(decryptedDataString); - - return { - id: encryptedSecret.id, - vaultId: encryptedSecret.vaultId, - title: encryptedSecret.title, - data: secretData, - createdAt: encryptedSecret.createdAt, - updatedAt: encryptedSecret.updatedAt, - }; - } catch (error) { - console.error("Failed to get secret with decrypted data:", error); - throw new Error( - "Failed to decrypt secret. Please check your password and try again." - ); - } - } - async updateSecret( secretId: string, updatedSecret: UpdateSecretInput, @@ -242,21 +160,10 @@ export class SecretsClient { // Encrypt the new data with the vault key const dataString = JSON.stringify(updatedSecret.data); - const data = new TextEncoder().encode(dataString); - const iv = crypto.getRandomValues(new Uint8Array(12)); - - const encrypted = await crypto.subtle.encrypt( - { - name: "AES-GCM", - iv, - }, + serverUpdates.encryptedData = await this.cryptoService.encryptSecret({ + secret: dataString, vaultKey, - data - ); - - // Pack IV and encrypted data - const packedData = this.pack(iv, encrypted); - serverUpdates.encryptedData = this.arrayBufferToBase64(packedData); + }); } const response = await updateSecret({ @@ -299,21 +206,10 @@ export class SecretsClient { vaultKeys.set(secretWithVault.vault.id, vaultKey); } - const encryptedData = this.base64ToArrayBuffer( - secretWithVault.encryptedData - ); - const { iv, data } = this.unpack(encryptedData); - - const decrypted = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv, - }, + const decryptedDataString = await this.cryptoService.decryptSecret({ + encryptedSecret: secretWithVault.encryptedData, vaultKey, - data - ); - - const decryptedDataString = new TextDecoder().decode(decrypted); + }); const secretData: SecretData = JSON.parse(decryptedDataString); decryptedSecrets.push({ @@ -345,49 +241,4 @@ export class SecretsClient { ); } } - - private pack( - iv: Uint8Array, - data: ArrayBuffer | ArrayBufferView - ): ArrayBuffer { - const dataBytes = ArrayBuffer.isView(data) - ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) - : new Uint8Array(data); - const out = new Uint8Array(iv.byteLength + dataBytes.byteLength); - out.set(iv, 0); - out.set(dataBytes, iv.byteLength); - return out.buffer; - } - - private unpack(src: ArrayBuffer | ArrayBufferView): { - iv: Uint8Array; - data: Uint8Array; - } { - const bytes = ArrayBuffer.isView(src) - ? new Uint8Array(src.buffer, src.byteOffset, src.byteLength) - : new Uint8Array(src); - const iv = bytes.subarray(0, 12); - const data = bytes.subarray(12); - return { iv, data }; - } - - private base64ToArrayBuffer(base64: string): ArrayBuffer { - const binaryString = atob(base64); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - // eslint-disable-next-line security/detect-object-injection - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer as ArrayBuffer; - } - - private arrayBufferToBase64(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); - let binary = ""; - for (let i = 0; i < bytes.byteLength; i++) { - // eslint-disable-next-line security/detect-object-injection - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); - } } From 07a3ef4f46b27757127e3091101d7c38f4331cf2 Mon Sep 17 00:00:00 2001 From: Erion Spahija Date: Sun, 6 Jul 2025 14:31:27 +0200 Subject: [PATCH 09/24] Masterpassword Policy --- components/auth/onboarding-form.tsx | 55 ++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/components/auth/onboarding-form.tsx b/components/auth/onboarding-form.tsx index d9b7eda..5513a92 100644 --- a/components/auth/onboarding-form.tsx +++ b/components/auth/onboarding-form.tsx @@ -1,9 +1,9 @@ "use client"; import { useUser } from "@clerk/nextjs"; -import { AlertCircle } from "lucide-react"; +import { AlertCircle, Check, X } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { finishOnboarding } from "@/app/actions/_userActions"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -22,6 +22,14 @@ import { cn } from "@/lib/utils"; const cryptoService = new CryptoService(); +const passwordRequirements = [ + { id: "length", label: "Length: at least 12 characters", test: (pwd: string) => pwd.length >= 12 }, + { id: "uppercase", label: "At least 1 uppercase letter (A–Z)", test: (pwd: string) => /[A-Z]/.test(pwd) }, + { id: "lowercase", label: "At least 1 lowercase letter (a–z)", test: (pwd: string) => /[a-z]/.test(pwd) }, + { id: "number", label: "At least 1 number (0–9)", test: (pwd: string) => /\d/.test(pwd) }, + { id: "special", label: "At least 1 special character (!@#$%^&*()_+-", test: (pwd: string) => /[!@#$%^&*()_+\-]/.test(pwd) }, +]; + export function SignUpForm({ className, ...props @@ -33,10 +41,26 @@ export function SignUpForm({ const router = useRouter(); const { user } = useUser(); + const passwordChecks = useMemo(() => { + return passwordRequirements.map(req => ({ + ...req, + isValid: req.test(password) + })); + }, [password]); + + const isPasswordValid = passwordChecks.every(check => check.isValid); + const handleSignUp = async (e: React.FormEvent) => { e.preventDefault(); setError(""); setIsLoading(true); + + if (!isPasswordValid) { + setError("Password does not meet all requirements"); + setIsLoading(false); + return; + } + if (password !== repeatPassword) { setError("Passwords do not match"); setIsLoading(false); @@ -51,7 +75,7 @@ export function SignUpForm({ salt, publicKey, wrappedPrivateKey, - wrappedDefaultVaultKey, + wrappedDefaultVaultKey: wrappedDefaultVaultKey, }); // Handle error responses @@ -133,6 +157,23 @@ export function SignUpForm({ value={password} onChange={e => setPassword(e.target.value)} /> + {password && ( +
+

Password requirements:

+ {passwordChecks.map((check) => ( +
+ {check.isValid ? ( + + ) : ( + + )} + + {check.label} + +
+ ))} +
+ )}
@@ -148,7 +189,11 @@ export function SignUpForm({ onChange={e => setRepeatPassword(e.target.value)} />
-
); } + + From 9e3fa38434af2326c98ad4d64c4bd1aea3aa458d Mon Sep 17 00:00:00 2001 From: Erion Spahija Date: Sun, 6 Jul 2025 14:49:00 +0200 Subject: [PATCH 10/24] Run Prettier --- components/auth/onboarding-form.tsx | 55 ++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/components/auth/onboarding-form.tsx b/components/auth/onboarding-form.tsx index 5513a92..a369190 100644 --- a/components/auth/onboarding-form.tsx +++ b/components/auth/onboarding-form.tsx @@ -23,11 +23,31 @@ import { cn } from "@/lib/utils"; const cryptoService = new CryptoService(); const passwordRequirements = [ - { id: "length", label: "Length: at least 12 characters", test: (pwd: string) => pwd.length >= 12 }, - { id: "uppercase", label: "At least 1 uppercase letter (A–Z)", test: (pwd: string) => /[A-Z]/.test(pwd) }, - { id: "lowercase", label: "At least 1 lowercase letter (a–z)", test: (pwd: string) => /[a-z]/.test(pwd) }, - { id: "number", label: "At least 1 number (0–9)", test: (pwd: string) => /\d/.test(pwd) }, - { id: "special", label: "At least 1 special character (!@#$%^&*()_+-", test: (pwd: string) => /[!@#$%^&*()_+\-]/.test(pwd) }, + { + id: "length", + label: "Length: at least 12 characters", + test: (pwd: string) => pwd.length >= 12, + }, + { + id: "uppercase", + label: "At least 1 uppercase letter (A–Z)", + test: (pwd: string) => /[A-Z]/.test(pwd), + }, + { + id: "lowercase", + label: "At least 1 lowercase letter (a–z)", + test: (pwd: string) => /[a-z]/.test(pwd), + }, + { + id: "number", + label: "At least 1 number (0–9)", + test: (pwd: string) => /\d/.test(pwd), + }, + { + id: "special", + label: "At least 1 special character (!@#$%^&*()_+-", + test: (pwd: string) => /[!@#$%^&*()_+\-]/.test(pwd), + }, ]; export function SignUpForm({ @@ -44,7 +64,7 @@ export function SignUpForm({ const passwordChecks = useMemo(() => { return passwordRequirements.map(req => ({ ...req, - isValid: req.test(password) + isValid: req.test(password), })); }, [password]); @@ -159,15 +179,24 @@ export function SignUpForm({ /> {password && (
-

Password requirements:

- {passwordChecks.map((check) => ( -
+

+ Password requirements: +

+ {passwordChecks.map(check => ( +
{check.isValid ? ( ) : ( )} - + {check.label}
@@ -192,7 +221,9 @@ export function SignUpForm({
); } - - From 168ad72fd53db9fc169161a8a890e4a943164e5c Mon Sep 17 00:00:00 2001 From: Erion Spahija Date: Mon, 7 Jul 2025 22:57:35 +0200 Subject: [PATCH 11/24] Password requirement mit Zod --- components/auth/onboarding-form.tsx | 146 +++++++++++++++------------- package-lock.json | 46 ++++++++- package.json | 5 +- 3 files changed, 124 insertions(+), 73 deletions(-) diff --git a/components/auth/onboarding-form.tsx b/components/auth/onboarding-form.tsx index a369190..7617890 100644 --- a/components/auth/onboarding-form.tsx +++ b/components/auth/onboarding-form.tsx @@ -3,7 +3,7 @@ import { useUser } from "@clerk/nextjs"; import { AlertCircle, Check, X } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useState, useMemo } from "react"; +import { useState } from "react"; import { finishOnboarding } from "@/app/actions/_userActions"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -19,77 +19,85 @@ import { Label } from "@/components/ui/label"; import { CryptoService } from "@/lib/crypto"; import { isErrorResponse, getErrorInfo } from "@/lib/query-utils"; import { cn } from "@/lib/utils"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; const cryptoService = new CryptoService(); -const passwordRequirements = [ - { - id: "length", - label: "Length: at least 12 characters", - test: (pwd: string) => pwd.length >= 12, - }, - { - id: "uppercase", - label: "At least 1 uppercase letter (A–Z)", - test: (pwd: string) => /[A-Z]/.test(pwd), - }, - { - id: "lowercase", - label: "At least 1 lowercase letter (a–z)", - test: (pwd: string) => /[a-z]/.test(pwd), - }, - { - id: "number", - label: "At least 1 number (0–9)", - test: (pwd: string) => /\d/.test(pwd), - }, - { - id: "special", - label: "At least 1 special character (!@#$%^&*()_+-", - test: (pwd: string) => /[!@#$%^&*()_+\-]/.test(pwd), - }, -]; +const passwordSchema = z + .object({ + password: z + .string() + .min(12, "At least 12 characters") + .regex(/[A-Z]/, "At least 1 uppercase letter (A–Z)") + .regex(/[a-z]/, "At least 1 lowercase letter (a–z)") + .regex(/\d/, "At least 1 number (0–9)") + .regex(/[!@#$%^&*()_+\-]/, "At least 1 special character (!@#$%^&*()_+-)"), + repeatPassword: z.string(), + }) + .refine(data => data.password === data.repeatPassword, { + message: "Passwords do not match", + path: ["repeatPassword"], + }); + +type PasswordFormValues = z.infer; export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<"div">) { - const [password, setPassword] = useState(""); - const [repeatPassword, setRepeatPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); const router = useRouter(); const { user } = useUser(); - const passwordChecks = useMemo(() => { - return passwordRequirements.map(req => ({ - ...req, - isValid: req.test(password), - })); - }, [password]); - - const isPasswordValid = passwordChecks.every(check => check.isValid); - - const handleSignUp = async (e: React.FormEvent) => { - e.preventDefault(); + const { + register, + handleSubmit, + formState: { errors, isValid }, + watch, + } = useForm({ + resolver: zodResolver(passwordSchema), + mode: "onChange", + }); + + const password = watch("password") || ""; + + const passwordChecks = [ + { + id: "length", + label: "Length: at least 12 characters", + isValid: password.length >= 12, + }, + { + id: "uppercase", + label: "At least 1 uppercase letter (A–Z)", + isValid: /[A-Z]/.test(password), + }, + { + id: "lowercase", + label: "At least 1 lowercase letter (a–z)", + isValid: /[a-z]/.test(password), + }, + { + id: "number", + label: "At least 1 number (0–9)", + isValid: /\d/.test(password), + }, + { + id: "special", + label: "At least 1 special character (!@#$%^&*()_+-)", + isValid: /[!@#$%^&*()_+\-]/.test(password), + }, + ]; + + const onSubmit = async (data: PasswordFormValues) => { setError(""); setIsLoading(true); - - if (!isPasswordValid) { - setError("Password does not meet all requirements"); - setIsLoading(false); - return; - } - - if (password !== repeatPassword) { - setError("Passwords do not match"); - setIsLoading(false); - return; - } - try { const { publicKey, wrappedPrivateKey, salt, wrappedDefaultVaultKey } = - await cryptoService.onboarding(password); + await cryptoService.onboarding(data.password); const response = await finishOnboarding({ salt, @@ -98,22 +106,18 @@ export function SignUpForm({ wrappedDefaultVaultKey: wrappedDefaultVaultKey, }); - // Handle error responses if (isErrorResponse(response)) { const { error } = response; console.error(`[${error.code}] Onboarding failed: ${error.message}`); - const errorMessage = error.code === "ONBOARDING_FAILED" ? "Failed to complete onboarding. Please try again." : error.code === "UNAUTHORIZED" - ? "Authentication failed. Please sign in again." - : error.message; - + ? "Authentication failed. Please sign in again." + : error.message; setError(errorMessage); return; } - await user?.reload(); router.push("/"); } catch (error) { @@ -151,7 +155,7 @@ export function SignUpForm({ -
+ {error && (
setPassword(e.target.value)} + {...register("password")} /> {password && (
@@ -201,6 +204,11 @@ export function SignUpForm({
))} + {errors.password && ( +
+ {errors.password.message as string} +
+ )}
)}
@@ -214,15 +222,19 @@ export function SignUpForm({ id="repeat-password" type="password" required - value={repeatPassword} - onChange={e => setRepeatPassword(e.target.value)} + {...register("repeatPassword")} /> + {errors.repeatPassword && ( +
+ {errors.repeatPassword.message as string} +
+ )}