Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: onboarding #105

Merged
merged 8 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/backend/graphene/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/[team]/newdevice/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function NewDevice({ params }: { params: { team: string } }) {
icon: <MdOutlineKey />,
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.",
},
])

Expand Down Expand Up @@ -74,7 +74,7 @@ export default function NewDevice({ params }: { params: { team: string } }) {
icon: <MdOutlineKey />,
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.",
},
])

Expand Down
46 changes: 39 additions & 7 deletions frontend/app/[team]/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 (
<>
<div className="flex flex-col gap-4">
<Alert variant="info" icon={true}>
<div className="flex flex-col gap-2">
<p>Your recovery phrase is encrypted.</p>
<p>Your account keys are encrypted.</p>

<p>
Backup your account recovery phrase in a safe place if you haven&apos;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&apos;t already. If you
forget your sudo password, it is the only way to restore your accout keys.
</p>
</div>
</Alert>
<div>
<Button variant="primary" onClick={openModal} title="View recovery">
<FaEye /> View recovery
<FaEye /> View recovery info
</Button>
</div>
</div>
Expand Down Expand Up @@ -106,7 +132,13 @@ const ViewRecoveryDialog = () => {
</Dialog.Title>

<div className="py-4">
{recovery && <AccountSeedGen mnemonic={recovery} />}
{recovery && (
<AccountRecovery
mnemonic={recovery}
onDownload={handleDownloadRecoveryKit}
onCopy={handleCopyRecoveryKit}
/>
)}

{!recovery && (
<form onSubmit={handleDecryptRecovery}>
Expand Down Expand Up @@ -195,7 +227,7 @@ export default function Settings({ params }: { params: { team: string } }) {
</div>

<div className="flex flex-col gap-4">
<div className="text-lg font-medium">Recovery phrase</div>
<div className="text-lg font-medium">Recovery</div>
<ViewRecoveryDialog />
</div>

Expand Down
116 changes: 59 additions & 57 deletions frontend/app/invite/[invite]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
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'
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')

Expand Down Expand Up @@ -49,9 +49,9 @@ export default function Invite({ params }: { params: { invite: string } }) {

const [showWelcome, setShowWelcome] = useState<boolean>(true)
const [step, setStep] = useState<number>(0)
const [recoverySkipped, setRecoverySkipped] = useState<boolean>(false)

const [recoveryDownloaded, setRecoveryDownloaded] = useState<boolean>(false)
const [success, setSuccess] = useState<boolean>(false)
const [inputs, setInputs] = useState<Array<string>>([])
const [pw, setPw] = useState<string>('')
const [pw2, setPw2] = useState<string>('')
const [mnemonic, setMnemonic] = useState('')
Expand All @@ -73,26 +73,19 @@ export default function Invite({ params }: { params: { invite: string } }) {
const steps: Step[] = [
{
index: 0,
name: 'Set up recovery phrase',
icon: <MdKey />,
title: 'Recovery',
name: 'Sudo Password',
icon: <MdOutlinePassword />,
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: <MdOutlineVerifiedUser />,
title: 'Verify recovery phrase',
description: 'Please enter the your recovery phrase in the correct order below.',
},
{
index: 2,
name: 'Sudo password',
icon: <MdOutlinePassword />,
title: 'Set a sudo password',
name: 'Account recovery',
icon: <MdKey />,
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.',
},
]

Expand Down Expand Up @@ -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
}
Expand All @@ -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 = () => (
<div className="mx-auto my-auto max-w-2xl space-y-8 p-16 bg-zinc-100 dark:bg-zinc-800 text-black dark:text-white rounded-md shadow-2xl text-center">
<div className="space-y-2">
Expand Down Expand Up @@ -242,8 +218,8 @@ export default function Invite({ params }: { params: { invite: string } }) {
return (
<div className="mx-auto my-auto max-w-2xl space-y-8 p-16 bg-zinc-100 dark:bg-zinc-800 text-black dark:text-white rounded-md shadow-2xl text-center">
<div className="flex flex-col gap-y-2 items-center">
<h1 className="text-4xl text-black dark:text-white text-center font-bold">Success!</h1>
<p className="text-black/30 dark:text-white/40 text-center">Your account is setup!</p>
<h1 className="text-4xl text-black dark:text-white text-center font-bold">You're All Set</h1>
<p className="text-black/30 dark:text-white/40 text-center">Your account is ready to go!</p>
<div className="mx-auto pt-8">
<Button
variant="primary"
Expand All @@ -258,6 +234,33 @@ export default function Invite({ params }: { params: { invite: string } }) {
)
}

const handleDownloadRecoveryKit = async () => {
toast
.promise(
generateRecoveryPdf(
mnemonic,
session?.user?.email!,
invite.organisation.name,
session?.user?.name || undefined
),
{
pending: 'Generating recovery kit',
success: 'Downloaded recovery kit',
}
)
.then(() => setRecoveryDownloaded(true))
}

const handleCopyRecoveryKit = () => {
copyRecoveryKit(
mnemonic,
session?.user?.email!,
invite.organisation.name,
session?.user?.name || undefined
)
setRecoveryDownloaded(true)
}

return (
<>
<div>
Expand All @@ -280,16 +283,15 @@ export default function Invite({ params }: { params: { invite: string } }) {
<Stepper steps={steps} activeStep={step} />
</div>

{step === 0 && <AccountSeedGen mnemonic={mnemonic} />}
{step === 0 && <AccountPassword pw={pw} setPw={setPw} pw2={pw2} setPw2={setPw2} />}

{step === 1 && (
<AccountSeedChecker
<AccountRecovery
mnemonic={mnemonic}
inputs={inputs}
updateInputs={handleInputUpdate}
required={!recoverySkipped}
onDownload={handleDownloadRecoveryKit}
onCopy={handleCopyRecoveryKit}
/>
)}
{step === 2 && <AccountPassword pw={pw} setPw={setPw} pw2={pw2} setPw2={setPw2} />}

<div className="flex justify-between w-full">
<div>
Expand All @@ -300,13 +302,13 @@ export default function Invite({ params }: { params: { invite: string } }) {
)}
</div>
<div className="flex items-center gap-2">
{step !== 2 && (
<Button variant="secondary" type="button" onClick={skipRecoverySteps}>
Skip
</Button>
)}
<Button variant="primary" type="submit" isLoading={isloading}>
Next
<Button
variant="primary"
type="submit"
isLoading={isloading || loading}
disabled={step === steps.length - 1 && !recoveryDownloaded}
>
{step === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
Expand Down
Loading
Loading