5622 add a syncemail onboarding step (#5689)
- add sync email onboarding step - refactor calendar and email visibility enums - add a new table `keyValuePair` in `core` schema - add a new resolved boolean field `skipSyncEmail` in current user https://github.com/twentyhq/twenty/assets/29927851/de791475-5bfe-47f9-8e90-76c349fba56f
This commit is contained in:
@ -27,6 +27,7 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { UserStateModule } from 'src/engine/core-modules/user-state/user-state.module';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
@ -63,6 +64,7 @@ const jwtModule = JwtModule.registerAsync({
|
||||
]),
|
||||
HttpModule,
|
||||
UserWorkspaceModule,
|
||||
UserStateModule,
|
||||
],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
|
||||
@ -15,6 +15,10 @@ import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/strategies/googl
|
||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
|
||||
@Controller('auth/google-apis')
|
||||
export class GoogleAPIsAuthController {
|
||||
@ -22,6 +26,9 @@ export class GoogleAPIsAuthController {
|
||||
private readonly googleAPIsService: GoogleAPIsService,
|
||||
private readonly tokenService: TokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly userStateService: UserStateService,
|
||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -39,7 +46,15 @@ export class GoogleAPIsAuthController {
|
||||
) {
|
||||
const { user } = req;
|
||||
|
||||
const { email, accessToken, refreshToken, transientToken } = user;
|
||||
const {
|
||||
email,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
transientToken,
|
||||
redirectLocation,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
} = user;
|
||||
|
||||
const { workspaceMemberId, workspaceId } =
|
||||
await this.tokenService.verifyTransientToken(transientToken);
|
||||
@ -62,10 +77,25 @@ export class GoogleAPIsAuthController {
|
||||
workspaceId: workspaceId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
});
|
||||
|
||||
const userId = (
|
||||
await this.workspaceMemberService.find(workspaceMemberId, workspaceId)
|
||||
)?.userId;
|
||||
|
||||
if (userId) {
|
||||
await this.userStateService.skipSyncEmailOnboardingStep(
|
||||
userId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
`${this.environmentService.get('FRONT_BASE_URL')}/settings/accounts`,
|
||||
`${this.environmentService.get('FRONT_BASE_URL')}${
|
||||
redirectLocation || '/settings/accounts'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,26 @@ export class GoogleAPIsOauthGuard extends AuthGuard('google-apis') {
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const transientToken = request.query.transientToken;
|
||||
const redirectLocation = request.query.redirectLocation;
|
||||
const calendarVisibility = request.query.calendarVisibility;
|
||||
const messageVisibility = request.query.messageVisibility;
|
||||
|
||||
if (transientToken && typeof transientToken === 'string') {
|
||||
request.params.transientToken = transientToken;
|
||||
}
|
||||
|
||||
if (redirectLocation && typeof redirectLocation === 'string') {
|
||||
request.params.redirectLocation = redirectLocation;
|
||||
}
|
||||
|
||||
if (calendarVisibility && typeof calendarVisibility === 'string') {
|
||||
request.params.calendarVisibility = calendarVisibility;
|
||||
}
|
||||
|
||||
if (messageVisibility && typeof messageVisibility === 'string') {
|
||||
request.params.messageVisibility = messageVisibility;
|
||||
}
|
||||
|
||||
const activate = (await super.canActivate(context)) as boolean;
|
||||
|
||||
return activate;
|
||||
|
||||
@ -58,8 +58,16 @@ export class GoogleAPIsService {
|
||||
workspaceId: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
calendarVisibility: CalendarChannelVisibility | undefined;
|
||||
messageVisibility: MessageChannelVisibility | undefined;
|
||||
}) {
|
||||
const { handle, workspaceId, workspaceMemberId } = input;
|
||||
const {
|
||||
handle,
|
||||
workspaceId,
|
||||
workspaceMemberId,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
} = input;
|
||||
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
@ -104,7 +112,8 @@ export class GoogleAPIsService {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
type: MessageChannelType.EMAIL,
|
||||
handle,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
visibility:
|
||||
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
},
|
||||
workspaceId,
|
||||
manager,
|
||||
@ -116,7 +125,9 @@ export class GoogleAPIsService {
|
||||
id: v4(),
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
handle,
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
visibility:
|
||||
calendarVisibility ||
|
||||
CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
},
|
||||
workspaceId,
|
||||
manager,
|
||||
|
||||
@ -5,6 +5,8 @@ import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
||||
import { Request } from 'express';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
export type GoogleAPIsRequest = Omit<
|
||||
Request,
|
||||
@ -19,6 +21,9 @@ export type GoogleAPIsRequest = Omit<
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
transientToken: string;
|
||||
redirectLocation?: string;
|
||||
calendarVisibility?: CalendarChannelVisibility;
|
||||
messageVisibility?: MessageChannelVisibility;
|
||||
};
|
||||
};
|
||||
|
||||
@ -64,6 +69,9 @@ export class GoogleAPIsStrategy extends PassportStrategy(
|
||||
prompt: 'consent',
|
||||
state: JSON.stringify({
|
||||
transientToken: req.params.transientToken,
|
||||
redirectLocation: req.params.redirectLocation,
|
||||
calendarVisibility: req.params.calendarVisibility,
|
||||
messageVisibility: req.params.messageVisibility,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -92,6 +100,9 @@ export class GoogleAPIsStrategy extends PassportStrategy(
|
||||
accessToken,
|
||||
refreshToken,
|
||||
transientToken: state.transientToken,
|
||||
redirectLocation: state.redirectLocation,
|
||||
calendarVisibility: state.calendarVisibility,
|
||||
messageVisibility: state.messageVisibility,
|
||||
};
|
||||
|
||||
done(null, user);
|
||||
|
||||
@ -2,15 +2,11 @@ import { ObjectType, Field, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { TimelineCalendarEventParticipant } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
export enum TimelineCalendarEventVisibility {
|
||||
METADATA = 'METADATA',
|
||||
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
|
||||
}
|
||||
|
||||
registerEnumType(TimelineCalendarEventVisibility, {
|
||||
name: 'TimelineCalendarEventVisibility',
|
||||
description: 'Visibility of the calendar event',
|
||||
registerEnumType(CalendarChannelVisibility, {
|
||||
name: 'CalendarChannelVisibility',
|
||||
description: 'Visibility of the calendar channel',
|
||||
});
|
||||
|
||||
@ObjectType('LinkMetadata')
|
||||
@ -57,6 +53,6 @@ export class TimelineCalendarEvent {
|
||||
@Field(() => [TimelineCalendarEventParticipant])
|
||||
participants: TimelineCalendarEventParticipant[];
|
||||
|
||||
@Field(() => TimelineCalendarEventVisibility)
|
||||
visibility: TimelineCalendarEventVisibility;
|
||||
@Field(() => CalendarChannelVisibility)
|
||||
visibility: CalendarChannelVisibility;
|
||||
}
|
||||
|
||||
@ -4,12 +4,12 @@ import { Any } from 'typeorm';
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
|
||||
import { TimelineCalendarEventVisibility } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event.dto';
|
||||
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
|
||||
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class TimelineCalendarEventService {
|
||||
@ -107,8 +107,8 @@ export class TimelineCalendarEventService {
|
||||
const visibility = event.calendarChannelEventAssociations.some(
|
||||
(association) => association.calendarChannel.visibility === 'METADATA',
|
||||
)
|
||||
? TimelineCalendarEventVisibility.METADATA
|
||||
: TimelineCalendarEventVisibility.SHARE_EVERYTHING;
|
||||
? CalendarChannelVisibility.METADATA
|
||||
: CalendarChannelVisibility.SHARE_EVERYTHING;
|
||||
|
||||
return {
|
||||
...omit(event, [
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Entity({ name: 'keyValuePair', schema: 'core' })
|
||||
@ObjectType('KeyValuePair')
|
||||
@Unique('IndexOnKeyUserIdWorkspaceIdUnique', ['key', 'userId', 'workspaceId'])
|
||||
export class KeyValuePair {
|
||||
@IDField(() => UUIDScalarType)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.keyValuePairs, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: Relation<User>;
|
||||
|
||||
@Column({ nullable: true })
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => Workspace, (workspace) => workspace.keyValuePairs, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'workspaceId' })
|
||||
workspace: Relation<Workspace>;
|
||||
|
||||
@Column({ nullable: true })
|
||||
workspaceId: string;
|
||||
|
||||
@Field(() => String)
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
key: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
value: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
deletedAt: Date | null;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([KeyValuePair], 'core'),
|
||||
TypeORMModule,
|
||||
],
|
||||
}),
|
||||
],
|
||||
exports: [KeyValuePairService],
|
||||
providers: [KeyValuePairService],
|
||||
})
|
||||
export class KeyValuePairModule {}
|
||||
@ -0,0 +1,55 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { UserStates } from 'src/engine/core-modules/user-state/enums/user-states.enum';
|
||||
import { UserStateEmailSyncValues } from 'src/engine/core-modules/user-state/enums/user-state-email-sync-values.enum';
|
||||
|
||||
export enum KeyValueTypes {
|
||||
USER_STATE = 'USER_STATE',
|
||||
}
|
||||
|
||||
type KeyValuePairs = {
|
||||
[KeyValueTypes.USER_STATE]: {
|
||||
[UserStates.SYNC_EMAIL_ONBOARDING_STEP]: UserStateEmailSyncValues;
|
||||
};
|
||||
};
|
||||
|
||||
export class KeyValuePairService<TYPE extends keyof KeyValuePairs> {
|
||||
constructor(
|
||||
@InjectRepository(KeyValuePair, 'core')
|
||||
private readonly keyValuePairRepository: Repository<KeyValuePair>,
|
||||
) {}
|
||||
|
||||
async get<K extends keyof KeyValuePairs[TYPE]>(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
key: K,
|
||||
) {
|
||||
return await this.keyValuePairRepository.findOne({
|
||||
where: {
|
||||
userId,
|
||||
workspaceId,
|
||||
key: key as string,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async set<K extends keyof KeyValuePairs[TYPE]>(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
key: K,
|
||||
value: KeyValuePairs[TYPE][K],
|
||||
) {
|
||||
await this.keyValuePairRepository.upsert(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: key as string,
|
||||
value: value as string,
|
||||
},
|
||||
{ conflictPaths: ['userId', 'workspaceId', 'key'] },
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,13 @@
|
||||
import { ObjectType, Field } from '@nestjs/graphql';
|
||||
import { ObjectType, Field, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
registerEnumType(MessageChannelVisibility, {
|
||||
name: 'MessageChannelVisibility',
|
||||
description: 'Visibility of the message channel',
|
||||
});
|
||||
|
||||
@ObjectType('TimelineThread')
|
||||
export class TimelineThread {
|
||||
@ -11,8 +17,8 @@ export class TimelineThread {
|
||||
@Field()
|
||||
read: boolean;
|
||||
|
||||
@Field()
|
||||
visibility: string;
|
||||
@Field(() => MessageChannelVisibility)
|
||||
visibility: MessageChannelVisibility;
|
||||
|
||||
@Field()
|
||||
firstParticipant: TimelineThreadParticipant;
|
||||
|
||||
@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
|
||||
import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
type TimelineThreadParticipant = {
|
||||
personId: string;
|
||||
@ -352,7 +353,7 @@ export class TimelineMessagingService {
|
||||
const threadVisibility:
|
||||
| {
|
||||
id: string;
|
||||
visibility: 'metadata' | 'subject' | 'share_everything';
|
||||
visibility: MessageChannelVisibility;
|
||||
}[]
|
||||
| undefined = await this.workspaceDataSourceService.executeRawQuery(
|
||||
`
|
||||
@ -372,11 +373,11 @@ export class TimelineMessagingService {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const visibilityValues = ['metadata', 'subject', 'share_everything'];
|
||||
const visibilityValues = Object.values(MessageChannelVisibility);
|
||||
|
||||
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants:
|
||||
| {
|
||||
[key: string]: 'metadata' | 'subject' | 'share_everything';
|
||||
[key: string]: MessageChannelVisibility;
|
||||
}
|
||||
| undefined = threadVisibility?.reduce(
|
||||
(threadVisibilityAcc, threadVisibility) => {
|
||||
@ -385,7 +386,8 @@ export class TimelineMessagingService {
|
||||
Math.max(
|
||||
visibilityValues.indexOf(threadVisibility.visibility),
|
||||
visibilityValues.indexOf(
|
||||
threadVisibilityAcc[threadVisibility.id] ?? 'metadata',
|
||||
threadVisibilityAcc[threadVisibility.id] ??
|
||||
MessageChannelVisibility.METADATA,
|
||||
),
|
||||
)
|
||||
];
|
||||
@ -396,7 +398,7 @@ export class TimelineMessagingService {
|
||||
);
|
||||
|
||||
const threadVisibilityByThreadId: {
|
||||
[key: string]: 'metadata' | 'subject' | 'share_everything';
|
||||
[key: string]: MessageChannelVisibility;
|
||||
} = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => {
|
||||
// If the workspace member is not in the participants of the thread, use the visibility value from the query
|
||||
threadVisibilityAcc[messageThreadId] =
|
||||
@ -405,8 +407,8 @@ export class TimelineMessagingService {
|
||||
)
|
||||
? threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants?.[
|
||||
messageThreadId
|
||||
] ?? 'metadata'
|
||||
: 'share_everything';
|
||||
] ?? MessageChannelVisibility.METADATA
|
||||
: MessageChannelVisibility.SHARE_EVERYTHING;
|
||||
|
||||
return threadVisibilityAcc;
|
||||
}, {});
|
||||
@ -461,7 +463,9 @@ export class TimelineMessagingService {
|
||||
lastTwoParticipants,
|
||||
lastMessageReceivedAt: thread.lastMessageReceivedAt,
|
||||
lastMessageBody: thread.lastMessageBody,
|
||||
visibility: threadVisibilityByThreadId?.[messageThreadId] ?? 'metadata',
|
||||
visibility:
|
||||
threadVisibilityByThreadId?.[messageThreadId] ??
|
||||
MessageChannelVisibility.METADATA,
|
||||
subject: threadSubject,
|
||||
numberOfMessagesInThread: numberOfMessages,
|
||||
participantCount: threadActiveParticipants.length,
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
|
||||
|
||||
export const DEFAULT_USER_STATE: UserState = {
|
||||
skipSyncEmailOnboardingStep: true,
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class SkipSyncEmailOnboardingStep {
|
||||
@Field(() => Boolean, {
|
||||
description: 'Boolean that confirms query was dispatched',
|
||||
})
|
||||
success: boolean;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('UserState')
|
||||
export class UserState {
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
skipSyncEmailOnboardingStep: boolean | null;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export enum UserStateEmailSyncValues {
|
||||
SKIPPED = 'SKIPPED',
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export enum UserStates {
|
||||
SYNC_EMAIL_ONBOARDING_STEP = 'SYNC_EMAIL_ONBOARDING_STEP',
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
|
||||
import { UserStateResolver } from 'src/engine/core-modules/user-state/user-state.resolver';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
|
||||
|
||||
@Module({
|
||||
imports: [DataSourceModule, KeyValuePairModule],
|
||||
exports: [UserStateService],
|
||||
providers: [UserStateService, UserStateResolver],
|
||||
})
|
||||
export class UserStateModule {}
|
||||
@ -0,0 +1,28 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { SkipSyncEmailOnboardingStep } from 'src/engine/core-modules/user-state/dtos/skip-sync-email.entity-onboarding-step';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Resolver(() => UserState)
|
||||
export class UserStateResolver {
|
||||
constructor(private readonly userStateService: UserStateService) {}
|
||||
|
||||
@Mutation(() => SkipSyncEmailOnboardingStep)
|
||||
async skipSyncEmailOnboardingStep(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<SkipSyncEmailOnboardingStep> {
|
||||
return await this.userStateService.skipSyncEmailOnboardingStep(
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
|
||||
import {
|
||||
KeyValuePairService,
|
||||
KeyValueTypes,
|
||||
} from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
|
||||
import { UserStates } from 'src/engine/core-modules/user-state/enums/user-states.enum';
|
||||
import { UserStateEmailSyncValues } from 'src/engine/core-modules/user-state/enums/user-state-email-sync-values.enum';
|
||||
import { SkipSyncEmailOnboardingStep } from 'src/engine/core-modules/user-state/dtos/skip-sync-email.entity-onboarding-step';
|
||||
|
||||
@Injectable()
|
||||
export class UserStateService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
||||
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||
private readonly keyValuePairService: KeyValuePairService<KeyValueTypes.USER_STATE>,
|
||||
) {}
|
||||
|
||||
async getUserState(user: User, workspace: Workspace): Promise<UserState> {
|
||||
const connectedAccounts =
|
||||
await this.connectedAccountRepository.getAllByUserId(
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (connectedAccounts?.length) {
|
||||
return {
|
||||
skipSyncEmailOnboardingStep: true,
|
||||
};
|
||||
}
|
||||
|
||||
const skipSyncEmail = await this.keyValuePairService.get(
|
||||
user.id,
|
||||
workspace.id,
|
||||
UserStates.SYNC_EMAIL_ONBOARDING_STEP,
|
||||
);
|
||||
|
||||
return {
|
||||
skipSyncEmailOnboardingStep:
|
||||
!!skipSyncEmail &&
|
||||
skipSyncEmail.value === UserStateEmailSyncValues.SKIPPED,
|
||||
};
|
||||
}
|
||||
|
||||
async skipSyncEmailOnboardingStep(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<SkipSyncEmailOnboardingStep> {
|
||||
await this.keyValuePairService.set(
|
||||
userId,
|
||||
workspaceId,
|
||||
UserStates.SYNC_EMAIL_ONBOARDING_STEP,
|
||||
UserStateEmailSyncValues.SKIPPED,
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@ -2,14 +2,13 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
@ -17,6 +17,8 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
|
||||
|
||||
@Entity({ name: 'user', schema: 'core' })
|
||||
@ObjectType('User')
|
||||
@ -100,10 +102,18 @@ export class User {
|
||||
})
|
||||
appTokens: Relation<AppToken[]>;
|
||||
|
||||
@OneToMany(() => KeyValuePair, (keyValuePair) => keyValuePair.user, {
|
||||
cascade: true,
|
||||
})
|
||||
keyValuePairs: Relation<KeyValuePair[]>;
|
||||
|
||||
@Field(() => WorkspaceMember, { nullable: true })
|
||||
workspaceMember: Relation<WorkspaceMember>;
|
||||
|
||||
@Field(() => [UserWorkspace])
|
||||
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
|
||||
workspaces: Relation<UserWorkspace[]>;
|
||||
|
||||
@Field(() => UserState, { nullable: false })
|
||||
state: UserState;
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { UserStateModule } from 'src/engine/core-modules/user-state/user-state.module';
|
||||
|
||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||
|
||||
@ -27,6 +28,7 @@ import { UserService } from './services/user.service';
|
||||
}),
|
||||
DataSourceModule,
|
||||
FileUploadModule,
|
||||
UserStateModule,
|
||||
WorkspaceModule,
|
||||
],
|
||||
exports: [UserService],
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import {
|
||||
Resolver,
|
||||
Query,
|
||||
Args,
|
||||
Parent,
|
||||
ResolveField,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
@ -17,6 +17,7 @@ import { Repository } from 'typeorm';
|
||||
import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
@ -26,8 +27,11 @@ import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
|
||||
import { UserService } from './services/user.service';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
|
||||
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
|
||||
import { DEFAULT_USER_STATE } from 'src/engine/core-modules/user-state/constants/default-user-state';
|
||||
|
||||
const getHMACKey = (email?: string, key?: string | null) => {
|
||||
if (!email || !key) return null;
|
||||
@ -43,6 +47,7 @@ export class UserResolver {
|
||||
constructor(
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly userStateService: UserStateService,
|
||||
private readonly userService: UserService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
@ -113,4 +118,16 @@ export class UserResolver {
|
||||
// Proceed with user deletion
|
||||
return this.userService.deleteUser(userId);
|
||||
}
|
||||
|
||||
@ResolveField(() => UserState)
|
||||
async state(
|
||||
@Parent() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<UserState> {
|
||||
if (!user || !workspace) {
|
||||
return DEFAULT_USER_STATE;
|
||||
}
|
||||
|
||||
return this.userStateService.getUserState(user, workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
|
||||
@Entity({ name: 'workspace', schema: 'core' })
|
||||
@ObjectType('Workspace')
|
||||
@ -63,6 +64,11 @@ export class Workspace {
|
||||
})
|
||||
appTokens: Relation<AppToken[]>;
|
||||
|
||||
@OneToMany(() => KeyValuePair, (keyValuePair) => keyValuePair.workspace, {
|
||||
cascade: true,
|
||||
})
|
||||
keyValuePairs: Relation<KeyValuePair[]>;
|
||||
|
||||
@OneToMany(() => User, (user) => user.defaultWorkspace)
|
||||
users: Relation<User[]>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user