diff --git a/backend/backend/graphene/types.py b/backend/backend/graphene/types.py index f6144b263..2eb1d2ccd 100644 --- a/backend/backend/graphene/types.py +++ b/backend/backend/graphene/types.py @@ -38,7 +38,7 @@ def resolve_recovery(self, info): user=info.context.user, organisation=self, deleted_at=None) return org_member.wrapped_recovery - def resolve_idenity_key(self, info): + def resolve_identity_key(self, info): org_member = OrganisationMember.objects.get( user=info.context.user, organisation=self, deleted_at=None) return org_member.identity_key diff --git a/frontend/app/[team]/newdevice/page.tsx b/frontend/app/[team]/newdevice/page.tsx index 5facf3fa7..d119422b8 100644 --- a/frontend/app/[team]/newdevice/page.tsx +++ b/frontend/app/[team]/newdevice/page.tsx @@ -34,7 +34,7 @@ export default function NewDevice({ params }: { params: { team: string } }) { icon: , title: 'Sudo password', description: - "Please set up a strong 'sudo' password to continue. This will be used to to perform administrative tasks and to encrypt keys locally on this device.", + "Please set up a strong 'sudo' password to continue. This will be used to encrypt keys and perform administrative tasks.", }, ]) @@ -74,7 +74,7 @@ export default function NewDevice({ params }: { params: { team: string } }) { icon: , title: 'Sudo password', description: - "Please set up a strong 'sudo' password to continue. This will be used to to perform administrative tasks and to encrypt keys locally on this device.", + "Please set up a strong 'sudo' password to continue. This will be used to encrypt keys and perform administrative tasks.", }, ]) diff --git a/frontend/app/[team]/settings/page.tsx b/frontend/app/[team]/settings/page.tsx index c2e33cad5..6cb7cc37f 100644 --- a/frontend/app/[team]/settings/page.tsx +++ b/frontend/app/[team]/settings/page.tsx @@ -4,14 +4,16 @@ import { Alert } from '@/components/common/Alert' import { Avatar } from '@/components/common/Avatar' import { Button } from '@/components/common/Button' import { ModeToggle } from '@/components/common/ModeToggle' -import { AccountSeedGen } from '@/components/onboarding/AccountSeedGen' +import { AccountRecovery } from '@/components/onboarding/AccountRecovery' import { RoleLabel } from '@/components/users/RoleLabel' import { organisationContext } from '@/contexts/organisationContext' import { cryptoUtils } from '@/utils/auth' +import { copyRecoveryKit, generateRecoveryPdf } from '@/utils/recovery' import { Dialog, Transition } from '@headlessui/react' import { useSession } from 'next-auth/react' import { Fragment, useContext, useState } from 'react' import { FaEye, FaEyeSlash, FaMoon, FaSun, FaTimes } from 'react-icons/fa' +import { toast } from 'react-toastify' const ViewRecoveryDialog = () => { const { activeOrganisation } = useContext(organisationContext) @@ -49,22 +51,46 @@ const ViewRecoveryDialog = () => { setIsOpen(true) } + const handleDownloadRecoveryKit = async () => { + toast.promise( + generateRecoveryPdf( + recovery, + session?.user?.email!, + activeOrganisation!.name, + session?.user?.name || undefined + ), + { + pending: 'Generating recovery kit', + success: 'Downloaded recovery kit', + } + ) + } + + const handleCopyRecoveryKit = () => { + copyRecoveryKit( + recovery, + session?.user?.email!, + activeOrganisation!.name, + session?.user?.name || undefined + ) + } + return ( <>
-

Your recovery phrase is encrypted.

+

Your account keys are encrypted.

- Backup your account recovery phrase in a safe place if you haven't already. If - you forget your sudo password, it is the only way to restore your accout keys. + Store your account recovery kit in a safe place if you haven't already. If you + forget your sudo password, it is the only way to restore your accout keys.

@@ -106,7 +132,13 @@ const ViewRecoveryDialog = () => {
- {recovery && } + {recovery && ( + + )} {!recovery && (
@@ -195,7 +227,7 @@ export default function Settings({ params }: { params: { team: string } }) {
-
Recovery phrase
+
Recovery
diff --git a/frontend/app/invite/[invite]/page.tsx b/frontend/app/invite/[invite]/page.tsx index e83933a73..b7bce32a4 100644 --- a/frontend/app/invite/[invite]/page.tsx +++ b/frontend/app/invite/[invite]/page.tsx @@ -3,7 +3,7 @@ import { cryptoUtils } from '@/utils/auth' import VerifyInvite from '@/graphql/queries/organisation/validateOrganisationInvite.gql' import AcceptOrganisationInvite from '@/graphql/mutations/organisation/acceptInvite.gql' -import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { useLazyQuery, useMutation } from '@apollo/client' import { HeroPattern } from '@/components/common/HeroPattern' import { Button } from '@/components/common/Button' import { FaArrowRight } from 'react-icons/fa' @@ -11,14 +11,14 @@ import Loading from '@/app/loading' import { useEffect, useState } from 'react' import { Step, Stepper } from '@/components/onboarding/Stepper' import { AccountPassword } from '@/components/onboarding/AccountPassword' -import { AccountSeedChecker } from '@/components/onboarding/AccountSeedChecker' -import { AccountSeedGen } from '@/components/onboarding/AccountSeedGen' -import { MdKey, MdOutlineVerifiedUser, MdOutlinePassword } from 'react-icons/md' +import { AccountRecovery } from '@/components/onboarding/AccountRecovery' +import { MdKey, MdOutlinePassword } from 'react-icons/md' import { toast } from 'react-toastify' import { OrganisationMemberInviteType } from '@/apollo/graphql' import { useSession } from 'next-auth/react' import { setLocalKeyring } from '@/utils/localStorage' import { Logo } from '@/components/common/Logo' +import { copyRecoveryKit, generateRecoveryPdf } from '@/utils/recovery' const bip39 = require('bip39') @@ -49,9 +49,9 @@ export default function Invite({ params }: { params: { invite: string } }) { const [showWelcome, setShowWelcome] = useState(true) const [step, setStep] = useState(0) - const [recoverySkipped, setRecoverySkipped] = useState(false) + + const [recoveryDownloaded, setRecoveryDownloaded] = useState(false) const [success, setSuccess] = useState(false) - const [inputs, setInputs] = useState>([]) const [pw, setPw] = useState('') const [pw2, setPw2] = useState('') const [mnemonic, setMnemonic] = useState('') @@ -73,26 +73,19 @@ export default function Invite({ params }: { params: { invite: string } }) { const steps: Step[] = [ { index: 0, - name: 'Set up recovery phrase', - icon: , - title: 'Recovery', + name: 'Sudo Password', + icon: , + title: 'Set a sudo password', description: - "This is your 24 word recovery phrase. You can use it log in to your Phase account if you forget the sudo password. It's used to derive your encryption keys. Only you have access to it. Please write it down or store it somewhere safe like a password manager.", + 'This will be used to encrypt your account keys. You will be need to enter this password to perform administrative tasks.', }, { index: 1, - name: 'Verify recovery phrase', - icon: , - title: 'Verify recovery phrase', - description: 'Please enter the your recovery phrase in the correct order below.', - }, - { - index: 2, - name: 'Sudo password', - icon: , - title: 'Set a sudo password', + name: 'Account recovery', + icon: , + title: 'Account Recovery', description: - 'Please set up a strong sudo password to continue. This will be used to to perform administrative tasks and to secure your account keys.', + 'If you forget your sudo password, you will need to use a recovery kit to regain access to your account.', }, ] @@ -160,23 +153,15 @@ export default function Invite({ params }: { params: { invite: string } }) { }) } - const handleInputUpdate = (newValue: string, index: number) => { - if (newValue.split(' ').length === 24) { - setInputs(newValue.split(' ')) - } else setInputs(inputs.map((input: string, i: number) => (index === i ? newValue : input))) - } - const validateCurrentStep = () => { - if (step === 1 && !recoverySkipped) { - if (inputs.join(' ') !== mnemonic && !recoverySkipped) { - errorToast('Incorrect account recovery key!') - return false - } - } else if (step === 2) { + if (step === 0) { if (pw !== pw2) { errorToast("Passwords don't match") return false } + } else if (step === 1 && !recoveryDownloaded) { + errorToast('Please download the your account recovery kit!') + return false } return true } @@ -198,20 +183,11 @@ export default function Invite({ params }: { params: { invite: string } }) { if (step !== 0) setStep(step - 1) } - const skipRecoverySteps = () => { - setRecoverySkipped(true) - setStep(2) - } - useEffect(() => { setMnemonic(bip39.generateMnemonic(256)) const id = crypto.randomUUID() }, []) - useEffect(() => { - setInputs([...Array(mnemonic.split(' ').length)].map(() => '')) - }, [mnemonic]) - const WelcomePane = () => (
@@ -242,8 +218,8 @@ export default function Invite({ params }: { params: { invite: string } }) { return (
-

Success!

-

Your account is setup!

+

You're All Set

+

Your account is ready to go!

- )} -
diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx index 7585e9cda..d028301ba 100644 --- a/frontend/app/signup/page.tsx +++ b/frontend/app/signup/page.tsx @@ -4,41 +4,34 @@ import { Button } from '@/components/common/Button' import { HeroPattern } from '@/components/common/HeroPattern' import { Step, Stepper } from '@/components/onboarding/Stepper' import { useEffect, useState } from 'react' -import { - MdOutlineVerifiedUser, - MdGroups, - MdOutlineKey, - MdKey, - MdOutlinePassword, -} from 'react-icons/md' +import { MdGroups, MdKey, MdOutlinePassword } from 'react-icons/md' import { TeamName } from '@/components/onboarding/TeamName' -import { AccountSeedGen } from '@/components/onboarding/AccountSeedGen' -import { AccountSeedChecker } from '@/components/onboarding/AccountSeedChecker' +import { AccountRecovery } from '@/components/onboarding/AccountRecovery' import { AccountPassword } from '@/components/onboarding/AccountPassword' import { cryptoUtils } from '@/utils/auth' import { useSession } from 'next-auth/react' import { toast } from 'react-toastify' -import { gql, useMutation } from '@apollo/client' +import { useMutation } from '@apollo/client' import { useRouter } from 'next/navigation' -import Link from 'next/link' import { CreateOrg } from '@/graphql/mutations/createOrganisation.gql' import { setLocalKeyring } from '@/utils/localStorage' +import { copyRecoveryKit, generateRecoveryPdf } from '@/utils/recovery' const bip39 = require('bip39') const Onboard = () => { const { data: session } = useSession() - const [name, setName] = useState('') + const [teamName, setTeamName] = useState('') const [pw, setPw] = useState('') const [pw2, setPw2] = useState('') const [mnemonic, setMnemonic] = useState('') const [orgId, setOrgId] = useState('') const [inputs, setInputs] = useState>([]) const [step, setStep] = useState(0) - const [showWelcome, setShowWelcome] = useState(true) + const [createOrganisation, { data, loading, error }] = useMutation(CreateOrg) const [isloading, setIsLoading] = useState(false) - const [seedDownloaded, setSeedDownloaded] = useState(false) + const [recoveryDownloaded, setRecoveryDownloaded] = useState(false) const [success, setSuccess] = useState(false) const router = useRouter() @@ -46,61 +39,55 @@ const Onboard = () => { toast.error(message) } - const handleInputUpdate = (newValue: string, index: number) => { - if (newValue.split(' ').length === 24) { - setInputs(newValue.split(' ')) - } else setInputs(inputs.map((input: string, i: number) => (index === i ? newValue : input))) - } - const steps: Step[] = [ { index: 0, - name: 'Team name', + name: 'Team Name', icon: , title: 'Choose a name for your team', - description: 'Your team name must be alphanumeric.', + description: ( +
+ Your team name can be alphanumeric. + +
[a-zA-Z0-9]
+
+
+ ), }, { index: 1, - name: 'Set up recovery phrase', - icon: , - title: 'Recovery', + name: 'Sudo Password', + icon: , + title: 'Set a sudo password', description: - "This is your 24 word recovery phrase. You can use it log in to your Phase account if you forget the sudo password. It's used to derive your encryption keys. Only you have access to it. Please write it down or store it somewhere safe like a password manager.", + 'This will be used to encrypt your account keys. You will be need to enter this password to perform administrative tasks.', }, { index: 2, - name: 'Verify recovery phrase', - icon: , - title: 'Verify recovery phrase', - description: 'Please enter the your recovery phrase in the correct order below.', - }, - { - index: 3, - name: 'Sudo password', - icon: , - title: 'Set a sudo password', + name: 'Account recovery', + icon: , + title: 'Account Recovery', description: - 'Please set up a strong sudo password to continue. This will be used to to perform administrative tasks and to encrypt keys locally on this device.', + 'If you forget your sudo password, you will need to use a recovery kit to regain access to your account.', }, ] const validateCurrentStep = () => { if (step === 0) { - if (!name) { + if (!teamName) { errorToast('Please enter a team name') //return false } - } else if (step === 2) { - if (inputs.join(' ') !== mnemonic && !seedDownloaded) { - errorToast('Incorrect account recovery key!') - return false // TODO: UNCOMMENT THIS!! - } - } else if (step === 3) { + } else if (step === 1) { if (pw !== pw2) { errorToast("Passwords don't match") return false } + } else if (step === 2) { + if (!recoveryDownloaded) { + errorToast('Please download the your account recovery kit!') + return false + } } return true } @@ -132,6 +119,28 @@ const Onboard = () => { ) } + const handleDownloadRecoveryKit = async () => { + toast + .promise( + generateRecoveryPdf( + mnemonic, + session?.user?.email!, + teamName, + session?.user?.name || undefined + ), + { + pending: 'Generating recovery kit', + success: 'Downloaded recovery kit', + } + ) + .then(() => setRecoveryDownloaded(true)) + } + + const handleCopyRecoveryKit = () => { + copyRecoveryKit(mnemonic, session?.user?.email!, teamName, session?.user?.name || undefined) + setRecoveryDownloaded(true) + } + const handleAccountInit = async () => { return new Promise(async (resolve, reject) => { setIsLoading(true) @@ -141,7 +150,7 @@ const Onboard = () => { const result = await createOrganisation({ variables: { id: orgId, - name, + name: teamName, identityKey: publicKey, wrappedKeyring: encryptedKeyring, wrappedRecovery: encryptedMnemonic, @@ -182,10 +191,6 @@ const Onboard = () => { if (step !== 0) setStep(step - 1) } - const skipSeedCheckerStep = () => { - if (seedDownloaded) setStep(3) - } - useEffect(() => { setMnemonic(bip39.generateMnemonic(256)) const id = crypto.randomUUID() @@ -202,34 +207,16 @@ const Onboard = () => { setInputs([...Array(mnemonic.split(' ').length)].map(() => '')) }, [mnemonic]) - const WelcomePane = () => { - return ( -
-

- Welcome to Phase -

-

- Setting up your account will take just a few minutes -

-
- -
-
- ) - } - const SuccessPane = () => { return (
-

Success!

-

Your account is setup!

+

You're All Set

+

Your account is ready to go!

@@ -243,27 +230,29 @@ const Onboard = () => {
- {showWelcome && } - {!showWelcome && !success && ( + {!success && (
+ {step >= 0 && ( +
+ Welcome to Phase +
+ )}
- {step === 0 && } - {step === 1 && } + {step === 0 && } + {step === 1 && } {step === 2 && ( - )} - {step === 3 && }
@@ -274,13 +263,13 @@ const Onboard = () => { )}
- {seedDownloaded && step === 2 && ( - - )} -
diff --git a/frontend/components/onboarding/AccountRecovery.tsx b/frontend/components/onboarding/AccountRecovery.tsx new file mode 100644 index 000000000..fcb286fd2 --- /dev/null +++ b/frontend/components/onboarding/AccountRecovery.tsx @@ -0,0 +1,47 @@ +import { FaCopy, FaFileDownload } from 'react-icons/fa' +import { Button } from '../common/Button' +import { Alert } from '../common/Alert' + +export const AccountRecovery = (props: { + mnemonic: string + onDownload: Function + onCopy: Function +}) => { + return ( +
+
+
Recovery kit
+

+ This recovery kit contains your account recovery phrase along with your account name, + email and other information to help you recover your account if you get locked out. +

+

+ Please download the recovery kit and keep it somewhere safe. You can also copy this + recovery kit and store it in a password manager. +

+ + +

+ If you forget your sudo password and lose your{' '} + recovery kit, your account cannot be recovered! +

+
+ +
+ + +
+
+
+ ) +} diff --git a/frontend/components/onboarding/AccountSeedGen.tsx b/frontend/components/onboarding/AccountSeedGen.tsx deleted file mode 100644 index a249e3c45..000000000 --- a/frontend/components/onboarding/AccountSeedGen.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { copyToClipBoard } from '@/utils/clipboard' -import { FaCopy } from 'react-icons/fa' -import { toast } from 'react-toastify' -import { Button } from '../common/Button' - -export const AccountSeedGen = (props: { mnemonic: string }) => { - const handleCopyClick = async () => { - const copied = await copyToClipBoard(props.mnemonic) - copied ? toast.success('Copied to clipboard') : toast.error('Failed to copy') - } - - return ( -
-
- -
- {props.mnemonic.split(' ').map((word: string, index: number) => ( -
- {index + 1} - {word} - ***** -
- ))} -
- ) -} diff --git a/frontend/components/onboarding/Stepper.tsx b/frontend/components/onboarding/Stepper.tsx index b4084679b..f2a5d7c8a 100644 --- a/frontend/components/onboarding/Stepper.tsx +++ b/frontend/components/onboarding/Stepper.tsx @@ -5,7 +5,7 @@ export type Step = { name: string icon: React.ReactNode title: string - description: string + description: React.ReactNode } interface StepperProps { @@ -78,7 +78,7 @@ export const Stepper = (props: StepperProps) => {
{props.steps[props.activeStep].title}
-

+

{props.steps[props.activeStep].description}

diff --git a/frontend/package.json b/frontend/package.json index 491ae57d6..377eb35a3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "graphql-tag": "^2.12.6", "install": "^0.13.0", "jsonwebtoken": "^9.0.0", + "jspdf": "^2.5.1", "libsodium-wrappers-sumo": "^0.7.11", "next": "13.1.6", "next-auth": "^4.20.1", diff --git a/frontend/public/assets/images/decrypt.svg b/frontend/public/assets/images/decrypt.svg deleted file mode 100644 index 43d5f2637..000000000 --- a/frontend/public/assets/images/decrypt.svg +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/assets/images/gradient-1.svg b/frontend/public/assets/images/gradient-1.svg deleted file mode 100644 index 08aa135e7..000000000 --- a/frontend/public/assets/images/gradient-1.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/assets/images/gradient-purple.svg b/frontend/public/assets/images/gradient-purple.svg deleted file mode 100644 index 8d299084f..000000000 --- a/frontend/public/assets/images/gradient-purple.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/assets/images/grello-gradient.svg b/frontend/public/assets/images/grello-gradient.svg deleted file mode 100644 index dd07a8e75..000000000 --- a/frontend/public/assets/images/grello-gradient.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/assets/images/hero-bg.webp b/frontend/public/assets/images/hero-bg.webp deleted file mode 100644 index 029183ca6..000000000 Binary files a/frontend/public/assets/images/hero-bg.webp and /dev/null differ diff --git a/frontend/public/assets/images/logo.png b/frontend/public/assets/images/logo.png new file mode 100644 index 000000000..7ff7fbea6 Binary files /dev/null and b/frontend/public/assets/images/logo.png differ diff --git a/frontend/public/assets/images/meta.png b/frontend/public/assets/images/meta.png deleted file mode 100644 index c9f42cc71..000000000 Binary files a/frontend/public/assets/images/meta.png and /dev/null differ diff --git a/frontend/public/assets/images/sandbox.svg b/frontend/public/assets/images/sandbox.svg deleted file mode 100644 index c42fe442f..000000000 --- a/frontend/public/assets/images/sandbox.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/frontend/utils/recovery.ts b/frontend/utils/recovery.ts new file mode 100644 index 000000000..0e47a8780 --- /dev/null +++ b/frontend/utils/recovery.ts @@ -0,0 +1,166 @@ +import jsPDF from 'jspdf' +import { toast } from 'react-toastify' +import { copyToClipBoard } from './clipboard' + +const PHASE_LOGO = `data:image/png;base64,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` + +export const generateRecoveryPdf = async ( + mnemonic: string, + email: string, + organisation: string, + name?: string +) => { + const title = 'Phase Recovery Kit' + const subtitle = `This is a recovery kit for your Phase account. \nYou can use this to recover your account keys if you forget your sudo password.` + const hostname = `${window.location.protocol}//${window.location.host}` + + // Create a new jsPDF instance + const pdf = new jsPDF() + + // Draw the black rectangle for the header + pdf.setFillColor(0, 0, 0) + pdf.rect(0, 0, pdf.internal.pageSize.getWidth(), 60, 'F') + + // Set the title + pdf.setTextColor(255, 255, 255) + pdf.setFontSize(20) + pdf.setFont('helvetica', 'bold') + pdf.text(title, 10, 25) + + // Set the subtitle + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'regular', '400') + pdf.setFontSize(11) + pdf.text(subtitle, 10, 35) + + // Add the logo + const imgProps = pdf.getImageProperties(PHASE_LOGO) + const imgWidth = 30 + const imgHeight = (imgProps.height * imgWidth) / imgProps.width // scale the height to maintain aspect ratio + const pageWidth = pdf.internal.pageSize.getWidth() + pdf.addImage(PHASE_LOGO, 'PNG', pageWidth - imgWidth - 10, 10, imgWidth, imgHeight) + + const lineSpace = 6 + const paragraphSpace = 12 + + // Define cursor x and y starting positions + let xPosition = 10 + let yPosition = 80 + + //Name + if (name) { + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Name', xPosition, yPosition) + yPosition += lineSpace + + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(14) + pdf.text(name, xPosition, yPosition) + yPosition += paragraphSpace + } + + //Email + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Email', xPosition, yPosition) + yPosition += lineSpace + + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(14) + pdf.text(email, xPosition, yPosition) + yPosition += paragraphSpace + + //Org + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Organisation', xPosition, yPosition) + yPosition += lineSpace + + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(14) + pdf.text(organisation, xPosition, yPosition) + yPosition += paragraphSpace + + //Phase instance host + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Login URL', xPosition, yPosition) + yPosition += lineSpace + + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(14) + pdf.text(hostname, xPosition, yPosition) + yPosition += paragraphSpace * 2 + + //Mnemonic + pdf.setTextColor(115, 115, 115) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(11) + pdf.text('Recovery phrase', xPosition, yPosition) + yPosition += lineSpace + + // Define the size of the grid cells + const cellWidth = pdf.internal.pageSize.getWidth() / 4 + const cellHeight = 10 + + // Split the mnemonic into words + const words = mnemonic.split(' ') + + // Loop over each word and place it in the PDF + words.forEach((word, index) => { + // Add the word number before the word + pdf.setFontSize(14) + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'bold') + pdf.text(word, xPosition, yPosition) + + // Increment the x position to the next column + xPosition += cellWidth + + // If we've reached the end of a row, reset x and increment y + if ((index + 1) % 4 === 0) { + xPosition = 10 + yPosition += cellHeight + } + }) + + yPosition += 10 + pdf.setTextColor(23, 23, 23) + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(10) + pdf.text(`Generated on ${new Date().toDateString()}`, 10, 280) + + // Save the PDF + pdf.save(`phase-recovery-kit--${organisation}.pdf`) +} + +export const copyRecoveryKit = async ( + mnemonic: string, + email: string, + organisation: string, + name?: string +) => { + const hostname = `${window.location.protocol}//${window.location.host}` + + const recoveryKit = ` + Phase Recovery Kit\n\n + ${name ? `Name: ${name}` : ''}\n + Email: ${email}\n + Organsation: ${organisation}\n + LoginUrl: ${hostname}\n + Recovery phrase: ${mnemonic}\n + Generated on ${new Date().toDateString()} + ` + + const copied = await copyToClipBoard(recoveryKit) + copied ? toast.info('Copied to clipboard', { autoClose: 2000 }) : toast.error('Failed to copy') +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 2a723259c..f8a69e376 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -481,6 +481,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0": version "7.21.0" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz" @@ -1647,6 +1654,11 @@ version "15.7.5" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" +"@types/raf@^3.4.0": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.2.tgz#4e2ea094fcce9ed693cd236e530c0c88f2e09e68" + integrity sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A== + "@types/react-dom@18.0.11": version "18.0.11" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz" @@ -2070,6 +2082,11 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + auto-bind@~4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" @@ -2151,6 +2168,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -2239,6 +2261,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" @@ -2358,6 +2385,20 @@ caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001517: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz#44b87a406c901269adcdb834713e23582dd71856" integrity sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg== +canvg@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3" + integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/raf" "^3.4.0" + core-js "^3.8.3" + raf "^3.4.1" + regenerator-runtime "^0.13.7" + rgbcolor "^1.0.1" + stackblur-canvas "^2.0.0" + svg-pathdata "^6.0.3" + capital-case@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" @@ -2670,6 +2711,11 @@ cookie@^0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" +core-js@^3.6.0, core-js@^3.8.3: + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.2.tgz#312bbf6996a3a517c04c99b9909cdd27138d1ceb" + integrity sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ== + cosmiconfig@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.0.0.tgz#e9feae014eab580f858f8a0288f38997a7bebe97" @@ -2717,6 +2763,13 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" @@ -2989,6 +3042,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dompurify@^2.2.0: + version "2.4.7" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc" + integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ== + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -3491,7 +3549,7 @@ fbjs@^3.0.0: setimmediate "^1.0.5" ua-parser-js "^1.0.35" -fflate@^0.4.1: +fflate@^0.4.1, fflate@^0.4.8: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== @@ -3948,6 +4006,14 @@ hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: dependencies: lru-cache "^7.5.1" +html2canvas@^1.0.0-rc.5: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" @@ -4503,6 +4569,21 @@ jsonwebtoken@^9.0.0: ms "^2.1.1" semver "^7.3.8" +jspdf@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-2.5.1.tgz#00c85250abf5447a05f3b32ab9935ab4a56592cc" + integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA== + dependencies: + "@babel/runtime" "^7.14.0" + atob "^2.1.2" + btoa "^1.2.1" + fflate "^0.4.8" + optionalDependencies: + canvg "^3.0.6" + core-js "^3.6.0" + dompurify "^2.2.0" + html2canvas "^1.0.0-rc.5" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" @@ -5667,6 +5748,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -5875,6 +5961,13 @@ quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + rdk@>=6.4.4: version "6.4.4" resolved "https://registry.npmjs.org/rdk/-/rdk-6.4.4.tgz" @@ -6040,7 +6133,7 @@ reaviz@^14.4.2: react-fast-compare "^3.2.1" transformation-matrix "^2.9.0" -regenerator-runtime@^0.13.11: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.7: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== @@ -6158,6 +6251,11 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rgbcolor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" + integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw== + right-align@^0.1.1: version "0.1.3" resolved "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz" @@ -6429,6 +6527,11 @@ ssri@^9.0.0: dependencies: minipass "^3.1.1" +stackblur-canvas@^2.0.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz#7876bab4ea99bfc97b69ce662614d7a1afb2d71b" + integrity sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg== + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz" @@ -6558,6 +6661,11 @@ supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" +svg-pathdata@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" + integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== + swap-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-2.0.2.tgz#671aedb3c9c137e2985ef51c51f9e98445bf70d9" @@ -6644,6 +6752,13 @@ tar@^6.1.11, tar@^6.1.13, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" @@ -6906,6 +7021,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"