Skip to content
Open
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
26 changes: 17 additions & 9 deletions cypress/e2e/00-auth/t30-multi-org-redirect.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,20 @@ describe('Multi-organization redirect flows', () => {
invitationUrl = $link.attr('href') || $link.text().trim()
cy.log('Invitation URL captured:', invitationUrl)

// Force a full page navigation by visiting the URL
// This will clear the session and disconnect User B
cy.visit(invitationUrl, { failOnStatusCode: false })
// Log out User B before opening the invite
cy.visit(invitationUrl, {
failOnStatusCode: false,
onBeforeLoad: (win) => win.localStorage.clear(),
})
})

// 5. User A accepts the invitation to Org2 - enter the same password to avoid DB issues
// 5. User A accepts the invite with their password
cy.url().should('include', '/invitation/')
cy.get('input[name="password"]', { timeout: 10000 }).should('be.visible')
cy.get('input[name="password"]').type(testUsers.userA.password)
cy.get('[data-test="submit-button"]').click()
cy.get('input[name="password"]', { timeout: 10000 })
.scrollIntoView()
.should('be.visible')
.type(testUsers.userA.password)
cy.get('[data-test="log-in-button"]').click()

// User A should now have access to both organizations
cy.url().should('match', /\/(analytics|customers)/)
Expand Down Expand Up @@ -118,7 +122,9 @@ describe('Multi-organization redirect flows', () => {
cy.get('input[name="name"]').type('Customer Org1 Multi-Org Test')
cy.get('input[name="externalId"]').type(`customer-org1-${Date.now()}`)
cy.get(`[data-test="${SUBMIT_CUSTOMER_DATA_TEST}"]`).click()
cy.url().should('include', '/customer/')
cy.url()
.should('not.include', '/customer/create')
.and('match', /\/customer\/[^/?#]+$/)
// Save the customer URL from Org1
cy.url().then((org1CustomerUrl) => {
const customerIdMatch = org1CustomerUrl.match(/\/customer\/([^/]+)/)
Expand Down Expand Up @@ -157,7 +163,9 @@ describe('Multi-organization redirect flows', () => {
cy.get('input[name="name"]').type('Customer for Org Switch Test')
cy.get('input[name="externalId"]').type(`customer-org-switch-${Date.now()}`)
cy.get(`[data-test="${SUBMIT_CUSTOMER_DATA_TEST}"]`).click()
cy.url().should('include', '/customer/')
cy.url()
.should('not.include', '/customer/create')
.and('match', /\/customer\/[^/?#]+$/)

const urlToAvoidAfterLogin = cy.url()

Expand Down
20 changes: 3 additions & 17 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,10 @@ import { SIGNUP_SUBMIT_BUTTON_TEST_ID } from '~/pages/auth/signUpTestIds'
import { userEmail, userPassword } from './reusableConstants'

/**
* Paths that pass through `cy.visitApp()` unchanged (no slug prepended).
*
* Extends the app's `NEVER_SLUG_PREFIXES` with auth entry pages that are
* reachable only from outside the app (signup, invitation, password reset)
* — those aren't in `NEVER_SLUG_PREFIXES` because the in-app wrappers
* (`useNavigate` / `<Link>`) never build `navigate('/sign-up')` calls, but
* Cypress tests do visit those pages directly.
*
* Importing `NEVER_SLUG_PREFIXES` from the source keeps the two lists in
* sync — any new public route added there is reflected here automatically.
* Paths that `cy.visitApp()` should not prefix with an organization slug.
* Includes app routes and auth pages opened directly by Cypress.
*/
const PUBLIC_PATHS = [
...NEVER_SLUG_PREFIXES,
'/sign-up',
'/invitation',
'/password-reset',
'/forgot-password',
]
const PUBLIC_PATHS = [...NEVER_SLUG_PREFIXES, '/sign-up', '/password-reset', '/forgot-password']

/**
* Regex matching the first authenticated URL after login/signup.
Expand Down
1 change: 0 additions & 1 deletion src/core/router/AuthRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,5 @@ export const authRoutes: CustomRouteObject[] = [
{
path: INVITATION_ROUTE_FORM,
element: <Invitation />,
invitation: true,
},
]
8 changes: 7 additions & 1 deletion src/core/router/slugPrefixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@
* Logout goes through `logOut(client)` in cacheUtils (never `navigate`),
* so no logout route is listed.
*/
export const NEVER_SLUG_PREFIXES = ['/customer-portal', '/forbidden', '/404', '/login']
export const NEVER_SLUG_PREFIXES = [
'/customer-portal',
'/forbidden',
'/404',
'/login',
'/invitation',
]
1 change: 0 additions & 1 deletion src/core/router/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export interface CustomRouteObject extends Omit<RouteObject, 'children' | 'path'
path?: string | string[]
private?: boolean
onlyPublic?: boolean
invitation?: boolean
redirect?: string
children?: CustomRouteObject[]
permissions?: Array<keyof TMembershipPermissions> // AND logic (all must be true)
Expand Down
86 changes: 80 additions & 6 deletions src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ export type Scalars = {
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
/** Represents non-fractional signed whole numeric values. Since the value may exceed the size of a 32-bit integer, it's encoded as a string. */
/**
* Represents non-fractional signed whole numeric values. Since the value may
* exceed the size of a 32-bit integer, it's encoded as a string.
*/
BigInt: { input: any; output: any; }
ChargeFilterValues: { input: any; output: any; }
/** Api Logs HTTP status */
Expand All @@ -33,7 +36,8 @@ export type Scalars = {
export type AcceptInviteInput = {
/** A unique identifier for the client performing the mutation. */
clientMutationId?: InputMaybe<Scalars['String']['input']>;
email: Scalars['String']['input'];
/** @deprecated The email is resolved from the invitation token. */
email?: InputMaybe<Scalars['String']['input']>;
password: Scalars['String']['input'];
/** Uniq token of the Invite */
token: Scalars['String']['input'];
Expand Down Expand Up @@ -4912,6 +4916,7 @@ export type Invite = {
__typename?: 'Invite';
acceptedAt?: Maybe<Scalars['ISO8601DateTime']['output']>;
email: Scalars['String']['output'];
existingUser: Scalars['Boolean']['output'];
id: Scalars['ID']['output'];
organization: Organization;
recipient: Membership;
Expand Down Expand Up @@ -5183,6 +5188,14 @@ export type ItemMetadata = {
value?: Maybe<Scalars['String']['output']>;
};

/** Autogenerated input type of JoinOrganization */
export type JoinOrganizationInput = {
/** A unique identifier for the client performing the mutation. */
clientMutationId?: InputMaybe<Scalars['String']['input']>;
/** Unique token of the Invite */
token: Scalars['String']['input'];
};

export enum LagoApiError {
AddressLocationNotFound = 'AddressLocationNotFound',
EntityNotFoundError = 'EntityNotFoundError',
Expand Down Expand Up @@ -5697,6 +5710,8 @@ export type Mutation = {
googleLoginUser?: Maybe<LoginUser>;
/** Register a new user with Google Oauth */
googleRegisterUser?: Maybe<RegisterUser>;
/** Joins the organization of an Invite as the authenticated user */
joinOrganization?: Maybe<Membership>;
/** Opens a session for an existing user */
loginUser?: Maybe<LoginUser>;
/** Mark payment dispute as lost */
Expand Down Expand Up @@ -6515,6 +6530,11 @@ export type MutationGoogleRegisterUserArgs = {
};


export type MutationJoinOrganizationArgs = {
input: JoinOrganizationInput;
};


export type MutationLoginUserArgs = {
input: LoginUserInput;
};
Expand Down Expand Up @@ -15035,21 +15055,28 @@ export type GetinviteQueryVariables = Exact<{
}>;


export type GetinviteQuery = { __typename?: 'Query', invite?: { __typename?: 'Invite', id: string, email: string, organization: { __typename?: 'Organization', id: string, name: string } } | null };
export type GetinviteQuery = { __typename?: 'Query', invite?: { __typename?: 'Invite', id: string, email: string, existingUser: boolean, organization: { __typename?: 'Organization', id: string, name: string, slug: string } } | null };

export type AcceptInviteMutationVariables = Exact<{
input: AcceptInviteInput;
}>;


export type AcceptInviteMutation = { __typename?: 'Mutation', acceptInvite?: { __typename?: 'RegisterUser', token: string } | null };
export type AcceptInviteMutation = { __typename?: 'Mutation', acceptInvite?: { __typename?: 'RegisterUser', token: string, organization: { __typename?: 'Organization', id: string, slug: string } } | null };

export type JoinOrganizationMutationVariables = Exact<{
input: JoinOrganizationInput;
}>;


export type JoinOrganizationMutation = { __typename?: 'Mutation', joinOrganization?: { __typename?: 'Membership', id: string, organization: { __typename?: 'Organization', id: string, slug: string } } | null };

export type GoogleAcceptInviteMutationVariables = Exact<{
input: GoogleAcceptInviteInput;
}>;


export type GoogleAcceptInviteMutation = { __typename?: 'Mutation', googleAcceptInvite?: { __typename?: 'RegisterUser', token: string } | null };
export type GoogleAcceptInviteMutation = { __typename?: 'Mutation', googleAcceptInvite?: { __typename?: 'RegisterUser', token: string, organization: { __typename?: 'Organization', id: string, slug: string } } | null };

export type FetchOktaAuthorizeUrlMutationVariables = Exact<{
input: OktaAuthorizeInput;
Expand Down Expand Up @@ -37986,9 +38013,11 @@ export const GetinviteDocument = gql`
invite(token: $token) {
id
email
existingUser
organization {
id
name
slug
}
}
}
Expand Down Expand Up @@ -38033,6 +38062,10 @@ export const AcceptInviteDocument = gql`
mutation acceptInvite($input: AcceptInviteInput!) {
acceptInvite(input: $input) {
token
organization {
id
slug
}
}
}
`;
Expand Down Expand Up @@ -38062,10 +38095,51 @@ export function useAcceptInviteMutation(baseOptions?: Apollo.MutationHookOptions
export type AcceptInviteMutationHookResult = ReturnType<typeof useAcceptInviteMutation>;
export type AcceptInviteMutationResult = Apollo.MutationResult<AcceptInviteMutation>;
export type AcceptInviteMutationOptions = Apollo.BaseMutationOptions<AcceptInviteMutation, AcceptInviteMutationVariables>;
export const JoinOrganizationDocument = gql`
mutation joinOrganization($input: JoinOrganizationInput!) {
joinOrganization(input: $input) {
id
organization {
id
slug
}
}
}
`;
export type JoinOrganizationMutationFn = Apollo.MutationFunction<JoinOrganizationMutation, JoinOrganizationMutationVariables>;

/**
* __useJoinOrganizationMutation__
*
* To run a mutation, you first call `useJoinOrganizationMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useJoinOrganizationMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [joinOrganizationMutation, { data, loading, error }] = useJoinOrganizationMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useJoinOrganizationMutation(baseOptions?: Apollo.MutationHookOptions<JoinOrganizationMutation, JoinOrganizationMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<JoinOrganizationMutation, JoinOrganizationMutationVariables>(JoinOrganizationDocument, options);
}
export type JoinOrganizationMutationHookResult = ReturnType<typeof useJoinOrganizationMutation>;
export type JoinOrganizationMutationResult = Apollo.MutationResult<JoinOrganizationMutation>;
export type JoinOrganizationMutationOptions = Apollo.BaseMutationOptions<JoinOrganizationMutation, JoinOrganizationMutationVariables>;
export const GoogleAcceptInviteDocument = gql`
mutation googleAcceptInvite($input: GoogleAcceptInviteInput!) {
googleAcceptInvite(input: $input) {
token
organization {
id
slug
}
}
}
`;
Expand Down Expand Up @@ -46920,4 +46994,4 @@ export function useGetBillableMetricsForWalletSuspenseQuery(baseOptions?: Apollo
export type GetBillableMetricsForWalletQueryHookResult = ReturnType<typeof useGetBillableMetricsForWalletQuery>;
export type GetBillableMetricsForWalletLazyQueryHookResult = ReturnType<typeof useGetBillableMetricsForWalletLazyQuery>;
export type GetBillableMetricsForWalletSuspenseQueryHookResult = ReturnType<typeof useGetBillableMetricsForWalletSuspenseQuery>;
export type GetBillableMetricsForWalletQueryResult = Apollo.QueryResult<GetBillableMetricsForWalletQuery, GetBillableMetricsForWalletQueryVariables>;
export type GetBillableMetricsForWalletQueryResult = Apollo.QueryResult<GetBillableMetricsForWalletQuery, GetBillableMetricsForWalletQueryVariables>;
5 changes: 0 additions & 5 deletions src/hooks/core/useLocationHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,6 @@ export const useLocationHistory: UseLocationHistoryReturn = () => {
} else if (isAuthenticated && !isCurrentUserLoading) {
handleAuthenticatedRouteEnter(routeConfig, location)
} else if (!routeConfig?.children && !routeConfig.onlyPublic) {
// In the invitation for page, once users are logged in, we redirect them to the home page
if (routeConfig.invitation && isAuthenticated) {
// We can then safely redirect to the home page.
navigate(HOME_ROUTE)
}
/**
* We add the current location to the history only if :
* - Current route has no children (to avoid adding Layout route which will result in duplicates)
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useCurrentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type UseCurrentUser = () => {
loading: boolean
currentUser?: CurrentUserInfosFragment
currentMembership?: CurrentUserInfosFragment['memberships'][0]
refetchCurrentUserInfos: () => void
refetchCurrentUserInfos: ReturnType<typeof useGetCurrentUserInfosQuery>['refetch']
}

export const useCurrentUser: UseCurrentUser = () => {
Expand Down
Loading
Loading