Skip to content

Commit

Permalink
feat(identity): identity merge
Browse files Browse the repository at this point in the history
  • Loading branch information
szkl committed Feb 29, 2024
1 parent a8fd3cc commit 320f41c
Show file tree
Hide file tree
Showing 9 changed files with 573 additions and 13 deletions.
4 changes: 4 additions & 0 deletions apps/passport/app/routes/authorize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
const connectResult =
new URL(request.url).searchParams.get('rollup_result') ?? undefined

if (connectResult === 'ACCOUNT_LINKED_ERROR') {
throw redirect('/merge-identity')
}

//Request parameter pre-checks
if (!clientId)
throw new BadRequestError({ message: 'client_id is required' })
Expand Down
16 changes: 16 additions & 0 deletions apps/passport/app/routes/merge-identity/cancel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { redirect, type LoaderFunction } from '@remix-run/cloudflare'

import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors'

import { destroyIdentityMergeState } from '~/session.server'

export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
async ({ request, context }) => {
const headers = new Headers()
headers.append(
'Set-Cookie',
await destroyIdentityMergeState(request, context.env)
)
return redirect('/authenticate/cancel', { headers })
}
)
230 changes: 230 additions & 0 deletions apps/passport/app/routes/merge-identity/confirm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { useContext, useEffect } from 'react'
import { type LoaderFunction } from '@remix-run/cloudflare'
import { useFetcher, useLoaderData, Form } from '@remix-run/react'

import { ImArrowDown } from 'react-icons/im'

import { BadRequestError } from '@proofzero/errors'
import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors'

import { Button } from '@proofzero/design-system/src/atoms/buttons/Button'
import { Text } from '@proofzero/design-system/src/atoms/text/Text'
import { ToastType, toast } from '@proofzero/design-system/src/atoms/toast'
import { ThemeContext } from '@proofzero/design-system/src/contexts/theme'

import sideGraphics from '~/assets/auth-side-graphics.svg'
import dangerVector from '~/assets/warning.svg'

import { getCoreClient } from '~/platform.server'

import {
getIdentityMergeState,
getValidatedSessionContext,
} from '~/session.server'

export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
async ({ request, context }) => {
const mergeIdentityState = await getIdentityMergeState(request, context.env)
if (!mergeIdentityState)
throw new BadRequestError({
message: 'missing merge identity state',
})

const { source, target } = mergeIdentityState

const { jwt, identityURN } = await getValidatedSessionContext(
request,
context.authzQueryParams,
context.env,
context.traceSpan
)

if (identityURN !== target)
throw new BadRequestError({
message: 'invalid merge identity state',
})

const coreClient = getCoreClient({
context,
jwt,
})

const preview = await coreClient.identity.mergePreview.query({
source,
target,
})

return {
source: preview.source,
target: preview.target,
}
}
)

type LoaderData = {
source: UserProps
target: UserProps
}

