(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:
Abdullah.
2025-11-05 15:29:37 +05:00
committed by GitHub
parent cc7343a8f2
commit 0fcfcec426
80 changed files with 756 additions and 358 deletions
@@ -9,4 +9,7 @@ export class ValidatePasswordResetTokenOutput {
@Field(() => String)
email: string;
@Field(() => Boolean)
hasPassword: boolean;
}
@@ -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 () => {
@@ -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),
};
}