Skip to content

Commit

Permalink
refactor: keyring ux (#188)
Browse files Browse the repository at this point in the history
* refactor: sunset local storage for keyrings, update keyring + recovery UX

* fix: copy typo

* feat: remove ad-hoc sudo passsword dialogs

* fix: update sidebar bg

* fix: add navbar to workspace selection page

* chore: remove unused imports

* feat: add utils to store and retrieve sudo password on device

* fix: misc fixes to handle route and context changes when switching workspaces

* refactor: misc updates to keyring dialog

* feat: add util to remove a stored password

* fix: add trusted device management in settings

* fix: only update route manually if it doesn't match the active organisation

* fix: update existing password instead of creating duplicates

* feat: add toggle switch component

* feat: add shadow to split button meny dropdown

* feat: rework 'remember password' style and copy

* fix: make trusted device check in settings page more reactive

* fix: tweak ui, copy
  • Loading branch information
rohan-chaturvedi authored Feb 24, 2024
1 parent 784f09b commit 50868e8
Show file tree
Hide file tree
Showing 26 changed files with 707 additions and 634 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,6 @@ export default function Environment({

return (
<div className="max-h-screen overflow-y-auto w-full text-black dark:text-white">
{organisation && <UnlockKeyringDialog organisationId={organisation.id} />}
{keyring !== null && !loading && (
<div className="flex flex-col py-4 gap-4">
<div className="flex items-center gap-8">
Expand Down
87 changes: 1 addition & 86 deletions frontend/app/[team]/apps/[app]/members/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { useSession } from 'next-auth/react'
import { Avatar } from '@/components/common/Avatar'
import { KeyringContext } from '@/contexts/keyringContext'
import { unwrapEnvSecretsForUser, wrapEnvSecretsForUser } from '@/utils/environments'
import { OrganisationKeyring, cryptoUtils } from '@/utils/auth'
import { userIsAdmin } from '@/utils/permissions'
import { RoleLabel } from '@/components/users/RoleLabel'
import { Alert } from '@/components/common/Alert'
Expand All @@ -40,7 +39,7 @@ import Link from 'next/link'
export default function Members({ params }: { params: { team: string; app: string } }) {
const { data } = useQuery(GetAppMembers, { variables: { appId: params.app } })

const { keyring, setKeyring } = useContext(KeyringContext)
const { keyring } = useContext(KeyringContext)
const { activeOrganisation: organisation } = useContext(organisationContext)

const activeUserIsAdmin = organisation ? userIsAdmin(organisation.role!) : false
Expand All @@ -49,21 +48,6 @@ export default function Members({ params }: { params: { team: string; app: strin

const { data: session } = useSession()

const validateKeyring = async (password: string) => {
return new Promise<OrganisationKeyring>(async (resolve) => {
if (keyring) resolve(keyring)
else {
const decryptedKeyring = await cryptoUtils.getKeyring(
session?.user?.email!,
organisation!.id,
password
)
setKeyring(decryptedKeyring)
resolve(decryptedKeyring)
}
})
}

const AddMemberDialog = () => {
const [getMembers, { data: orgMembersData }] = useLazyQuery(GetOrganisationMembers)

Expand Down Expand Up @@ -109,8 +93,6 @@ export default function Members({ params }: { params: { team: string; app: strin
const [query, setQuery] = useState('')
const [envScope, setEnvScope] = useState<Array<Record<string, string>>>([])
const [showEnvHint, setShowEnvHint] = useState<boolean>(false)
const [password, setPassword] = useState<string>('')
const [showPw, setShowPw] = useState<boolean>(false)

const filteredPeople =
query === ''
Expand All @@ -136,8 +118,6 @@ export default function Members({ params }: { params: { team: string; app: strin
return false
}

const keyring = await validateKeyring(password)

const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[]

const envKeyPromises = appEnvironments
Expand Down Expand Up @@ -392,37 +372,6 @@ export default function Members({ params }: { params: { team: string; app: strin
</div>
</Listbox.Options>
</Transition>

{!keyring && (
<div className="space-y-4 w-full">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="password"
>
Sudo password
</label>
<div className="flex justify-between w-full bg-zinc-100 dark:bg-zinc-800 ring-1 ring-inset ring-neutral-500/40 focus-within:ring-1 focus-within:ring-inset focus-within:ring-emerald-500 rounded-md p-px">
<input
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPw ? 'text' : 'password'}
minLength={16}
required
autoFocus
className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md ph-no-capture"
/>
<button
className="bg-zinc-100 dark:bg-zinc-800 px-4 text-neutral-500 rounded-md"
type="button"
onClick={() => setShowPw(!showPw)}
tabIndex={-1}
>
{showPw ? <FaEyeSlash /> : <FaEye />}
</button>
</div>
</div>
)}
</>
)}
</Listbox>
Expand Down Expand Up @@ -570,8 +519,6 @@ export default function Members({ params }: { params: { team: string; app: strin

const [envScope, setEnvScope] = useState<Array<Record<string, string>>>([])
const [showEnvHint, setShowEnvHint] = useState<boolean>(false)
const [password, setPassword] = useState<string>('')
const [showPw, setShowPw] = useState<boolean>(false)

const memberIsAdmin = userIsAdmin(props.member.role) || false

Expand Down Expand Up @@ -618,8 +565,6 @@ export default function Members({ params }: { params: { team: string; app: strin
return false
}

const keyring = await validateKeyring(password)

const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[]

const envKeyPromises = appEnvironments
Expand Down Expand Up @@ -803,36 +748,6 @@ export default function Members({ params }: { params: { team: string; app: strin
)}
</Listbox>
</div>
{!keyring && !memberIsAdmin && (
<div className="space-y-2 w-full">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="password"
>
Sudo password
</label>
<div className="flex justify-between w-full bg-zinc-100 dark:bg-zinc-800 ring-1 ring-inset ring-neutral-500/40 focus-within:ring-1 focus-within:ring-inset focus-within:ring-emerald-500 rounded-md p-px">
<input
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPw ? 'text' : 'password'}
minLength={16}
required
autoFocus
className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md ph-no-capture"
/>
<button
className="bg-zinc-100 dark:bg-zinc-800 px-4 text-neutral-500 rounded-md"
type="button"
onClick={() => setShowPw(!showPw)}
tabIndex={-1}
>
{showPw ? <FaEyeSlash /> : <FaEye />}
</button>
</div>
</div>
)}

{memberIsAdmin && (
<Alert variant="info" icon={true}>
Expand Down
1 change: 0 additions & 1 deletion frontend/app/[team]/apps/[app]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,6 @@ export default function Secrets({ params }: { params: { team: string; app: strin

return (
<div className="max-h-screen overflow-y-auto w-full text-black dark:text-white grid gap-16 relative">
{organisation && <UnlockKeyringDialog organisationId={organisation.id} />}
{keyring !== null &&
(setupRequired ? (
<div className="flex flex-col gap-4 w-full items-center p-16">
Expand Down
57 changes: 1 addition & 56 deletions frontend/app/[team]/apps/[app]/syncing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { userIsAdmin } from '@/utils/permissions'

export default function Syncing({ params }: { params: { team: string; app: string } }) {
const { activeOrganisation: organisation } = useContext(organisationContext)
const { keyring, setKeyring } = useContext(KeyringContext)
const { keyring } = useContext(KeyringContext)

const searchParams = useSearchParams()

Expand All @@ -38,25 +38,6 @@ export default function Syncing({ params }: { params: { team: string; app: strin
pollInterval: 10000,
})

const [getCloudflarePages] = useLazyQuery(GetCfPages)

const { data: session } = useSession()

const validateKeyring = async (password: string) => {
return new Promise<OrganisationKeyring>(async (resolve) => {
if (keyring) resolve(keyring)
else {
const decryptedKeyring = await cryptoUtils.getKeyring(
session?.user?.email!,
organisation!.id,
password
)
setKeyring(decryptedKeyring)
resolve(decryptedKeyring)
}
})
}

const activeUserIsAdmin = organisation ? userIsAdmin(organisation.role!) : false

const EnableSyncingDialog = () => {
Expand All @@ -71,9 +52,6 @@ export default function Syncing({ params }: { params: { team: string; app: strin

const [isOpen, setIsOpen] = useState<boolean>(false)

const [password, setPassword] = useState<string>('')
const [showPw, setShowPw] = useState<boolean>(false)

const closeModal = () => {
setIsOpen(false)
}
Expand All @@ -85,8 +63,6 @@ export default function Syncing({ params }: { params: { team: string; app: strin
const handleEnableSyncing = async (e: { preventDefault: () => void }) => {
e.preventDefault()

const keyring = await validateKeyring(password)

const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[]

const envKeyPromises = appEnvironments.map(async (env: EnvironmentType) => {
Expand Down Expand Up @@ -191,37 +167,6 @@ export default function Syncing({ params }: { params: { team: string; app: strin
environments.
</Alert>

{!keyring && (
<div className="space-y-2 w-full">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="password"
>
Sudo password
</label>
<div className="flex justify-between w-full bg-zinc-100 dark:bg-zinc-800 ring-1 ring-inset ring-neutral-500/40 focus-within:ring-1 focus-within:ring-inset focus-within:ring-emerald-500 rounded-md p-px">
<input
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPw ? 'text' : 'password'}
minLength={16}
required
autoFocus
className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md ph-no-capture"
/>
<button
className="bg-zinc-100 dark:bg-zinc-800 px-4 text-neutral-500 rounded-md"
type="button"
onClick={() => setShowPw(!showPw)}
tabIndex={-1}
>
{showPw ? <FaEyeSlash /> : <FaEye />}
</button>
</div>
</div>
)}

<div className="flex items-center gap-4">
<Button variant="secondary" type="button" onClick={closeModal}>
Cancel
Expand Down
1 change: 0 additions & 1 deletion frontend/app/[team]/apps/[app]/tokens/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,6 @@ export default function Tokens({ params }: { params: { team: string; app: string
return (
<div className="w-full overflow-y-auto relative text-black dark:text-white space-y-16">
<section className="max-w-screen-xl">
{organisation && <UnlockKeyringDialog organisationId={organisation.id} />}
{keyring !== null && (
<div className="flex gap-8 mt-6 divide-x divide-neutral-500/20 items-start">
<div className="space-y-4 border-l border-neutral-500/40 h-min">
Expand Down
23 changes: 16 additions & 7 deletions frontend/app/[team]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import '@/app/globals.css'
import { HeroPattern } from '@/components/common/HeroPattern'
import { NavBar } from '@/components/layout/Navbar'
import Sidebar from '@/components/layout/Sidebar'
import { OrganisationProvider, organisationContext } from '@/contexts/organisationContext'
import { organisationContext } from '@/contexts/organisationContext'
import clsx from 'clsx'
import { usePathname, useRouter } from 'next/navigation'
import { useContext, useEffect } from 'react'
import { notFound } from 'next/navigation'

import UnlockKeyringDialog from '@/components/auth/UnlockKeyringDialog'

export default function RootLayout({
children,
Expand All @@ -29,19 +30,26 @@ export default function RootLayout({
router.push('/signup')
}

// try and get org being access from route params in the list of organisations for this user
// try and get org being accessed from route params in the list of organisations for this user
const org = organisations.find((org) => org.name === params.team)

// update active organisation if it exists
if (org) setActiveOrganisation(org)
// else update the route to the active organisation
else router.push(`/${activeOrganisation!.name}`)
// if there's only one available organisation
else if (organisations.length === 1) setActiveOrganisation(organisations[0])
// else send to home
else router.push(`/`)
}
}, [activeOrganisation, organisations, params.team, router, loading, setActiveOrganisation])
}, [organisations, params.team, router, loading, setActiveOrganisation])

useEffect(() => {
if (activeOrganisation && params.team !== activeOrganisation.name)
router.push(`/${activeOrganisation.name}`)
}, [activeOrganisation, params.team, router])

const path = usePathname()

const showNav = !path?.split('/').includes('newdevice')
const showNav = !path?.split('/').includes('recovery')

return (
<div
Expand All @@ -51,6 +59,7 @@ export default function RootLayout({
)}
>
<HeroPattern />
{activeOrganisation && <UnlockKeyringDialog organisation={activeOrganisation} />}
{showNav && <NavBar team={params.team} />}
{showNav && <Sidebar />}
<div className={clsx('min-h-screen overflow-auto', showNav && 'pt-16')}>{children}</div>
Expand Down
3 changes: 0 additions & 3 deletions frontend/app/[team]/members/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -794,9 +794,6 @@ export default function Members({ params }: { params: { team: string } }) {
</table>
</div>
</div>
{activeUserIsAdmin && organisation && (
<UnlockKeyringDialog organisationId={organisation.id} />
)}
</section>
)
}
Loading

0 comments on commit 50868e8

Please sign in to comment.