(Breaking change) Switch between set password and change password on the settings page. (#15582)
Here is what the PR does: - Surface password state in validatePasswordResetToken, returning hasPassword so the client can tell whether a user is setting or changing their password. - Consume that flag throughout the front end (mock data, stories, GraphQL types) and update the Reset/Set Password modal to swap the heading/button label and success toast accordingly. - After a successful password set/reset, immediately update the logged-in user’s hasPassword flag so the Settings screen reflects the new state without a reload. Modal has two states now - reset password modal uses change password state since it made intuitive sense. <p align="center"> <img width="404" height="397" alt="image" src="https://github.com/user-attachments/assets/c54cc581-1248-4395-833d-0202758e1947" /> </p> <p align="center"> <img width="403" height="393" alt="image" src="https://github.com/user-attachments/assets/d8a39a95-27e6-4037-86f2-1f74176002ba" /> </p> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
+3
@@ -9,4 +9,7 @@ export class ValidatePasswordResetTokenOutput {
|
||||
|
||||
@Field(() => String)
|
||||
email: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
+5
-1
@@ -259,7 +259,11 @@ describe('ResetPasswordService', () => {
|
||||
|
||||
const result = await service.validatePasswordResetToken('validToken');
|
||||
|
||||
expect(result).toEqual({ id: '1', email: 'test@example.com' });
|
||||
expect(result).toEqual({
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
hasPassword: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error for an invalid token', async () => {
|
||||
|
||||
+13
-4
@@ -10,7 +10,11 @@ import ms from 'ms';
|
||||
import { PasswordResetLinkEmail } from 'twenty-emails';
|
||||
import { type APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { assertIsDefinedOrThrow, getAppPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
assertIsDefinedOrThrow,
|
||||
getAppPath,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { IsNull, MoreThan, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
@@ -25,13 +29,13 @@ import { type EmailPasswordResetLinkOutput } from 'src/engine/core-modules/auth/
|
||||
import { type InvalidatePasswordOutput } from 'src/engine/core-modules/auth/dto/invalidate-password.dto';
|
||||
import { type PasswordResetToken } from 'src/engine/core-modules/auth/dto/password-reset-token.dto';
|
||||
import { type ValidatePasswordResetTokenOutput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.dto';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
|
||||
@Injectable()
|
||||
export class ResetPasswordService {
|
||||
@@ -120,6 +124,7 @@ export class ResetPasswordService {
|
||||
email,
|
||||
new AuthException('User not found', AuthExceptionCode.INVALID_INPUT),
|
||||
);
|
||||
const hasPassword = isDefined(user.passwordHash);
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: resetToken.workspaceId,
|
||||
@@ -145,6 +150,7 @@ export class ResetPasswordService {
|
||||
long: true,
|
||||
},
|
||||
),
|
||||
hasPassword,
|
||||
locale,
|
||||
};
|
||||
|
||||
@@ -153,9 +159,11 @@ export class ResetPasswordService {
|
||||
const html = await render(emailTemplate, { pretty: true });
|
||||
const text = await render(emailTemplate, { plainText: true });
|
||||
|
||||
const resetPasswordMsg = msg`Action Needed to Reset Password`;
|
||||
const i18n = this.i18nService.getI18nInstance(locale);
|
||||
const subject = i18n._(resetPasswordMsg);
|
||||
const subjectTemplate = hasPassword
|
||||
? msg`Action Needed to Reset Password`
|
||||
: msg`Action Needed to Set Password`;
|
||||
const subject = i18n._(subjectTemplate);
|
||||
|
||||
await this.emailService.send({
|
||||
from: `${this.twentyConfigService.get(
|
||||
@@ -202,6 +210,7 @@ export class ResetPasswordService {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
hasPassword: isDefined(user.passwordHash),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user