Skip to content

Commit

Permalink
feat: support MessageAction
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Jul 14, 2024
1 parent 490629c commit 9b58dd7
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ export const sendConfirmationCode = (user: UserEntity): Promise<void> =>
subject: 'Your verification code',
text: `Your confirmation code is ${user.confirmationCode}`,
});

export const sendTemporaryPassword = (user: UserEntity, password: string): Promise<void> =>
sendMail({
to: { name: user.name, address: user.email },
subject: 'Your temporary password',
text: `Your temporary password is ${password}`,
});
58 changes: 42 additions & 16 deletions server/domain/user/useCase/adminUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Prisma } from '@prisma/client';
import assert from 'assert';
import type {
AdminCreateUserTarget,
Expand All @@ -6,6 +7,7 @@ import type {
AdminInitiateAuthTarget,
AdminSetUserPasswordTarget,
} from 'common/types/auth';
import type { UserEntity } from 'common/types/user';
import { userPoolQuery } from 'domain/userPool/repository/userPoolQuery';
import { brandedId } from 'service/brandedId';
import { prismaClient, transaction } from 'service/prismaClient';
Expand All @@ -14,6 +16,45 @@ import { adminMethod } from '../model/adminMethod';
import { userCommand } from '../repository/userCommand';
import { userQuery } from '../repository/userQuery';
import { genTokens } from '../service/genTokens';
import { sendTemporaryPassword } from '../service/sendAuthMail';

const resendTempPass = async (
tx: Prisma.TransactionClient,
req: AdminCreateUserTarget['reqBody'],
): Promise<UserEntity> => {
assert(req.Username);

const user = await userQuery.findByName(tx, req.Username);
await sendTemporaryPassword(user, user.password);

return user;
};

const createUser = async (
tx: Prisma.TransactionClient,
req: AdminCreateUserTarget['reqBody'],
): Promise<UserEntity> => {
const email = req.UserAttributes?.find((attr) => attr.Name === 'email')?.Value;

assert(req.Username);
assert(email);
assert(req.UserPoolId);

const userPool = await userPoolQuery.findById(tx, req.UserPoolId);
const idCount = await userQuery.countId(tx, req.Username);
const password = req.TemporaryPassword ?? `TempPass-${Date.now()}`;
const user = adminMethod.createVerifiedUser(idCount, {
name: req.Username,
password,
email,
userPoolId: userPool.id,
});

await userCommand.save(tx, user);
if (req.MessageAction !== 'SUPPRESS') await sendTemporaryPassword(user, password);

return user;
};

export const adminUseCase = {
getUser: async (req: AdminGetUserTarget['reqBody']): Promise<AdminGetUserTarget['resBody']> => {
Expand All @@ -29,22 +70,7 @@ export const adminUseCase = {
},
createUser: (req: AdminCreateUserTarget['reqBody']): Promise<AdminCreateUserTarget['resBody']> =>
transaction(async (tx) => {
const email = req.UserAttributes?.find((attr) => attr.Name === 'email')?.Value;

assert(email);
assert(req.UserPoolId);
assert(req.Username);

const userPool = await userPoolQuery.findById(tx, req.UserPoolId);
const idCount = await userQuery.countId(tx, req.Username);
const user = adminMethod.createVerifiedUser(idCount, {
name: req.Username,
password: req.TemporaryPassword ?? `TempPass-${Date.now()}`,
email,
userPoolId: userPool.id,
});

await userCommand.save(tx, user);
const user = await (req.MessageAction === 'RESEND' ? resendTempPass : createUser)(tx, req);

return {
User: {
Expand Down
2 changes: 1 addition & 1 deletion server/domain/user/useCase/authUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { EXPIRES_SEC } from 'service/constants';
import { transaction } from 'service/prismaClient';
import type { AccessTokenJwt } from 'service/types';
import { genCodeDeliveryDetails } from '../service/genCodeDeliveryDetails';
import { sendConfirmationCode } from '../service/sendConfirmationCode';
import { sendConfirmationCode } from '../service/sendAuthMail';

export const authUseCase = {
signUp: (req: SignUpTarget['reqBody']): Promise<SignUpTarget['resBody']> =>
Expand Down
36 changes: 17 additions & 19 deletions server/tests/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
AdminInitiateAuthCommand,
} from '@aws-sdk/client-cognito-identity-provider';
import api from 'api/$api';
import assert from 'assert';
import axios from 'axios';
import { cognitoClient } from 'service/cognito';
import { COOKIE_NAME } from 'service/constants';
Expand All @@ -22,29 +21,28 @@ export const testUserName = 'test-user';
export const testPassword = 'Test-user-password1';

export const createUserClient = async (): Promise<typeof noCookieClient> => {
const command1 = new AdminCreateUserCommand({
UserPoolId: DEFAULT_USER_POOL_ID,
Username: testUserName,
TemporaryPassword: testPassword,
UserAttributes: [{ Name: 'email', Value: `${ulid()}@example.com` }],
});

await cognitoClient.send(command1);

const command2 = new AdminInitiateAuthCommand({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
UserPoolId: DEFAULT_USER_POOL_ID,
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
AuthParameters: { USERNAME: testUserName, PASSWORD: testPassword },
});
await cognitoClient.send(
new AdminCreateUserCommand({
UserPoolId: DEFAULT_USER_POOL_ID,
Username: testUserName,
TemporaryPassword: testPassword,
UserAttributes: [{ Name: 'email', Value: `${ulid()}@example.com` }],
}),
);

const res = await cognitoClient.send(command2);
assert(res.AuthenticationResult);
const res = await cognitoClient.send(
new AdminInitiateAuthCommand({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
UserPoolId: DEFAULT_USER_POOL_ID,
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
AuthParameters: { USERNAME: testUserName, PASSWORD: testPassword },
}),
);

const agent = axios.create({
baseURL,
headers: {
cookie: `${COOKIE_NAME}=${res.AuthenticationResult.IdToken}`,
cookie: `${COOKIE_NAME}=${res.AuthenticationResult?.IdToken}`,
'Content-Type': 'text/plain',
},
});
Expand Down
38 changes: 27 additions & 11 deletions server/tests/api/signUp.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import assert from 'assert';
import { InbucketAPIClient } from 'inbucket-js-client';
import { DEFAULT_USER_POOL_CLIENT_ID } from 'service/envValues';
import {
AdminInitiateAuthCommand,
GetUserCommand,
} from '@aws-sdk/client-cognito-identity-provider';
import { cognitoClient } from 'service/cognito';
import { DEFAULT_USER_POOL_CLIENT_ID, DEFAULT_USER_POOL_ID } from 'service/envValues';
import { ulid } from 'ulid';
import { expect, test } from 'vitest';
import { noCookieClient } from './apiClient';
import { fetchMailBodyAndTrash } from './utils';

test('signUp', async () => {
const Username = 'user';
const Password = 'Test-client-password2';
const email = `${ulid()}@example.com`;

await noCookieClient.post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.SignUp' },
body: {
Username,
Password: 'Test-client-password2',
Password,
UserAttributes: [{ Name: 'email', Value: email }],
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
},
Expand All @@ -24,21 +29,32 @@ test('signUp', async () => {
body: { ClientId: DEFAULT_USER_POOL_CLIENT_ID, Username },
});

assert(process.env.INBUCKET_URL);
const token = await cognitoClient
.send(
new AdminInitiateAuthCommand({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
UserPoolId: DEFAULT_USER_POOL_ID,
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
AuthParameters: { USERNAME: Username, PASSWORD: Password },
}),
)
.then((res) => res.AuthenticationResult?.IdToken);

const inbucketClient = new InbucketAPIClient(process.env.INBUCKET_URL);
const inbox = await inbucketClient.mailbox(email);
const message = await inbucketClient.message(email, inbox[0].id);
const user = await cognitoClient.send(new GetUserCommand({ AccessToken: token ?? '' }));

expect(
user.UserAttributes?.some((attr) => attr.Name === 'email_verified' && attr.Value === 'false'),
).toBeTruthy();

const message = await fetchMailBodyAndTrash(email);
const res = await noCookieClient.post({
headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.ConfirmSignUp' },
body: {
ClientId: DEFAULT_USER_POOL_CLIENT_ID,
ConfirmationCode: message.body.text.trim().split(' ').at(-1) ?? '',
ConfirmationCode: message.split(' ').at(-1) ?? '',
Username,
},
});

await inbucketClient.deleteMessage(email, inbox[0].id);

expect(res.status).toBe(200);
});
15 changes: 15 additions & 0 deletions server/tests/api/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
import assert from 'assert';
import { InbucketAPIClient } from 'inbucket-js-client';

export const GET = (api: { $path: () => string }): string => `GET: ${api.$path()}`;
export const POST = (api: { $path: () => string }): string => `POST: ${api.$path()}`;
export const PATCH = (api: { $path: () => string }): string => `PATCH: ${api.$path()}`;
export const DELETE = (api: { $path: () => string }): string => `DELETE: ${api.$path()}`;

assert(process.env.INBUCKET_URL);

export const inbucketClient = new InbucketAPIClient(process.env.INBUCKET_URL);

export const fetchMailBodyAndTrash = async (email: string): Promise<string> => {
const mailbox = await inbucketClient.mailbox(email);
const message = await inbucketClient.message(email, mailbox[0].id);
await inbucketClient.deleteMessage(email, mailbox[0].id);

return message.body.text.trim();
};
55 changes: 51 additions & 4 deletions server/tests/sdk/admin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,27 @@ import {
import { cognitoClient } from 'service/cognito';
import { DEFAULT_USER_POOL_CLIENT_ID, DEFAULT_USER_POOL_ID } from 'service/envValues';
import { createUserClient, testPassword, testUserName } from 'tests/api/apiClient';
import { fetchMailBodyAndTrash, inbucketClient } from 'tests/api/utils';
import { ulid } from 'ulid';
import { expect, test } from 'vitest';

test('AdminCreateUserCommand', async () => {
const tmpPass = `TmpPass-${Date.now()}`;
test('AdminCreateUserCommand - specify TemporaryPassword', async () => {
const email = `${ulid()}@example.com`;

await cognitoClient.send(
new AdminCreateUserCommand({
UserPoolId: DEFAULT_USER_POOL_ID,
Username: testUserName,
TemporaryPassword: tmpPass,
UserAttributes: [{ Name: 'email', Value: `${ulid()}@example.com` }],
TemporaryPassword: `TmpPass-${Date.now()}`,
MessageAction: 'SUPPRESS',
UserAttributes: [{ Name: 'email', Value: email }],
}),
);

const mailbox = await inbucketClient.mailbox(email);

expect(mailbox).toHaveLength(0);

const res1 = await cognitoClient.send(
new AdminGetUserCommand({ UserPoolId: DEFAULT_USER_POOL_ID, Username: testUserName }),
);
Expand Down Expand Up @@ -56,6 +62,47 @@ test('AdminCreateUserCommand', async () => {
expect(tokens.AuthenticationResult).toBeTruthy();
});

test('AdminCreateUserCommand - unset TemporaryPassword', async () => {
const email = `${ulid()}@example.com`;

await cognitoClient.send(
new AdminCreateUserCommand({
UserPoolId: DEFAULT_USER_POOL_ID,
Username: testUserName,
UserAttributes: [{ Name: 'email', Value: email }],
}),
);

const message1 = await fetchMailBodyAndTrash(email);

await cognitoClient.send(
new AdminCreateUserCommand({
UserPoolId: DEFAULT_USER_POOL_ID,
Username: testUserName,
MessageAction: 'RESEND',
UserAttributes: [{ Name: 'email', Value: email }],
}),
);

const message2 = await fetchMailBodyAndTrash(email);

expect(message1).toBe(message2);

await cognitoClient.send(
new AdminSetUserPasswordCommand({
UserPoolId: DEFAULT_USER_POOL_ID,
Username: testUserName,
Password: testPassword,
}),
);

const res = await cognitoClient.send(
new AdminGetUserCommand({ UserPoolId: DEFAULT_USER_POOL_ID, Username: testUserName }),
);

expect(res.UserStatus).toBe(UserStatusType.CONFIRMED);
});

test('AdminDeleteUserCommand', async () => {
const userClient = await createUserClient();

Expand Down

0 comments on commit 9b58dd7

Please sign in to comment.