diff --git a/apps/console/app/components/IconPicker/index.tsx b/apps/console/app/components/IconPicker/index.tsx index f5e36fc7f8..d69a319bb7 100644 --- a/apps/console/app/components/IconPicker/index.tsx +++ b/apps/console/app/components/IconPicker/index.tsx @@ -195,6 +195,7 @@ export default function IconPicker({ Upload + {iconURL && } { )} + {errors?.upsertAppContactAddress && ( + + {errors.upsertAppContactAddress} + + )} + This will be used for notifications about your application diff --git a/apps/console/app/routes/apps/delete.tsx b/apps/console/app/routes/apps/delete.tsx index d033083b46..afd904b276 100644 --- a/apps/console/app/routes/apps/delete.tsx +++ b/apps/console/app/routes/apps/delete.tsx @@ -9,7 +9,11 @@ import { getErrorCause, getRollupReqFunctionErrorWrapper, } from '@proofzero/utils/errors' -import { BadRequestError, InternalServerError } from '@proofzero/errors' +import { + BadRequestError, + InternalServerError, + UnauthorizedError, +} from '@proofzero/errors' export const action: ActionFunction = getRollupReqFunctionErrorWrapper( async ({ request, context }) => { @@ -33,6 +37,8 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( const traceparent = context.traceSpan.getTraceParent() if (cause instanceof BadRequestError) { throw cause + } else if (cause instanceof UnauthorizedError) { + throw error } else { console.error(error) throw JsonError( diff --git a/apps/console/app/routes/onboarding.tsx b/apps/console/app/routes/onboarding.tsx index 54b1906a83..b4312e260d 100644 --- a/apps/console/app/routes/onboarding.tsx +++ b/apps/console/app/routes/onboarding.tsx @@ -63,7 +63,7 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper( : undefined return json({ - url: request.url, + currentPageURL: request.url, profile, connectedEmails, PASSPORT_URL: context.env.PASSPORT_URL, @@ -87,12 +87,12 @@ export const shouldRevalidate = ({ } export default function Onboarding() { - const { connectedEmails, PASSPORT_URL, profile, url, targetIG } = + const { connectedEmails, PASSPORT_URL, profile, currentPageURL, targetIG } = useLoaderData<{ connectedEmails: DropdownSelectListItem[] PASSPORT_URL: string profile: Profile - url: string + currentPageURL: string targetIG: | { name: string @@ -102,7 +102,9 @@ export default function Onboarding() { }>() const currentPage = - new URL(url).searchParams.get('rollup_result') || targetIG ? 1 : 0 + new URL(currentPageURL).searchParams.get('rollup_result') || targetIG + ? 1 + : 0 useConnectResult() @@ -119,7 +121,13 @@ export default function Onboarding() { } >
diff --git a/apps/console/app/routes/onboarding/index.tsx b/apps/console/app/routes/onboarding/index.tsx index 6861b6c82b..2e5839d078 100644 --- a/apps/console/app/routes/onboarding/index.tsx +++ b/apps/console/app/routes/onboarding/index.tsx @@ -10,7 +10,11 @@ import { } from '@proofzero/design-system/src/atoms/dropdown/DropdownSelectList' import { useFetcher, useNavigate, useOutletContext } from '@remix-run/react' import { getEmailIcon } from '@proofzero/utils/getNormalisedConnectedAccounts' -import { redirectToPassport } from '~/utils' +import { + OnboardTypeValues, + RedirectQueryParamKeys, + redirectToPassport, +} from '~/utils' import { HiOutlineArrowLeft, HiOutlineMail } from 'react-icons/hi' import { Input } from '@proofzero/design-system/src/atoms/form/Input' import { DocumentationBadge } from '~/components/DocumentationBadge' @@ -125,8 +129,8 @@ const Option = ({ description: string selected?: boolean disabled?: boolean - setSelectedType: (value: 'solo' | 'team') => void - type: 'solo' | 'team' + setSelectedType: (value: OnboardTypeValues) => void + type: OnboardTypeValues }) => { return (
void page: number - setOrgType: (value: 'solo' | 'team') => void - orgType: 'solo' | 'team' + setOrgType: (value: OnboardTypeValues) => void + orgType: OnboardTypeValues }) => { return (
setOrgType('solo')} - type="solo" + selected={orgType === OnboardTypeValues.Solo} + setSelectedType={() => setOrgType(OnboardTypeValues.Solo)} + type={OnboardTypeValues.Solo} />
diff --git a/apps/console/app/services/billing/stripe.ts b/apps/console/app/services/billing/stripe.ts index f18d70eb58..79e76d41b8 100644 --- a/apps/console/app/services/billing/stripe.ts +++ b/apps/console/app/services/billing/stripe.ts @@ -1,5 +1,6 @@ import { InternalServerError } from '@proofzero/errors' import { type CoreClientType } from '@proofzero/platform-clients/core' +import { IDENTITY_GROUP_OPTIONS } from '@proofzero/platform/identity/src/constants' import { type ReconcileAppsSubscriptionsOutput } from '@proofzero/platform/starbase/src/jsonrpc/methods/reconcileAppSubscriptions' import { ServicePlanType } from '@proofzero/types/billing' import { @@ -354,25 +355,26 @@ export const reconcileSubscriptions = async ( if (seatQuantities) { const { quantity: stripeSeatQuantity } = seatQuantities - const groupSeats = await coreClient.billing.getIdentityGroupSeats.query( - { + const usedSeats = + await coreClient.billing.getUsedIdentityGroupSeats.query({ URN: URN as IdentityGroupURN, - } - ) + }) // If the group has more seats than the subscription, set payment failed // because this flag is responsible for displaying the "Payment failed" // in the UI if ( !paidInvoice || - (groupSeats && groupSeats.quantity > stripeSeatQuantity!) + usedSeats > + stripeSeatQuantity! + IDENTITY_GROUP_OPTIONS.maxFreeMembers ) { await coreClient.billing.setPaymentFailed.mutate({ URN: URN as IdentityGroupURN, }) } else if ( paidInvoice && - (!groupSeats || groupSeats.quantity <= stripeSeatQuantity!) + usedSeats <= + stripeSeatQuantity! + IDENTITY_GROUP_OPTIONS.maxFreeMembers ) { await coreClient.billing.setPaymentFailed.mutate({ URN: URN as IdentityGroupURN, diff --git a/apps/console/app/types.ts b/apps/console/app/types.ts index 95924052a5..eca67c3c7e 100644 --- a/apps/console/app/types.ts +++ b/apps/console/app/types.ts @@ -40,7 +40,7 @@ export type errorsAuthProps = { } export type errorsTeamProps = { - upserteAppContactAddress?: string + upsertAppContactAddress?: string } export type AuthorizedProfile = AuthorizedUser diff --git a/apps/console/app/utils.ts b/apps/console/app/utils.ts index 23866e1ae5..9f5c91b4ba 100644 --- a/apps/console/app/utils.ts +++ b/apps/console/app/utils.ts @@ -107,29 +107,66 @@ export const setPurchaseToastNotification = ({ } } +export enum RedirectQueryParamKeys { + OnboardType = 'onboard_type', +} + +export enum OnboardTypeValues { + Team = 'team', + Solo = 'solo', +} + +type RedirectQueryParams = { + onboard_type?: OnboardTypeValues +} + +const validExtraParams: Record = { + [RedirectQueryParamKeys.OnboardType]: [ + OnboardTypeValues.Team, + OnboardTypeValues.Solo, + ], +} + export const redirectToPassport = ({ PASSPORT_URL, login_hint, scope = '', state = 'skip', rollup_action, + redirectQueryParams, }: { PASSPORT_URL: string login_hint: string scope?: string state?: string rollup_action?: string + redirectQueryParams?: RedirectQueryParams }) => { const currentURL = new URL(window.location.href) currentURL.search = '' + if (redirectQueryParams) { + for (const [key, value] of Object.entries(redirectQueryParams)) { + const enumKey = key as RedirectQueryParamKeys + if ( + enumKey in validExtraParams && + validExtraParams[enumKey].includes(value) + ) { + currentURL.searchParams.append(key, value) + } + } + } + const qp = new URLSearchParams() qp.append('scope', scope) qp.append('state', state) qp.append('client_id', 'console') - qp.append('redirect_uri', currentURL.toString()) - if (rollup_action) qp.append('rollup_action', rollup_action) + + if (rollup_action) { + qp.append('rollup_action', rollup_action) + } + qp.append('login_hint', login_hint) window.location.href = `${PASSPORT_URL}/authorize?${qp.toString()}` diff --git a/apps/passport/app/routes/authenticate/$clientId/index.tsx b/apps/passport/app/routes/authenticate/$clientId/index.tsx index 74c6348cd0..43abcc70ab 100644 --- a/apps/passport/app/routes/authenticate/$clientId/index.tsx +++ b/apps/passport/app/routes/authenticate/$clientId/index.tsx @@ -231,7 +231,7 @@ const InnerComponent = ({ size="sm" > - {!rollup_action?.startsWith('groupconnect') && ( + {!rollup_action?.startsWith('group') && (

- - + + "{invitationData.inviterAlias}"
has invited you to join group
- + "{invitationData.groupName}" - + To accept please authenticate with your
=> { const { identityGroupURN, accountURN } = input - const caller = router.createCaller(ctx) + await groupAdminValidatorByIdentityGroupURN(ctx, identityGroupURN) - const { edges: membershipEdges } = await caller.edges.getEdges({ - query: { - src: { - baseUrn: ctx.identityURN, - }, - tag: EDGE_MEMBER_OF_IDENTITY_GROUP, - dst: { - baseUrn: identityGroupURN, - }, - }, - }) - if (membershipEdges.length === 0) { - throw new UnauthorizedError({ - message: 'Caller is not a member of the identity group', - }) - } + const caller = router.createCaller(ctx) const { edges } = await caller.edges.getEdges({ query: { diff --git a/platform/account/src/jsonrpc/router.ts b/platform/account/src/jsonrpc/router.ts index ac9b0ea2a8..1e45cee027 100644 --- a/platform/account/src/jsonrpc/router.ts +++ b/platform/account/src/jsonrpc/router.ts @@ -128,6 +128,10 @@ import { ConnectIdentityGroupEmailOutputSchema, connectIdentityGroupEmail, } from './methods/identity-groups/connectIdentityGroupEmail' +import { + AuthorizationTokenFromHeader, + ValidateJWT, +} from '@proofzero/platform-middleware/jwt' const t = initTRPC.context().create({ errorFormatter }) @@ -350,6 +354,8 @@ export const appRouter = t.router({ connectIdentityGroupEmail: t.procedure .use(LogUsage) .use(Analytics) + .use(AuthorizationTokenFromHeader) + .use(ValidateJWT) .use(parse3RN) .use(setAccountNodeClient) .use(initAccountNode) diff --git a/platform/billing/src/jsonrpc/methods/getUsedIdentityGroupSeats.ts b/platform/billing/src/jsonrpc/methods/getUsedIdentityGroupSeats.ts new file mode 100644 index 0000000000..7deac2c9d4 --- /dev/null +++ b/platform/billing/src/jsonrpc/methods/getUsedIdentityGroupSeats.ts @@ -0,0 +1,34 @@ +import { z } from 'zod' +import { Context } from '../../context' +import { IdentityGroupURNValidator } from '@proofzero/platform-middleware/inputValidators' +import { initIdentityGroupNodeByName } from '@proofzero/platform.identity/src/nodes' + +export const GetUsedIdentityGroupSeatsInputSchema = z.object({ + URN: IdentityGroupURNValidator, +}) +export type GetUsedIdentityGroupSeatsInput = z.infer< + typeof GetUsedIdentityGroupSeatsInputSchema +> + +export const GetUsedIdentityGroupSeatsOutputSchema = z.number() +export type GetUsedIdentityGroupSeatsOutput = z.infer< + typeof GetUsedIdentityGroupSeatsOutputSchema +> + +export const getUsedIdentityGroupSeats = async ({ + input, + ctx, +}: { + input: GetUsedIdentityGroupSeatsInput + ctx: Context +}): Promise => { + const ownerNode = initIdentityGroupNodeByName( + input.URN, + ctx.env.IdentityGroup + ) + + const orderedMembers = await ownerNode.class.getOrderedMembers() + const invitations = await ownerNode.class.getInvitations() + + return orderedMembers.length + invitations.length +} diff --git a/platform/billing/src/jsonrpc/router.ts b/platform/billing/src/jsonrpc/router.ts index 17b6fa03da..c99e2ceeb7 100644 --- a/platform/billing/src/jsonrpc/router.ts +++ b/platform/billing/src/jsonrpc/router.ts @@ -38,6 +38,11 @@ import { SetPaymentFailedInput, setPaymentFailed, } from './methods/setPaymentFailed' +import { + GetUsedIdentityGroupSeatsInputSchema, + GetUsedIdentityGroupSeatsOutputSchema, + getUsedIdentityGroupSeats, +} from './methods/getUsedIdentityGroupSeats' const t = initTRPC.context().create({ errorFormatter }) @@ -69,6 +74,12 @@ export const appRouter = t.router({ .use(Analytics) .input(CancelServicePlansInput) .mutation(cancelServicePlans), + getUsedIdentityGroupSeats: t.procedure + .use(LogUsage) + .use(Analytics) + .input(GetUsedIdentityGroupSeatsInputSchema) + .output(GetUsedIdentityGroupSeatsOutputSchema) + .query(getUsedIdentityGroupSeats), getIdentityGroupSeats: t.procedure .use(LogUsage) .use(Analytics)