Skip to content

Commit

Permalink
1147 add feature to resend new user emails (#1343)
Browse files Browse the repository at this point in the history
Signed-off-by: Hristiyan <[email protected]>
Signed-off-by: Svetoslav Borislavov <[email protected]>
Co-authored-by: Svetoslav Borislavov <[email protected]>
  • Loading branch information
icoxxx and SvetBorislavov authored Dec 13, 2024
1 parent 8e6b4bb commit 2aca253
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 14 deletions.
4 changes: 4 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,10 @@ The Initial Developer of [email protected],
is David Mark Clements (https://github.com/davidmarkclements/atomic-sleep).
Copyright David Mark Clements. All Rights Reserved.

The Initial Developer of [email protected],
is Matt Zabriskie (https://github.com/axios/axios).
Copyright Matt Zabriskie. All Rights Reserved.

The Initial Developer of [email protected],
is Matt Zabriskie (https://github.com/axios/axios).
Copyright Matt Zabriskie. All Rights Reserved.
Expand Down
24 changes: 20 additions & 4 deletions back-end/apps/api/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,31 @@ describe('AuthController', () => {
expect(await controller.signUp({ email: '[email protected]' }, request)).toBe(result);
});

it('should throw an error if the user already exists', async () => {
it('should throw an error if the user already exists or return an updated user if it exists but its status is NEW', async () => {
jest.mocked(request.get).mockImplementationOnce(() => 'localhost');
jest
.spyOn(controller, 'signUp')
.mockRejectedValue(new UnprocessableEntityException('Email already exists.'));

.spyOn(authService, 'signUpByAdmin')
.mockRejectedValueOnce(new UnprocessableEntityException('Email already exists.'));
await expect(controller.signUp({ email: '[email protected]' }, request)).rejects.toThrow(
'Email already exists.',
);

jest.spyOn(authService, 'signUpByAdmin').mockResolvedValueOnce({
id: 1,
email: '[email protected]',
status: UserStatus.NEW,
password: 'newHashedPassword',
} as User);

const result = await controller.signUp({ email: '[email protected]' }, request);

expect(result).toEqual(
expect.objectContaining({
email: '[email protected]',
status: UserStatus.NEW,
password: 'newHashedPassword',
}),
);
});

it('should throw an error if no email is supplied', async () => {
Expand Down
33 changes: 33 additions & 0 deletions back-end/apps/api/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,39 @@ describe('AuthService', () => {
);
});

it('should update the password and resend an email for an existing user with status NEW', async () => {
const dto: SignUpUserDto = { email: '[email protected]' };

jest.spyOn(userService, 'getUser').mockResolvedValue({
id: 1,
email: dto.email,
status: UserStatus.NEW,
deletedAt: null,
} as User);

jest.spyOn(userService, 'getSaltedHash').mockResolvedValue('hashedPassword');

jest.spyOn(userService, 'updateUserById').mockResolvedValue({
id: 1,
email: dto.email,
status: UserStatus.NEW,
password: 'hashedPassword',
} as User);

await service.signUpByAdmin(dto, 'http://localhost');

expect(userService.getUser).toHaveBeenCalledWith({ email: dto.email }, true);

expect(userService.getSaltedHash).toHaveBeenCalledWith(expect.any(String));

expect(userService.updateUserById).toHaveBeenCalledWith(1, { password: 'hashedPassword' });

expect(notificationsService.emit).toHaveBeenCalledWith(
'notify_email',
expect.objectContaining({ email: dto.email }),
);
});

it('should login user', async () => {
const { user } = await invokeLogin(false);

Expand Down
14 changes: 12 additions & 2 deletions back-end/apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@ export class AuthService {
async signUpByAdmin(dto: SignUpUserDto, url: string): Promise<User> {
const tempPassword = this.generatePassword();

const user = await this.usersService.createUser(dto.email, tempPassword);
const existingUser = await this.usersService.getUser({ email: dto.email }, true);
let user: User;

if (existingUser && !existingUser.deletedAt && existingUser.status === UserStatus.NEW) {
const hashedPass = await this.usersService.getSaltedHash(tempPassword);
user = await this.usersService.updateUserById(existingUser.id, { password: hashedPass });
} else {
user = await this.usersService.createUser(dto.email, tempPassword);
}

this.notificationsService.emit<undefined, NotifyEmailDto>(NOTIFY_EMAIL, {
subject: 'Hedera Transaction Tool Registration',
Expand Down Expand Up @@ -141,7 +149,9 @@ export class AuthService {
/* Generate a random password */
private generatePassword() {
const getRandomLetters = (length: number) =>
Array.from({ length }, () => String.fromCharCode(97 + Math.floor(Math.random() * 26))).join('');
Array.from({ length }, () => String.fromCharCode(97 + Math.floor(Math.random() * 26))).join(
'',
);

return `${getRandomLetters(5)}-${getRandomLetters(5)}`;
}
Expand Down
26 changes: 25 additions & 1 deletion back-end/apps/api/test/spec/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,35 @@ describe('Auth (e2e)', () => {
await loginEndpoint.post({ email: user.email, password: dummyNew.password }).expect(200);
});

it('(POST) should update password and resend email if users status is NEW and the sender is an admin', async () => {
const userRepo = await getRepository(User);

const user = await getUser('userNew');

await endpoint
.post({ email: user.email }, null, adminAuthToken)
.expect(201)
.then(res => {
expect(res.body).toEqual({
email: user.email,
createdAt: expect.any(String),
id: expect.any(Number),
});
});

const updatedUser = await userRepo.findOne({ where: { email: user.email } });

expect(updatedUser).toBeDefined();
expect(updatedUser?.password).not.toBe(user.password);
expect(updatedUser?.status).toBe(UserStatus.NEW);
});

it('(POST) should not register new user if already exists', async () => {
const user = await getUser('user');
await endpoint
.post(
{
email: validEmail,
email: user.email,
},
null,
adminAuthToken,
Expand Down
41 changes: 34 additions & 7 deletions front-end/src/renderer/components/Contacts/ContactDetails.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { HederaAccount } from '@prisma/client';
import type { AccountInfo, Contact } from '@main/shared/interfaces';
import { useToast } from 'vue-toast-notification';
import { onBeforeMount, ref, watch } from 'vue';
Expand All @@ -12,8 +13,9 @@ import useContactsStore from '@renderer/stores/storeContacts';
import { addContact, updateContact } from '@renderer/services/contactsService';
import { getAccountsByPublicKeysParallel } from '@renderer/services/mirrorNodeDataService';
import { signUp } from '@renderer/services/organization';
import { isLoggedInOrganization, isUserLoggedIn } from '@renderer/utils';
import { getErrorMessage, isLoggedInOrganization, isUserLoggedIn } from '@renderer/utils';
import AppButton from '@renderer/components/ui/AppButton.vue';
import AppInput from '@renderer/components/ui/AppInput.vue';
Expand All @@ -26,6 +28,9 @@ const props = defineProps<{
linkedAccounts: HederaAccount[];
}>();
/* Composables */
const toast = useToast();
/* Stores */
const user = useUserStore();
const network = useNetworkStore();
Expand Down Expand Up @@ -89,6 +94,18 @@ const handleAccountsLookup = async () => {
);
};
const handleResend = async () => {
try {
if (user.selectedOrganization?.serverUrl) {
const email = props.contact.user.email;
await signUp(user.selectedOrganization.serverUrl, email);
}
toast.success('Email sent successfully');
} catch (error: unknown) {
toast.error(getErrorMessage(error, 'Error while sending email. Please try again.'));
}
};
/* Hooks */
onBeforeMount(async () => {
await handleAccountsLookup();
Expand Down Expand Up @@ -126,13 +143,23 @@ watch(
></span>
</p>
</div>
<div class="d-flex gap-3">
<div
v-if="
isLoggedInOrganization(user.selectedOrganization) &&
user.selectedOrganization.admin &&
contact.user.id !== user.selectedOrganization.userId
"
class="d-flex gap-3"
>
<AppButton
v-if="contact.user.status === 'NEW'"
data-testid="button-resend-email-from-contact-list"
class="min-w-unset"
color="secondary"
@click="handleResend"
>Resend email</AppButton
>
<AppButton
v-if="
isLoggedInOrganization(user.selectedOrganization) &&
user.selectedOrganization.admin &&
contact.user.id !== user.selectedOrganization.userId
"
data-testid="button-remove-account-from-contact-list"
class="min-w-unset"
color="danger"
Expand Down

0 comments on commit 2aca253

Please sign in to comment.