feat(auth): add default workspace support for user handling (#9099)

Introduce `defaultWorkspaceId` to improve workspace redirection logic.
Updated GraphQL schema, server logic, and frontend components
accordingly to prioritize default workspaces when available.

## Summary
This PR adds a mechanism to handle and prioritize default workspace
selection for users during authentication. It updates the logic in
multiple components and services to ensure users are redirected to their
default workspaces if no specific selection is provided.

### Main changes:
- **GraphQL Schema Updates**:
- Enhanced `UserExists` GraphQL entity with a new `defaultWorkspaceId`
field to specify the user's default workspace.
  - Updated queries and mutations to handle the `defaultWorkspaceId`.

- **Client-Side Updates**:
- Enhanced `useAuth` hook to include logic for managing default
workspace redirection.
- Adjusted UI logic in `SignInUpGlobalScopeForm` to utilize the
`defaultWorkspaceId`.

- **Server-Side Adjustments**:
- Modified `AuthService` to include `defaultWorkspaceId` in
`checkUserExists`.
- Default workspace logic added to the backend flow for consistent
handling.

- **Tests/Helpers**:
- Added utility and type changes to integrate the new backend response
changes (e.g., `UserExists` GraphQL).
- **Subsequent function lifecycle** was adjusted to include recheck for
workspace token states when performing sign-in flows.
This commit is contained in:
Antoine Moreaux
2024-12-17 19:56:19 +01:00
committed by GitHub
parent 55dc5983a2
commit 07bde4883e
7 changed files with 23 additions and 16 deletions

View File

@ -1333,6 +1333,7 @@ export type UserEdge = {
export type UserExists = {
__typename?: 'UserExists';
availableWorkspaces: Array<AvailableWorkspaceOutput>;
defaultWorkspaceId: Scalars['String'];
exists: Scalars['Boolean'];
};
@ -1927,7 +1928,7 @@ export type CheckUserExistsQueryVariables = Exact<{
}>;
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, subdomain: string, logo?: string | null, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } };
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, defaultWorkspaceId: string, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, subdomain: string, logo?: string | null, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } };
export type GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>;
@ -3069,6 +3070,7 @@ export const CheckUserExistsDocument = gql`
__typename
... on UserExists {
exists
defaultWorkspaceId
availableWorkspaces {
id
displayName

View File

@ -6,6 +6,7 @@ export const CHECK_USER_EXISTS = gql`
__typename
... on UserExists {
exists
defaultWorkspaceId
availableWorkspaces {
id
displayName

View File

@ -136,8 +136,10 @@ export const useAuth = () => {
await client.clearStore();
sessionStorage.clear();
localStorage.clear();
// We need to explicitly clear the state to trigger the cookie deletion which include the parent domain
setLastAuthenticateWorkspaceDomain(null);
},
[client, goToRecoilSnapshot],
[client, goToRecoilSnapshot, setLastAuthenticateWorkspaceDomain],
);
const handleChallenge = useCallback(

View File

@ -83,21 +83,19 @@ export const SignInUpGlobalScopeForm = () => {
},
onCompleted: (data) => {
requestFreshCaptchaToken();
if (data.checkUserExists.__typename === 'UserExists') {
if (
isDefined(data?.checkUserExists.availableWorkspaces) &&
data.checkUserExists.availableWorkspaces.length >= 1
) {
return redirectToWorkspaceDomain(
data?.checkUserExists.availableWorkspaces[0].subdomain,
pathname,
{
email: form.getValues('email'),
},
);
const response = data.checkUserExists;
if (response.__typename === 'UserExists') {
if (response.availableWorkspaces.length >= 1) {
const workspace =
response.availableWorkspaces.find(
(workspace) => workspace.id === response.defaultWorkspaceId,
) ?? response.availableWorkspaces[0];
return redirectToWorkspaceDomain(workspace.subdomain, pathname, {
email: form.getValues('email'),
});
}
}
if (data.checkUserExists.__typename === 'UserNotExists') {
if (response.__typename === 'UserNotExists') {
setSignInUpMode(SignInUpMode.SignUp);
setSignInUpStep(SignInUpStep.Password);
}

View File

@ -7,6 +7,9 @@ export class UserExists {
@Field(() => Boolean)
exists: true;
@Field(() => String)
defaultWorkspaceId: string;
@Field(() => [AvailableWorkspaceOutput])
availableWorkspaces: Array<AvailableWorkspaceOutput>;
}

View File

@ -257,6 +257,7 @@ export class AuthService {
if (userValidator.isDefined(user)) {
return {
exists: true,
defaultWorkspaceId: user.defaultWorkspaceId,
availableWorkspaces: await this.findAvailableWorkspacesByEmail(email),
};
}

View File

@ -38,7 +38,7 @@ const bootstrap = async () => {
const logger = app.get(LoggerService);
const environmentService = app.get(EnvironmentService);
// TODO: Double check this as it's not working for now, it's going to be heplful for durable trees in twenty "orm"
// TODO: Double check this as it's not working for now, it's going to be helpful for durable trees in twenty "orm"
// // Apply context id strategy for durable trees
// ContextIdFactory.apply(new AggregateByWorkspaceContextIdStrategy());