From 9e878997945f72de2ef131df5898bcc71853d47c Mon Sep 17 00:00:00 2001 From: oceans404 Date: Thu, 11 Apr 2024 11:01:00 -0700 Subject: [PATCH 1/4] add a password manager app --- .../nextjs/app/nillion-pw-manager/page.tsx | 240 ++++++++++++++++++ packages/nextjs/app/page.tsx | 13 + .../nextjs/components/nillion/Dropdown.tsx | 36 +++ .../nextjs/components/nillion/SecretForm.tsx | 133 ++++++---- 4 files changed, 371 insertions(+), 51 deletions(-) create mode 100644 packages/nextjs/app/nillion-pw-manager/page.tsx create mode 100644 packages/nextjs/components/nillion/Dropdown.tsx diff --git a/packages/nextjs/app/nillion-pw-manager/page.tsx b/packages/nextjs/app/nillion-pw-manager/page.tsx new file mode 100644 index 0000000..3d6ebd1 --- /dev/null +++ b/packages/nextjs/app/nillion-pw-manager/page.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { NextPage } from "next"; +import { useAccount } from "wagmi"; +import { CopyString } from "~~/components/nillion/CopyString"; +import Dropdown from "~~/components/nillion/Dropdown"; +import { NillionOnboarding } from "~~/components/nillion/NillionOnboarding"; +import RetrieveSecretCommand from "~~/components/nillion/RetrieveSecretCommand"; +import SecretForm from "~~/components/nillion/SecretForm"; +import { Address } from "~~/components/scaffold-eth"; +import { getUserKeyFromSnap } from "~~/utils/nillion/getUserKeyFromSnap"; +import { retrieveSecretBlob } from "~~/utils/nillion/retrieveSecretBlob"; +import { storeSecretsBlob } from "~~/utils/nillion/storeSecretsBlob"; + +interface StoredSecrets { + [secretName: string]: string; // store_id +} +const Home: NextPage = () => { + const { address: connectedAddress } = useAccount(); + const [connectedToSnap, setConnectedToSnap] = useState(false); + const [userKey, setUserKey] = useState(null); + const [userId, setUserId] = useState(null); + const [nillion, setNillion] = useState(null); + const [nillionClient, setNillionClient] = useState(null); + const [selectedSecretName, setSelectedSecretName] = useState(""); + const [selectedStoreId, setSelectedStoreId] = useState(null); + const [latestSecretName, setLatestSecretName] = useState(null); + const [storedSecrets, setStoredSecrets] = useState({}); + const [retrievedValue, setRetrievedValue] = useState(null); + + async function handleConnectToSnap() { + const snapResponse = await getUserKeyFromSnap(); + setUserKey(snapResponse?.user_key || null); + setConnectedToSnap(snapResponse?.connectedToSnap || false); + } + + useEffect(() => { + if (userKey) { + const getNillionClientLibrary = async () => { + const nillionClientUtil = await import("~~/utils/nillion/nillionClient"); + const libraries = await nillionClientUtil.getNillionClient(userKey); + setNillion(libraries.nillion); + setNillionClient(libraries.nillionClient); + return libraries.nillionClient; + }; + + getNillionClientLibrary().then(nillionClient => { + const user_id = nillionClient.user_id; + setUserId(user_id); + }); + } + }, [userKey]); + + async function handleSecretFormSubmit( + secretName: string, + secretValue: string, + permissionedUserIdForRetrieveSecret: string | null, + permissionedUserIdForUpdateSecret: string | null, + permissionedUserIdForDeleteSecret: string | null, + ) { + await storeSecretsBlob( + nillion, + nillionClient, + [{ name: secretName, value: secretValue }], + permissionedUserIdForRetrieveSecret ? [permissionedUserIdForRetrieveSecret] : [], + permissionedUserIdForUpdateSecret ? [permissionedUserIdForUpdateSecret] : [], + permissionedUserIdForDeleteSecret ? [permissionedUserIdForDeleteSecret] : [], + ).then((store_id: string) => { + setStoredSecrets(prevSecrets => ({ + ...prevSecrets, + [secretName]: store_id, + })); + + setLatestSecretName(secretName); + }); + } + + async function handleRetrieveSecretBlob(store_id: string, secret_name: string) { + await retrieveSecretBlob(nillionClient, store_id, secret_name).then(setRetrievedValue); + } + + const handleSecretDropdownSelection = (secretName: string) => { + setSelectedSecretName(secretName); + setSelectedStoreId(storedSecrets[secretName]); + }; + + const resetNillion = () => { + setConnectedToSnap(false); + setUserKey(null); + setUserId(null); + setNillion(null); + setNillionClient(null); + }; + + const resetForm = () => { + setLatestSecretName(null); + setRetrievedValue(null); + }; + + useEffect(() => { + if (!connectedAddress) { + resetNillion(); + } + }, [connectedAddress]); + + return ( + <> +
+
+

+ Nillion Password Manager + + {!connectedAddress &&

Connect your MetaMask Flask wallet

} + {connectedAddress && connectedToSnap && !userKey && ( + + + + )} +

+ + {connectedAddress && ( +
+

Connected Wallet Address:

+
+
+ )} + + {connectedAddress && !connectedToSnap && ( + + )} + + {connectedToSnap && ( +
+ {userKey && ( +
+
+

+ 🤫 Nillion User Key from{" "} + + MetaMask Flask + + : +

+ + +
+ + {userId && ( +
+

Connected as Nillion User ID:

+ +
+ )} +
+ )} +
+ )} +
+ +
+
+ {!connectedToSnap ? ( + + ) : ( +
+ {/* Store secret blob */} +
+

Store a new password

+
+
+ {latestSecretName ? ( + <> + + + + ) : ( + + )} +
+
+
+ + {/* Retrieve secret blob */} + +
+

Retrieve passwords from Nillion

+
+
+
+ ({ value: s, label: s }))} + onDropdownUpdate={selectedName => handleSecretDropdownSelection(selectedName)} + itemName="a password" + disabled={Object.keys(storedSecrets).length === 0} + /> +
+ + + + {retrievedValue &&

✅ Retrieved value: {retrievedValue}

} +
+
+
+
+ )} +
+
+
+ + ); +}; + +export default Home; diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index cc92660..c6a25f3 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -38,6 +38,19 @@ const Home: NextPage = () => { packages/hardhat/contracts

+ +
+ Nillion demo apps + + Nillion Password Manager + {" "} + + Nillion Hello World + {" "} + + Nillion Blind Compute + {" "} +
diff --git a/packages/nextjs/components/nillion/Dropdown.tsx b/packages/nextjs/components/nillion/Dropdown.tsx new file mode 100644 index 0000000..cc3048a --- /dev/null +++ b/packages/nextjs/components/nillion/Dropdown.tsx @@ -0,0 +1,36 @@ +import React, { useState } from "react"; + +interface DropdownOption { + value: string; + label: string; +} + +interface DropdownProps { + options: DropdownOption[]; + onDropdownUpdate: (value: string) => void; + disabled?: boolean; + itemName?: string; +} + +const Dropdown: React.FC = ({ options, onDropdownUpdate, itemName, disabled = false }) => { + const [selectedOption, setSelectedOption] = useState(null); + + const handleSelect = (e: React.ChangeEvent) => { + const selectedValue = e.target.value; + setSelectedOption(selectedValue); + onDropdownUpdate(selectedValue); + }; + + return ( + + ); +}; + +export default Dropdown; diff --git a/packages/nextjs/components/nillion/SecretForm.tsx b/packages/nextjs/components/nillion/SecretForm.tsx index 942b3ae..9d841b5 100644 --- a/packages/nextjs/components/nillion/SecretForm.tsx +++ b/packages/nextjs/components/nillion/SecretForm.tsx @@ -13,6 +13,9 @@ interface SecretFormProps { isDisabled?: boolean; isLoading?: boolean; secretType: "text" | "number"; // text for SecretBlob, number for SecretInteger + customSecretName?: boolean; + hidePermissions?: boolean; + itemName?: string; } const SecretForm: React.FC = ({ @@ -20,8 +23,12 @@ const SecretForm: React.FC = ({ secretName, isDisabled = false, isLoading = false, - secretType, // Destructure this prop + customSecretName = false, + secretType, + hidePermissions = false, + itemName = "secret", }) => { + const [secretNameFromForm, setSecretNameFromForm] = useState(secretName); const [secret, setSecret] = useState(""); const [loading, setLoading] = useState(isLoading); const [permissionedUserIdForRetrieveSecret, setPermissionedUserIdForRetrieveSecret] = useState(""); @@ -33,7 +40,7 @@ const SecretForm: React.FC = ({ e.preventDefault(); setLoading(true); onSubmit( - secretName, + secretNameFromForm, secret, permissionedUserIdForRetrieveSecret, permissionedUserIdForUpdateSecret, @@ -51,13 +58,31 @@ const SecretForm: React.FC = ({ "Storing secret..." ) : (
-

Store secret: {secretName}

+ {!customSecretName &&

Store secret: {secretName}

} + {customSecretName && ( +
+ + setSecretNameFromForm(e.target.value)} + required + disabled={isDisabled} + className={`mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm ${ + isDisabled ? "cursor-not-allowed bg-gray-100" : "bg-white" + }`} + /> +
+ )} +
setSecret(e.target.value)} @@ -70,7 +95,7 @@ const SecretForm: React.FC = ({
{/* can only compute on secret integers - don't show for SecretBlobs */} - {secretType === "number" && ( + {!hidePermissions && secretType === "number" && (
)} -
- - setPermissionedUserIdForRetrieveSecret(e.target.value)} - disabled={isDisabled} - className={`mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm ${ - isDisabled ? "cursor-not-allowed bg-gray-100" : "bg-white" - }`} - /> -
+ {!hidePermissions && ( +
+ + setPermissionedUserIdForRetrieveSecret(e.target.value)} + disabled={isDisabled} + className={`mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm ${ + isDisabled ? "cursor-not-allowed bg-gray-100" : "bg-white" + }`} + /> +
+ )} -
- - setPermissionedUserIdForUpdateSecret(e.target.value)} - disabled={isDisabled} - className={`mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm ${ - isDisabled ? "cursor-not-allowed bg-gray-100" : "bg-white" - }`} - /> -
+ {!hidePermissions && ( +
+ + setPermissionedUserIdForUpdateSecret(e.target.value)} + disabled={isDisabled} + className={`mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm ${ + isDisabled ? "cursor-not-allowed bg-gray-100" : "bg-white" + }`} + /> +
+ )} -
- - setPermissionedUserIdForDeleteSecret(e.target.value)} - disabled={isDisabled} - className={`mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm ${ - isDisabled ? "cursor-not-allowed bg-gray-100" : "bg-white" - }`} - /> -
+ {!hidePermissions && ( +
+ + setPermissionedUserIdForDeleteSecret(e.target.value)} + disabled={isDisabled} + className={`mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm ${ + isDisabled ? "cursor-not-allowed bg-gray-100" : "bg-white" + }`} + /> +
+ )}