Skip to content

Commit

Permalink
Merge pull request #328 from shawakash/account_ui
Browse files Browse the repository at this point in the history
feat: added acount/privatekey page
  • Loading branch information
shawakash authored Mar 29, 2024
2 parents 8fee153 + de6535c commit b8e10ff
Show file tree
Hide file tree
Showing 30 changed files with 686 additions and 166 deletions.
9 changes: 9 additions & 0 deletions .changeset/wise-suns-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@paybox/common": patch
"@paybox/recoil": patch
"@paybox/api": patch
"@paybox/backend-common": patch
"@paybox/zeus": patch
---

feat: encrypting private keys for account with password
25 changes: 25 additions & 0 deletions apps/web/app/account/[id]/privatekey/components/alert-msg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert"
import { RocketIcon } from "lucide-react"

export function AlertMsg({
message,
title = "Heads up!",
variant = "default",
}: {
message: string
title?: string
variant?: "default" | "destructive",
}) {
return (
<Alert variant={variant}>
<RocketIcon className="h-4 w-4" color="#ff4545" />
<AlertTitle className="text-[#ff4545]">{title}</AlertTitle>
<AlertDescription>{message}</AlertDescription>
</Alert>
)

}
121 changes: 121 additions & 0 deletions apps/web/app/account/[id]/privatekey/components/key-dialog-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { CopyIcon } from "@radix-ui/react-icons"

import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { AlertTriangle } from 'lucide-react';
import {
Card,
CardContent,
CardHeader,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { Textarea } from "@/components/ui/textarea";
import React from "react";
import { useRecoilValue } from "recoil";
import { accountPrivateKeysAtom } from "@paybox/recoil";

export function PrivateKeyDialogBox({
open,
setOpen,
// keys,
}: {
open: boolean,
setOpen: (open: boolean) => void,
// keys: { network: string, privateKey: string }[]
}) {
const keys = useRecoilValue(accountPrivateKeysAtom);
const [copyText, setCopyText] = React.useState<string>("Copy")

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="">
<Tabs defaultValue={keys[0].network} className="">
<DialogHeader>
<DialogTitle>
<TabsList className="grid w-2/3 grid-cols-2">
{keys.map((key, index) => (
<TabsTrigger key={index} value={key.network}>
{key.network}
</TabsTrigger>
))}
</TabsList>

</DialogTitle>
<DialogDescription>

</DialogDescription>
</DialogHeader>

{keys.map((key, index) => (
<TabsContent value={key.network} key={index}>
<Card className="rounded-b-none">
<CardHeader>
<Alert variant={"default"}>
<AlertTriangle className="h-4 w-4" color="#ff4545" />
<AlertTitle className="text-[#ff4545]">Caution!</AlertTitle>
<AlertDescription className="text-[#ff4545]">Do not share your private keys. Indiviual bearing this has full control on this account!</AlertDescription>
</Alert>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex flex-col items-center gap-y-4">
<div className="w-full">
<Label htmlFor="key" className="sr-only">
Key
</Label>
<Textarea
id="key"
defaultValue={key.privateKey}
readOnly
className="min-w-max resize-none w-full"
/>
</div>
<Button
type="submit"
size="sm"
className="px-3 flex gap-x-2 font-semibold"
onClick={() => {
navigator.clipboard.writeText(key.privateKey)
setCopyText("Copied!")
setTimeout(() => {
setCopyText("Copy")
}, 5000)
}}
>
<CopyIcon className="h-4 w-4" />
<div className="">
{copyText}
</div>
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
))}
<DialogFooter className="sm:justify-start">
<DialogClose asChild>
<Button type="button" variant="secondary" className="w-full rounded-t-none">
Close
</Button>
</DialogClose>
</DialogFooter>
</Tabs>
</DialogContent>
</Dialog>
)
}
140 changes: 140 additions & 0 deletions apps/web/app/account/[id]/privatekey/components/private-key-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"

import { toast } from "sonner";
import React from 'react';
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { AccountGetPrivateKey, BACKEND_URL, responseStatus } from "@paybox/common"
import { CardContent, CardFooter } from "@/components/ui/card"
import { PrivateKeyDialogBox } from "./key-dialog-box"
import { useRecoilState, useSetRecoilState } from "recoil"
import { accountPrivateKeysAtom } from "@paybox/recoil"
import { decryptWithPassword } from "@/lib/helper"


