From 9b58dd77db1819ac0dead87da94e85827881184e Mon Sep 17 00:00:00 2001 From: solufa Date: Mon, 15 Jul 2024 01:05:36 +0900 Subject: [PATCH] feat: support MessageAction --- ...endConfirmationCode.ts => sendAuthMail.ts} | 7 +++ server/domain/user/useCase/adminUseCase.ts | 58 ++++++++++++++----- server/domain/user/useCase/authUseCase.ts | 2 +- server/tests/api/apiClient.ts | 36 ++++++------ server/tests/api/signUp.test.ts | 38 ++++++++---- server/tests/api/utils.ts | 15 +++++ server/tests/sdk/admin.test.ts | 55 ++++++++++++++++-- 7 files changed, 160 insertions(+), 51 deletions(-) rename server/domain/user/service/{sendConfirmationCode.ts => sendAuthMail.ts} (57%) diff --git a/server/domain/user/service/sendConfirmationCode.ts b/server/domain/user/service/sendAuthMail.ts similarity index 57% rename from server/domain/user/service/sendConfirmationCode.ts rename to server/domain/user/service/sendAuthMail.ts index 6880523..0665a99 100644 --- a/server/domain/user/service/sendConfirmationCode.ts +++ b/server/domain/user/service/sendAuthMail.ts @@ -7,3 +7,10 @@ export const sendConfirmationCode = (user: UserEntity): Promise => subject: 'Your verification code', text: `Your confirmation code is ${user.confirmationCode}`, }); + +export const sendTemporaryPassword = (user: UserEntity, password: string): Promise => + sendMail({ + to: { name: user.name, address: user.email }, + subject: 'Your temporary password', + text: `Your temporary password is ${password}`, + }); diff --git a/server/domain/user/useCase/adminUseCase.ts b/server/domain/user/useCase/adminUseCase.ts index 78e231f..db2d8a2 100644 --- a/server/domain/user/useCase/adminUseCase.ts +++ b/server/domain/user/useCase/adminUseCase.ts @@ -1,3 +1,4 @@ +import type { Prisma } from '@prisma/client'; import assert from 'assert'; import type { AdminCreateUserTarget, @@ -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'; @@ -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 => { + 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 => { + 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 => { @@ -29,22 +70,7 @@ export const adminUseCase = { }, createUser: (req: AdminCreateUserTarget['reqBody']): Promise => 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: { diff --git a/server/domain/user/useCase/authUseCase.ts b/server/domain/user/useCase/authUseCase.ts index 35d1ec2..05e896e 100644 --- a/server/domain/user/useCase/authUseCase.ts +++ b/server/domain/user/useCase/authUseCase.ts @@ -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 => diff --git a/server/tests/api/apiClient.ts b/server/tests/api/apiClient.ts index e390be8..75d9ffd 100644 --- a/server/tests/api/apiClient.ts +++ b/server/tests/api/apiClient.ts @@ -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'; @@ -22,29 +21,28 @@ export const testUserName = 'test-user'; export const testPassword = 'Test-user-password1'; export const createUserClient = async (): Promise => { - 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', }, }); diff --git a/server/tests/api/signUp.test.ts b/server/tests/api/signUp.test.ts index 842b176..b505888 100644 --- a/server/tests/api/signUp.test.ts +++ b/server/tests/api/signUp.test.ts @@ -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, }, @@ -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); }); diff --git a/server/tests/api/utils.ts b/server/tests/api/utils.ts index 41c52eb..db851ea 100644 --- a/server/tests/api/utils.ts +++ b/server/tests/api/utils.ts @@ -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 => { + 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(); +}; diff --git a/server/tests/sdk/admin.test.ts b/server/tests/sdk/admin.test.ts index 4c93eac..644d29a 100644 --- a/server/tests/sdk/admin.test.ts +++ b/server/tests/sdk/admin.test.ts @@ -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 }), ); @@ -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();