refacto(*): remove everything about default workspace (#9157)
## Summary - [x] Remove defaultWorkspace in user - [x] Remove all occurrence of defaultWorkspace and defaultWorkspaceId - [x] Improve activate workspace flow - [x] Improve security on social login - [x] Add `ImpersonateGuard` - [x] Allow to use impersonation with couple `User/Workspace` - [x] Prevent unexpected reload on activate workspace - [x] Scope login token with workspaceId Fix https://github.com/twentyhq/twenty/issues/9033#event-15714863042
This commit is contained in:
@ -1,5 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
schema: (process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') + '/metadata',
|
schema:
|
||||||
|
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
|
||||||
|
'/metadata',
|
||||||
documents: [
|
documents: [
|
||||||
'./src/modules/databases/graphql/**/*.ts',
|
'./src/modules/databases/graphql/**/*.ts',
|
||||||
'./src/modules/object-metadata/graphql/*.ts',
|
'./src/modules/object-metadata/graphql/*.ts',
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
schema: (process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') + '/graphql',
|
schema:
|
||||||
|
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
|
||||||
|
'/graphql',
|
||||||
documents: [
|
documents: [
|
||||||
'!./src/modules/databases/**',
|
'!./src/modules/databases/**',
|
||||||
'!./src/modules/object-metadata/**',
|
'!./src/modules/object-metadata/**',
|
||||||
|
|||||||
@ -176,6 +176,7 @@ export type ClientConfig = {
|
|||||||
__typename?: 'ClientConfig';
|
__typename?: 'ClientConfig';
|
||||||
analyticsEnabled: Scalars['Boolean']['output'];
|
analyticsEnabled: Scalars['Boolean']['output'];
|
||||||
api: ApiConfig;
|
api: ApiConfig;
|
||||||
|
authProviders: AuthProviders;
|
||||||
billing: Billing;
|
billing: Billing;
|
||||||
captcha: Captcha;
|
captcha: Captcha;
|
||||||
chromeExtensionId?: Maybe<Scalars['String']['output']>;
|
chromeExtensionId?: Maybe<Scalars['String']['output']>;
|
||||||
@ -358,13 +359,6 @@ export type EmailPasswordResetLink = {
|
|||||||
success: Scalars['Boolean']['output'];
|
success: Scalars['Boolean']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExchangeAuthCode = {
|
|
||||||
__typename?: 'ExchangeAuthCode';
|
|
||||||
accessToken: AuthToken;
|
|
||||||
loginToken: AuthToken;
|
|
||||||
refreshToken: AuthToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExecuteServerlessFunctionInput = {
|
export type ExecuteServerlessFunctionInput = {
|
||||||
/** Id of the serverless function to execute */
|
/** Id of the serverless function to execute */
|
||||||
id: Scalars['UUID']['input'];
|
id: Scalars['UUID']['input'];
|
||||||
@ -581,12 +575,11 @@ export type Mutation = {
|
|||||||
editSSOIdentityProvider: EditSsoOutput;
|
editSSOIdentityProvider: EditSsoOutput;
|
||||||
emailPasswordResetLink: EmailPasswordResetLink;
|
emailPasswordResetLink: EmailPasswordResetLink;
|
||||||
enablePostgresProxy: PostgresCredentials;
|
enablePostgresProxy: PostgresCredentials;
|
||||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
|
||||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||||
generateApiKeyToken: ApiKeyToken;
|
generateApiKeyToken: ApiKeyToken;
|
||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||||
impersonate: Verify;
|
impersonate: AuthTokens;
|
||||||
publishServerlessFunction: ServerlessFunction;
|
publishServerlessFunction: ServerlessFunction;
|
||||||
renewToken: AuthTokens;
|
renewToken: AuthTokens;
|
||||||
resendWorkspaceInvitation: SendInvitationsOutput;
|
resendWorkspaceInvitation: SendInvitationsOutput;
|
||||||
@ -613,7 +606,7 @@ export type Mutation = {
|
|||||||
uploadProfilePicture: Scalars['String']['output'];
|
uploadProfilePicture: Scalars['String']['output'];
|
||||||
uploadWorkspaceLogo: Scalars['String']['output'];
|
uploadWorkspaceLogo: Scalars['String']['output'];
|
||||||
userLookupAdminPanel: UserLookup;
|
userLookupAdminPanel: UserLookup;
|
||||||
verify: Verify;
|
verify: AuthTokens;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -762,13 +755,6 @@ export type MutationEmailPasswordResetLinkArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationExchangeAuthorizationCodeArgs = {
|
|
||||||
authorizationCode: Scalars['String']['input'];
|
|
||||||
clientSecret?: InputMaybe<Scalars['String']['input']>;
|
|
||||||
codeVerifier?: InputMaybe<Scalars['String']['input']>;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationExecuteOneServerlessFunctionArgs = {
|
export type MutationExecuteOneServerlessFunctionArgs = {
|
||||||
input: ExecuteServerlessFunctionInput;
|
input: ExecuteServerlessFunctionInput;
|
||||||
};
|
};
|
||||||
@ -787,6 +773,7 @@ export type MutationGetAuthorizationUrlArgs = {
|
|||||||
|
|
||||||
export type MutationImpersonateArgs = {
|
export type MutationImpersonateArgs = {
|
||||||
userId: Scalars['String']['input'];
|
userId: Scalars['String']['input'];
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1593,9 +1580,8 @@ export type User = {
|
|||||||
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
||||||
canImpersonate: Scalars['Boolean']['output'];
|
canImpersonate: Scalars['Boolean']['output'];
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
|
currentWorkspace?: Maybe<Workspace>;
|
||||||
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
||||||
defaultWorkspace: Workspace;
|
|
||||||
defaultWorkspaceId: Scalars['String']['output'];
|
|
||||||
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||||
disabled?: Maybe<Scalars['Boolean']['output']>;
|
disabled?: Maybe<Scalars['Boolean']['output']>;
|
||||||
email: Scalars['String']['output'];
|
email: Scalars['String']['output'];
|
||||||
@ -1681,12 +1667,6 @@ export type ValidatePasswordResetToken = {
|
|||||||
id: Scalars['String']['output'];
|
id: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Verify = {
|
|
||||||
__typename?: 'Verify';
|
|
||||||
tokens: AuthTokenPair;
|
|
||||||
user: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowAction = {
|
export type WorkflowAction = {
|
||||||
__typename?: 'WorkflowAction';
|
__typename?: 'WorkflowAction';
|
||||||
id: Scalars['UUID']['output'];
|
id: Scalars['UUID']['output'];
|
||||||
|
|||||||
@ -25,12 +25,6 @@ export type ActivateWorkspaceInput = {
|
|||||||
displayName?: InputMaybe<Scalars['String']>;
|
displayName?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActivateWorkspaceOutput = {
|
|
||||||
__typename?: 'ActivateWorkspaceOutput';
|
|
||||||
loginToken: AuthToken;
|
|
||||||
workspace: Workspace;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Analytics = {
|
export type Analytics = {
|
||||||
__typename?: 'Analytics';
|
__typename?: 'Analytics';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -260,13 +254,6 @@ export type EmailPasswordResetLink = {
|
|||||||
success: Scalars['Boolean'];
|
success: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExchangeAuthCode = {
|
|
||||||
__typename?: 'ExchangeAuthCode';
|
|
||||||
accessToken: AuthToken;
|
|
||||||
loginToken: AuthToken;
|
|
||||||
refreshToken: AuthToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExecuteServerlessFunctionInput = {
|
export type ExecuteServerlessFunctionInput = {
|
||||||
/** Id of the serverless function to execute */
|
/** Id of the serverless function to execute */
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
@ -382,6 +369,12 @@ export enum IdentityProviderType {
|
|||||||
Saml = 'SAML'
|
Saml = 'SAML'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ImpersonateOutput = {
|
||||||
|
__typename?: 'ImpersonateOutput';
|
||||||
|
loginToken: AuthToken;
|
||||||
|
workspace: WorkspaceSubdomainAndId;
|
||||||
|
};
|
||||||
|
|
||||||
export type IndexConnection = {
|
export type IndexConnection = {
|
||||||
__typename?: 'IndexConnection';
|
__typename?: 'IndexConnection';
|
||||||
/** Array of edges. */
|
/** Array of edges. */
|
||||||
@ -445,7 +438,7 @@ export enum MessageChannelVisibility {
|
|||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
activateWorkflowVersion: Scalars['Boolean'];
|
activateWorkflowVersion: Scalars['Boolean'];
|
||||||
activateWorkspace: ActivateWorkspaceOutput;
|
activateWorkspace: Workspace;
|
||||||
addUserToWorkspace: User;
|
addUserToWorkspace: User;
|
||||||
addUserToWorkspaceByInviteToken: User;
|
addUserToWorkspaceByInviteToken: User;
|
||||||
authorizeApp: AuthorizeApp;
|
authorizeApp: AuthorizeApp;
|
||||||
@ -470,18 +463,17 @@ export type Mutation = {
|
|||||||
editSSOIdentityProvider: EditSsoOutput;
|
editSSOIdentityProvider: EditSsoOutput;
|
||||||
emailPasswordResetLink: EmailPasswordResetLink;
|
emailPasswordResetLink: EmailPasswordResetLink;
|
||||||
enablePostgresProxy: PostgresCredentials;
|
enablePostgresProxy: PostgresCredentials;
|
||||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
|
||||||
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
|
||||||
generateApiKeyToken: ApiKeyToken;
|
generateApiKeyToken: ApiKeyToken;
|
||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
getAuthorizationUrl: GetAuthorizationUrlOutput;
|
||||||
impersonate: Verify;
|
impersonate: ImpersonateOutput;
|
||||||
publishServerlessFunction: ServerlessFunction;
|
publishServerlessFunction: ServerlessFunction;
|
||||||
renewToken: AuthTokens;
|
renewToken: AuthTokens;
|
||||||
resendWorkspaceInvitation: SendInvitationsOutput;
|
resendWorkspaceInvitation: SendInvitationsOutput;
|
||||||
runWorkflowVersion: WorkflowRun;
|
runWorkflowVersion: WorkflowRun;
|
||||||
sendInvitations: SendInvitationsOutput;
|
sendInvitations: SendInvitationsOutput;
|
||||||
signUp: LoginToken;
|
signUp: SignUpOutput;
|
||||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||||
switchWorkspace: PublicWorkspaceDataOutput;
|
switchWorkspace: PublicWorkspaceDataOutput;
|
||||||
track: Analytics;
|
track: Analytics;
|
||||||
@ -497,7 +489,7 @@ export type Mutation = {
|
|||||||
uploadProfilePicture: Scalars['String'];
|
uploadProfilePicture: Scalars['String'];
|
||||||
uploadWorkspaceLogo: Scalars['String'];
|
uploadWorkspaceLogo: Scalars['String'];
|
||||||
userLookupAdminPanel: UserLookup;
|
userLookupAdminPanel: UserLookup;
|
||||||
verify: Verify;
|
verify: AuthTokens;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -606,13 +598,6 @@ export type MutationEmailPasswordResetLinkArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationExchangeAuthorizationCodeArgs = {
|
|
||||||
authorizationCode: Scalars['String'];
|
|
||||||
clientSecret?: InputMaybe<Scalars['String']>;
|
|
||||||
codeVerifier?: InputMaybe<Scalars['String']>;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationExecuteOneServerlessFunctionArgs = {
|
export type MutationExecuteOneServerlessFunctionArgs = {
|
||||||
input: ExecuteServerlessFunctionInput;
|
input: ExecuteServerlessFunctionInput;
|
||||||
};
|
};
|
||||||
@ -631,6 +616,7 @@ export type MutationGetAuthorizationUrlArgs = {
|
|||||||
|
|
||||||
export type MutationImpersonateArgs = {
|
export type MutationImpersonateArgs = {
|
||||||
userId: Scalars['String'];
|
userId: Scalars['String'];
|
||||||
|
workspaceId: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1121,6 +1107,12 @@ export type SetupSsoOutput = {
|
|||||||
type: IdentityProviderType;
|
type: IdentityProviderType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SignUpOutput = {
|
||||||
|
__typename?: 'SignUpOutput';
|
||||||
|
loginToken: AuthToken;
|
||||||
|
workspace: WorkspaceSubdomainAndId;
|
||||||
|
};
|
||||||
|
|
||||||
/** Sort Directions */
|
/** Sort Directions */
|
||||||
export enum SortDirection {
|
export enum SortDirection {
|
||||||
Asc = 'ASC',
|
Asc = 'ASC',
|
||||||
@ -1302,9 +1294,8 @@ export type User = {
|
|||||||
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
||||||
canImpersonate: Scalars['Boolean'];
|
canImpersonate: Scalars['Boolean'];
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
|
currentWorkspace?: Maybe<Workspace>;
|
||||||
defaultAvatarUrl?: Maybe<Scalars['String']>;
|
defaultAvatarUrl?: Maybe<Scalars['String']>;
|
||||||
defaultWorkspace: Workspace;
|
|
||||||
defaultWorkspaceId: Scalars['String'];
|
|
||||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||||
disabled?: Maybe<Scalars['Boolean']>;
|
disabled?: Maybe<Scalars['Boolean']>;
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@ -1333,7 +1324,6 @@ export type UserEdge = {
|
|||||||
export type UserExists = {
|
export type UserExists = {
|
||||||
__typename?: 'UserExists';
|
__typename?: 'UserExists';
|
||||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||||
defaultWorkspaceId: Scalars['String'];
|
|
||||||
exists: Scalars['Boolean'];
|
exists: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1381,12 +1371,6 @@ export type ValidatePasswordResetToken = {
|
|||||||
id: Scalars['String'];
|
id: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Verify = {
|
|
||||||
__typename?: 'Verify';
|
|
||||||
tokens: AuthTokenPair;
|
|
||||||
user: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowAction = {
|
export type WorkflowAction = {
|
||||||
__typename?: 'WorkflowAction';
|
__typename?: 'WorkflowAction';
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
@ -1471,6 +1455,7 @@ export type WorkspaceEdge = {
|
|||||||
|
|
||||||
export type WorkspaceInfo = {
|
export type WorkspaceInfo = {
|
||||||
__typename?: 'WorkspaceInfo';
|
__typename?: 'WorkspaceInfo';
|
||||||
|
allowImpersonation: Scalars['Boolean'];
|
||||||
featureFlags: Array<FeatureFlag>;
|
featureFlags: Array<FeatureFlag>;
|
||||||
id: Scalars['String'];
|
id: Scalars['String'];
|
||||||
logo?: Maybe<Scalars['String']>;
|
logo?: Maybe<Scalars['String']>;
|
||||||
@ -1524,6 +1509,12 @@ export type WorkspaceNameAndId = {
|
|||||||
id: Scalars['String'];
|
id: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkspaceSubdomainAndId = {
|
||||||
|
__typename?: 'WorkspaceSubdomainAndId';
|
||||||
|
id: Scalars['String'];
|
||||||
|
subdomain: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type BillingCustomer = {
|
export type BillingCustomer = {
|
||||||
__typename?: 'billingCustomer';
|
__typename?: 'billingCustomer';
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
@ -1877,10 +1868,11 @@ export type GetAuthorizationUrlMutation = { __typename?: 'Mutation', getAuthoriz
|
|||||||
|
|
||||||
export type ImpersonateMutationVariables = Exact<{
|
export type ImpersonateMutationVariables = Exact<{
|
||||||
userId: Scalars['String'];
|
userId: Scalars['String'];
|
||||||
|
workspaceId: Scalars['String'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'ImpersonateOutput', workspace: { __typename?: 'WorkspaceSubdomainAndId', subdomain: string, id: string }, loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
|
||||||
|
|
||||||
export type RenewTokenMutationVariables = Exact<{
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
appToken: Scalars['String'];
|
appToken: Scalars['String'];
|
||||||
@ -1898,7 +1890,7 @@ export type SignUpMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
|
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceSubdomainAndId', id: string, subdomain: string } } };
|
||||||
|
|
||||||
export type SwitchWorkspaceMutationVariables = Exact<{
|
export type SwitchWorkspaceMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String'];
|
workspaceId: Scalars['String'];
|
||||||
@ -1920,7 +1912,7 @@ export type VerifyMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type CheckUserExistsQueryVariables = Exact<{
|
export type CheckUserExistsQueryVariables = Exact<{
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@ -1928,7 +1920,7 @@ export type CheckUserExistsQueryVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
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 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 GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -1993,7 +1985,7 @@ export type UserLookupAdminPanelMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: string, value: boolean }> }> } };
|
export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: string, value: boolean }> }> } };
|
||||||
|
|
||||||
export type CreateOidcIdentityProviderMutationVariables = Exact<{
|
export type CreateOidcIdentityProviderMutationVariables = Exact<{
|
||||||
input: SetupOidcSsoInput;
|
input: SetupOidcSsoInput;
|
||||||
@ -2028,7 +2020,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key:
|
|||||||
|
|
||||||
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
|
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> };
|
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> };
|
||||||
|
|
||||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -2045,7 +2037,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
|||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> } };
|
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null, subdomain: string } | null }> } };
|
||||||
|
|
||||||
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||||
workflowVersionId: Scalars['String'];
|
workflowVersionId: Scalars['String'];
|
||||||
@ -2143,7 +2135,7 @@ export type ActivateWorkspaceMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'ActivateWorkspaceOutput', workspace: { __typename?: 'Workspace', id: any, subdomain: string }, loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
|
export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'Workspace', id: any, subdomain: string } };
|
||||||
|
|
||||||
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
|
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -2308,7 +2300,7 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
workspaceMembers {
|
workspaceMembers {
|
||||||
...WorkspaceMemberQueryFragment
|
...WorkspaceMemberQueryFragment
|
||||||
}
|
}
|
||||||
defaultWorkspace {
|
currentWorkspace {
|
||||||
id
|
id
|
||||||
displayName
|
displayName
|
||||||
logo
|
logo
|
||||||
@ -2823,18 +2815,18 @@ export type GetAuthorizationUrlMutationHookResult = ReturnType<typeof useGetAuth
|
|||||||
export type GetAuthorizationUrlMutationResult = Apollo.MutationResult<GetAuthorizationUrlMutation>;
|
export type GetAuthorizationUrlMutationResult = Apollo.MutationResult<GetAuthorizationUrlMutation>;
|
||||||
export type GetAuthorizationUrlMutationOptions = Apollo.BaseMutationOptions<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>;
|
export type GetAuthorizationUrlMutationOptions = Apollo.BaseMutationOptions<GetAuthorizationUrlMutation, GetAuthorizationUrlMutationVariables>;
|
||||||
export const ImpersonateDocument = gql`
|
export const ImpersonateDocument = gql`
|
||||||
mutation Impersonate($userId: String!) {
|
mutation Impersonate($userId: String!, $workspaceId: String!) {
|
||||||
impersonate(userId: $userId) {
|
impersonate(userId: $userId, workspaceId: $workspaceId) {
|
||||||
user {
|
workspace {
|
||||||
...UserQueryFragment
|
subdomain
|
||||||
|
id
|
||||||
}
|
}
|
||||||
tokens {
|
loginToken {
|
||||||
...AuthTokensFragment
|
...AuthTokenFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${UserQueryFragmentFragmentDoc}
|
${AuthTokenFragmentFragmentDoc}`;
|
||||||
${AuthTokensFragmentFragmentDoc}`;
|
|
||||||
export type ImpersonateMutationFn = Apollo.MutationFunction<ImpersonateMutation, ImpersonateMutationVariables>;
|
export type ImpersonateMutationFn = Apollo.MutationFunction<ImpersonateMutation, ImpersonateMutationVariables>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2851,6 +2843,7 @@ export type ImpersonateMutationFn = Apollo.MutationFunction<ImpersonateMutation,
|
|||||||
* const [impersonateMutation, { data, loading, error }] = useImpersonateMutation({
|
* const [impersonateMutation, { data, loading, error }] = useImpersonateMutation({
|
||||||
* variables: {
|
* variables: {
|
||||||
* userId: // value for 'userId'
|
* userId: // value for 'userId'
|
||||||
|
* workspaceId: // value for 'workspaceId'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
@ -2908,6 +2901,10 @@ export const SignUpDocument = gql`
|
|||||||
loginToken {
|
loginToken {
|
||||||
...AuthTokenFragment
|
...AuthTokenFragment
|
||||||
}
|
}
|
||||||
|
workspace {
|
||||||
|
id
|
||||||
|
subdomain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${AuthTokenFragmentFragmentDoc}`;
|
${AuthTokenFragmentFragmentDoc}`;
|
||||||
@ -3028,16 +3025,12 @@ export type UpdatePasswordViaResetTokenMutationOptions = Apollo.BaseMutationOpti
|
|||||||
export const VerifyDocument = gql`
|
export const VerifyDocument = gql`
|
||||||
mutation Verify($loginToken: String!) {
|
mutation Verify($loginToken: String!) {
|
||||||
verify(loginToken: $loginToken) {
|
verify(loginToken: $loginToken) {
|
||||||
user {
|
|
||||||
...UserQueryFragment
|
|
||||||
}
|
|
||||||
tokens {
|
tokens {
|
||||||
...AuthTokensFragment
|
...AuthTokensFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${UserQueryFragmentFragmentDoc}
|
${AuthTokensFragmentFragmentDoc}`;
|
||||||
${AuthTokensFragmentFragmentDoc}`;
|
|
||||||
export type VerifyMutationFn = Apollo.MutationFunction<VerifyMutation, VerifyMutationVariables>;
|
export type VerifyMutationFn = Apollo.MutationFunction<VerifyMutation, VerifyMutationVariables>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3070,7 +3063,6 @@ export const CheckUserExistsDocument = gql`
|
|||||||
__typename
|
__typename
|
||||||
... on UserExists {
|
... on UserExists {
|
||||||
exists
|
exists
|
||||||
defaultWorkspaceId
|
|
||||||
availableWorkspaces {
|
availableWorkspaces {
|
||||||
id
|
id
|
||||||
displayName
|
displayName
|
||||||
@ -3507,6 +3499,7 @@ export const UserLookupAdminPanelDocument = gql`
|
|||||||
name
|
name
|
||||||
logo
|
logo
|
||||||
totalUsers
|
totalUsers
|
||||||
|
allowImpersonation
|
||||||
users {
|
users {
|
||||||
id
|
id
|
||||||
email
|
email
|
||||||
@ -4281,16 +4274,11 @@ export type AddUserToWorkspaceByInviteTokenMutationOptions = Apollo.BaseMutation
|
|||||||
export const ActivateWorkspaceDocument = gql`
|
export const ActivateWorkspaceDocument = gql`
|
||||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||||
activateWorkspace(data: $input) {
|
activateWorkspace(data: $input) {
|
||||||
workspace {
|
id
|
||||||
id
|
subdomain
|
||||||
subdomain
|
|
||||||
}
|
|
||||||
loginToken {
|
|
||||||
...AuthTokenFragment
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${AuthTokenFragmentFragmentDoc}`;
|
`;
|
||||||
export type ActivateWorkspaceMutationFn = Apollo.MutationFunction<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>;
|
export type ActivateWorkspaceMutationFn = Apollo.MutationFunction<ActivateWorkspaceMutation, ActivateWorkspaceMutationVariables>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -254,10 +254,10 @@ const SettingsAdmin = lazy(() =>
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const SettingsAdminFeatureFlags = lazy(() =>
|
const SettingsAdminContent = lazy(() =>
|
||||||
import('~/pages/settings/admin-panel/SettingsAdminFeatureFlags').then(
|
import('@/settings/admin-panel/components/SettingsAdminContent').then(
|
||||||
(module) => ({
|
(module) => ({
|
||||||
default: module.SettingsAdminFeatureFlags,
|
default: module.SettingsAdminContent,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -402,7 +402,7 @@ export const SettingsRoutes = ({
|
|||||||
<Route path={SettingsPath.AdminPanel} element={<SettingsAdmin />} />
|
<Route path={SettingsPath.AdminPanel} element={<SettingsAdmin />} />
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.FeatureFlags}
|
path={SettingsPath.FeatureFlags}
|
||||||
element={<SettingsAdminFeatureFlags />}
|
element={<SettingsAdminContent />}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import { gql } from '@apollo/client';
|
|||||||
|
|
||||||
// TODO: Fragments should be used instead of duplicating the user fields !
|
// TODO: Fragments should be used instead of duplicating the user fields !
|
||||||
export const IMPERSONATE = gql`
|
export const IMPERSONATE = gql`
|
||||||
mutation Impersonate($userId: String!) {
|
mutation Impersonate($userId: String!, $workspaceId: String!) {
|
||||||
impersonate(userId: $userId) {
|
impersonate(userId: $userId, workspaceId: $workspaceId) {
|
||||||
user {
|
workspace {
|
||||||
...UserQueryFragment
|
subdomain
|
||||||
|
id
|
||||||
}
|
}
|
||||||
tokens {
|
loginToken {
|
||||||
...AuthTokensFragment
|
...AuthTokenFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,10 @@ export const SIGN_UP = gql`
|
|||||||
loginToken {
|
loginToken {
|
||||||
...AuthTokenFragment
|
...AuthTokenFragment
|
||||||
}
|
}
|
||||||
|
workspace {
|
||||||
|
id
|
||||||
|
subdomain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -3,9 +3,6 @@ import { gql } from '@apollo/client';
|
|||||||
export const VERIFY = gql`
|
export const VERIFY = gql`
|
||||||
mutation Verify($loginToken: String!) {
|
mutation Verify($loginToken: String!) {
|
||||||
verify(loginToken: $loginToken) {
|
verify(loginToken: $loginToken) {
|
||||||
user {
|
|
||||||
...UserQueryFragment
|
|
||||||
}
|
|
||||||
tokens {
|
tokens {
|
||||||
...AuthTokensFragment
|
...AuthTokensFragment
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ export const CHECK_USER_EXISTS = gql`
|
|||||||
__typename
|
__typename
|
||||||
... on UserExists {
|
... on UserExists {
|
||||||
exists
|
exists
|
||||||
defaultWorkspaceId
|
|
||||||
availableWorkspaces {
|
availableWorkspaces {
|
||||||
id
|
id
|
||||||
displayName
|
displayName
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ChallengeDocument,
|
ChallengeDocument,
|
||||||
|
GetCurrentUserDocument,
|
||||||
SignUpDocument,
|
SignUpDocument,
|
||||||
VerifyDocument,
|
VerifyDocument,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
@ -8,6 +9,7 @@ export const queries = {
|
|||||||
challenge: ChallengeDocument,
|
challenge: ChallengeDocument,
|
||||||
verify: VerifyDocument,
|
verify: VerifyDocument,
|
||||||
signup: SignUpDocument,
|
signup: SignUpDocument,
|
||||||
|
getCurrentUser: GetCurrentUserDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const email = 'test@test.com';
|
export const email = 'test@test.com';
|
||||||
@ -22,6 +24,7 @@ export const variables = {
|
|||||||
},
|
},
|
||||||
verify: { loginToken: token },
|
verify: { loginToken: token },
|
||||||
signup: {},
|
signup: {},
|
||||||
|
getCurrentUser: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const results = {
|
export const results = {
|
||||||
@ -32,7 +35,14 @@ export const results = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
verify: {
|
verify: {
|
||||||
user: {
|
tokens: {
|
||||||
|
accessToken: { token, expiresAt: 'expiresAt' },
|
||||||
|
refreshToken: { token, expiresAt: 'expiresAt' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
signUp: { loginToken: { token, expiresAt: 'expiresAt' } },
|
||||||
|
getCurrentUser: {
|
||||||
|
currentUser: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
firstName: 'firstName',
|
firstName: 'firstName',
|
||||||
lastName: 'lastName',
|
lastName: 'lastName',
|
||||||
@ -49,7 +59,7 @@ export const results = {
|
|||||||
avatarUrl: 'avatarUrl',
|
avatarUrl: 'avatarUrl',
|
||||||
locale: 'locale',
|
locale: 'locale',
|
||||||
},
|
},
|
||||||
defaultWorkspace: {
|
currentWorkspace: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
displayName: 'displayName',
|
displayName: 'displayName',
|
||||||
logo: 'logo',
|
logo: 'logo',
|
||||||
@ -65,13 +75,7 @@ export const results = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tokens: {
|
|
||||||
accessToken: { token, expiresAt: 'expiresAt' },
|
|
||||||
refreshToken: { token, expiresAt: 'expiresAt' },
|
|
||||||
},
|
|
||||||
signup: {},
|
|
||||||
},
|
},
|
||||||
signUp: { loginToken: { token, expiresAt: 'expiresAt' } },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mocks = [
|
export const mocks = [
|
||||||
@ -108,4 +112,13 @@ export const mocks = [
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: queries.getCurrentUser,
|
||||||
|
variables: variables.getCurrentUser,
|
||||||
|
},
|
||||||
|
result: jest.fn(() => ({
|
||||||
|
data: results.getCurrentUser,
|
||||||
|
})),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { expect } from '@storybook/test';
|
import { expect } from '@storybook/test';
|
||||||
import { act, renderHook } from '@testing-library/react';
|
import { ReactNode, act } from 'react';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||||
import { iconsState } from 'twenty-ui';
|
import { iconsState } from 'twenty-ui';
|
||||||
|
|
||||||
@ -15,6 +14,7 @@ import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthPro
|
|||||||
|
|
||||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||||
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
|
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
|
||||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
<MockedProvider mocks={mocks} addTypename={false}>
|
<MockedProvider mocks={mocks} addTypename={false}>
|
||||||
@ -59,6 +59,7 @@ describe('useAuth', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks[1].result).toHaveBeenCalled();
|
expect(mocks[1].result).toHaveBeenCalled();
|
||||||
|
expect(mocks[3].result).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle credential sign-in', async () => {
|
it('should handle credential sign-in', async () => {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
snapshot_UNSTABLE,
|
snapshot_UNSTABLE,
|
||||||
useGotoRecoilSnapshot,
|
useGotoRecoilSnapshot,
|
||||||
useRecoilCallback,
|
useRecoilCallback,
|
||||||
|
useRecoilValue,
|
||||||
useSetRecoilState,
|
useSetRecoilState,
|
||||||
} from 'recoil';
|
} from 'recoil';
|
||||||
import { iconsState } from 'twenty-ui';
|
import { iconsState } from 'twenty-ui';
|
||||||
@ -23,6 +24,7 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
|||||||
import {
|
import {
|
||||||
useChallengeMutation,
|
useChallengeMutation,
|
||||||
useCheckUserExistsLazyQuery,
|
useCheckUserExistsLazyQuery,
|
||||||
|
useGetCurrentUserLazyQuery,
|
||||||
useSignUpMutation,
|
useSignUpMutation,
|
||||||
useVerifyMutation,
|
useVerifyMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
@ -48,6 +50,8 @@ import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/h
|
|||||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||||
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
||||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
@ -62,15 +66,19 @@ export const useAuth = () => {
|
|||||||
const setCurrentWorkspaceMembers = useSetRecoilState(
|
const setCurrentWorkspaceMembers = useSetRecoilState(
|
||||||
currentWorkspaceMembersState,
|
currentWorkspaceMembersState,
|
||||||
);
|
);
|
||||||
|
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||||
|
|
||||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||||
const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
|
const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
|
||||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||||
const { redirect } = useRedirect();
|
const { redirect } = useRedirect();
|
||||||
|
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||||
|
|
||||||
const [challenge] = useChallengeMutation();
|
const [challenge] = useChallengeMutation();
|
||||||
const [signUp] = useSignUpMutation();
|
const [signUp] = useSignUpMutation();
|
||||||
const [verify] = useVerifyMutation();
|
const [verify] = useVerifyMutation();
|
||||||
|
const [getCurrentUser] = useGetCurrentUserLazyQuery();
|
||||||
|
|
||||||
const { isOnAWorkspaceSubdomain } =
|
const { isOnAWorkspaceSubdomain } =
|
||||||
useIsCurrentLocationOnAWorkspaceSubdomain();
|
useIsCurrentLocationOnAWorkspaceSubdomain();
|
||||||
const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation();
|
const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation();
|
||||||
@ -165,6 +173,98 @@ export const useAuth = () => {
|
|||||||
[challenge],
|
[challenge],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadCurrentUser = useCallback(async () => {
|
||||||
|
const currentUserResult = await getCurrentUser();
|
||||||
|
|
||||||
|
const user = currentUserResult.data?.currentUser;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('No current user result');
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspaceMember = null;
|
||||||
|
|
||||||
|
setCurrentUser(user);
|
||||||
|
|
||||||
|
if (isDefined(user.workspaceMembers)) {
|
||||||
|
const workspaceMembers = user.workspaceMembers.map((workspaceMember) => ({
|
||||||
|
...workspaceMember,
|
||||||
|
colorScheme: workspaceMember.colorScheme as ColorScheme,
|
||||||
|
locale: workspaceMember.locale ?? 'en',
|
||||||
|
}));
|
||||||
|
|
||||||
|
setCurrentWorkspaceMembers(workspaceMembers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(user.workspaceMember)) {
|
||||||
|
workspaceMember = {
|
||||||
|
...user.workspaceMember,
|
||||||
|
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
|
||||||
|
locale: user.workspaceMember?.locale ?? 'en',
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentWorkspaceMember(workspaceMember);
|
||||||
|
|
||||||
|
// TODO: factorize with UserProviderEffect
|
||||||
|
setDateTimeFormat({
|
||||||
|
timeZone:
|
||||||
|
workspaceMember.timeZone && workspaceMember.timeZone !== 'system'
|
||||||
|
? workspaceMember.timeZone
|
||||||
|
: detectTimeZone(),
|
||||||
|
dateFormat: isDefined(user.workspaceMember.dateFormat)
|
||||||
|
? getDateFormatFromWorkspaceDateFormat(
|
||||||
|
user.workspaceMember.dateFormat,
|
||||||
|
)
|
||||||
|
: DateFormat[detectDateFormat()],
|
||||||
|
timeFormat: isDefined(user.workspaceMember.timeFormat)
|
||||||
|
? getTimeFormatFromWorkspaceTimeFormat(
|
||||||
|
user.workspaceMember.timeFormat,
|
||||||
|
)
|
||||||
|
: TimeFormat[detectTimeFormat()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = user.currentWorkspace ?? null;
|
||||||
|
|
||||||
|
setCurrentWorkspace(workspace);
|
||||||
|
|
||||||
|
if (isDefined(workspace) && isOnAWorkspaceSubdomain) {
|
||||||
|
setLastAuthenticateWorkspaceDomain({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
subdomain: workspace.subdomain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(user.workspaces)) {
|
||||||
|
const validWorkspaces = user.workspaces
|
||||||
|
.filter(
|
||||||
|
({ workspace }) => workspace !== null && workspace !== undefined,
|
||||||
|
)
|
||||||
|
.map((validWorkspace) => validWorkspace.workspace)
|
||||||
|
.filter(isDefined);
|
||||||
|
|
||||||
|
setWorkspaces(validWorkspaces);
|
||||||
|
}
|
||||||
|
setIsAppWaitingForFreshObjectMetadataState(true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
workspaceMember,
|
||||||
|
workspace,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
getCurrentUser,
|
||||||
|
isOnAWorkspaceSubdomain,
|
||||||
|
setCurrentUser,
|
||||||
|
setCurrentWorkspace,
|
||||||
|
setCurrentWorkspaceMember,
|
||||||
|
setCurrentWorkspaceMembers,
|
||||||
|
setDateTimeFormat,
|
||||||
|
setIsAppWaitingForFreshObjectMetadataState,
|
||||||
|
setLastAuthenticateWorkspaceDomain,
|
||||||
|
setWorkspaces,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleVerify = useCallback(
|
const handleVerify = useCallback(
|
||||||
async (loginToken: string) => {
|
async (loginToken: string) => {
|
||||||
const verifyResult = await verify({
|
const verifyResult = await verify({
|
||||||
@ -181,74 +281,7 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
setTokenPair(verifyResult.data?.verify.tokens);
|
setTokenPair(verifyResult.data?.verify.tokens);
|
||||||
|
|
||||||
const user = verifyResult.data?.verify.user;
|
const { user, workspaceMember, workspace } = await loadCurrentUser();
|
||||||
|
|
||||||
let workspaceMember = null;
|
|
||||||
|
|
||||||
setCurrentUser(user);
|
|
||||||
|
|
||||||
if (isDefined(user.workspaceMembers)) {
|
|
||||||
const workspaceMembers = user.workspaceMembers.map(
|
|
||||||
(workspaceMember) => ({
|
|
||||||
...workspaceMember,
|
|
||||||
colorScheme: workspaceMember.colorScheme as ColorScheme,
|
|
||||||
locale: workspaceMember.locale ?? 'en',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
setCurrentWorkspaceMembers(workspaceMembers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDefined(user.workspaceMember)) {
|
|
||||||
workspaceMember = {
|
|
||||||
...user.workspaceMember,
|
|
||||||
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
|
|
||||||
locale: user.workspaceMember?.locale ?? 'en',
|
|
||||||
};
|
|
||||||
|
|
||||||
setCurrentWorkspaceMember(workspaceMember);
|
|
||||||
|
|
||||||
// TODO: factorize with UserProviderEffect
|
|
||||||
setDateTimeFormat({
|
|
||||||
timeZone:
|
|
||||||
workspaceMember.timeZone && workspaceMember.timeZone !== 'system'
|
|
||||||
? workspaceMember.timeZone
|
|
||||||
: detectTimeZone(),
|
|
||||||
dateFormat: isDefined(user.workspaceMember.dateFormat)
|
|
||||||
? getDateFormatFromWorkspaceDateFormat(
|
|
||||||
user.workspaceMember.dateFormat,
|
|
||||||
)
|
|
||||||
: DateFormat[detectDateFormat()],
|
|
||||||
timeFormat: isDefined(user.workspaceMember.timeFormat)
|
|
||||||
? getTimeFormatFromWorkspaceTimeFormat(
|
|
||||||
user.workspaceMember.timeFormat,
|
|
||||||
)
|
|
||||||
: TimeFormat[detectTimeFormat()],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace = user.defaultWorkspace ?? null;
|
|
||||||
|
|
||||||
setCurrentWorkspace(workspace);
|
|
||||||
|
|
||||||
if (isDefined(workspace) && isOnAWorkspaceSubdomain) {
|
|
||||||
setLastAuthenticateWorkspaceDomain({
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
subdomain: workspace.subdomain,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDefined(verifyResult.data?.verify.user.workspaces)) {
|
|
||||||
const validWorkspaces = verifyResult.data?.verify.user.workspaces
|
|
||||||
.filter(
|
|
||||||
({ workspace }) => workspace !== null && workspace !== undefined,
|
|
||||||
)
|
|
||||||
.map((validWorkspace) => validWorkspace.workspace)
|
|
||||||
.filter(isDefined);
|
|
||||||
|
|
||||||
setWorkspaces(validWorkspaces);
|
|
||||||
}
|
|
||||||
setIsAppWaitingForFreshObjectMetadataState(true);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
@ -257,19 +290,7 @@ export const useAuth = () => {
|
|||||||
tokens: verifyResult.data?.verify.tokens,
|
tokens: verifyResult.data?.verify.tokens,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[verify, setTokenPair, loadCurrentUser],
|
||||||
verify,
|
|
||||||
setTokenPair,
|
|
||||||
setCurrentUser,
|
|
||||||
setCurrentWorkspace,
|
|
||||||
isOnAWorkspaceSubdomain,
|
|
||||||
setIsAppWaitingForFreshObjectMetadataState,
|
|
||||||
setCurrentWorkspaceMembers,
|
|
||||||
setCurrentWorkspaceMember,
|
|
||||||
setDateTimeFormat,
|
|
||||||
setLastAuthenticateWorkspaceDomain,
|
|
||||||
setWorkspaces,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCrendentialsSignIn = useCallback(
|
const handleCrendentialsSignIn = useCallback(
|
||||||
@ -328,6 +349,16 @@ export const useAuth = () => {
|
|||||||
throw new Error('No login token');
|
throw new Error('No login token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMultiWorkspaceEnabled) {
|
||||||
|
return redirectToWorkspaceDomain(
|
||||||
|
signUpResult.data.signUp.workspace.subdomain,
|
||||||
|
AppPath.Verify,
|
||||||
|
{
|
||||||
|
loginToken: signUpResult.data.signUp.loginToken.token,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { user, workspace, workspaceMember } = await handleVerify(
|
const { user, workspace, workspaceMember } = await handleVerify(
|
||||||
signUpResult.data?.signUp.loginToken.token,
|
signUpResult.data?.signUp.loginToken.token,
|
||||||
);
|
);
|
||||||
@ -336,7 +367,13 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
return { user, workspaceMember, workspace };
|
return { user, workspaceMember, workspace };
|
||||||
},
|
},
|
||||||
[setIsVerifyPendingState, signUp, handleVerify],
|
[
|
||||||
|
setIsVerifyPendingState,
|
||||||
|
signUp,
|
||||||
|
isMultiWorkspaceEnabled,
|
||||||
|
handleVerify,
|
||||||
|
redirectToWorkspaceDomain,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildRedirectUrl = useCallback(
|
const buildRedirectUrl = useCallback(
|
||||||
@ -357,6 +394,7 @@ export const useAuth = () => {
|
|||||||
params.workspacePersonalInviteToken,
|
params.workspacePersonalInviteToken,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(workspaceSubdomain)) {
|
if (isDefined(workspaceSubdomain)) {
|
||||||
url.searchParams.set('workspaceSubdomain', workspaceSubdomain);
|
url.searchParams.set('workspaceSubdomain', workspaceSubdomain);
|
||||||
}
|
}
|
||||||
@ -390,6 +428,8 @@ export const useAuth = () => {
|
|||||||
challenge: handleChallenge,
|
challenge: handleChallenge,
|
||||||
verify: handleVerify,
|
verify: handleVerify,
|
||||||
|
|
||||||
|
loadCurrentUser,
|
||||||
|
|
||||||
checkUserExists: { checkUserExistsData, checkUserExistsQuery },
|
checkUserExists: { checkUserExistsData, checkUserExistsQuery },
|
||||||
clearSession,
|
clearSession,
|
||||||
signOut: handleSignOut,
|
signOut: handleSignOut,
|
||||||
|
|||||||
@ -86,10 +86,7 @@ export const SignInUpGlobalScopeForm = () => {
|
|||||||
const response = data.checkUserExists;
|
const response = data.checkUserExists;
|
||||||
if (response.__typename === 'UserExists') {
|
if (response.__typename === 'UserExists') {
|
||||||
if (response.availableWorkspaces.length >= 1) {
|
if (response.availableWorkspaces.length >= 1) {
|
||||||
const workspace =
|
const workspace = response.availableWorkspaces[0];
|
||||||
response.availableWorkspaces.find(
|
|
||||||
(workspace) => workspace.id === response.defaultWorkspaceId,
|
|
||||||
) ?? response.availableWorkspaces[0];
|
|
||||||
return redirectToWorkspaceDomain(workspace.subdomain, pathname, {
|
return redirectToWorkspaceDomain(workspace.subdomain, pathname, {
|
||||||
email: form.getValues('email'),
|
email: form.getValues('email'),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -134,7 +134,7 @@ export const queries = {
|
|||||||
workspaceMembers {
|
workspaceMembers {
|
||||||
...WorkspaceMemberQueryFragment
|
...WorkspaceMemberQueryFragment
|
||||||
}
|
}
|
||||||
defaultWorkspace {
|
currentWorkspace {
|
||||||
id
|
id
|
||||||
displayName
|
displayName
|
||||||
logo
|
logo
|
||||||
@ -281,7 +281,7 @@ export const responseData = {
|
|||||||
timeFormat: '24',
|
timeFormat: '24',
|
||||||
},
|
},
|
||||||
workspaceMembers: [],
|
workspaceMembers: [],
|
||||||
defaultWorkspace: {
|
currentWorkspace: {
|
||||||
id: 'test-workspace-id',
|
id: 'test-workspace-id',
|
||||||
displayName: 'Test Workspace',
|
displayName: 'Test Workspace',
|
||||||
logo: null,
|
logo: null,
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs';
|
import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs';
|
||||||
import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement';
|
import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
|
||||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
import { Table } from '@/ui/layout/table/components/Table';
|
import { Table } from '@/ui/layout/table/components/Table';
|
||||||
@ -22,11 +18,13 @@ import {
|
|||||||
H1TitleFontColor,
|
H1TitleFontColor,
|
||||||
H2Title,
|
H2Title,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
|
IconUser,
|
||||||
isDefined,
|
isDefined,
|
||||||
Section,
|
Section,
|
||||||
Toggle,
|
Toggle,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
|
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
|
||||||
|
|
||||||
const StyledLinkContainer = styled.div`
|
const StyledLinkContainer = styled.div`
|
||||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||||
@ -66,8 +64,16 @@ const StyledContentContainer = styled.div`
|
|||||||
padding: ${({ theme }) => theme.spacing(4)} 0;
|
padding: ${({ theme }) => theme.spacing(4)} 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsAdminFeatureFlags = () => {
|
export const SettingsAdminContent = () => {
|
||||||
const [userIdentifier, setUserIdentifier] = useState('');
|
const [userIdentifier, setUserIdentifier] = useState('');
|
||||||
|
const [userId, setUserId] = useState('');
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleImpersonate,
|
||||||
|
isLoading: isImpersonateLoading,
|
||||||
|
error: impersonateError,
|
||||||
|
canImpersonate,
|
||||||
|
} = useImpersonate();
|
||||||
|
|
||||||
const { activeTabId, setActiveTabId } = useTabList(
|
const { activeTabId, setActiveTabId } = useTabList(
|
||||||
SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID,
|
SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID,
|
||||||
@ -86,6 +92,10 @@ export const SettingsAdminFeatureFlags = () => {
|
|||||||
|
|
||||||
const result = await handleUserLookup(userIdentifier);
|
const result = await handleUserLookup(userIdentifier);
|
||||||
|
|
||||||
|
if (isDefined(result?.user?.id) && !error) {
|
||||||
|
setUserId(result.user.id.trim());
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(result?.workspaces) &&
|
isDefined(result?.workspaces) &&
|
||||||
result.workspaces.length > 0 &&
|
result.workspaces.length > 0 &&
|
||||||
@ -126,6 +136,21 @@ export const SettingsAdminFeatureFlags = () => {
|
|||||||
}`}
|
}`}
|
||||||
description={'Total Users'}
|
description={'Total Users'}
|
||||||
/>
|
/>
|
||||||
|
{canImpersonate && (
|
||||||
|
<Button
|
||||||
|
Icon={IconUser}
|
||||||
|
variant="primary"
|
||||||
|
accent="blue"
|
||||||
|
title={'Impersonate'}
|
||||||
|
onClick={() => handleImpersonate(userId, activeWorkspace.id)}
|
||||||
|
disabled={
|
||||||
|
isImpersonateLoading ||
|
||||||
|
activeWorkspace.allowImpersonation === false
|
||||||
|
}
|
||||||
|
dataTestId="impersonate-button"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<StyledTable>
|
<StyledTable>
|
||||||
<TableRow
|
<TableRow
|
||||||
gridAutoColumns="1fr 100px"
|
gridAutoColumns="1fr 100px"
|
||||||
@ -162,82 +187,69 @@ export const SettingsAdminFeatureFlags = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer
|
<>
|
||||||
title="Feature Flags"
|
<Section>
|
||||||
links={[
|
<H2Title
|
||||||
{
|
title="Feature Flags & Impersonation"
|
||||||
children: 'Other',
|
description="Look up users and manage their workspace feature flags or impersonate it."
|
||||||
href: getSettingsPagePath(SettingsPath.AdminPanel),
|
/>
|
||||||
},
|
|
||||||
{
|
|
||||||
children: 'Server Admin Panel',
|
|
||||||
href: getSettingsPagePath(SettingsPath.AdminPanel),
|
|
||||||
},
|
|
||||||
{ children: 'Feature Flags' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SettingsPageContainer>
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
|
||||||
title="Feature Flags Management"
|
|
||||||
description="Look up users and manage their workspace feature flags."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledLinkContainer>
|
<StyledLinkContainer>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={userIdentifier}
|
value={userIdentifier}
|
||||||
onChange={setUserIdentifier}
|
onChange={setUserIdentifier}
|
||||||
onInputEnter={handleSearch}
|
onInputEnter={handleSearch}
|
||||||
placeholder="Enter user ID or email address"
|
placeholder="Enter user ID or email address"
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
|
||||||
</StyledLinkContainer>
|
|
||||||
<Button
|
|
||||||
Icon={IconSearch}
|
|
||||||
variant="primary"
|
|
||||||
accent="blue"
|
|
||||||
title="Search"
|
|
||||||
onClick={handleSearch}
|
|
||||||
disabled={!userIdentifier.trim() || isLoading}
|
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledLinkContainer>
|
||||||
|
<Button
|
||||||
|
Icon={IconSearch}
|
||||||
|
variant="primary"
|
||||||
|
accent="blue"
|
||||||
|
title="Search"
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={!userIdentifier.trim() || isLoading}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
|
||||||
{error && <StyledErrorSection>{error}</StyledErrorSection>}
|
{(error || impersonateError) && (
|
||||||
</Section>
|
<StyledErrorSection>{error ?? impersonateError}</StyledErrorSection>
|
||||||
|
|
||||||
{shouldShowUserData && (
|
|
||||||
<Section>
|
|
||||||
<StyledUserInfo>
|
|
||||||
<H1Title title="User Info" fontColor={H1TitleFontColor.Primary} />
|
|
||||||
<H2Title
|
|
||||||
title={`${userLookupResult.user.firstName || ''} ${
|
|
||||||
userLookupResult.user.lastName || ''
|
|
||||||
}`.trim()}
|
|
||||||
description="User Name"
|
|
||||||
/>
|
|
||||||
<H2Title
|
|
||||||
title={userLookupResult.user.email}
|
|
||||||
description="User Email"
|
|
||||||
/>
|
|
||||||
<H2Title title={userLookupResult.user.id} description="User ID" />
|
|
||||||
</StyledUserInfo>
|
|
||||||
|
|
||||||
<H1Title title="Workspaces" fontColor={H1TitleFontColor.Primary} />
|
|
||||||
<StyledTabListContainer>
|
|
||||||
<TabList
|
|
||||||
tabs={tabs}
|
|
||||||
tabListInstanceId={SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID}
|
|
||||||
behaveAsLinks={false}
|
|
||||||
/>
|
|
||||||
</StyledTabListContainer>
|
|
||||||
<StyledContentContainer>
|
|
||||||
{renderWorkspaceContent()}
|
|
||||||
</StyledContentContainer>
|
|
||||||
</Section>
|
|
||||||
)}
|
)}
|
||||||
</SettingsPageContainer>
|
</Section>
|
||||||
</SubMenuTopBarContainer>
|
|
||||||
|
{shouldShowUserData && (
|
||||||
|
<Section>
|
||||||
|
<StyledUserInfo>
|
||||||
|
<H1Title title="User Info" fontColor={H1TitleFontColor.Primary} />
|
||||||
|
<H2Title
|
||||||
|
title={`${userLookupResult.user.firstName || ''} ${
|
||||||
|
userLookupResult.user.lastName || ''
|
||||||
|
}`.trim()}
|
||||||
|
description="User Name"
|
||||||
|
/>
|
||||||
|
<H2Title
|
||||||
|
title={userLookupResult.user.email}
|
||||||
|
description="User Email"
|
||||||
|
/>
|
||||||
|
<H2Title title={userLookupResult.user.id} description="User ID" />
|
||||||
|
</StyledUserInfo>
|
||||||
|
|
||||||
|
<H1Title title="Workspaces" fontColor={H1TitleFontColor.Primary} />
|
||||||
|
<StyledTabListContainer>
|
||||||
|
<TabList
|
||||||
|
tabs={tabs}
|
||||||
|
tabListInstanceId={SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID}
|
||||||
|
behaveAsLinks={false}
|
||||||
|
/>
|
||||||
|
</StyledTabListContainer>
|
||||||
|
<StyledContentContainer>
|
||||||
|
{renderWorkspaceContent()}
|
||||||
|
</StyledContentContainer>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Button, H2Title, IconUser, Section } from 'twenty-ui';
|
|
||||||
|
|
||||||
const StyledLinkContainer = styled.div`
|
|
||||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledErrorSection = styled.div`
|
|
||||||
color: ${({ theme }) => theme.font.color.danger};
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsAdminImpersonateUsers = () => {
|
|
||||||
const [userId, setUserId] = useState('');
|
|
||||||
const { handleImpersonate, isLoading, error, canImpersonate } =
|
|
||||||
useImpersonate();
|
|
||||||
|
|
||||||
if (!canImpersonate) {
|
|
||||||
return (
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
|
||||||
title="Impersonate"
|
|
||||||
description="You don't have permission to impersonate other users. Please contact your administrator if you need this access."
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Section>
|
|
||||||
<H2Title title="Impersonate" description="Impersonate a user." />
|
|
||||||
<StyledContainer>
|
|
||||||
<StyledLinkContainer>
|
|
||||||
<TextInput
|
|
||||||
value={userId}
|
|
||||||
onChange={setUserId}
|
|
||||||
placeholder="Enter user ID or email address"
|
|
||||||
fullWidth
|
|
||||||
disabled={isLoading}
|
|
||||||
dataTestId="impersonate-input"
|
|
||||||
onInputEnter={() => handleImpersonate(userId)}
|
|
||||||
/>
|
|
||||||
</StyledLinkContainer>
|
|
||||||
<Button
|
|
||||||
Icon={IconUser}
|
|
||||||
variant="primary"
|
|
||||||
accent="blue"
|
|
||||||
title={'Impersonate'}
|
|
||||||
onClick={() => handleImpersonate(userId)}
|
|
||||||
disabled={!userId.trim() || isLoading}
|
|
||||||
dataTestId="impersonate-button"
|
|
||||||
/>
|
|
||||||
</StyledContainer>
|
|
||||||
{error && <StyledErrorSection>{error}</StyledErrorSection>}
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -14,6 +14,7 @@ export const USER_LOOKUP_ADMIN_PANEL = gql`
|
|||||||
name
|
name
|
||||||
logo
|
logo
|
||||||
totalUsers
|
totalUsers
|
||||||
|
allowImpersonation
|
||||||
users {
|
users {
|
||||||
id
|
id
|
||||||
email
|
email
|
||||||
|
|||||||
@ -1,24 +1,21 @@
|
|||||||
import { useAuth } from '@/auth/hooks/useAuth';
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { useImpersonateMutation } from '~/generated/graphql';
|
import { useImpersonateMutation } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||||
|
|
||||||
export const useImpersonate = () => {
|
export const useImpersonate = () => {
|
||||||
const { clearSession } = useAuth();
|
const [currentUser] = useRecoilState(currentUserState);
|
||||||
const { redirect } = useRedirect();
|
|
||||||
const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
|
|
||||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
|
||||||
const [impersonate] = useImpersonateMutation();
|
const [impersonate] = useImpersonateMutation();
|
||||||
|
|
||||||
|
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleImpersonate = async (userId: string) => {
|
const handleImpersonate = async (userId: string, workspaceId: string) => {
|
||||||
if (!userId.trim()) {
|
if (!userId.trim()) {
|
||||||
setError('Please enter a user ID');
|
setError('Please enter a user ID');
|
||||||
return;
|
return;
|
||||||
@ -29,7 +26,7 @@ export const useImpersonate = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const impersonateResult = await impersonate({
|
const impersonateResult = await impersonate({
|
||||||
variables: { userId },
|
variables: { userId, workspaceId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDefined(impersonateResult.errors)) {
|
if (isDefined(impersonateResult.errors)) {
|
||||||
@ -40,11 +37,11 @@ export const useImpersonate = () => {
|
|||||||
throw new Error('No impersonate result');
|
throw new Error('No impersonate result');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user, tokens } = impersonateResult.data.impersonate;
|
const { loginToken, workspace } = impersonateResult.data.impersonate;
|
||||||
await clearSession();
|
|
||||||
setCurrentUser(user);
|
return redirectToWorkspaceDomain(workspace.subdomain, AppPath.Verify, {
|
||||||
setTokenPair(tokens);
|
loginToken: loginToken.token,
|
||||||
redirect(AppPath.Index);
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError('Failed to impersonate user. Please try again.');
|
setError('Failed to impersonate user. Please try again.');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@ -12,4 +12,5 @@ export type WorkspaceInfo = {
|
|||||||
lastName?: string | null;
|
lastName?: string | null;
|
||||||
}[];
|
}[];
|
||||||
featureFlags: FeatureFlag[];
|
featureFlags: FeatureFlag[];
|
||||||
|
allowImpersonation: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -52,7 +52,10 @@ export const UserProviderEffect = () => {
|
|||||||
if (!isDefined(queryData?.currentUser)) return;
|
if (!isDefined(queryData?.currentUser)) return;
|
||||||
|
|
||||||
setCurrentUser(queryData.currentUser);
|
setCurrentUser(queryData.currentUser);
|
||||||
setCurrentWorkspace(queryData.currentUser.defaultWorkspace);
|
|
||||||
|
if (isDefined(queryData.currentUser.currentWorkspace)) {
|
||||||
|
setCurrentWorkspace(queryData.currentUser.currentWorkspace);
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
workspaceMember,
|
workspaceMember,
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
|||||||
workspaceMembers {
|
workspaceMembers {
|
||||||
...WorkspaceMemberQueryFragment
|
...WorkspaceMemberQueryFragment
|
||||||
}
|
}
|
||||||
defaultWorkspace {
|
currentWorkspace {
|
||||||
id
|
id
|
||||||
displayName
|
displayName
|
||||||
logo
|
logo
|
||||||
|
|||||||
@ -3,13 +3,8 @@ import { gql } from '@apollo/client';
|
|||||||
export const ACTIVATE_WORKSPACE = gql`
|
export const ACTIVATE_WORKSPACE = gql`
|
||||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||||
activateWorkspace(data: $input) {
|
activateWorkspace(data: $input) {
|
||||||
workspace {
|
id
|
||||||
id
|
subdomain
|
||||||
subdomain
|
|
||||||
}
|
|
||||||
loginToken {
|
|
||||||
...AuthTokenFragment
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -2,16 +2,12 @@ import styled from '@emotion/styled';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { H2Title, Loader, MainButton } from 'twenty-ui';
|
import { H2Title, Loader, MainButton } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { SubTitle } from '@/auth/components/SubTitle';
|
import { SubTitle } from '@/auth/components/SubTitle';
|
||||||
import { Title } from '@/auth/components/Title';
|
import { Title } from '@/auth/components/Title';
|
||||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
|
|
||||||
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
|
|
||||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
|
||||||
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
|
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
|
||||||
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
@ -22,9 +18,7 @@ import {
|
|||||||
useActivateWorkspaceMutation,
|
useActivateWorkspaceMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
import { AppPath } from '@/types/AppPath';
|
|
||||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
const StyledContentContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -50,12 +44,9 @@ type Form = z.infer<typeof validationSchema>;
|
|||||||
export const CreateWorkspace = () => {
|
export const CreateWorkspace = () => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const onboardingStatus = useOnboardingStatus();
|
const onboardingStatus = useOnboardingStatus();
|
||||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
|
||||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
|
||||||
|
|
||||||
|
const { loadCurrentUser } = useAuth();
|
||||||
const [activateWorkspace] = useActivateWorkspaceMutation();
|
const [activateWorkspace] = useActivateWorkspaceMutation();
|
||||||
const apolloMetadataClient = useApolloMetadataClient();
|
|
||||||
const setIsCurrentUserLoaded = useSetRecoilState(isCurrentUserLoadedState);
|
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
const {
|
const {
|
||||||
@ -81,39 +72,17 @@ export const CreateWorkspace = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsCurrentUserLoaded(false);
|
|
||||||
|
|
||||||
if (isDefined(result.data) && isMultiWorkspaceEnabled) {
|
|
||||||
return redirectToWorkspaceDomain(
|
|
||||||
result.data.activateWorkspace.workspace.subdomain,
|
|
||||||
AppPath.Verify,
|
|
||||||
{
|
|
||||||
loginToken: result.data.activateWorkspace.loginToken.token,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await apolloMetadataClient?.refetchQueries({
|
|
||||||
include: [FIND_MANY_OBJECT_METADATA_ITEMS],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDefined(result.errors)) {
|
if (isDefined(result.errors)) {
|
||||||
throw result.errors ?? new Error('Unknown error');
|
throw result.errors ?? new Error('Unknown error');
|
||||||
}
|
}
|
||||||
|
await loadCurrentUser();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
enqueueSnackBar(error?.message, {
|
enqueueSnackBar(error?.message, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[activateWorkspace, enqueueSnackBar, loadCurrentUser],
|
||||||
activateWorkspace,
|
|
||||||
setIsCurrentUserLoaded,
|
|
||||||
isMultiWorkspaceEnabled,
|
|
||||||
apolloMetadataClient,
|
|
||||||
redirectToWorkspaceDomain,
|
|
||||||
enqueueSnackBar,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
import { SettingsAdminImpersonateUsers } from '@/settings/admin-panel/components/SettingsAdminImpersonateUsers';
|
|
||||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
import { useTheme } from '@emotion/react';
|
import { SettingsAdminContent } from '@/settings/admin-panel/components/SettingsAdminContent';
|
||||||
import { IconFlag, UndecoratedLink } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const SettingsAdmin = () => {
|
export const SettingsAdmin = () => {
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
title="Server Admin Panel"
|
title="Server Admin Panel"
|
||||||
@ -21,18 +17,7 @@ export const SettingsAdmin = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsAdminImpersonateUsers />
|
<SettingsAdminContent />
|
||||||
<UndecoratedLink to={getSettingsPagePath(SettingsPath.FeatureFlags)}>
|
|
||||||
<SettingsCard
|
|
||||||
Icon={
|
|
||||||
<IconFlag
|
|
||||||
size={theme.icon.size.lg}
|
|
||||||
stroke={theme.icon.stroke.sm}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Feature Flags"
|
|
||||||
/>
|
|
||||||
</UndecoratedLink>
|
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -25,7 +25,7 @@ type MockedUser = Pick<
|
|||||||
> & {
|
> & {
|
||||||
workspaceMember: WorkspaceMember | null;
|
workspaceMember: WorkspaceMember | null;
|
||||||
locale: string;
|
locale: string;
|
||||||
defaultWorkspace: Workspace;
|
currentWorkspace: Workspace;
|
||||||
workspaces: Array<{ workspace: Workspace }>;
|
workspaces: Array<{ workspace: Workspace }>;
|
||||||
workspaceMembers: WorkspaceMember[];
|
workspaceMembers: WorkspaceMember[];
|
||||||
};
|
};
|
||||||
@ -112,7 +112,7 @@ export const mockedUserData: MockedUser = {
|
|||||||
supportUserHash:
|
supportUserHash:
|
||||||
'a95afad9ff6f0b364e2a3fd3e246a1a852c22b6e55a3ca33745a86c201f9c10d',
|
'a95afad9ff6f0b364e2a3fd3e246a1a852c22b6e55a3ca33745a86c201f9c10d',
|
||||||
workspaceMember: mockedWorkspaceMemberData,
|
workspaceMember: mockedWorkspaceMemberData,
|
||||||
defaultWorkspace: mockDefaultWorkspace,
|
currentWorkspace: mockDefaultWorkspace,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
workspaces: [{ workspace: mockDefaultWorkspace }],
|
workspaces: [{ workspace: mockDefaultWorkspace }],
|
||||||
workspaceMembers: [mockedWorkspaceMemberData],
|
workspaceMembers: [mockedWorkspaceMemberData],
|
||||||
@ -134,7 +134,7 @@ export const mockedOnboardingUserData = (
|
|||||||
supportUserHash:
|
supportUserHash:
|
||||||
'4fb61d34ed3a4aeda2476d4b308b5162db9e1809b2b8277e6fdc6efc4a609254',
|
'4fb61d34ed3a4aeda2476d4b308b5162db9e1809b2b8277e6fdc6efc4a609254',
|
||||||
workspaceMember: null,
|
workspaceMember: null,
|
||||||
defaultWorkspace: mockDefaultWorkspace,
|
currentWorkspace: mockDefaultWorkspace,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
workspaces: [{ workspace: mockDefaultWorkspace }],
|
workspaces: [{ workspace: mockDefaultWorkspace }],
|
||||||
onboardingStatus: onboardingStatus || null,
|
onboardingStatus: onboardingStatus || null,
|
||||||
|
|||||||
@ -24,7 +24,6 @@ export const seedUsers = async (
|
|||||||
'lastName',
|
'lastName',
|
||||||
'email',
|
'email',
|
||||||
'passwordHash',
|
'passwordHash',
|
||||||
'defaultWorkspaceId',
|
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values([
|
.values([
|
||||||
@ -35,7 +34,6 @@ export const seedUsers = async (
|
|||||||
email: 'noah@demo.dev',
|
email: 'noah@demo.dev',
|
||||||
passwordHash:
|
passwordHash:
|
||||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||||
defaultWorkspaceId: workspaceId,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEMO_SEED_USER_IDS.HUGO,
|
id: DEMO_SEED_USER_IDS.HUGO,
|
||||||
@ -44,7 +42,6 @@ export const seedUsers = async (
|
|||||||
email: 'hugo@demo.dev',
|
email: 'hugo@demo.dev',
|
||||||
passwordHash:
|
passwordHash:
|
||||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||||
defaultWorkspaceId: workspaceId,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEMO_SEED_USER_IDS.TIM,
|
id: DEMO_SEED_USER_IDS.TIM,
|
||||||
@ -53,7 +50,6 @@ export const seedUsers = async (
|
|||||||
email: 'tim@apple.dev',
|
email: 'tim@apple.dev',
|
||||||
passwordHash:
|
passwordHash:
|
||||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||||
defaultWorkspaceId: workspaceId,
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
|
|||||||
@ -24,7 +24,6 @@ export const seedUsers = async (
|
|||||||
'lastName',
|
'lastName',
|
||||||
'email',
|
'email',
|
||||||
'passwordHash',
|
'passwordHash',
|
||||||
'defaultWorkspaceId',
|
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values([
|
.values([
|
||||||
@ -35,7 +34,6 @@ export const seedUsers = async (
|
|||||||
email: 'tim@apple.dev',
|
email: 'tim@apple.dev',
|
||||||
passwordHash:
|
passwordHash:
|
||||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||||
defaultWorkspaceId: workspaceId,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_USER_IDS.JONY,
|
id: DEV_SEED_USER_IDS.JONY,
|
||||||
@ -44,7 +42,6 @@ export const seedUsers = async (
|
|||||||
email: 'jony.ive@apple.dev',
|
email: 'jony.ive@apple.dev',
|
||||||
passwordHash:
|
passwordHash:
|
||||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||||
defaultWorkspaceId: workspaceId,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_USER_IDS.PHIL,
|
id: DEV_SEED_USER_IDS.PHIL,
|
||||||
@ -53,7 +50,6 @@ export const seedUsers = async (
|
|||||||
email: 'phil.schiler@apple.dev',
|
email: 'phil.schiler@apple.dev',
|
||||||
passwordHash:
|
passwordHash:
|
||||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||||
defaultWorkspaceId: workspaceId,
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class RemoveDefaultWorkspaceId1734544295083
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'RemoveDefaultWorkspaceId1734544295083';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" DROP COLUMN "defaultWorkspaceId"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" ADD "defaultWorkspaceId" uuid NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."user" ADD CONSTRAINT "FK_2ec910029395fa7655621c88908" FOREIGN KEY ("defaultWorkspaceId") REFERENCES "core"."workspace"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,49 +6,41 @@ import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/imper
|
|||||||
import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input';
|
import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input';
|
||||||
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
|
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
|
||||||
import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input';
|
import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input';
|
||||||
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
|
|
||||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
|
||||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
|
||||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard';
|
||||||
|
import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output';
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@UseFilters(AuthGraphqlApiExceptionFilter)
|
@UseFilters(AuthGraphqlApiExceptionFilter)
|
||||||
export class AdminPanelResolver {
|
export class AdminPanelResolver {
|
||||||
constructor(private adminService: AdminPanelService) {}
|
constructor(private adminService: AdminPanelService) {}
|
||||||
|
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
||||||
@Mutation(() => Verify)
|
@Mutation(() => ImpersonateOutput)
|
||||||
async impersonate(
|
async impersonate(
|
||||||
@Args() impersonateInput: ImpersonateInput,
|
@Args() { workspaceId, userId }: ImpersonateInput,
|
||||||
@AuthUser() user: User,
|
): Promise<ImpersonateOutput> {
|
||||||
): Promise<Verify> {
|
return await this.adminService.impersonate(userId, workspaceId);
|
||||||
return await this.adminService.impersonate(impersonateInput.userId, user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
||||||
@Mutation(() => UserLookup)
|
@Mutation(() => UserLookup)
|
||||||
async userLookupAdminPanel(
|
async userLookupAdminPanel(
|
||||||
@Args() userLookupInput: UserLookupInput,
|
@Args() userLookupInput: UserLookupInput,
|
||||||
@AuthUser() user: User,
|
|
||||||
): Promise<UserLookup> {
|
): Promise<UserLookup> {
|
||||||
return await this.adminService.userLookup(
|
return await this.adminService.userLookup(userLookupInput.userIdentifier);
|
||||||
userLookupInput.userIdentifier,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async updateWorkspaceFeatureFlag(
|
async updateWorkspaceFeatureFlag(
|
||||||
@Args() updateFlagInput: UpdateWorkspaceFeatureFlagInput,
|
@Args() updateFlagInput: UpdateWorkspaceFeatureFlagInput,
|
||||||
@AuthUser() user: User,
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
await this.adminService.updateWorkspaceFeatureFlags(
|
await this.adminService.updateWorkspaceFeatureFlags(
|
||||||
updateFlagInput.workspaceId,
|
updateFlagInput.workspaceId,
|
||||||
updateFlagInput.featureFlag,
|
updateFlagInput.featureFlag,
|
||||||
user,
|
|
||||||
updateFlagInput.value,
|
updateFlagInput.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -8,18 +8,18 @@ import {
|
|||||||
AuthException,
|
AuthException,
|
||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
} from 'src/engine/core-modules/auth/auth.exception';
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
|
||||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
|
||||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||||
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
|
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminPanelService {
|
export class AdminPanelService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly accessTokenService: AccessTokenService,
|
private readonly loginTokenService: LoginTokenService,
|
||||||
private readonly refreshTokenService: RefreshTokenService,
|
|
||||||
@InjectRepository(User, 'core')
|
@InjectRepository(User, 'core')
|
||||||
private readonly userRepository: Repository<User>,
|
private readonly userRepository: Repository<User>,
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
@ -28,64 +28,48 @@ export class AdminPanelService {
|
|||||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async impersonate(userIdentifier: string, userImpersonating: User) {
|
async impersonate(userId: string, workspaceId: string) {
|
||||||
if (!userImpersonating.canImpersonate) {
|
|
||||||
throw new AuthException(
|
|
||||||
'User cannot impersonate',
|
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEmail = userIdentifier.includes('@');
|
|
||||||
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: isEmail ? { email: userIdentifier } : { id: userIdentifier },
|
where: {
|
||||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
id: userId,
|
||||||
|
workspaces: {
|
||||||
|
workspaceId,
|
||||||
|
workspace: {
|
||||||
|
allowImpersonation: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ['workspaces', 'workspaces.workspace'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
userValidator.assertIsDefinedOrThrow(
|
||||||
throw new AuthException(
|
user,
|
||||||
'User not found',
|
new AuthException('User not found', AuthExceptionCode.INVALID_INPUT),
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.defaultWorkspace.allowImpersonation) {
|
workspaceValidator.assertIsDefinedOrThrow(
|
||||||
throw new AuthException(
|
user.workspaces[0].workspace,
|
||||||
|
new AuthException(
|
||||||
'Impersonation not allowed',
|
'Impersonation not allowed',
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
),
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = await this.accessTokenService.generateAccessToken(
|
|
||||||
user.id,
|
|
||||||
user.defaultWorkspaceId,
|
|
||||||
);
|
);
|
||||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
|
||||||
user.id,
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.defaultWorkspaceId,
|
user.email,
|
||||||
|
user.workspaces[0].workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
workspace: {
|
||||||
tokens: {
|
id: user.workspaces[0].workspace.id,
|
||||||
accessToken,
|
subdomain: user.workspaces[0].workspace.subdomain,
|
||||||
refreshToken,
|
|
||||||
},
|
},
|
||||||
|
loginToken,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async userLookup(
|
async userLookup(userIdentifier: string): Promise<UserLookup> {
|
||||||
userIdentifier: string,
|
|
||||||
userImpersonating: User,
|
|
||||||
): Promise<UserLookup> {
|
|
||||||
if (!userImpersonating.canImpersonate) {
|
|
||||||
throw new AuthException(
|
|
||||||
'User cannot access user info',
|
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEmail = userIdentifier.includes('@');
|
const isEmail = userIdentifier.includes('@');
|
||||||
|
|
||||||
const targetUser = await this.userRepository.findOne({
|
const targetUser = await this.userRepository.findOne({
|
||||||
@ -99,12 +83,10 @@ export class AdminPanelService {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!targetUser) {
|
userValidator.assertIsDefinedOrThrow(
|
||||||
throw new AuthException(
|
targetUser,
|
||||||
'User not found',
|
new AuthException('User not found', AuthExceptionCode.INVALID_INPUT),
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allFeatureFlagKeys = Object.values(FeatureFlagKey);
|
const allFeatureFlagKeys = Object.values(FeatureFlagKey);
|
||||||
|
|
||||||
@ -120,6 +102,7 @@ export class AdminPanelService {
|
|||||||
name: userWorkspace.workspace.displayName ?? '',
|
name: userWorkspace.workspace.displayName ?? '',
|
||||||
totalUsers: userWorkspace.workspace.workspaceUsers.length,
|
totalUsers: userWorkspace.workspace.workspaceUsers.length,
|
||||||
logo: userWorkspace.workspace.logo,
|
logo: userWorkspace.workspace.logo,
|
||||||
|
allowImpersonation: userWorkspace.workspace.allowImpersonation,
|
||||||
users: userWorkspace.workspace.workspaceUsers.map((workspaceUser) => ({
|
users: userWorkspace.workspace.workspaceUsers.map((workspaceUser) => ({
|
||||||
id: workspaceUser.user.id,
|
id: workspaceUser.user.id,
|
||||||
email: workspaceUser.user.email,
|
email: workspaceUser.user.email,
|
||||||
@ -140,27 +123,17 @@ export class AdminPanelService {
|
|||||||
async updateWorkspaceFeatureFlags(
|
async updateWorkspaceFeatureFlags(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
featureFlag: FeatureFlagKey,
|
featureFlag: FeatureFlagKey,
|
||||||
userImpersonating: User,
|
|
||||||
value: boolean,
|
value: boolean,
|
||||||
) {
|
) {
|
||||||
if (!userImpersonating.canImpersonate) {
|
|
||||||
throw new AuthException(
|
|
||||||
'User cannot update feature flags',
|
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace = await this.workspaceRepository.findOne({
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
relations: ['featureFlags'],
|
relations: ['featureFlags'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!workspace) {
|
workspaceValidator.assertIsDefinedOrThrow(
|
||||||
throw new AuthException(
|
workspace,
|
||||||
'Workspace not found',
|
new AuthException('Workspace not found', AuthExceptionCode.INVALID_INPUT),
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingFlag = workspace.featureFlags?.find(
|
const existingFlag = workspace.featureFlags?.find(
|
||||||
(flag) => flag.key === featureFlag,
|
(flag) => flag.key === featureFlag,
|
||||||
|
|||||||
@ -8,4 +8,9 @@ export class ImpersonateInput {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||||
|
import { WorkspaceSubdomainAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ImpersonateOutput {
|
||||||
|
@Field(() => AuthToken)
|
||||||
|
loginToken: AuthToken;
|
||||||
|
|
||||||
|
@Field(() => WorkspaceSubdomainAndId)
|
||||||
|
workspace: WorkspaceSubdomainAndId;
|
||||||
|
}
|
||||||
@ -25,6 +25,9 @@ class WorkspaceInfo {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
allowImpersonation: boolean;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
logo?: string;
|
logo?: string;
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-
|
|||||||
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
||||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||||
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
|
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
|
||||||
import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||||
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
||||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||||
import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/switch-workspace.service';
|
import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/switch-workspace.service';
|
||||||
@ -103,7 +103,8 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
SwitchWorkspaceService,
|
SwitchWorkspaceService,
|
||||||
TransientTokenService,
|
TransientTokenService,
|
||||||
ApiKeyService,
|
ApiKeyService,
|
||||||
OAuthService,
|
// reenable when working on: https://github.com/twentyhq/twenty/issues/9143
|
||||||
|
// OAuthService,
|
||||||
],
|
],
|
||||||
exports: [AccessTokenService, LoginTokenService, RefreshTokenService],
|
exports: [AccessTokenService, LoginTokenService, RefreshTokenService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { AuthResolver } from './auth.resolver';
|
|||||||
|
|
||||||
import { ApiKeyService } from './services/api-key.service';
|
import { ApiKeyService } from './services/api-key.service';
|
||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
import { OAuthService } from './services/oauth.service';
|
// import { OAuthService } from './services/oauth.service';
|
||||||
import { ResetPasswordService } from './services/reset-password.service';
|
import { ResetPasswordService } from './services/reset-password.service';
|
||||||
import { SwitchWorkspaceService } from './services/switch-workspace.service';
|
import { SwitchWorkspaceService } from './services/switch-workspace.service';
|
||||||
import { LoginTokenService } from './token/services/login-token.service';
|
import { LoginTokenService } from './token/services/login-token.service';
|
||||||
@ -80,10 +80,10 @@ describe('AuthResolver', () => {
|
|||||||
provide: TransientTokenService,
|
provide: TransientTokenService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
provide: OAuthService,
|
// provide: OAuthService,
|
||||||
useValue: {},
|
// useValue: {},
|
||||||
},
|
// },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideGuard(CaptchaGuard)
|
.overrideGuard(CaptchaGuard)
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.ent
|
|||||||
import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input';
|
import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input';
|
||||||
import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity';
|
import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity';
|
||||||
import { EmailPasswordResetLinkInput } from 'src/engine/core-modules/auth/dto/email-password-reset-link.input';
|
import { EmailPasswordResetLinkInput } from 'src/engine/core-modules/auth/dto/email-password-reset-link.input';
|
||||||
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
|
||||||
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
|
||||||
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
||||||
import { TransientToken } from 'src/engine/core-modules/auth/dto/transient-token.entity';
|
import { TransientToken } from 'src/engine/core-modules/auth/dto/transient-token.entity';
|
||||||
import { UpdatePasswordViaResetTokenInput } from 'src/engine/core-modules/auth/dto/update-password-via-reset-token.input';
|
import { UpdatePasswordViaResetTokenInput } from 'src/engine/core-modules/auth/dto/update-password-via-reset-token.input';
|
||||||
@ -16,7 +14,7 @@ import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/val
|
|||||||
import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.input';
|
import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.input';
|
||||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||||
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
||||||
import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||||
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
||||||
import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/switch-workspace.service';
|
import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/switch-workspace.service';
|
||||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||||
@ -31,7 +29,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
|||||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
import { SwitchWorkspaceInput } from 'src/engine/core-modules/auth/dto/switch-workspace.input';
|
import { SwitchWorkspaceInput } from 'src/engine/core-modules/auth/dto/switch-workspace.input';
|
||||||
import { PublicWorkspaceDataOutput } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
import { PublicWorkspaceDataOutput } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||||
import {
|
import {
|
||||||
AuthException,
|
AuthException,
|
||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
@ -39,6 +37,8 @@ import {
|
|||||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||||
|
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
|
||||||
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
|
|
||||||
import { ChallengeInput } from './dto/challenge.input';
|
import { ChallengeInput } from './dto/challenge.input';
|
||||||
import { LoginToken } from './dto/login-token.entity';
|
import { LoginToken } from './dto/login-token.entity';
|
||||||
@ -46,7 +46,6 @@ import { SignUpInput } from './dto/sign-up.input';
|
|||||||
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
||||||
import { UserExistsOutput } from './dto/user-exists.entity';
|
import { UserExistsOutput } from './dto/user-exists.entity';
|
||||||
import { CheckUserExistsInput } from './dto/user-exists.input';
|
import { CheckUserExistsInput } from './dto/user-exists.input';
|
||||||
import { Verify } from './dto/verify.entity';
|
|
||||||
import { VerifyInput } from './dto/verify.input';
|
import { VerifyInput } from './dto/verify.input';
|
||||||
import { WorkspaceInviteHashValid } from './dto/workspace-invite-hash-valid.entity';
|
import { WorkspaceInviteHashValid } from './dto/workspace-invite-hash-valid.entity';
|
||||||
import { WorkspaceInviteHashValidInput } from './dto/workspace-invite-hash.input';
|
import { WorkspaceInviteHashValidInput } from './dto/workspace-invite-hash.input';
|
||||||
@ -64,7 +63,7 @@ export class AuthResolver {
|
|||||||
private loginTokenService: LoginTokenService,
|
private loginTokenService: LoginTokenService,
|
||||||
private switchWorkspaceService: SwitchWorkspaceService,
|
private switchWorkspaceService: SwitchWorkspaceService,
|
||||||
private transientTokenService: TransientTokenService,
|
private transientTokenService: TransientTokenService,
|
||||||
private oauthService: OAuthService,
|
// private oauthService: OAuthService,
|
||||||
private domainManagerService: DomainManagerService,
|
private domainManagerService: DomainManagerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -101,7 +100,9 @@ export class AuthResolver {
|
|||||||
@OriginHeader() origin: string,
|
@OriginHeader() origin: string,
|
||||||
): Promise<LoginToken> {
|
): Promise<LoginToken> {
|
||||||
const workspace =
|
const workspace =
|
||||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||||
|
origin,
|
||||||
|
);
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new AuthException(
|
throw new AuthException(
|
||||||
@ -112,18 +113,19 @@ export class AuthResolver {
|
|||||||
const user = await this.authService.challenge(challengeInput, workspace);
|
const user = await this.authService.challenge(challengeInput, workspace);
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
user.email,
|
||||||
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { loginToken };
|
return { loginToken };
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(CaptchaGuard)
|
@UseGuards(CaptchaGuard)
|
||||||
@Mutation(() => LoginToken)
|
@Mutation(() => SignUpOutput)
|
||||||
async signUp(
|
async signUp(
|
||||||
@Args() signUpInput: SignUpInput,
|
@Args() signUpInput: SignUpInput,
|
||||||
@OriginHeader() origin: string,
|
@OriginHeader() origin: string,
|
||||||
): Promise<LoginToken> {
|
): Promise<SignUpOutput> {
|
||||||
const user = await this.authService.signInUp({
|
const { user, workspace } = await this.authService.signInUp({
|
||||||
...signUpInput,
|
...signUpInput,
|
||||||
targetWorkspaceSubdomain:
|
targetWorkspaceSubdomain:
|
||||||
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
|
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
|
||||||
@ -133,19 +135,26 @@ export class AuthResolver {
|
|||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
user.email,
|
||||||
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { loginToken };
|
return {
|
||||||
|
loginToken,
|
||||||
|
workspace: {
|
||||||
|
id: workspace.id,
|
||||||
|
subdomain: workspace.subdomain,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ExchangeAuthCode)
|
// @Mutation(() => ExchangeAuthCode)
|
||||||
async exchangeAuthorizationCode(
|
// async exchangeAuthorizationCode(
|
||||||
@Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
// @Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||||
) {
|
// ) {
|
||||||
return await this.oauthService.verifyAuthorizationCode(
|
// return await this.oauthService.verifyAuthorizationCode(
|
||||||
exchangeAuthCodeInput,
|
// exchangeAuthCodeInput,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Mutation(() => TransientToken)
|
@Mutation(() => TransientToken)
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||||
@ -165,25 +174,35 @@ export class AuthResolver {
|
|||||||
await this.transientTokenService.generateTransientToken(
|
await this.transientTokenService.generateTransientToken(
|
||||||
workspaceMember.id,
|
workspaceMember.id,
|
||||||
user.id,
|
user.id,
|
||||||
user.defaultWorkspaceId,
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { transientToken };
|
return { transientToken };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Verify)
|
@Mutation(() => AuthTokens)
|
||||||
async verify(
|
async verify(
|
||||||
@Args() verifyInput: VerifyInput,
|
@Args() verifyInput: VerifyInput,
|
||||||
@OriginHeader() origin: string,
|
@OriginHeader() origin: string,
|
||||||
): Promise<Verify> {
|
): Promise<AuthTokens> {
|
||||||
const workspace =
|
const workspace =
|
||||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||||
|
origin,
|
||||||
|
);
|
||||||
|
|
||||||
const { sub: email } = await this.loginTokenService.verifyLoginToken(
|
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||||
verifyInput.loginToken,
|
|
||||||
);
|
|
||||||
|
|
||||||
return await this.authService.verify(email, workspace?.id);
|
const { sub: email, workspaceId } =
|
||||||
|
await this.loginTokenService.verifyLoginToken(verifyInput.loginToken);
|
||||||
|
|
||||||
|
if (workspaceId !== workspace.id) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Token is not valid for this workspace',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.authService.verify(email, workspace.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => AuthorizeApp)
|
@Mutation(() => AuthorizeApp)
|
||||||
@ -191,10 +210,12 @@ export class AuthResolver {
|
|||||||
async authorizeApp(
|
async authorizeApp(
|
||||||
@Args() authorizeAppInput: AuthorizeAppInput,
|
@Args() authorizeAppInput: AuthorizeAppInput,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
): Promise<AuthorizeApp> {
|
): Promise<AuthorizeApp> {
|
||||||
return await this.authService.generateAuthorizationCode(
|
return await this.authService.generateAuthorizationCode(
|
||||||
authorizeAppInput,
|
authorizeAppInput,
|
||||||
user,
|
user,
|
||||||
|
workspace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -71,8 +71,9 @@ export class GoogleAuthController {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||||
targetWorkspaceSubdomain ===
|
(targetWorkspaceSubdomain ===
|
||||||
this.environmentService.get('DEFAULT_SUBDOMAIN')
|
this.environmentService.get('DEFAULT_SUBDOMAIN') ||
|
||||||
|
!targetWorkspaceSubdomain)
|
||||||
) {
|
) {
|
||||||
const workspaceWithGoogleAuthActive =
|
const workspaceWithGoogleAuthActive =
|
||||||
await this.workspaceRepository.findOne({
|
await this.workspaceRepository.findOne({
|
||||||
@ -84,7 +85,7 @@ export class GoogleAuthController {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relations: ['userWorkspaces', 'userWorkspaces.user'],
|
relations: ['workspaceUsers', 'workspaceUsers.user'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workspaceWithGoogleAuthActive) {
|
if (workspaceWithGoogleAuthActive) {
|
||||||
@ -93,16 +94,18 @@ export class GoogleAuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.authService.signInUp(signInUpParams);
|
const { user, workspace } =
|
||||||
|
await this.authService.signInUp(signInUpParams);
|
||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
user.email,
|
||||||
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
await this.authService.computeRedirectURI(
|
this.authService.computeRedirectURI(
|
||||||
loginToken.token,
|
loginToken.token,
|
||||||
user.defaultWorkspace.subdomain,
|
workspace.subdomain,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import {
|
|||||||
UseFilters,
|
UseFilters,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||||
import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard';
|
import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard';
|
||||||
@ -18,6 +20,7 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
|
|||||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
@Controller('auth/microsoft')
|
@Controller('auth/microsoft')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -27,6 +30,8 @@ export class MicrosoftAuthController {
|
|||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -43,43 +48,57 @@ export class MicrosoftAuthController {
|
|||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const {
|
const signInUpParams = req.user;
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
picture,
|
|
||||||
workspaceInviteHash,
|
|
||||||
workspacePersonalInviteToken,
|
|
||||||
targetWorkspaceSubdomain,
|
|
||||||
} = req.user;
|
|
||||||
|
|
||||||
const user = await this.authService.signInUp({
|
if (
|
||||||
email,
|
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||||
firstName,
|
(signInUpParams.targetWorkspaceSubdomain ===
|
||||||
lastName,
|
this.environmentService.get('DEFAULT_SUBDOMAIN') ||
|
||||||
picture,
|
!signInUpParams.targetWorkspaceSubdomain)
|
||||||
workspaceInviteHash,
|
) {
|
||||||
workspacePersonalInviteToken,
|
const workspaceWithGoogleAuthActive =
|
||||||
targetWorkspaceSubdomain,
|
await this.workspaceRepository.findOne({
|
||||||
|
where: {
|
||||||
|
isMicrosoftAuthEnabled: true,
|
||||||
|
workspaceUsers: {
|
||||||
|
user: {
|
||||||
|
email: signInUpParams.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ['userWorkspaces', 'userWorkspaces.user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workspaceWithGoogleAuthActive) {
|
||||||
|
signInUpParams.targetWorkspaceSubdomain =
|
||||||
|
workspaceWithGoogleAuthActive.subdomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user, workspace } = await this.authService.signInUp({
|
||||||
|
...signInUpParams,
|
||||||
fromSSO: true,
|
fromSSO: true,
|
||||||
authProvider: 'microsoft',
|
authProvider: 'microsoft',
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
user.email,
|
||||||
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
await this.authService.computeRedirectURI(
|
this.authService.computeRedirectURI(
|
||||||
loginToken.token,
|
loginToken.token,
|
||||||
user.defaultWorkspace.subdomain,
|
workspace.subdomain,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof AuthException) {
|
if (err instanceof AuthException) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.domainManagerService.computeRedirectErrorUrl({
|
this.domainManagerService.computeRedirectErrorUrl({
|
||||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
subdomain:
|
||||||
|
req.user.targetWorkspaceSubdomain ??
|
||||||
|
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||||
errorMessage: err.message,
|
errorMessage: err.message,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export class SSOAuthController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
await this.authService.computeRedirectURI(
|
this.authService.computeRedirectURI(
|
||||||
loginToken.token,
|
loginToken.token,
|
||||||
identityProvider.workspace.subdomain,
|
identityProvider.workspace.subdomain,
|
||||||
),
|
),
|
||||||
@ -113,7 +113,7 @@ export class SSOAuthController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
await this.authService.computeRedirectURI(
|
this.authService.computeRedirectURI(
|
||||||
loginToken.token,
|
loginToken.token,
|
||||||
identityProvider.workspace.subdomain,
|
identityProvider.workspace.subdomain,
|
||||||
),
|
),
|
||||||
@ -183,7 +183,10 @@ export class SSOAuthController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
identityProvider,
|
identityProvider,
|
||||||
loginToken: await this.loginTokenService.generateLoginToken(user.email),
|
loginToken: await this.loginTokenService.generateLoginToken(
|
||||||
|
user.email,
|
||||||
|
identityProvider.workspace.id,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { WorkspaceSubdomainAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
|
||||||
|
|
||||||
|
import { AuthToken } from './token.entity';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class SignUpOutput {
|
||||||
|
@Field(() => AuthToken)
|
||||||
|
loginToken: AuthToken;
|
||||||
|
|
||||||
|
@Field(() => WorkspaceSubdomainAndId)
|
||||||
|
workspace: WorkspaceSubdomainAndId;
|
||||||
|
}
|
||||||
@ -7,9 +7,6 @@ export class UserExists {
|
|||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
exists: true;
|
exists: true;
|
||||||
|
|
||||||
@Field(() => String)
|
|
||||||
defaultWorkspaceId: string;
|
|
||||||
|
|
||||||
@Field(() => [AvailableWorkspaceOutput])
|
@Field(() => [AvailableWorkspaceOutput])
|
||||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
import { Field, ObjectType } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
|
||||||
|
|
||||||
import { AuthTokens } from './token.entity';
|
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class Verify extends AuthTokens {
|
|
||||||
@Field(() => User)
|
|
||||||
user: DeepPartial<User>;
|
|
||||||
}
|
|
||||||
@ -33,7 +33,6 @@ import {
|
|||||||
UserExists,
|
UserExists,
|
||||||
UserNotExists,
|
UserNotExists,
|
||||||
} from 'src/engine/core-modules/auth/dto/user-exists.entity';
|
} from 'src/engine/core-modules/auth/dto/user-exists.entity';
|
||||||
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
|
|
||||||
import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/workspace-invite-hash-valid.entity';
|
import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/workspace-invite-hash-valid.entity';
|
||||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||||
@ -42,12 +41,12 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
|||||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||||
@ -57,7 +56,6 @@ export class AuthService {
|
|||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly refreshTokenService: RefreshTokenService,
|
private readonly refreshTokenService: RefreshTokenService,
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||||
private readonly signInUpService: SignInUpService,
|
private readonly signInUpService: SignInUpService,
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
@ -188,7 +186,7 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(email: string, workspaceId?: string): Promise<Verify> {
|
async verify(email: string, workspaceId: string): Promise<AuthTokens> {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
throw new AuthException(
|
throw new AuthException(
|
||||||
'Email is required',
|
'Email is required',
|
||||||
@ -196,31 +194,8 @@ export class AuthService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userWithIdAndDefaultWorkspaceId = await this.userRepository.findOne({
|
|
||||||
select: ['defaultWorkspaceId', 'id'],
|
|
||||||
where: { email },
|
|
||||||
});
|
|
||||||
|
|
||||||
userValidator.assertIsDefinedOrThrow(
|
|
||||||
userWithIdAndDefaultWorkspaceId,
|
|
||||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
workspaceId &&
|
|
||||||
userWithIdAndDefaultWorkspaceId.defaultWorkspaceId !== workspaceId
|
|
||||||
) {
|
|
||||||
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
|
||||||
userWithIdAndDefaultWorkspaceId.id,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: {
|
where: { email },
|
||||||
email,
|
|
||||||
},
|
|
||||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
userValidator.assertIsDefinedOrThrow(
|
userValidator.assertIsDefinedOrThrow(
|
||||||
@ -233,15 +208,14 @@ export class AuthService {
|
|||||||
|
|
||||||
const accessToken = await this.accessTokenService.generateAccessToken(
|
const accessToken = await this.accessTokenService.generateAccessToken(
|
||||||
user.id,
|
user.id,
|
||||||
user.defaultWorkspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||||
user.id,
|
user.id,
|
||||||
user.defaultWorkspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
|
||||||
tokens: {
|
tokens: {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
@ -257,7 +231,6 @@ export class AuthService {
|
|||||||
if (userValidator.isDefined(user)) {
|
if (userValidator.isDefined(user)) {
|
||||||
return {
|
return {
|
||||||
exists: true,
|
exists: true,
|
||||||
defaultWorkspaceId: user.defaultWorkspaceId,
|
|
||||||
availableWorkspaces: await this.findAvailableWorkspacesByEmail(email),
|
availableWorkspaces: await this.findAvailableWorkspacesByEmail(email),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -278,6 +251,7 @@ export class AuthService {
|
|||||||
async generateAuthorizationCode(
|
async generateAuthorizationCode(
|
||||||
authorizeAppInput: AuthorizeAppInput,
|
authorizeAppInput: AuthorizeAppInput,
|
||||||
user: User,
|
user: User,
|
||||||
|
workspace: Workspace,
|
||||||
): Promise<AuthorizeApp> {
|
): Promise<AuthorizeApp> {
|
||||||
// TODO: replace with db call to - third party app table
|
// TODO: replace with db call to - third party app table
|
||||||
const apps = [
|
const apps = [
|
||||||
@ -329,14 +303,14 @@ export class AuthService {
|
|||||||
value: codeChallenge,
|
value: codeChallenge,
|
||||||
type: AppTokenType.CodeChallenge,
|
type: AppTokenType.CodeChallenge,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: workspace.id,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: authorizationCode,
|
value: authorizationCode,
|
||||||
type: AppTokenType.AuthorizationCode,
|
type: AppTokenType.AuthorizationCode,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: workspace.id,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -347,7 +321,7 @@ export class AuthService {
|
|||||||
value: authorizationCode,
|
value: authorizationCode,
|
||||||
type: AppTokenType.AuthorizationCode,
|
type: AppTokenType.AuthorizationCode,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: workspace.id,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -439,7 +413,7 @@ export class AuthService {
|
|||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
async computeRedirectURI(loginToken: string, subdomain?: string) {
|
computeRedirectURI(loginToken: string, subdomain?: string) {
|
||||||
const url = this.domainManagerService.buildWorkspaceURL({
|
const url = this.domainManagerService.buildWorkspaceURL({
|
||||||
subdomain,
|
subdomain,
|
||||||
pathname: '/verify',
|
pathname: '/verify',
|
||||||
|
|||||||
@ -1,155 +1,157 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
// import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
// import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
//
|
||||||
import crypto from 'crypto';
|
// import crypto from 'crypto';
|
||||||
|
//
|
||||||
import { Repository } from 'typeorm';
|
// import { Repository } from 'typeorm';
|
||||||
|
//
|
||||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
// import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
import {
|
// import {
|
||||||
AuthException,
|
// AuthException,
|
||||||
AuthExceptionCode,
|
// AuthExceptionCode,
|
||||||
} from 'src/engine/core-modules/auth/auth.exception';
|
// } from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
// import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
||||||
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
// import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
||||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
// import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
// import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
// import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
// import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
|
// import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||||
@Injectable()
|
//
|
||||||
export class OAuthService {
|
// @Injectable()
|
||||||
constructor(
|
// export class OAuthService {
|
||||||
@InjectRepository(User, 'core')
|
// constructor(
|
||||||
private readonly userRepository: Repository<User>,
|
// @InjectRepository(User, 'core')
|
||||||
@InjectRepository(AppToken, 'core')
|
// private readonly userRepository: Repository<User>,
|
||||||
private readonly appTokenRepository: Repository<AppToken>,
|
// @InjectRepository(AppToken, 'core')
|
||||||
private readonly accessTokenService: AccessTokenService,
|
// private readonly appTokenRepository: Repository<AppToken>,
|
||||||
private readonly refreshTokenService: RefreshTokenService,
|
// private readonly accessTokenService: AccessTokenService,
|
||||||
private readonly loginTokenService: LoginTokenService,
|
// private readonly refreshTokenService: RefreshTokenService,
|
||||||
) {}
|
// private readonly loginTokenService: LoginTokenService,
|
||||||
|
// ) {}
|
||||||
async verifyAuthorizationCode(
|
//
|
||||||
exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
// async verifyAuthorizationCode(
|
||||||
): Promise<ExchangeAuthCode> {
|
// exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||||
const { authorizationCode, codeVerifier } = exchangeAuthCodeInput;
|
// ): Promise<ExchangeAuthCode> {
|
||||||
|
// const { authorizationCode, codeVerifier } = exchangeAuthCodeInput;
|
||||||
if (!authorizationCode) {
|
//
|
||||||
throw new AuthException(
|
// if (!authorizationCode) {
|
||||||
'Authorization code not found',
|
// throw new AuthException(
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
// 'Authorization code not found',
|
||||||
);
|
// AuthExceptionCode.INVALID_INPUT,
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
let userId = '';
|
//
|
||||||
|
// let userId = '';
|
||||||
if (codeVerifier) {
|
//
|
||||||
const authorizationCodeAppToken = await this.appTokenRepository.findOne({
|
// if (codeVerifier) {
|
||||||
where: {
|
// const authorizationCodeAppToken = await this.appTokenRepository.findOne({
|
||||||
value: authorizationCode,
|
// where: {
|
||||||
},
|
// value: authorizationCode,
|
||||||
});
|
// },
|
||||||
|
// });
|
||||||
if (!authorizationCodeAppToken) {
|
//
|
||||||
throw new AuthException(
|
// if (!authorizationCodeAppToken) {
|
||||||
'Authorization code does not exist',
|
// throw new AuthException(
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
// 'Authorization code does not exist',
|
||||||
);
|
// AuthExceptionCode.INVALID_INPUT,
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
if (!(authorizationCodeAppToken.expiresAt.getTime() >= Date.now())) {
|
//
|
||||||
throw new AuthException(
|
// if (!(authorizationCodeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||||
'Authorization code expired.',
|
// throw new AuthException(
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
// 'Authorization code expired.',
|
||||||
);
|
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
const codeChallenge = crypto
|
//
|
||||||
.createHash('sha256')
|
// const codeChallenge = crypto
|
||||||
.update(codeVerifier)
|
// .createHash('sha256')
|
||||||
.digest()
|
// .update(codeVerifier)
|
||||||
.toString('base64')
|
// .digest()
|
||||||
.replace(/\+/g, '-')
|
// .toString('base64')
|
||||||
.replace(/\//g, '_')
|
// .replace(/\+/g, '-')
|
||||||
.replace(/=/g, '');
|
// .replace(/\//g, '_')
|
||||||
|
// .replace(/=/g, '');
|
||||||
const codeChallengeAppToken = await this.appTokenRepository.findOne({
|
//
|
||||||
where: {
|
// const codeChallengeAppToken = await this.appTokenRepository.findOne({
|
||||||
value: codeChallenge,
|
// where: {
|
||||||
},
|
// value: codeChallenge,
|
||||||
});
|
// },
|
||||||
|
// });
|
||||||
if (!codeChallengeAppToken || !codeChallengeAppToken.userId) {
|
//
|
||||||
throw new AuthException(
|
// if (!codeChallengeAppToken || !codeChallengeAppToken.userId) {
|
||||||
'code verifier doesnt match the challenge',
|
// throw new AuthException(
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
// 'code verifier doesnt match the challenge',
|
||||||
);
|
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
if (!(codeChallengeAppToken.expiresAt.getTime() >= Date.now())) {
|
//
|
||||||
throw new AuthException(
|
// if (!(codeChallengeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||||
'code challenge expired.',
|
// throw new AuthException(
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
// 'code challenge expired.',
|
||||||
);
|
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
if (codeChallengeAppToken.userId !== authorizationCodeAppToken.userId) {
|
//
|
||||||
throw new AuthException(
|
// if (codeChallengeAppToken.userId !== authorizationCodeAppToken.userId) {
|
||||||
'authorization code / code verifier was not created by same client',
|
// throw new AuthException(
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
// 'authorization code / code verifier was not created by same client',
|
||||||
);
|
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
if (codeChallengeAppToken.revokedAt) {
|
//
|
||||||
throw new AuthException(
|
// if (codeChallengeAppToken.revokedAt) {
|
||||||
'Token has been revoked.',
|
// throw new AuthException(
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
// 'Token has been revoked.',
|
||||||
);
|
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
await this.appTokenRepository.save({
|
//
|
||||||
id: codeChallengeAppToken.id,
|
// await this.appTokenRepository.save({
|
||||||
revokedAt: new Date(),
|
// id: codeChallengeAppToken.id,
|
||||||
});
|
// revokedAt: new Date(),
|
||||||
|
// });
|
||||||
userId = codeChallengeAppToken.userId;
|
//
|
||||||
}
|
// userId = codeChallengeAppToken.userId;
|
||||||
|
// }
|
||||||
const user = await this.userRepository.findOne({
|
//
|
||||||
where: { id: userId },
|
// const user = await this.userRepository.findOne({
|
||||||
relations: ['defaultWorkspace'],
|
// where: { id: userId },
|
||||||
});
|
// relations: ['defaultWorkspace'],
|
||||||
|
// });
|
||||||
if (!user) {
|
//
|
||||||
throw new AuthException(
|
// userValidator.assertIsDefinedOrThrow(
|
||||||
'User who generated the token does not exist',
|
// user,
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
// new AuthException(
|
||||||
);
|
// 'User who generated the token does not exist',
|
||||||
}
|
// AuthExceptionCode.INVALID_INPUT,
|
||||||
|
// ),
|
||||||
if (!user.defaultWorkspace) {
|
// );
|
||||||
throw new AuthException(
|
//
|
||||||
'User does not have a default workspace',
|
// if (!user.defaultWorkspace) {
|
||||||
AuthExceptionCode.INVALID_DATA,
|
// throw new AuthException(
|
||||||
);
|
// 'User does not have a default workspace',
|
||||||
}
|
// AuthExceptionCode.INVALID_DATA,
|
||||||
|
// );
|
||||||
const accessToken = await this.accessTokenService.generateAccessToken(
|
// }
|
||||||
user.id,
|
//
|
||||||
user.defaultWorkspaceId,
|
// const accessToken = await this.accessTokenService.generateAccessToken(
|
||||||
);
|
// user.id,
|
||||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
// user.defaultWorkspaceId,
|
||||||
user.id,
|
// );
|
||||||
user.defaultWorkspaceId,
|
// const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||||
);
|
// user.id,
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
// user.defaultWorkspaceId,
|
||||||
user.email,
|
// );
|
||||||
);
|
// const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
|
// user.email,
|
||||||
return {
|
// );
|
||||||
accessToken,
|
//
|
||||||
refreshToken,
|
// return {
|
||||||
loginToken,
|
// accessToken,
|
||||||
};
|
// refreshToken,
|
||||||
}
|
// loginToken,
|
||||||
}
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@ -15,11 +15,11 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
|
|||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||||
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceActivationStatus,
|
WorkspaceActivationStatus,
|
||||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
|
||||||
|
|
||||||
jest.mock('bcrypt');
|
jest.mock('bcrypt');
|
||||||
|
|
||||||
@ -120,12 +120,15 @@ describe('SignInUpService', () => {
|
|||||||
provide: DomainManagerService,
|
provide: DomainManagerService,
|
||||||
useValue: {
|
useValue: {
|
||||||
generateSubdomain: jest.fn().mockReturnValue('testSubDomain'),
|
generateSubdomain: jest.fn().mockReturnValue('testSubDomain'),
|
||||||
|
getWorkspaceBySubdomainOrDefaultWorkspace: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: UserService,
|
provide: UserService,
|
||||||
useValue: {
|
useValue: {
|
||||||
saveDefaultWorkspaceIfUserHasAccessOrThrow: jest.fn(),
|
hasUserAccessToWorkspaceOrThrow: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -148,7 +151,10 @@ describe('SignInUpService', () => {
|
|||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(service, 'signUpOnNewWorkspace')
|
.spyOn(service, 'signUpOnNewWorkspace')
|
||||||
.mockResolvedValueOnce({} as User);
|
.mockResolvedValueOnce({ user: {}, workspace: {} } as {
|
||||||
|
user: User;
|
||||||
|
workspace: Workspace;
|
||||||
|
});
|
||||||
|
|
||||||
await service.signInUp({
|
await service.signInUp({
|
||||||
email: 'test@test.com',
|
email: 'test@test.com',
|
||||||
@ -172,7 +178,6 @@ describe('SignInUpService', () => {
|
|||||||
id: 'user-id',
|
id: 'user-id',
|
||||||
email,
|
email,
|
||||||
passwordHash: undefined,
|
passwordHash: undefined,
|
||||||
defaultWorkspace: { id: 'workspace-id' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||||
@ -189,7 +194,7 @@ describe('SignInUpService', () => {
|
|||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
targetWorkspaceSubdomain: 'testSubDomain',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual(existingUser);
|
expect(result).toEqual({ user: existingUser, workspace: {} });
|
||||||
});
|
});
|
||||||
it('signInUp - sso - new user - existing invitation', async () => {
|
it('signInUp - sso - new user - existing invitation', async () => {
|
||||||
const email = 'newuser@test.com';
|
const email = 'newuser@test.com';
|
||||||
@ -248,7 +253,11 @@ describe('SignInUpService', () => {
|
|||||||
id: 'user-id',
|
id: 'user-id',
|
||||||
email,
|
email,
|
||||||
passwordHash: undefined,
|
passwordHash: undefined,
|
||||||
defaultWorkspace: { id: 'workspace-id' },
|
};
|
||||||
|
|
||||||
|
const workspace = {
|
||||||
|
id: workspaceId,
|
||||||
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
};
|
};
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||||
@ -259,10 +268,7 @@ describe('SignInUpService', () => {
|
|||||||
);
|
);
|
||||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
workspace: {
|
workspace,
|
||||||
id: workspaceId,
|
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
||||||
@ -277,7 +283,7 @@ describe('SignInUpService', () => {
|
|||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
targetWorkspaceSubdomain: 'testSubDomain',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual(existingUser);
|
expect(result).toEqual({ user: existingUser, workspace });
|
||||||
expect(userWorkspaceServiceAddUserToWorkspaceMock).toHaveBeenCalledTimes(1);
|
expect(userWorkspaceServiceAddUserToWorkspaceMock).toHaveBeenCalledTimes(1);
|
||||||
expect(
|
expect(
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
||||||
@ -287,15 +293,16 @@ describe('SignInUpService', () => {
|
|||||||
const email = 'newuser@test.com';
|
const email = 'newuser@test.com';
|
||||||
const workspaceId = 'workspace-id';
|
const workspaceId = 'workspace-id';
|
||||||
const workspacePersonalInviteToken = 'personal-token-value';
|
const workspacePersonalInviteToken = 'personal-token-value';
|
||||||
|
const workspace = {
|
||||||
|
id: workspaceId,
|
||||||
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
|
};
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(null);
|
UserFindOneMock.mockReturnValueOnce(null);
|
||||||
|
|
||||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
workspace: {
|
workspace,
|
||||||
id: workspaceId,
|
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
||||||
@ -345,7 +352,6 @@ describe('SignInUpService', () => {
|
|||||||
id: 'user-id',
|
id: 'user-id',
|
||||||
email,
|
email,
|
||||||
passwordHash: undefined,
|
passwordHash: undefined,
|
||||||
defaultWorkspace: { id: 'workspace-id' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||||
@ -379,7 +385,6 @@ describe('SignInUpService', () => {
|
|||||||
id: 'user-id',
|
id: 'user-id',
|
||||||
email,
|
email,
|
||||||
passwordHash: 'hash-of-validPassword123',
|
passwordHash: 'hash-of-validPassword123',
|
||||||
defaultWorkspace: { id: 'workspace-id' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { HttpService } from '@nestjs/axios';
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { isDefined } from 'class-validator';
|
|
||||||
import FileType from 'file-type';
|
import FileType from 'file-type';
|
||||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared';
|
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
@ -37,6 +36,7 @@ import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.
|
|||||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||||
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
export type SignInUpServiceInput = {
|
export type SignInUpServiceInput = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -106,7 +106,6 @@ export class SignInUpService {
|
|||||||
|
|
||||||
const existingUser = await this.userRepository.findOne({
|
const existingUser = await this.userRepository.findOne({
|
||||||
where: { email },
|
where: { email },
|
||||||
relations: ['defaultWorkspace'],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser && !fromSSO) {
|
if (existingUser && !fromSSO) {
|
||||||
@ -123,53 +122,22 @@ export class SignInUpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeInvitation =
|
const signInUpWithInvitationResult = await this.signInUpWithInvitation({
|
||||||
fromSSO && !workspacePersonalInviteToken && !workspaceInviteHash
|
email,
|
||||||
? await this.workspaceInvitationService.findInvitationByWorkspaceSubdomainAndUserEmail(
|
workspacePersonalInviteToken,
|
||||||
{
|
workspaceInviteHash,
|
||||||
subdomain: targetWorkspaceSubdomain,
|
targetWorkspaceSubdomain,
|
||||||
email,
|
fromSSO,
|
||||||
},
|
firstName,
|
||||||
)
|
lastName,
|
||||||
: undefined;
|
picture,
|
||||||
|
authProvider,
|
||||||
|
passwordHash,
|
||||||
|
existingUser,
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (isDefined(signInUpWithInvitationResult)) {
|
||||||
workspacePersonalInviteToken ||
|
return signInUpWithInvitationResult;
|
||||||
workspaceInviteHash ||
|
|
||||||
maybeInvitation
|
|
||||||
) {
|
|
||||||
const invitationValidation =
|
|
||||||
workspacePersonalInviteToken || workspaceInviteHash || maybeInvitation
|
|
||||||
? await this.workspaceInvitationService.validateInvitation({
|
|
||||||
workspacePersonalInviteToken:
|
|
||||||
workspacePersonalInviteToken ?? maybeInvitation?.value,
|
|
||||||
workspaceInviteHash,
|
|
||||||
email,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
invitationValidation?.isValid === true &&
|
|
||||||
invitationValidation.workspace
|
|
||||||
) {
|
|
||||||
const updatedUser = await this.signInUpOnExistingWorkspace({
|
|
||||||
email,
|
|
||||||
passwordHash,
|
|
||||||
workspace: invitationValidation.workspace,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
picture,
|
|
||||||
existingUser,
|
|
||||||
authProvider,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
|
||||||
invitationValidation.workspace.id,
|
|
||||||
email,
|
|
||||||
);
|
|
||||||
|
|
||||||
return updatedUser;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
@ -182,27 +150,111 @@ export class SignInUpService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetWorkspaceSubdomain) {
|
const workspace =
|
||||||
const workspace = await this.workspaceRepository.findOne({
|
await this.domainManagerService.getWorkspaceBySubdomainOrDefaultWorkspace(
|
||||||
where: { subdomain: targetWorkspaceSubdomain },
|
targetWorkspaceSubdomain,
|
||||||
select: ['id'],
|
);
|
||||||
|
|
||||||
|
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||||
|
|
||||||
|
return await this.validateSignIn({
|
||||||
|
user: existingUser,
|
||||||
|
workspace,
|
||||||
|
authProvider,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async signInUpWithInvitation({
|
||||||
|
email,
|
||||||
|
workspacePersonalInviteToken,
|
||||||
|
workspaceInviteHash,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
picture,
|
||||||
|
fromSSO,
|
||||||
|
targetWorkspaceSubdomain,
|
||||||
|
authProvider,
|
||||||
|
passwordHash,
|
||||||
|
existingUser,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
workspacePersonalInviteToken?: string;
|
||||||
|
workspaceInviteHash?: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
picture?: string | null;
|
||||||
|
authProvider?: WorkspaceAuthProvider;
|
||||||
|
passwordHash?: string;
|
||||||
|
existingUser: User | null;
|
||||||
|
fromSSO: boolean;
|
||||||
|
targetWorkspaceSubdomain?: string;
|
||||||
|
}) {
|
||||||
|
const maybeInvitation =
|
||||||
|
fromSSO && !workspacePersonalInviteToken && !workspaceInviteHash
|
||||||
|
? await this.workspaceInvitationService.findInvitationByWorkspaceSubdomainAndUserEmail(
|
||||||
|
{
|
||||||
|
subdomain: targetWorkspaceSubdomain,
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const invitationValidation =
|
||||||
|
workspacePersonalInviteToken || workspaceInviteHash || maybeInvitation
|
||||||
|
? await this.workspaceInvitationService.validateInvitation({
|
||||||
|
workspacePersonalInviteToken:
|
||||||
|
workspacePersonalInviteToken ?? maybeInvitation?.value,
|
||||||
|
workspaceInviteHash,
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
invitationValidation?.isValid === true &&
|
||||||
|
invitationValidation.workspace
|
||||||
|
) {
|
||||||
|
const updatedUser = await this.signInUpOnExistingWorkspace({
|
||||||
|
email,
|
||||||
|
passwordHash,
|
||||||
|
workspace: invitationValidation.workspace,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
picture,
|
||||||
|
existingUser,
|
||||||
|
authProvider,
|
||||||
});
|
});
|
||||||
|
|
||||||
workspaceValidator.assertIsExist(
|
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||||
workspace,
|
invitationValidation.workspace.id,
|
||||||
new AuthException(
|
email,
|
||||||
'Workspace not found',
|
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
return {
|
||||||
existingUser.id,
|
user: updatedUser,
|
||||||
workspace.id,
|
workspace: invitationValidation.workspace,
|
||||||
);
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateSignIn({
|
||||||
|
user,
|
||||||
|
workspace,
|
||||||
|
authProvider,
|
||||||
|
}: {
|
||||||
|
user: User;
|
||||||
|
workspace: Workspace;
|
||||||
|
authProvider: SignInUpServiceInput['authProvider'];
|
||||||
|
}) {
|
||||||
|
if (authProvider) {
|
||||||
|
workspaceValidator.isAuthEnabledOrThrow(authProvider, workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
return existingUser;
|
await this.userService.hasUserAccessToWorkspaceOrThrow(
|
||||||
|
user.id,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { user, workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
async signInUpOnExistingWorkspace({
|
async signInUpOnExistingWorkspace({
|
||||||
@ -227,7 +279,7 @@ export class SignInUpService {
|
|||||||
const isNewUser = !isDefined(existingUser);
|
const isNewUser = !isDefined(existingUser);
|
||||||
let user = existingUser;
|
let user = existingUser;
|
||||||
|
|
||||||
workspaceValidator.assertIsExist(
|
workspaceValidator.assertIsDefinedOrThrow(
|
||||||
workspace,
|
workspace,
|
||||||
new AuthException(
|
new AuthException(
|
||||||
'Workspace not found',
|
'Workspace not found',
|
||||||
@ -244,14 +296,7 @@ export class SignInUpService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (authProvider) {
|
if (authProvider) {
|
||||||
workspaceValidator.isAuthEnabledOrThrow(
|
workspaceValidator.isAuthEnabledOrThrow(authProvider, workspace);
|
||||||
authProvider,
|
|
||||||
workspace,
|
|
||||||
new AuthException(
|
|
||||||
`${authProvider} auth is not enabled for this workspace`,
|
|
||||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNewUser) {
|
if (isNewUser) {
|
||||||
@ -264,7 +309,6 @@ export class SignInUpService {
|
|||||||
defaultAvatarUrl: imagePath,
|
defaultAvatarUrl: imagePath,
|
||||||
canImpersonate: false,
|
canImpersonate: false,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
defaultWorkspace: workspace,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
user = await this.userRepository.save(userToCreate);
|
user = await this.userRepository.save(userToCreate);
|
||||||
@ -364,10 +408,7 @@ export class SignInUpService {
|
|||||||
|
|
||||||
user.defaultAvatarUrl = await this.uploadPicture(picture, workspace.id);
|
user.defaultAvatarUrl = await this.uploadPicture(picture, workspace.id);
|
||||||
|
|
||||||
const userCreated = this.userRepository.create({
|
const userCreated = this.userRepository.create(user);
|
||||||
...user,
|
|
||||||
defaultWorkspace: workspace,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newUser = await this.userRepository.save(userCreated);
|
const newUser = await this.userRepository.save(userCreated);
|
||||||
|
|
||||||
@ -383,7 +424,7 @@ export class SignInUpService {
|
|||||||
value: true,
|
value: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return newUser;
|
return { user: newUser, workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadPicture(
|
async uploadPicture(
|
||||||
|
|||||||
@ -17,9 +17,6 @@ describe('SwitchWorkspaceService', () => {
|
|||||||
let service: SwitchWorkspaceService;
|
let service: SwitchWorkspaceService;
|
||||||
let userRepository: Repository<User>;
|
let userRepository: Repository<User>;
|
||||||
let workspaceRepository: Repository<Workspace>;
|
let workspaceRepository: Repository<Workspace>;
|
||||||
let userService: UserService;
|
|
||||||
let accessTokenService: AccessTokenService;
|
|
||||||
let refreshTokenService: RefreshTokenService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -45,18 +42,16 @@ describe('SwitchWorkspaceService', () => {
|
|||||||
generateRefreshToken: jest.fn(),
|
generateRefreshToken: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: UserService,
|
|
||||||
useValue: {
|
|
||||||
saveDefaultWorkspaceIfUserHasAccessOrThrow: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: EnvironmentService,
|
provide: EnvironmentService,
|
||||||
useValue: {
|
useValue: {
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: UserService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@ -67,9 +62,6 @@ describe('SwitchWorkspaceService', () => {
|
|||||||
workspaceRepository = module.get<Repository<Workspace>>(
|
workspaceRepository = module.get<Repository<Workspace>>(
|
||||||
getRepositoryToken(Workspace, 'core'),
|
getRepositoryToken(Workspace, 'core'),
|
||||||
);
|
);
|
||||||
accessTokenService = module.get<AccessTokenService>(AccessTokenService);
|
|
||||||
refreshTokenService = module.get<RefreshTokenService>(RefreshTokenService);
|
|
||||||
userService = module.get<UserService>(UserService);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@ -191,44 +183,4 @@ describe('SwitchWorkspaceService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generateSwitchWorkspaceToken', () => {
|
|
||||||
it('should generate and return auth tokens', async () => {
|
|
||||||
const mockUser = { id: 'user-id' };
|
|
||||||
const mockWorkspace = { id: 'workspace-id' };
|
|
||||||
const mockAccessToken = { token: 'access-token', expiresAt: new Date() };
|
|
||||||
const mockRefreshToken = 'refresh-token';
|
|
||||||
|
|
||||||
jest.spyOn(userRepository, 'save').mockResolvedValue({} as User);
|
|
||||||
jest
|
|
||||||
.spyOn(accessTokenService, 'generateAccessToken')
|
|
||||||
.mockResolvedValue(mockAccessToken);
|
|
||||||
jest
|
|
||||||
.spyOn(refreshTokenService, 'generateRefreshToken')
|
|
||||||
.mockResolvedValue(mockRefreshToken as any);
|
|
||||||
|
|
||||||
const result = await service.generateSwitchWorkspaceToken(
|
|
||||||
mockUser as User,
|
|
||||||
mockWorkspace as Workspace,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
tokens: {
|
|
||||||
accessToken: mockAccessToken,
|
|
||||||
refreshToken: mockRefreshToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
userService.saveDefaultWorkspaceIfUserHasAccessOrThrow,
|
|
||||||
).toHaveBeenCalledWith(mockUser.id, mockWorkspace.id);
|
|
||||||
expect(accessTokenService.generateAccessToken).toHaveBeenCalledWith(
|
|
||||||
mockUser.id,
|
|
||||||
mockWorkspace.id,
|
|
||||||
);
|
|
||||||
expect(refreshTokenService.generateRefreshToken).toHaveBeenCalledWith(
|
|
||||||
mockUser.id,
|
|
||||||
mockWorkspace.id,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,50 +7,31 @@ import {
|
|||||||
AuthException,
|
AuthException,
|
||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
} from 'src/engine/core-modules/auth/auth.exception';
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
|
||||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
|
||||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||||
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
|
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SwitchWorkspaceService {
|
export class SwitchWorkspaceService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(User, 'core')
|
|
||||||
private readonly userRepository: Repository<User>,
|
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly accessTokenService: AccessTokenService,
|
|
||||||
private readonly refreshTokenService: RefreshTokenService,
|
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async switchWorkspace(user: User, workspaceId: string) {
|
async switchWorkspace(user: User, workspaceId: string) {
|
||||||
const userExists = await this.userRepository.findBy({ id: user.id });
|
|
||||||
|
|
||||||
if (!userExists) {
|
|
||||||
throw new AuthException(
|
|
||||||
'User not found',
|
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace = await this.workspaceRepository.findOne({
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
relations: ['workspaceUsers', 'workspaceSSOIdentityProviders'],
|
relations: ['workspaceUsers', 'workspaceSSOIdentityProviders'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!workspace) {
|
workspaceValidator.assertIsDefinedOrThrow(
|
||||||
throw new AuthException(
|
workspace,
|
||||||
'workspace doesnt exist',
|
new AuthException('Workspace not found', AuthExceptionCode.INVALID_INPUT),
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!workspace.workspaceUsers
|
!workspace.workspaceUsers
|
||||||
@ -63,11 +44,6 @@ export class SwitchWorkspaceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userRepository.save({
|
|
||||||
id: user.id,
|
|
||||||
defaultWorkspace: workspace,
|
|
||||||
});
|
|
||||||
|
|
||||||
const systemEnabledProviders: AuthProviders = {
|
const systemEnabledProviders: AuthProviders = {
|
||||||
google: this.environmentService.get('AUTH_GOOGLE_ENABLED'),
|
google: this.environmentService.get('AUTH_GOOGLE_ENABLED'),
|
||||||
magicLink: false,
|
magicLink: false,
|
||||||
@ -87,30 +63,4 @@ export class SwitchWorkspaceService {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateSwitchWorkspaceToken(
|
|
||||||
user: User,
|
|
||||||
workspace: Workspace,
|
|
||||||
): Promise<AuthTokens> {
|
|
||||||
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
|
||||||
user.id,
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = await this.accessTokenService.generateAccessToken(
|
|
||||||
user.id,
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
|
||||||
user.id,
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
tokens: {
|
|
||||||
accessToken: token,
|
|
||||||
refreshToken,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,10 @@ import { EnvironmentService } from 'src/engine/core-modules/environment/environm
|
|||||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import {
|
||||||
|
Workspace,
|
||||||
|
WorkspaceActivationStatus,
|
||||||
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
import { AccessTokenService } from './access-token.service';
|
import { AccessTokenService } from './access-token.service';
|
||||||
@ -22,6 +25,7 @@ describe('AccessTokenService', () => {
|
|||||||
let jwtWrapperService: JwtWrapperService;
|
let jwtWrapperService: JwtWrapperService;
|
||||||
let environmentService: EnvironmentService;
|
let environmentService: EnvironmentService;
|
||||||
let userRepository: Repository<User>;
|
let userRepository: Repository<User>;
|
||||||
|
let workspaceRepository: Repository<Workspace>;
|
||||||
let twentyORMGlobalManager: TwentyORMGlobalManager;
|
let twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -84,6 +88,9 @@ describe('AccessTokenService', () => {
|
|||||||
userRepository = module.get<Repository<User>>(
|
userRepository = module.get<Repository<User>>(
|
||||||
getRepositoryToken(User, 'core'),
|
getRepositoryToken(User, 'core'),
|
||||||
);
|
);
|
||||||
|
workspaceRepository = module.get<Repository<Workspace>>(
|
||||||
|
getRepositoryToken(Workspace, 'core'),
|
||||||
|
);
|
||||||
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
|
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
|
||||||
TwentyORMGlobalManager,
|
TwentyORMGlobalManager,
|
||||||
);
|
);
|
||||||
@ -99,14 +106,19 @@ describe('AccessTokenService', () => {
|
|||||||
const workspaceId = 'workspace-id';
|
const workspaceId = 'workspace-id';
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
id: userId,
|
id: userId,
|
||||||
defaultWorkspace: { id: workspaceId, activationStatus: 'ACTIVE' },
|
};
|
||||||
defaultWorkspaceId: workspaceId,
|
const mockWorkspace = {
|
||||||
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
|
id: workspaceId,
|
||||||
};
|
};
|
||||||
const mockWorkspaceMember = { id: 'workspace-member-id' };
|
const mockWorkspaceMember = { id: 'workspace-member-id' };
|
||||||
const mockToken = 'mock-token';
|
const mockToken = 'mock-token';
|
||||||
|
|
||||||
jest.spyOn(environmentService, 'get').mockReturnValue('1h');
|
jest.spyOn(environmentService, 'get').mockReturnValue('1h');
|
||||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser as User);
|
jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser as User);
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceRepository, 'findOne')
|
||||||
|
.mockResolvedValue(mockWorkspace as Workspace);
|
||||||
jest
|
jest
|
||||||
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
|
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
|
||||||
.mockResolvedValue({
|
.mockResolvedValue({
|
||||||
|
|||||||
@ -20,9 +20,14 @@ import {
|
|||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { WorkspaceActivationStatus } from 'src/engine/core-modules/workspace/workspace.entity';
|
import {
|
||||||
|
Workspace,
|
||||||
|
WorkspaceActivationStatus,
|
||||||
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
|
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccessTokenService {
|
export class AccessTokenService {
|
||||||
@ -32,6 +37,8 @@ export class AccessTokenService {
|
|||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
@InjectRepository(User, 'core')
|
@InjectRepository(User, 'core')
|
||||||
private readonly userRepository: Repository<User>,
|
private readonly userRepository: Repository<User>,
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -45,33 +52,25 @@ export class AccessTokenService {
|
|||||||
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
relations: ['defaultWorkspace'],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
userValidator.assertIsDefinedOrThrow(
|
||||||
throw new AuthException(
|
user,
|
||||||
'User is not found',
|
new AuthException('User is not found', AuthExceptionCode.INVALID_INPUT),
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.defaultWorkspace) {
|
|
||||||
throw new AuthException(
|
|
||||||
'User does not have a default workspace',
|
|
||||||
AuthExceptionCode.INVALID_DATA,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenWorkspaceId = workspaceId ?? user.defaultWorkspaceId;
|
|
||||||
let tokenWorkspaceMemberId: string | undefined;
|
let tokenWorkspaceMemberId: string | undefined;
|
||||||
|
|
||||||
if (
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
user.defaultWorkspace.activationStatus ===
|
where: { id: workspaceId },
|
||||||
WorkspaceActivationStatus.ACTIVE
|
});
|
||||||
) {
|
|
||||||
|
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||||
|
|
||||||
|
if (workspace.activationStatus === WorkspaceActivationStatus.ACTIVE) {
|
||||||
const workspaceMemberRepository =
|
const workspaceMemberRepository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||||
tokenWorkspaceId,
|
workspaceId,
|
||||||
'workspaceMember',
|
'workspaceMember',
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -93,7 +92,7 @@ export class AccessTokenService {
|
|||||||
|
|
||||||
const jwtPayload: JwtPayload = {
|
const jwtPayload: JwtPayload = {
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
workspaceId: workspaceId ? workspaceId : user.defaultWorkspaceId,
|
workspaceId,
|
||||||
workspaceMemberId: tokenWorkspaceMemberId,
|
workspaceMemberId: tokenWorkspaceMemberId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,7 @@ describe('LoginTokenService', () => {
|
|||||||
const mockSecret = 'mock-secret';
|
const mockSecret = 'mock-secret';
|
||||||
const mockExpiresIn = '1h';
|
const mockExpiresIn = '1h';
|
||||||
const mockToken = 'mock-token';
|
const mockToken = 'mock-token';
|
||||||
|
const workspaceId = 'workspace-id';
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(jwtWrapperService, 'generateAppSecret')
|
.spyOn(jwtWrapperService, 'generateAppSecret')
|
||||||
@ -54,18 +55,21 @@ describe('LoginTokenService', () => {
|
|||||||
jest.spyOn(environmentService, 'get').mockReturnValue(mockExpiresIn);
|
jest.spyOn(environmentService, 'get').mockReturnValue(mockExpiresIn);
|
||||||
jest.spyOn(jwtWrapperService, 'sign').mockReturnValue(mockToken);
|
jest.spyOn(jwtWrapperService, 'sign').mockReturnValue(mockToken);
|
||||||
|
|
||||||
const result = await service.generateLoginToken(email);
|
const result = await service.generateLoginToken(email, workspaceId);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
token: mockToken,
|
token: mockToken,
|
||||||
expiresAt: expect.any(Date),
|
expiresAt: expect.any(Date),
|
||||||
});
|
});
|
||||||
expect(jwtWrapperService.generateAppSecret).toHaveBeenCalledWith('LOGIN');
|
expect(jwtWrapperService.generateAppSecret).toHaveBeenCalledWith(
|
||||||
|
'LOGIN',
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
expect(environmentService.get).toHaveBeenCalledWith(
|
expect(environmentService.get).toHaveBeenCalledWith(
|
||||||
'LOGIN_TOKEN_EXPIRES_IN',
|
'LOGIN_TOKEN_EXPIRES_IN',
|
||||||
);
|
);
|
||||||
expect(jwtWrapperService.sign).toHaveBeenCalledWith(
|
expect(jwtWrapperService.sign).toHaveBeenCalledWith(
|
||||||
{ sub: email },
|
{ sub: email, workspaceId },
|
||||||
{ secret: mockSecret, expiresIn: mockExpiresIn },
|
{ secret: mockSecret, expiresIn: mockExpiresIn },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,14 +14,21 @@ export class LoginTokenService {
|
|||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateLoginToken(email: string): Promise<AuthToken> {
|
async generateLoginToken(
|
||||||
const secret = this.jwtWrapperService.generateAppSecret('LOGIN');
|
email: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<AuthToken> {
|
||||||
|
const secret = this.jwtWrapperService.generateAppSecret(
|
||||||
|
'LOGIN',
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
const expiresIn = this.environmentService.get('LOGIN_TOKEN_EXPIRES_IN');
|
const expiresIn = this.environmentService.get('LOGIN_TOKEN_EXPIRES_IN');
|
||||||
|
|
||||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||||
const jwtPayload = {
|
const jwtPayload = {
|
||||||
sub: email,
|
sub: email,
|
||||||
|
workspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -33,7 +40,9 @@ export class LoginTokenService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyLoginToken(loginToken: string): Promise<{ sub: string }> {
|
async verifyLoginToken(
|
||||||
|
loginToken: string,
|
||||||
|
): Promise<{ sub: string; workspaceId: string }> {
|
||||||
await this.jwtWrapperService.verifyWorkspaceToken(loginToken, 'LOGIN');
|
await this.jwtWrapperService.verifyWorkspaceToken(loginToken, 'LOGIN');
|
||||||
|
|
||||||
return this.jwtWrapperService.decode(loginToken, {
|
return this.jwtWrapperService.decode(loginToken, {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
|||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
|
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -24,6 +25,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
|||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
EmailModule,
|
EmailModule,
|
||||||
WorkspaceSSOModule,
|
WorkspaceSSOModule,
|
||||||
|
UserWorkspaceModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
RenewTokenService,
|
RenewTokenService,
|
||||||
|
|||||||
@ -40,11 +40,12 @@ export class BillingResolver {
|
|||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||||
async billingPortalSession(
|
async billingPortalSession(
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Args() { returnUrlPath }: BillingSessionInput,
|
@Args() { returnUrlPath }: BillingSessionInput,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
url: await this.billingPortalWorkspaceService.computeBillingPortalSessionURLOrThrow(
|
url: await this.billingPortalWorkspaceService.computeBillingPortalSessionURLOrThrow(
|
||||||
user.defaultWorkspaceId,
|
workspace.id,
|
||||||
returnUrlPath,
|
returnUrlPath,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -45,12 +45,13 @@ export class BillingPortalWorkspaceService {
|
|||||||
|
|
||||||
const stripeCustomerId = (
|
const stripeCustomerId = (
|
||||||
await this.billingSubscriptionRepository.findOneBy({
|
await this.billingSubscriptionRepository.findOneBy({
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: workspace.id,
|
||||||
})
|
})
|
||||||
)?.stripeCustomerId;
|
)?.stripeCustomerId;
|
||||||
|
|
||||||
const session = await this.stripeService.createCheckoutSession(
|
const session = await this.stripeService.createCheckoutSession(
|
||||||
user,
|
user,
|
||||||
|
workspace.id,
|
||||||
priceId,
|
priceId,
|
||||||
quantity,
|
quantity,
|
||||||
successUrl,
|
successUrl,
|
||||||
|
|||||||
@ -84,6 +84,7 @@ export class StripeService {
|
|||||||
|
|
||||||
async createCheckoutSession(
|
async createCheckoutSession(
|
||||||
user: User,
|
user: User,
|
||||||
|
workspaceId: string,
|
||||||
priceId: string,
|
priceId: string,
|
||||||
quantity: number,
|
quantity: number,
|
||||||
successUrl?: string,
|
successUrl?: string,
|
||||||
@ -100,7 +101,7 @@ export class StripeService {
|
|||||||
mode: 'subscription',
|
mode: 'subscription',
|
||||||
subscription_data: {
|
subscription_data: {
|
||||||
metadata: {
|
metadata: {
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
trial_period_days: this.environmentService.get(
|
trial_period_days: this.environmentService.get(
|
||||||
'BILLING_FREE_TRIAL_DURATION_IN_DAYS',
|
'BILLING_FREE_TRIAL_DURATION_IN_DAYS',
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Field, ObjectType } from '@nestjs/graphql';
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
class Billing {
|
class Billing {
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
|
|
||||||
|
export class DomainManagerException extends CustomException {
|
||||||
|
constructor(message: string, code: DomainManagerExceptionCode) {
|
||||||
|
super(message, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DomainManagerExceptionCode {
|
||||||
|
SUBDOMAIN_REQUIRED = 'SUBDOMAIN_REQUIRED',
|
||||||
|
}
|
||||||
@ -117,6 +117,14 @@ export class DomainManagerService {
|
|||||||
return subdomain;
|
return subdomain;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async getWorkspaceBySubdomainOrDefaultWorkspace(subdomain?: string) {
|
||||||
|
return subdomain
|
||||||
|
? await this.workspaceRepository.findOne({
|
||||||
|
where: { subdomain },
|
||||||
|
})
|
||||||
|
: await this.getDefaultWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
isDefaultSubdomain(subdomain: string) {
|
isDefaultSubdomain(subdomain: string) {
|
||||||
return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN');
|
return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN');
|
||||||
}
|
}
|
||||||
@ -137,7 +145,7 @@ export class DomainManagerService {
|
|||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefaultWorkspace() {
|
private async getDefaultWorkspace() {
|
||||||
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||||
const workspaces = await this.workspaceRepository.find({
|
const workspaces = await this.workspaceRepository.find({
|
||||||
order: {
|
order: {
|
||||||
@ -147,7 +155,6 @@ export class DomainManagerService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (workspaces.length > 1) {
|
if (workspaces.length > 1) {
|
||||||
// TODO AMOREAUX: this logger is trigger twice and the second time the message is undefined for an unknown reason
|
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
`In single-workspace mode, there should be only one workspace. Today there are ${workspaces.length} workspaces`,
|
`In single-workspace mode, there should be only one workspace. Today there are ${workspaces.length} workspaces`,
|
||||||
);
|
);
|
||||||
@ -161,7 +168,7 @@ export class DomainManagerService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspaceByOrigin(origin: string) {
|
async getWorkspaceByOriginOrDefaultWorkspace(origin: string) {
|
||||||
try {
|
try {
|
||||||
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||||
return this.getDefaultWorkspace();
|
return this.getDefaultWorkspace();
|
||||||
@ -171,12 +178,10 @@ export class DomainManagerService {
|
|||||||
|
|
||||||
if (!isDefined(subdomain)) return;
|
if (!isDefined(subdomain)) return;
|
||||||
|
|
||||||
const workspace = await this.workspaceRepository.findOne({
|
return await this.workspaceRepository.findOne({
|
||||||
where: { subdomain },
|
where: { subdomain },
|
||||||
relations: ['workspaceSSOIdentityProviders'],
|
relations: ['workspaceSSOIdentityProviders'],
|
||||||
});
|
});
|
||||||
|
|
||||||
return workspace;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new WorkspaceException(
|
throw new WorkspaceException(
|
||||||
'Workspace not found',
|
'Workspace not found',
|
||||||
|
|||||||
@ -4,7 +4,10 @@ import { BillingService } from 'src/engine/core-modules/billing/services/billing
|
|||||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { WorkspaceActivationStatus } from 'src/engine/core-modules/workspace/workspace.entity';
|
import {
|
||||||
|
Workspace,
|
||||||
|
WorkspaceActivationStatus,
|
||||||
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
export enum OnboardingStepKeys {
|
export enum OnboardingStepKeys {
|
||||||
ONBOARDING_CONNECT_ACCOUNT_PENDING = 'ONBOARDING_CONNECT_ACCOUNT_PENDING',
|
ONBOARDING_CONNECT_ACCOUNT_PENDING = 'ONBOARDING_CONNECT_ACCOUNT_PENDING',
|
||||||
@ -25,34 +28,33 @@ export class OnboardingService {
|
|||||||
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
|
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async isSubscriptionIncompleteOnboardingStatus(user: User) {
|
private async isSubscriptionIncompleteOnboardingStatus(workspace: Workspace) {
|
||||||
const hasSubscription =
|
const hasSubscription =
|
||||||
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
||||||
user.defaultWorkspaceId,
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return !hasSubscription;
|
return !hasSubscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isWorkspaceActivationPending(user: User) {
|
private isWorkspaceActivationPending(workspace: Workspace) {
|
||||||
return (
|
return (
|
||||||
user.defaultWorkspace.activationStatus ===
|
workspace.activationStatus === WorkspaceActivationStatus.PENDING_CREATION
|
||||||
WorkspaceActivationStatus.PENDING_CREATION
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOnboardingStatus(user: User) {
|
async getOnboardingStatus(user: User, workspace: Workspace) {
|
||||||
if (await this.isSubscriptionIncompleteOnboardingStatus(user)) {
|
if (await this.isSubscriptionIncompleteOnboardingStatus(workspace)) {
|
||||||
return OnboardingStatus.PLAN_REQUIRED;
|
return OnboardingStatus.PLAN_REQUIRED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isWorkspaceActivationPending(user)) {
|
if (this.isWorkspaceActivationPending(workspace)) {
|
||||||
return OnboardingStatus.WORKSPACE_ACTIVATION;
|
return OnboardingStatus.WORKSPACE_ACTIVATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userVars = await this.userVarsService.getAll({
|
const userVars = await this.userVarsService.getAll({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isProfileCreationPending =
|
const isProfileCreationPending =
|
||||||
|
|||||||
@ -110,18 +110,12 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
await this.createWorkspaceMember(workspace.id, user);
|
await this.createWorkspaceMember(workspace.id, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedUser = await this.userRepository.save({
|
|
||||||
id: user.id,
|
|
||||||
defaultWorkspace: workspace,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||||
workspace.id,
|
workspace.id,
|
||||||
user.email,
|
user.email,
|
||||||
);
|
);
|
||||||
|
|
||||||
return savedUser;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUserToWorkspaceByInviteToken(inviteToken: string, user: User) {
|
async addUserToWorkspaceByInviteToken(inviteToken: string, user: User) {
|
||||||
|
|||||||
@ -72,18 +72,13 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
return workspaceMemberRepository.find();
|
return workspaceMemberRepository.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(userId: string): Promise<User> {
|
private async deleteUserFromWorkspace({
|
||||||
const user = await this.userRepository.findOne({
|
userId,
|
||||||
where: {
|
workspaceId,
|
||||||
id: userId,
|
}: {
|
||||||
},
|
userId: string;
|
||||||
relations: ['defaultWorkspace'],
|
workspaceId: string;
|
||||||
});
|
}) {
|
||||||
|
|
||||||
assert(user, 'User not found');
|
|
||||||
|
|
||||||
const workspaceId = user.defaultWorkspaceId;
|
|
||||||
|
|
||||||
const dataSourceMetadata =
|
const dataSourceMetadata =
|
||||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -103,8 +98,6 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
|
|
||||||
if (workspaceMembers.length === 1) {
|
if (workspaceMembers.length === 1) {
|
||||||
await this.workspaceService.deleteWorkspace(workspaceId);
|
await this.workspaceService.deleteWorkspace(workspaceId);
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await workspaceDataSource?.query(
|
await workspaceDataSource?.query(
|
||||||
@ -131,6 +124,19 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
],
|
],
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(userId: string): Promise<User> {
|
||||||
|
const user = await this.userRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
relations: ['workspaces'],
|
||||||
|
});
|
||||||
|
|
||||||
|
userValidator.assertIsDefinedOrThrow(user);
|
||||||
|
|
||||||
|
await Promise.all(user.workspaces.map(this.deleteUserFromWorkspace));
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@ -154,16 +160,4 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
|
||||||
userId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
) {
|
|
||||||
await this.hasUserAccessToWorkspaceOrThrow(userId, workspaceId);
|
|
||||||
|
|
||||||
return await this.userRepository.save({
|
|
||||||
id: userId,
|
|
||||||
defaultWorkspaceId: workspaceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
Index,
|
Index,
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Relation,
|
Relation,
|
||||||
@ -81,16 +80,6 @@ export class User {
|
|||||||
@Column({ nullable: true, type: 'timestamptz' })
|
@Column({ nullable: true, type: 'timestamptz' })
|
||||||
deletedAt: Date;
|
deletedAt: Date;
|
||||||
|
|
||||||
@Field(() => Workspace, { nullable: false })
|
|
||||||
@ManyToOne(() => Workspace, (workspace) => workspace.users, {
|
|
||||||
onDelete: 'RESTRICT',
|
|
||||||
})
|
|
||||||
defaultWorkspace: Relation<Workspace>;
|
|
||||||
|
|
||||||
@Field()
|
|
||||||
@Column()
|
|
||||||
defaultWorkspaceId: string;
|
|
||||||
|
|
||||||
@OneToMany(() => AppToken, (appToken) => appToken.user, {
|
@OneToMany(() => AppToken, (appToken) => appToken.user, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
})
|
})
|
||||||
@ -110,4 +99,7 @@ export class User {
|
|||||||
|
|
||||||
@Field(() => OnboardingStatus, { nullable: true })
|
@Field(() => OnboardingStatus, { nullable: true })
|
||||||
onboardingStatus: OnboardingStatus;
|
onboardingStatus: OnboardingStatus;
|
||||||
|
|
||||||
|
@Field(() => Workspace, { nullable: true })
|
||||||
|
currentWorkspace: Relation<Workspace>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
|
|
||||||
|
export class UserException extends CustomException {
|
||||||
|
constructor(message: string, code: UserExceptionCode) {
|
||||||
|
super(message, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserExceptionCode {
|
||||||
|
USER_NOT_FOUND = 'USER_NOT_FOUND',
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
|
|||||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||||
|
|
||||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ import { UserService } from './services/user.service';
|
|||||||
TypeOrmModule.forFeature([KeyValuePair], 'core'),
|
TypeOrmModule.forFeature([KeyValuePair], 'core'),
|
||||||
UserVarsModule,
|
UserVarsModule,
|
||||||
AnalyticsModule,
|
AnalyticsModule,
|
||||||
|
DomainManagerModule,
|
||||||
],
|
],
|
||||||
exports: [UserService],
|
exports: [UserService],
|
||||||
providers: [UserService, UserResolver, TypeORMService],
|
providers: [UserService, UserResolver, TypeORMService],
|
||||||
|
|||||||
@ -44,6 +44,9 @@ import {
|
|||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
} from 'src/engine/core-modules/auth/auth.exception';
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||||
|
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||||
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||||
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
|
|
||||||
const getHMACKey = (email?: string, key?: string | null) => {
|
const getHMACKey = (email?: string, key?: string | null) => {
|
||||||
if (!email || !key) return null;
|
if (!email || !key) return null;
|
||||||
@ -66,28 +69,31 @@ export class UserResolver {
|
|||||||
private readonly userVarService: UserVarsService,
|
private readonly userVarService: UserVarsService,
|
||||||
private readonly fileService: FileService,
|
private readonly fileService: FileService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly analyticsService: AnalyticsService,
|
||||||
|
private readonly domainManagerService: DomainManagerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Query(() => User)
|
@Query(() => User)
|
||||||
async currentUser(
|
async currentUser(
|
||||||
@AuthUser() { id: userId }: User,
|
@AuthUser() { id: userId }: User,
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@OriginHeader() origin: string,
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
if (
|
const workspace =
|
||||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||||
workspaceId
|
origin,
|
||||||
) {
|
|
||||||
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||||
|
|
||||||
|
await this.userService.hasUserAccessToWorkspaceOrThrow(
|
||||||
|
userId,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
relations: ['workspaces', 'workspaces.workspace'],
|
||||||
});
|
});
|
||||||
|
|
||||||
userValidator.assertIsDefinedOrThrow(
|
userValidator.assertIsDefinedOrThrow(
|
||||||
@ -95,14 +101,17 @@ export class UserResolver {
|
|||||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
||||||
);
|
);
|
||||||
|
|
||||||
return user;
|
return { ...user, currentWorkspace: workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => GraphQLJSONObject)
|
@ResolveField(() => GraphQLJSONObject)
|
||||||
async userVars(@Parent() user: User): Promise<Record<string, any>> {
|
async userVars(
|
||||||
|
@Parent() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
const userVars = await this.userVarService.getAll({
|
const userVars = await this.userVarService.getAll({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userVarAllowList = [
|
const userVarAllowList = [
|
||||||
@ -127,13 +136,13 @@ export class UserResolver {
|
|||||||
): Promise<WorkspaceMember | null> {
|
): Promise<WorkspaceMember | null> {
|
||||||
const workspaceMember = await this.userService.loadWorkspaceMember(
|
const workspaceMember = await this.userService.loadWorkspaceMember(
|
||||||
user,
|
user,
|
||||||
workspace ?? user.defaultWorkspace,
|
workspace,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (workspaceMember && workspaceMember.avatarUrl) {
|
if (workspaceMember && workspaceMember.avatarUrl) {
|
||||||
const avatarUrlToken = await this.fileService.encodeFileToken({
|
const avatarUrlToken = await this.fileService.encodeFileToken({
|
||||||
workspaceMemberId: workspaceMember.id,
|
workspaceMemberId: workspaceMember.id,
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||||
@ -146,16 +155,18 @@ export class UserResolver {
|
|||||||
@ResolveField(() => [WorkspaceMember], {
|
@ResolveField(() => [WorkspaceMember], {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
async workspaceMembers(@Parent() user: User): Promise<WorkspaceMember[]> {
|
async workspaceMembers(
|
||||||
const workspaceMembers = await this.userService.loadWorkspaceMembers(
|
@Parent() user: User,
|
||||||
user.defaultWorkspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
);
|
): Promise<WorkspaceMember[]> {
|
||||||
|
const workspaceMembers =
|
||||||
|
await this.userService.loadWorkspaceMembers(workspace);
|
||||||
|
|
||||||
for (const workspaceMember of workspaceMembers) {
|
for (const workspaceMember of workspaceMembers) {
|
||||||
if (workspaceMember.avatarUrl) {
|
if (workspaceMember.avatarUrl) {
|
||||||
const avatarUrlToken = await this.fileService.encodeFileToken({
|
const avatarUrlToken = await this.fileService.encodeFileToken({
|
||||||
workspaceMemberId: workspaceMember.id,
|
workspaceMemberId: workspaceMember.id,
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||||
@ -221,7 +232,17 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => OnboardingStatus)
|
@ResolveField(() => OnboardingStatus)
|
||||||
async onboardingStatus(@Parent() user: User): Promise<OnboardingStatus> {
|
async onboardingStatus(
|
||||||
return this.onboardingService.getOnboardingStatus(user);
|
@Parent() user: User,
|
||||||
|
@OriginHeader() origin: string,
|
||||||
|
): Promise<OnboardingStatus> {
|
||||||
|
const workspace =
|
||||||
|
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||||
|
origin,
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||||
|
|
||||||
|
return this.onboardingService.getOnboardingStatus(user, workspace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { CustomException } from 'src/utils/custom-exception';
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
import {
|
||||||
|
UserException,
|
||||||
|
UserExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/user/user.exception';
|
||||||
|
|
||||||
const assertIsDefinedOrThrow = (
|
const assertIsDefinedOrThrow = (
|
||||||
user: User | undefined | null,
|
user: User | undefined | null,
|
||||||
exceptionToThrow: CustomException,
|
exceptionToThrow: CustomException = new UserException(
|
||||||
|
'User not found',
|
||||||
|
UserExceptionCode.USER_NOT_FOUND,
|
||||||
|
),
|
||||||
): asserts user is User => {
|
): asserts user is User => {
|
||||||
if (!isDefined(user)) {
|
if (!isDefined(user)) {
|
||||||
throw exceptionToThrow;
|
throw exceptionToThrow;
|
||||||
|
|||||||
@ -142,11 +142,10 @@ export class WorkspaceInvitationService {
|
|||||||
subdomain?: string;
|
subdomain?: string;
|
||||||
email: string;
|
email: string;
|
||||||
}) {
|
}) {
|
||||||
const workspace = this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')
|
const workspace =
|
||||||
? await this.workspaceRepository.findOneBy({
|
await this.domainManagerService.getWorkspaceBySubdomainOrDefaultWorkspace(
|
||||||
subdomain,
|
subdomain,
|
||||||
})
|
);
|
||||||
: await this.domainManagerService.getDefaultWorkspace();
|
|
||||||
|
|
||||||
if (!workspace) return;
|
if (!workspace) return;
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
import { Field, ObjectType } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
|
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class ActivateWorkspaceOutput {
|
|
||||||
@Field(() => Workspace)
|
|
||||||
workspace: Workspace;
|
|
||||||
|
|
||||||
@Field(() => AuthToken)
|
|
||||||
loginToken: AuthToken;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class WorkspaceSubdomainAndId {
|
||||||
|
@Field()
|
||||||
|
subdomain: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@ -51,7 +51,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
id: payload.id,
|
id: payload.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
workspaceValidator.assertIsExist(
|
workspaceValidator.assertIsDefinedOrThrow(
|
||||||
workspace,
|
workspace,
|
||||||
new WorkspaceException(
|
new WorkspaceException(
|
||||||
'Workspace not found',
|
'Workspace not found',
|
||||||
@ -81,55 +81,46 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async activateWorkspace(user: User, data: ActivateWorkspaceInput) {
|
async activateWorkspace(
|
||||||
|
user: User,
|
||||||
|
workspace: Workspace,
|
||||||
|
data: ActivateWorkspaceInput,
|
||||||
|
) {
|
||||||
if (!data.displayName || !data.displayName.length) {
|
if (!data.displayName || !data.displayName.length) {
|
||||||
throw new BadRequestException("'displayName' not provided");
|
throw new BadRequestException("'displayName' not provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingWorkspace = await this.workspaceRepository.findOneBy({
|
|
||||||
id: user.defaultWorkspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingWorkspace) {
|
|
||||||
throw new Error('Workspace not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
existingWorkspace.activationStatus ===
|
workspace.activationStatus === WorkspaceActivationStatus.ONGOING_CREATION
|
||||||
WorkspaceActivationStatus.ONGOING_CREATION
|
|
||||||
) {
|
) {
|
||||||
throw new Error('Workspace is already being created');
|
throw new Error('Workspace is already being created');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
existingWorkspace.activationStatus !==
|
workspace.activationStatus !== WorkspaceActivationStatus.PENDING_CREATION
|
||||||
WorkspaceActivationStatus.PENDING_CREATION
|
|
||||||
) {
|
) {
|
||||||
throw new Error('Workspace is not pending creation');
|
throw new Error('Workspace is not pending creation');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.workspaceRepository.update(user.defaultWorkspaceId, {
|
await this.workspaceRepository.update(workspace.id, {
|
||||||
activationStatus: WorkspaceActivationStatus.ONGOING_CREATION,
|
activationStatus: WorkspaceActivationStatus.ONGOING_CREATION,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.featureFlagService.enableFeatureFlags(
|
await this.featureFlagService.enableFeatureFlags(
|
||||||
DEFAULT_FEATURE_FLAGS,
|
DEFAULT_FEATURE_FLAGS,
|
||||||
user.defaultWorkspaceId,
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceManagerService.init(user.defaultWorkspaceId);
|
await this.workspaceManagerService.init(workspace.id);
|
||||||
await this.userWorkspaceService.createWorkspaceMember(
|
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
|
||||||
user.defaultWorkspaceId,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceRepository.update(user.defaultWorkspaceId, {
|
await this.workspaceRepository.update(workspace.id, {
|
||||||
displayName: data.displayName,
|
displayName: data.displayName,
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.workspaceRepository.findOneBy({
|
return await this.workspaceRepository.findOneBy({
|
||||||
id: user.defaultWorkspaceId,
|
id: workspace.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,41 +161,6 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async reassignOrRemoveUserDefaultWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
userId: string,
|
|
||||||
) {
|
|
||||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
|
||||||
where: { userId: userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userWorkspaces.length === 0) {
|
|
||||||
await this.userRepository.delete({ id: userId });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.userRepository.findOne({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error(`User ${userId} not found in workspace ${workspaceId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.defaultWorkspaceId === workspaceId) {
|
|
||||||
await this.userRepository.update(
|
|
||||||
{ id: userId },
|
|
||||||
{
|
|
||||||
defaultWorkspaceId: userWorkspaces[0].workspaceId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async isSubdomainAvailable(subdomain: string) {
|
async isSubdomainAvailable(subdomain: string) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
export const getAuthProvidersByWorkspace = ({
|
export const getAuthProvidersByWorkspace = ({
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p
|
|||||||
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
||||||
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
|
||||||
|
|
||||||
export enum WorkspaceActivationStatus {
|
export enum WorkspaceActivationStatus {
|
||||||
ONGOING_CREATION = 'ONGOING_CREATION',
|
ONGOING_CREATION = 'ONGOING_CREATION',
|
||||||
@ -89,9 +88,6 @@ export class Workspace {
|
|||||||
})
|
})
|
||||||
keyValuePairs: Relation<KeyValuePair[]>;
|
keyValuePairs: Relation<KeyValuePair[]>;
|
||||||
|
|
||||||
@OneToMany(() => User, (user) => user.defaultWorkspace)
|
|
||||||
users: Relation<User[]>;
|
|
||||||
|
|
||||||
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.workspace, {
|
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.workspace, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -22,11 +22,10 @@ import { FileService } from 'src/engine/core-modules/file/services/file.service'
|
|||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input';
|
import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input';
|
||||||
import { ActivateWorkspaceOutput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-output';
|
|
||||||
import {
|
import {
|
||||||
AuthProviders,
|
AuthProviders,
|
||||||
PublicWorkspaceDataOutput,
|
PublicWorkspaceDataOutput,
|
||||||
} from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
} from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||||
import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
|
import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
|
||||||
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
|
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
|
||||||
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util';
|
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util';
|
||||||
@ -74,21 +73,21 @@ export class WorkspaceResolver {
|
|||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ActivateWorkspaceOutput)
|
@Mutation(() => Workspace)
|
||||||
@UseGuards(UserAuthGuard)
|
@UseGuards(UserAuthGuard)
|
||||||
async activateWorkspace(
|
async activateWorkspace(
|
||||||
@Args('data') data: ActivateWorkspaceInput,
|
@Args('data') data: ActivateWorkspaceInput,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
@OriginHeader() origin: string,
|
||||||
) {
|
) {
|
||||||
const workspace = await this.workspaceService.activateWorkspace(user, data);
|
const workspace =
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||||
user.email,
|
origin,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||||
workspace,
|
|
||||||
loginToken,
|
return await this.workspaceService.activateWorkspace(user, workspace, data);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Workspace)
|
@Mutation(() => Workspace)
|
||||||
@ -188,9 +187,11 @@ export class WorkspaceResolver {
|
|||||||
@Query(() => PublicWorkspaceDataOutput)
|
@Query(() => PublicWorkspaceDataOutput)
|
||||||
async getPublicWorkspaceDataBySubdomain(@OriginHeader() origin: string) {
|
async getPublicWorkspaceDataBySubdomain(@OriginHeader() origin: string) {
|
||||||
const workspace =
|
const workspace =
|
||||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||||
|
origin,
|
||||||
|
);
|
||||||
|
|
||||||
workspaceValidator.assertIsExist(
|
workspaceValidator.assertIsDefinedOrThrow(
|
||||||
workspace,
|
workspace,
|
||||||
new WorkspaceException(
|
new WorkspaceException(
|
||||||
'Workspace not found',
|
'Workspace not found',
|
||||||
|
|||||||
@ -3,12 +3,22 @@ import {
|
|||||||
WorkspaceActivationStatus,
|
WorkspaceActivationStatus,
|
||||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { CustomException } from 'src/utils/custom-exception';
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||||
|
import {
|
||||||
|
WorkspaceException,
|
||||||
|
WorkspaceExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/workspace/workspace.exception';
|
||||||
|
|
||||||
const assertIsExist = (
|
const assertIsDefinedOrThrow = (
|
||||||
workspace: Workspace | undefined | null,
|
workspace: Workspace | undefined | null,
|
||||||
exceptionToThrow?: CustomException,
|
exceptionToThrow: CustomException = new WorkspaceException(
|
||||||
|
'Workspace not found',
|
||||||
|
WorkspaceExceptionCode.WORKSPACE_NOT_FOUND,
|
||||||
|
),
|
||||||
): asserts workspace is Workspace => {
|
): asserts workspace is Workspace => {
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw exceptionToThrow;
|
throw exceptionToThrow;
|
||||||
@ -28,7 +38,10 @@ const assertIsActive = (
|
|||||||
const isAuthEnabledOrThrow = (
|
const isAuthEnabledOrThrow = (
|
||||||
provider: WorkspaceAuthProvider,
|
provider: WorkspaceAuthProvider,
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
exceptionToThrowCustom: AuthException,
|
exceptionToThrowCustom: AuthException = new AuthException(
|
||||||
|
`${provider} auth is not enabled for this workspace`,
|
||||||
|
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||||
|
),
|
||||||
) => {
|
) => {
|
||||||
if (provider === 'google' && workspace.isGoogleAuthEnabled) return true;
|
if (provider === 'google' && workspace.isGoogleAuthEnabled) return true;
|
||||||
if (provider === 'microsoft' && workspace.isMicrosoftAuthEnabled) return true;
|
if (provider === 'microsoft' && workspace.isMicrosoftAuthEnabled) return true;
|
||||||
@ -38,11 +51,11 @@ const isAuthEnabledOrThrow = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const workspaceValidator: {
|
export const workspaceValidator: {
|
||||||
assertIsExist: typeof assertIsExist;
|
assertIsDefinedOrThrow: typeof assertIsDefinedOrThrow;
|
||||||
assertIsActive: typeof assertIsActive;
|
assertIsActive: typeof assertIsActive;
|
||||||
isAuthEnabledOrThrow: typeof isAuthEnabledOrThrow;
|
isAuthEnabledOrThrow: typeof isAuthEnabledOrThrow;
|
||||||
} = {
|
} = {
|
||||||
assertIsExist: assertIsExist,
|
assertIsDefinedOrThrow: assertIsDefinedOrThrow,
|
||||||
assertIsActive: assertIsActive,
|
assertIsActive: assertIsActive,
|
||||||
isAuthEnabledOrThrow: isAuthEnabledOrThrow,
|
isAuthEnabledOrThrow: isAuthEnabledOrThrow,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export class ImpersonateGuard implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
const ctx = GqlExecutionContext.create(context);
|
||||||
|
const request = ctx.getContext().req;
|
||||||
|
|
||||||
|
return request.user.canImpersonate === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { isDefined } from 'src/utils/is-defined';
|
|
||||||
|
|
||||||
export const buildWorkspaceURL = (
|
|
||||||
baseUrl: string,
|
|
||||||
subdomain: string | null,
|
|
||||||
{
|
|
||||||
withPathname,
|
|
||||||
withSearchParams,
|
|
||||||
}: {
|
|
||||||
withPathname?: string;
|
|
||||||
withSearchParams?: Record<string, string | number>;
|
|
||||||
} = {},
|
|
||||||
) => {
|
|
||||||
const url = new URL(baseUrl);
|
|
||||||
|
|
||||||
if (subdomain && subdomain.length > 0) {
|
|
||||||
url.hostname = subdomain + '.' + url.hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withPathname) {
|
|
||||||
url.pathname = withPathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withSearchParams) {
|
|
||||||
Object.entries(withSearchParams).forEach(([key, value]) => {
|
|
||||||
if (isDefined(value)) {
|
|
||||||
url.searchParams.set(key, value.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user