const PrivateKeyFrom = ({
accountId,
jwt
}: {
accountId: string,
jwt: string
}) => {

const [checked, setChecked] = React.useState<boolean>(false);
const [open, setOpen] = React.useState<boolean>(false);
const setPrivateKey = useSetRecoilState(accountPrivateKeysAtom);

const form = useForm<z.infer<typeof AccountGetPrivateKey>>({
resolver: zodResolver(AccountGetPrivateKey),
defaultValues: {
password: "",
accountId
},
});

function onSubmit(data: z.infer<typeof AccountGetPrivateKey>) {
const call = async () => {
const {status, sol, eth, hashPassword, msg}: {
status: responseStatus, sol: {privateKey: string}, eth: {privateKey: string}, hashPassword: string, msg: string
} = await fetch(`${BACKEND_URL}/account/privateKey`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${jwt}`
},
body: JSON.stringify(data),
cache: "force-cache"
}).then(res => res.json());

if (status == responseStatus.Error) {
return Promise.reject(msg);
}
return {status, sol, eth, hashPassword, msg};
}

toast.promise(call(), {
loading: "Fetching Private Key...",
success: ({status, sol, eth, hashPassword}) => {
setPrivateKey([
{network: "Solana", privateKey: decryptWithPassword(sol.privateKey, hashPassword)},
{network: "Ethereum", privateKey: decryptWithPassword(eth.privateKey, hashPassword)}
]);
setOpen(true);
return toast.success("Private Key Fetched Successfully");
},
error: (error) => {
return toast.error(error);
}
});
}

const setCheckedFn = (check: boolean) => {
setChecked(check)
}

return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="">
<CardContent>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Your Password..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter className="flex flex-col gap-y-5">
<div className="flex items-center space-x-2">
<Checkbox
id="terms_for_privateKey"
checked={checked}
onCheckedChange={setCheckedFn}
/>
<label
htmlFor="terms_for_privateKey"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I will not share my private key with anyone, including PayBox.
</label>
</div>
<Button className="w-full" disabled={!checked} type="submit">Submit</Button>
</CardFooter>
</form>
</Form>
{open &&
<PrivateKeyDialogBox
open={open}
setOpen={setOpen}
/>}
</>
)
}



export default PrivateKeyFrom

57 changes: 54 additions & 3 deletions apps/web/app/account/[id]/privatekey/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
export default function Page({ params }: { params: { id: string } }) {
import { AlertMsg } from "./components/alert-msg";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert"
import {
Card,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { EyeOff } from 'lucide-react';
import { RocketIcon } from "lucide-react"
import PrivateKeyFrom from "./components/private-key-form";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/util";

const getEncryptedPrivateKeys = async () => {

}

export default async function Page({ params }: { params: { id: string } }) {
console.log(params);
//TODO: FETCH THE TXN DATA FOR THIS FOR THIS ACCOUNT
return <div>From Private key: {params.id}</div>
const session = await getServerSession(authOptions);

return (
<>
<div className="flex flex-col gap-y-4 p-5">
<Card className="min-w-56 max-w-[500px]">
<CardHeader>
{/* <CardTitle>Private key</CardTitle> */}
<Alert variant={"default"}>
<RocketIcon className="h-4 w-4" color="#ff4545" />
<AlertTitle className="text-[#ff4545]">Heads up!</AlertTitle>
<AlertDescription>Your private is the only way to retreive your wallet!</AlertDescription>
</Alert>
<Alert variant={"default"}>
<EyeOff className="h-4 w-4" color="#ff4545" />
<AlertTitle className="text-[#ff4545]">Caution!</AlertTitle>
<AlertDescription>Don't let anyone see your private keys.</AlertDescription>
</Alert>
</CardHeader>
<PrivateKeyFrom
accountId={params.id}
key={params.id}
//@ts-ignore
jwt={session?.user.jwt}
/>
</Card>
</div>


</>
);

}
2 changes: 1 addition & 1 deletion apps/web/app/account/components/account-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function AccountSwitcher({
</SelectValue>
</SelectTrigger>
<SelectContent>
{accounts.map((account) => (
{accounts.map((account) => account && (
<SelectItem key={account.id} value={account.id}>
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
<Wallet className="h-5 w-5 " />
Expand Down
Loading

0 comments on commit b8e10ff

Please sign in to comment.