From 8d1a9ae3c945fd2104cdef9754a5bbbc6d2c82f7 Mon Sep 17 00:00:00 2001 From: solufa Date: Thu, 20 Jun 2024 21:29:57 +0900 Subject: [PATCH] feat: implement resendConfirmationCode --- server/api/@types/auth.ts | 22 +++++++----- server/api/controller.ts | 4 +++ .../user/service/genCodeDeliveryDetails.ts | 8 +++++ .../user/service/sendConfirmationCode.ts | 9 +++++ server/domain/user/useCase/authUseCase.ts | 34 +++++++++++-------- server/tests/api/signUp.test.ts | 5 +++ 6 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 server/domain/user/service/genCodeDeliveryDetails.ts create mode 100644 server/domain/user/service/sendConfirmationCode.ts diff --git a/server/api/@types/auth.ts b/server/api/@types/auth.ts index 2940908..79bc5e3 100644 --- a/server/api/@types/auth.ts +++ b/server/api/@types/auth.ts @@ -4,6 +4,12 @@ export type Jwks = { keys: [{ kid: string; alg: string }] }; type TargetBody = { reqBody: Req; resBody: Res }; +export type CodeDeliveryDetails = { + AttributeName: 'email'; + DeliveryMedium: 'EMAIL'; + Destination: string; +}; + export type SignUpTarget = TargetBody< { Username: string; @@ -11,15 +17,7 @@ export type SignUpTarget = TargetBody< UserAttributes: [{ Name: 'email'; Value: string }]; ClientId: MaybeId['userPoolClient']; }, - { - CodeDeliveryDetails: { - AttributeName: 'email'; - DeliveryMedium: 'EMAIL'; - Destination: string; - }; - UserConfirmed: boolean; - UserSub: EntityId['user']; - } + { CodeDeliveryDetails: CodeDeliveryDetails; UserConfirmed: boolean; UserSub: EntityId['user'] } >; export type ConfirmSignUpTarget = TargetBody< @@ -106,6 +104,11 @@ export type RevokeTokenTarget = TargetBody< Record >; +export type ResendConfirmationCodeTarget = TargetBody< + { ClientId: MaybeId['userPoolClient']; Username: string }, + { CodeDeliveryDetails: CodeDeliveryDetails } +>; + export type AmzTargets = { 'AWSCognitoIdentityProviderService.SignUp': SignUpTarget; 'AWSCognitoIdentityProviderService.ConfirmSignUp': ConfirmSignUpTarget; @@ -113,4 +116,5 @@ export type AmzTargets = { 'AWSCognitoIdentityProviderService.RespondToAuthChallenge': RespondToAuthChallengeTarget; 'AWSCognitoIdentityProviderService.GetUser': GetUserTarget; 'AWSCognitoIdentityProviderService.RevokeToken': RevokeTokenTarget; + 'AWSCognitoIdentityProviderService.ResendConfirmationCode': ResendConfirmationCodeTarget; }; diff --git a/server/api/controller.ts b/server/api/controller.ts index 694545e..3ea2f4c 100644 --- a/server/api/controller.ts +++ b/server/api/controller.ts @@ -68,6 +68,10 @@ const targets: { validator: z.object({ ClientId: brandedId.userPoolClient.maybe, Token: z.string() }), useCase: authUseCase.revokeToken, }, + 'AWSCognitoIdentityProviderService.ResendConfirmationCode': { + validator: z.object({ ClientId: brandedId.userPoolClient.maybe, Username: z.string() }), + useCase: authUseCase.resendConfirmationCode, + }, }; const main = async (target: T, body: AmzTargets[T]['reqBody']) => diff --git a/server/domain/user/service/genCodeDeliveryDetails.ts b/server/domain/user/service/genCodeDeliveryDetails.ts new file mode 100644 index 0000000..b664004 --- /dev/null +++ b/server/domain/user/service/genCodeDeliveryDetails.ts @@ -0,0 +1,8 @@ +import type { CodeDeliveryDetails } from 'api/@types/auth'; +import type { UserEntity } from 'api/@types/user'; + +export const genCodeDeliveryDetails = (user: UserEntity): CodeDeliveryDetails => ({ + AttributeName: 'email', + DeliveryMedium: 'EMAIL', + Destination: user.email.replace(/^(.).*@(.).+$/, '$1***@$2***'), +}); diff --git a/server/domain/user/service/sendConfirmationCode.ts b/server/domain/user/service/sendConfirmationCode.ts new file mode 100644 index 0000000..9a88b54 --- /dev/null +++ b/server/domain/user/service/sendConfirmationCode.ts @@ -0,0 +1,9 @@ +import type { UserEntity } from 'api/@types/user'; +import { sendMail } from 'service/sendMail'; + +export const sendConfirmationCode = (user: UserEntity): Promise => + sendMail({ + to: { name: user.name, address: user.email }, + subject: 'Your verification code', + text: `Your confirmation code is ${user.confirmationCode}`, + }); diff --git a/server/domain/user/useCase/authUseCase.ts b/server/domain/user/useCase/authUseCase.ts index 0646c66..f837360 100644 --- a/server/domain/user/useCase/authUseCase.ts +++ b/server/domain/user/useCase/authUseCase.ts @@ -2,6 +2,7 @@ import type { ConfirmSignUpTarget, GetUserTarget, RefreshTokenAuthTarget, + ResendConfirmationCodeTarget, RespondToAuthChallengeTarget, RevokeTokenTarget, SignUpTarget, @@ -17,8 +18,9 @@ import { userPoolQuery } from 'domain/userPool/repository/userPoolQuery'; import { jwtDecode } from 'jwt-decode'; import { cognitoAssert } from 'service/cognitoAssert'; import { transaction } from 'service/prismaClient'; -import { sendMail } from 'service/sendMail'; import type { AccessTokenJwt } from 'service/types'; +import { genCodeDeliveryDetails } from '../service/genCodeDeliveryDetails'; +import { sendConfirmationCode } from '../service/sendConfirmationCode'; export const authUseCase = { signUp: (req: SignUpTarget['reqBody']): Promise => @@ -39,18 +41,10 @@ export const authUseCase = { userPoolId: poolClient.userPoolId, }); await userCommand.save(tx, user); - await sendMail({ - to: { name: user.name, address: user.email }, - subject: 'Your verification code', - text: `Your confirmation code is ${user.confirmationCode}`, - }); + await sendConfirmationCode(user); return { - CodeDeliveryDetails: { - AttributeName: 'email', - DeliveryMedium: 'EMAIL', - Destination: req.UserAttributes[0].Value.replace(/^(.).*@(.).+$/, '$1***@$2***'), - }, + CodeDeliveryDetails: genCodeDeliveryDetails(user), UserConfirmed: false, UserSub: user.id, }; @@ -76,10 +70,7 @@ export const authUseCase = { await userCommand.save(tx, userWithChallenge); - return { - ChallengeName: 'PASSWORD_VERIFIER', - ChallengeParameters, - }; + return { ChallengeName: 'PASSWORD_VERIFIER', ChallengeParameters }; }), refreshTokenAuth: ( req: RefreshTokenAuthTarget['reqBody'], @@ -159,4 +150,17 @@ export const authUseCase = { return {}; }), + resendConfirmationCode: ( + req: ResendConfirmationCodeTarget['reqBody'], + ): Promise => + transaction(async (tx) => { + const poolClient = await userPoolQuery.findClientById(tx, req.ClientId); + const user = await userQuery.findByName(tx, req.Username); + + assert(poolClient.userPoolId === user.userPoolId); + + await sendConfirmationCode(user); + + return { CodeDeliveryDetails: genCodeDeliveryDetails(user) }; + }), }; diff --git a/server/tests/api/signUp.test.ts b/server/tests/api/signUp.test.ts index 0a9351f..6dd849c 100644 --- a/server/tests/api/signUp.test.ts +++ b/server/tests/api/signUp.test.ts @@ -18,6 +18,11 @@ test('signUp', async () => { }, }); + await noCookieClient.post({ + headers: { 'x-amz-target': 'AWSCognitoIdentityProviderService.ResendConfirmationCode' }, + body: { ClientId: DEFAULT_USER_POOL_CLIENT_ID, Username: 'user' }, + }); + assert(process.env.INBUCKET_URL); const inbucketClient = new InbucketAPIClient(process.env.INBUCKET_URL);