Skip to content

Commit

Permalink
Merge pull request #8 from NillionNetwork/pw-manager
Browse files Browse the repository at this point in the history
add a password manager app demo
  • Loading branch information
oceans404 authored Apr 12, 2024
2 parents 4650b14 + 94a3ed1 commit cd448b2
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 55 deletions.
241 changes: 241 additions & 0 deletions packages/nextjs/app/nillion-pw-manager/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
"use client";

import { useEffect, useState } from "react";
import type * as NillionTypes from "@nillion/nillion-client-js-browser/nillion_client_js_browser.d.ts";
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<boolean>(false);
const [userKey, setUserKey] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [nillion, setNillion] = useState<any>(null);
const [nillionClient, setNillionClient] = useState<any>(null);
const [selectedSecretName, setSelectedSecretName] = useState<string>("");
const [selectedStoreId, setSelectedStoreId] = useState<string | null>(null);
const [latestSecretName, setLatestSecretName] = useState<string | null>(null);
const [storedSecrets, setStoredSecrets] = useState<StoredSecrets>({});
const [retrievedValue, setRetrievedValue] = useState<string | null>(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 (
<>
<div className="flex items-center flex-col pt-10">
<div className="px-5 flex flex-col">
<h1 className="text-xl">
<span className="block text-4xl font-bold text-center">Nillion Password Manager</span>

{!connectedAddress && <p>Connect your MetaMask Flask wallet</p>}
{connectedAddress && connectedToSnap && !userKey && (
<a target="_blank" href="https://nillion-snap-site.vercel.app/" rel="noopener noreferrer">
<button className="btn btn-sm btn-primary mt-4">
No Nillion User Key - Generate and store user key here
</button>
</a>
)}
</h1>

{connectedAddress && (
<div className="flex justify-center items-center space-x-2">
<p className="my-2 font-medium">Connected Wallet Address:</p>
<Address address={connectedAddress} />
</div>
)}

{connectedAddress && !connectedToSnap && (
<button className="btn btn-sm btn-primary mt-4" onClick={handleConnectToSnap}>
Connect to Snap with your Nillion User Key
</button>
)}

{connectedToSnap && (
<div>
{userKey && (
<div>
<div className="flex justify-center items-center space-x-2">
<p className="my-2 font-medium">
🤫 Nillion User Key from{" "}
<a target="_blank" href="https://nillion-snap-site.vercel.app/" rel="noopener noreferrer">
MetaMask Flask
</a>
:
</p>

<CopyString str={userKey} />
</div>

{userId && (
<div className="flex justify-center items-center space-x-2">
<p className="my-2 font-medium">Connected as Nillion User ID:</p>
<CopyString str={userId} />
</div>
)}
</div>
)}
</div>
)}
</div>

<div className="flex-grow bg-base-300 w-full mt-16 px-8 py-12">
<div className="flex justify-center items-center gap-12 flex-col sm:flex-row">
{!connectedToSnap ? (
<NillionOnboarding />
) : (
<div className="flex flex-row justify-between">
{/* Store secret blob */}
<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center w-full rounded-3xl my-2 justify-between mx-5">
<h1 className="text-xl">Store a new password</h1>
<div className="flex flex-row w-full justify-between items-center my-10 mx-10">
<div className="flex-1 px-2">
{latestSecretName ? (
<>
<RetrieveSecretCommand
secretType="SecretBlob"
userKey={userKey}
storeId={storedSecrets[latestSecretName]}
secretName={latestSecretName || ""}
/>
<button className="btn btn-sm btn-primary mt-4" onClick={resetForm}>
Add another password
</button>
</>
) : (
<SecretForm
secretName={""}
onSubmit={handleSecretFormSubmit}
secretType="text"
isLoading={false}
// use customSecretName boolean prop to signal that the form should set the secret name
customSecretName
hidePermissions
itemName="password"
/>
)}
</div>
</div>
</div>

{/* Retrieve secret blob */}

<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center w-full rounded-3xl my-2 justify-between mx-5">
<h1 className="text-xl">Retrieve passwords from Nillion</h1>
<div className="flex flex-row w-full justify-between items-center my-10 mx-10">
<div className="flex-1 px-2 flex-col">
<div>
<Dropdown
options={Object.keys(storedSecrets).map(s => ({ value: s, label: s }))}
onDropdownUpdate={selectedName => handleSecretDropdownSelection(selectedName)}
itemName="a password"
disabled={Object.keys(storedSecrets).length === 0}
/>
</div>

<button
className="btn btn-sm btn-primary mt-4"
onClick={() => handleRetrieveSecretBlob(selectedStoreId || "", selectedSecretName)}
disabled={!selectedStoreId}
>
Retrieve and decode {selectedSecretName}
</button>

{retrievedValue && <p>✅ Retrieved value: {retrievedValue}</p>}
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</>
);
};

export default Home;
13 changes: 13 additions & 0 deletions packages/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ const Home: NextPage = () => {
packages/hardhat/contracts
</code>
</p>

<div className="flex flex-col bg-base-100 px-10 py-10 text-center items-center rounded-3xl">
<span className="block text-2xl font-bold pb-4">Nillion demo apps</span>
<Link href="/nillion-pw-manager" passHref className="link">
Nillion Password Manager
</Link>{" "}
<Link href="/nillion-hello-world-complete" passHref className="link">
Nillion Hello World
</Link>{" "}
<Link href="/nillion-compute" passHref className="link">
Nillion Blind Compute
</Link>{" "}
</div>
</div>

<div className="flex-grow bg-base-300 w-full mt-16 px-8 py-12">
Expand Down
6 changes: 3 additions & 3 deletions packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ export const menuLinks: HeaderMenuLink[] = [
href: "/",
},
{
label: "🖥️ Nillion Blind Computation Demo",
label: "🖥️ Blind Computation",
href: "/nillion-compute",
},
{
label: "🎯 Nillion Hello World",
label: "🎯 Hello World",
href: "/nillion-hello-world",
},
{
label: "✅ Nillion Hello World Demo",
label: "✅ Hello World",
href: "/nillion-hello-world-complete",
},
{
Expand Down
36 changes: 36 additions & 0 deletions packages/nextjs/components/nillion/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -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<DropdownProps> = ({ options, onDropdownUpdate, itemName, disabled = false }) => {
const [selectedOption, setSelectedOption] = useState<string | null>(null);

const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValue = e.target.value;
setSelectedOption(selectedValue);
onDropdownUpdate(selectedValue);
};

return (
<select value={selectedOption || ""} onChange={e => handleSelect(e)} disabled={disabled}>
<option value="">Select {itemName} name</option>
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.label}
</option>
))}
</select>
);
};

export default Dropdown;
Loading

0 comments on commit cd448b2

Please sign in to comment.