export default function Confirm() {
const { dark } = useContext(ThemeContext)
const { source, target } = useLoaderData<LoaderData>()
const fetcher = useFetcher<{ error?: { message: string } }>()

useEffect(() => {
if (fetcher.state !== 'idle') return
if (fetcher.type !== 'done') return
if (!fetcher.data) return
if (!fetcher.data.error) return
toast(ToastType.Error, fetcher.data.error, { duration: 2000 })
}, [fetcher])

return (
<>
<div className={`${dark ? 'dark' : ''}`}>
<div className="flex flex-row h-[100dvh] justify-center items-center bg-[#F9FAFB] dark:bg-gray-900">
<div
className="basis-2/5 h-[100dvh] w-full hidden lg:flex justify-center items-center bg-indigo-50 dark:bg-[#1F2937] overflow-hidden"
style={{
backgroundImage: `url(${sideGraphics})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
<div className="basis-full lg:basis-3/5">
<div className="flex flex-col w-[402px] min-h-fit m-auto p-6 space-y-8 box-border bg-white dark:bg-[#1F2937] border rounded-lg border-[#D1D5DB] dark:border-gray-600">
<div className="flex flex-col space-y-4">
<div className="flex justify-center space-y-4">
<img
src={dangerVector}
className="inline-block w-[48px] h-[48px] mr-4"
alt="danger"
/>
</div>
<div className="flex flex-col justify-content space-y-2">
<Text
size="xl"
weight="semibold"
className="leading-8 text-center text-[#2D333A]"
>
Confirm Identity Merge
</Text>
<Text
type="span"
className="leading-6 text-center text-orange-600"
>
This action permanently transfers <br />
<Text type="span" weight="semibold">
all accounts
</Text>{' '}
from one identity to other.
</Text>
</div>
<div className="flex flex-col items-center space-y-2">
<User
avatar={source.avatar}
displayName={source.displayName}
primaryAccountAlias={source.primaryAccountAlias}
accounts={source.accounts}
applications={source.applications}
/>
<ImArrowDown color="#D1D5DB" size={32} />
<User
avatar={target.avatar}
displayName={target.displayName}
primaryAccountAlias={target.primaryAccountAlias}
accounts={target.accounts}
applications={target.applications}
/>
</div>
<div className="flex justify-between">
<Form method="get" action="/merge-identity/cancel">
<Button
type="submit"
btnType="secondary-alt"
className="w-40"
>
Cancel
</Button>
</Form>
<fetcher.Form method="post" action="/merge-identity/merge">
<Button
type="submit"
btnType="primary-alt"
className="w-40"
>
Confirm Merge
</Button>
</fetcher.Form>
</div>
</div>
</div>
</div>
</div>
</div>
</>
)
}

type UserProps = {
avatar: string
displayName: string
primaryAccountAlias: string
accounts: number
applications: number
}

const User = ({
avatar,
displayName,
primaryAccountAlias,
accounts,
applications,
}: UserProps) => {
return (
<div className="w-[350px] border rounded-md border-gray-200">
<div className="min-w-full">
<div className="flex p-2 space-x-2 border-b">
<img
src={avatar}
alt="avatar"
className="w-[40px] h-[40px] rounded-full"
/>
<div>
<Text
size="sm"
weight="semibold"
className="leading-5 text-gray-600"
>
{displayName}
</Text>
<Text size="sm" weight="normal" className="leading-5 text-gray-400">
{primaryAccountAlias}
</Text>
</div>
</div>
<div>
<div className="p-2 space-y-2 leading-5 text-gray-500">
<div>
<Text type="span" size="sm" weight="semibold">
Accounts:{' '}
<Text type="span" size="sm" weight="normal">
{accounts}
</Text>
</Text>
</div>
<div>
<Text type="span" size="sm" weight="semibold">
Applications:{' '}
<Text type="span" size="sm" weight="normal">
{applications}
</Text>
</Text>
</div>
</div>
</div>
</div>
</div>
)
}
6 changes: 6 additions & 0 deletions apps/passport/app/routes/merge-identity/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { redirect } from '@remix-run/cloudflare'
import type { LoaderFunction } from '@remix-run/cloudflare'

export const loader: LoaderFunction = () => {
return redirect('/merge-identity/prompt')
}
71 changes: 71 additions & 0 deletions apps/passport/app/routes/merge-identity/merge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { redirect, type ActionFunction } from '@remix-run/cloudflare'

import { BadRequestError, ConflictError } from '@proofzero/errors'
import {
getErrorCause,
getRollupReqFunctionErrorWrapper,
} from '@proofzero/utils/errors'

import { getCoreClient } from '~/platform.server'

import {
destroyIdentityMergeState,
getAuthzCookieParams,
getIdentityMergeState,
getValidatedSessionContext,
} from '~/session.server'

import { getAuthzRedirectURL } from '~/utils/authenticate.server'

export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
async ({ request, context }) => {
const mergeIdentityState = await getIdentityMergeState(request, context.env)
if (!mergeIdentityState)
throw new BadRequestError({
message: 'missing merge identity state',
})

const { source, target } = mergeIdentityState

const { jwt, identityURN } = await getValidatedSessionContext(
request,
context.authzQueryParams,
context.env,
context.traceSpan
)

if (identityURN !== target) {
destroyIdentityMergeState(request, context.env)
throw new BadRequestError({
message: 'invalid merge identity state',
})
}

const coreClient = getCoreClient({
context,
jwt,
})

try {
await coreClient.identity.merge.mutate({ source, target })
} catch (e) {
const error = getErrorCause(e)
if (error instanceof ConflictError)
return {
error: {
message: error.message,
},
}
else throw error
}

const headers = new Headers()
headers.append(
'Set-Cookie',
await destroyIdentityMergeState(request, context.env)
)

const params = await getAuthzCookieParams(request, context.env)
return redirect(getAuthzRedirectURL(params), { headers })
}
)
Loading

0 comments on commit 320f41c

Please sign in to comment.