@ -24,7 +24,6 @@ import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
|
||||
import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output';
|
||||
import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input';
|
||||
@ -366,6 +365,7 @@ export class AuthResolver {
|
||||
const resetToken =
|
||||
await this.resetPasswordService.generatePasswordResetToken(
|
||||
emailPasswordResetInput.email,
|
||||
emailPasswordResetInput.workspaceId,
|
||||
);
|
||||
|
||||
return await this.resetPasswordService.sendEmailPasswordResetLink(
|
||||
@ -403,11 +403,4 @@ export class AuthResolver {
|
||||
args.passwordResetToken,
|
||||
);
|
||||
}
|
||||
|
||||
@Query(() => [AvailableWorkspaceOutput])
|
||||
async findAvailableWorkspacesByEmail(
|
||||
@Args('email') email: string,
|
||||
): Promise<AvailableWorkspaceOutput[]> {
|
||||
return this.userWorkspaceService.findAvailableWorkspacesByEmail(email);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class EmailPasswordResetLinkInput {
|
||||
@ -8,4 +8,9 @@ export class EmailPasswordResetLinkInput {
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@ -37,4 +37,7 @@ export class PasswordResetToken {
|
||||
|
||||
@Field(() => Date)
|
||||
passwordResetTokenExpiresAt: Date;
|
||||
|
||||
@Field(() => String)
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@ -20,9 +20,11 @@ import { ResetPasswordService } from './reset-password.service';
|
||||
describe('ResetPasswordService', () => {
|
||||
let service: ResetPasswordService;
|
||||
let userRepository: Repository<User>;
|
||||
let workspaceRepository: Repository<Workspace>;
|
||||
let appTokenRepository: Repository<AppToken>;
|
||||
let emailService: EmailService;
|
||||
let environmentService: EnvironmentService;
|
||||
let domainManagerService: DomainManagerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -32,6 +34,10 @@ describe('ResetPasswordService', () => {
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useClass: Repository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useClass: Repository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AppToken, 'core'),
|
||||
useClass: Repository,
|
||||
@ -52,6 +58,7 @@ describe('ResetPasswordService', () => {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue(new URL('http://localhost:3001')),
|
||||
buildWorkspaceURL: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -67,11 +74,16 @@ describe('ResetPasswordService', () => {
|
||||
userRepository = module.get<Repository<User>>(
|
||||
getRepositoryToken(User, 'core'),
|
||||
);
|
||||
workspaceRepository = module.get<Repository<Workspace>>(
|
||||
getRepositoryToken(Workspace, 'core'),
|
||||
);
|
||||
appTokenRepository = module.get<Repository<AppToken>>(
|
||||
getRepositoryToken(AppToken, 'core'),
|
||||
);
|
||||
emailService = module.get<EmailService>(EmailService);
|
||||
environmentService = module.get<EnvironmentService>(EnvironmentService);
|
||||
domainManagerService =
|
||||
module.get<DomainManagerService>(DomainManagerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@ -89,8 +101,10 @@ describe('ResetPasswordService', () => {
|
||||
jest.spyOn(appTokenRepository, 'save').mockResolvedValue({} as AppToken);
|
||||
jest.spyOn(environmentService, 'get').mockReturnValue('1h');
|
||||
|
||||
const result =
|
||||
await service.generatePasswordResetToken('test@example.com');
|
||||
const result = await service.generatePasswordResetToken(
|
||||
'test@example.com',
|
||||
'workspace-id',
|
||||
);
|
||||
|
||||
expect(result.passwordResetToken).toBeDefined();
|
||||
expect(result.passwordResetTokenExpiresAt).toBeDefined();
|
||||
@ -106,7 +120,10 @@ describe('ResetPasswordService', () => {
|
||||
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.generatePasswordResetToken('nonexistent@example.com'),
|
||||
service.generatePasswordResetToken(
|
||||
'nonexistent@example.com',
|
||||
'workspace-id',
|
||||
),
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
|
||||
@ -115,6 +132,7 @@ describe('ResetPasswordService', () => {
|
||||
const mockExistingToken = {
|
||||
userId: '1',
|
||||
type: AppTokenType.PasswordResetToken,
|
||||
workspaceId: 'workspace-id',
|
||||
expiresAt: addMilliseconds(new Date(), 3600000),
|
||||
};
|
||||
|
||||
@ -126,7 +144,7 @@ describe('ResetPasswordService', () => {
|
||||
.mockResolvedValue(mockExistingToken as AppToken);
|
||||
|
||||
await expect(
|
||||
service.generatePasswordResetToken('test@example.com'),
|
||||
service.generatePasswordResetToken('test@example.com', 'workspace-id'),
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
});
|
||||
@ -135,6 +153,7 @@ describe('ResetPasswordService', () => {
|
||||
it('should send a password reset email', async () => {
|
||||
const mockUser = { id: '1', email: 'test@example.com' };
|
||||
const mockToken = {
|
||||
workspaceId: 'workspace-id',
|
||||
passwordResetToken: 'token123',
|
||||
passwordResetTokenExpiresAt: new Date(),
|
||||
};
|
||||
@ -142,9 +161,19 @@ describe('ResetPasswordService', () => {
|
||||
jest
|
||||
.spyOn(userRepository, 'findOneBy')
|
||||
.mockResolvedValue(mockUser as User);
|
||||
jest
|
||||
.spyOn(workspaceRepository, 'findOneBy')
|
||||
.mockResolvedValue({ id: 'workspace-id' } as Workspace);
|
||||
jest
|
||||
.spyOn(environmentService, 'get')
|
||||
.mockReturnValue('http://localhost:3000');
|
||||
jest
|
||||
.spyOn(domainManagerService, 'buildWorkspaceURL')
|
||||
.mockReturnValue(
|
||||
new URL(
|
||||
'https://subdomain.localhost.com:3000/reset-password/passwordResetToken',
|
||||
),
|
||||
);
|
||||
|
||||
const result = await service.sendEmailPasswordResetLink(
|
||||
mockToken,
|
||||
|
||||
@ -28,6 +28,8 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
@Injectable()
|
||||
export class ResetPasswordService {
|
||||
@ -36,12 +38,17 @@ export class ResetPasswordService {
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(AppToken, 'core')
|
||||
private readonly appTokenRepository: Repository<AppToken>,
|
||||
private readonly emailService: EmailService,
|
||||
) {}
|
||||
|
||||
async generatePasswordResetToken(email: string): Promise<PasswordResetToken> {
|
||||
async generatePasswordResetToken(
|
||||
email: string,
|
||||
workspaceId: string,
|
||||
): Promise<PasswordResetToken> {
|
||||
const user = await this.userRepository.findOneBy({
|
||||
email,
|
||||
});
|
||||
@ -95,12 +102,14 @@ export class ResetPasswordService {
|
||||
|
||||
await this.appTokenRepository.save({
|
||||
userId: user.id,
|
||||
workspaceId: workspaceId,
|
||||
value: hashedResetToken,
|
||||
expiresAt,
|
||||
type: AppTokenType.PasswordResetToken,
|
||||
});
|
||||
|
||||
return {
|
||||
workspaceId,
|
||||
passwordResetToken: plainResetToken,
|
||||
passwordResetTokenExpiresAt: expiresAt,
|
||||
};
|
||||
@ -122,12 +131,19 @@ export class ResetPasswordService {
|
||||
);
|
||||
}
|
||||
|
||||
const frontBaseURL = this.domainManagerService.getBaseUrl();
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: resetToken.workspaceId,
|
||||
});
|
||||
|
||||
frontBaseURL.pathname = `/reset-password/${resetToken.passwordResetToken}`;
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
const link = this.domainManagerService.buildWorkspaceURL({
|
||||
workspace,
|
||||
pathname: `/reset-password/${resetToken.passwordResetToken}`,
|
||||
});
|
||||
|
||||
const emailData = {
|
||||
link: frontBaseURL.toString(),
|
||||
link: link.toString(),
|
||||
duration: ms(
|
||||
differenceInMilliseconds(
|
||||
resetToken.passwordResetTokenExpiresAt,
|
||||
|
||||
Reference in New Issue
Block a user