diff --git a/CHANGELOG.md b/CHANGELOG.md index 5069ff44b..3af26b806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,239 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [16.0.0] - 2023-08-XX + +### Overview + +#### Introducing account-linking + +With this release, we are introducing a new AccountLinking recipe, this will let you: + +- link accounts automatically, +- implement manual account linking flows. + +Check our [guide](https://supertokens.com/docs/thirdpartyemailpassword/common-customizations/account-linking/overview) for more information. + +To use this you'll need compatible versions: + +- Core>=7.0.0 +- supertokens-node>=16.0.0 (support is pending in other backend SDKs) +- supertokens-website>=17.0.3 +- supertokens-web-js>=0.8.0 +- supertokens-auth-react>=0.35.0 + +#### The new User object and primary vs non-primary users + +In this release, we've removed the recipes specific user types and instead introduced a new `User` class to support the "Primary user" concept introduced by account linking + +- The new `User` class now provides the same interface for all recipes. +- It contains an `isPrimary` field that you can use to differentiate between primary and recipe users +- The `loginMethods` array contains objects that covers all props of the old (recipe specific) user types, with the exception of the id. Please check the migration section below to get the exact mapping between old and new props. +- Non-primary users: + - The `loginMethods` array should contain exactly 1 element. + - `user.id` will be the same as `user.loginMethods[0].recipeUserId.getAsString()`. + - `user.id` will change if it is linked to another user. + - They can become a primary user if, and only if there are no other primary users with the same email, third party info or phone number as this user across all the tenants that this user is a part of. +- Primary users + - The `loginMethods` array can have 1 or more elements, each corresponding to a single recipe user. + - `user.id` will not change even if other users are linked to it. + - Other non-primary users can be linked to it. The user ID of the linked accounts will now be the primary users ID. +- Check [here](https://supertokens.com/docs/thirdpartyemailpassword/common-customizations/account-linking/overview#primary-user-vs-non-primary-user) for more information about differences between primary and recipe users. + +#### Primary vs RecipeUserId + +Because of account linking we've introduced a new Primary user concept (see above). In most cases, you should only use the primary user id (`user.id` or `session.getUserId()`) if you are associating data to users. Still, in some cases you need to specifically refer to a login method, which is covered by the new `RecipeUserId` class: + +- You can get it: + - From a session by: `session.getRecipeUserId()`. + - By finding the appropriate entry in the `loginMethods` array of a `User` object (see above): `user.loginMethods[0].recipeUserId`. +- It wraps a simple string value that you can get by calling `recipeUserId.getAsString()`. +- We've introduced it to differentiate between primary and recipe user ids in our APIs on a type level. +- Check [here](https://supertokens.com/docs/thirdpartyemailpassword/user-object#primary-vs-recipe-user-id) for more information. + +### Breaking changes + +- Now only supporting CDI 4.0. Compatible with core version >= 7.0 +- Now supporting FDI 1.18 +- Removed the recipe specific `User` type, now all functions are using the new generic `User` type. + - Check [here](https://supertokens.com/docs/thirdpartyemailpassword/user-object) for more information. +- The `build` function and the `fetchValue` callback of session claims now take a new `recipeUserId` param. + - This affects built-in claims: `EmailVerificationClaim`, `UserRoleClaim`, `PermissionClaim`, `AllowedDomainsClaim`. + - This will affect all custom claims as well built on our base classes. +- Now ignoring protected props in the payload in `createNewSession` and `createNewSessionWithoutRequestResponse` +- `createdNewUser` has been renamed to `createdNewRecipeUser` in sign up related APIs and functions + +- EmailPassword: + - removed `getUserById`, `getUserByEmail`. You should use `supertokens.getUser`, and `supertokens. listUsersByAccountInfo` instead + - added `consumePasswordResetToken`. This function allows the consumption of the reset password token without changing the password. It will return OK if the token was valid. + - added an overrideable `createNewRecipeUser` function that is called during sign up and password reset flow (in case a new email password user is being created on the fly). This is mostly for internal use. + - `recipeUserId` is added to the input of `getContent` of the email delivery config + - `email` was added to the input of `createResetPasswordToken` , `sendResetPasswordEmail`, `createResetPasswordLink` + - `updateEmailOrPassword` : + - now takes `recipeUserId` instead of `userId` + - can return the new `EMAIL_CHANGE_NOT_ALLOWED_ERROR` status + - `signIn`: + - returns new `recipeUserId` prop in the `status: OK` case + - `signUp`: + - returns new `recipeUserId` prop in the `status: OK` case + - `signInPOST`: + - can return status `SIGN_IN_NOT_ALLOWED` + - `signUpPOST`: + - can return status `SIGN_UP_NOT_ALLOWED` + - `generatePasswordResetTokenPOST`: + - can now return `PASSWORD_RESET_NOT_ALLOWED` + - `passwordResetPOST`: + - now returns the `user` and the `email` whose password was reset + - can now return `PASSWORD_POLICY_VIOLATED_ERROR` +- EmailVerification: + - `createEmailVerificationToken`, `createEmailVerificationLink`, `isEmailVerified`, `revokeEmailVerificationTokens` , `unverifyEmail`: + - now takes `recipeUserId` instead of `userId` + - `sendEmailVerificationEmail` : + - now takes an additional `recipeUserId` parameter + - `verifyEmailUsingToken`: + - now takes a new `attemptAccountLinking` parameter + - returns the `recipeUserId` instead of `id` + - `sendEmail` now requires a new `recipeUserId` as part of the user info + - `getEmailForUserId` config option was renamed to `getEmailForRecipeUserId` + - `verifyEmailPOST`, `generateEmailVerifyTokenPOST`: returns an optional `newSession` in case the current user session needs to be updated +- Passwordless: + - removed `getUserById`, `getUserByEmail`, `getUserByPhoneNumber` + - `updateUser` : + - now takes `recipeUserId` instead of `userId` + - can return `"EMAIL_CHANGE_NOT_ALLOWED_ERROR` and `PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR` statuses + - `createCodePOST` and `consumeCodePOST` can now return `SIGN_IN_UP_NOT_ALLOWED` +- Session: + - access tokens and session objects now contain the recipe user id + - Support for new access token version + - `recipeUserId` is now added to the payload of the `TOKEN_THEFT_DETECTED` error + - `createNewSession`: now takes `recipeUserId` instead of `userId` + - Removed `validateClaimsInJWTPayload` + - `revokeAllSessionsForUser` now takes an optional `revokeSessionsForLinkedAccounts` param + - `getAllSessionHandlesForUser` now takes an optional `fetchSessionsForAllLinkedAccounts` param + - `regenerateAccessToken` return value now includes `recipeUserId` + - `getGlobalClaimValidators` and `validateClaims` now get a new `recipeUserId` param + - Added `getRecipeUserId` to the session class +- ThirdParty: + - The `signInUp` override: + - gets a new `isVerified` param + - can return new status: `SIGN_IN_UP_NOT_ALLOWED` + - `manuallyCreateOrUpdateUser`: + - gets a new `isVerified` param + - can return new statuses: `EMAIL_CHANGE_NOT_ALLOWED_ERROR`, `SIGN_IN_UP_NOT_ALLOWED` + - Removed `getUserByThirdPartyInfo`, `getUsersByEmail`, `getUserById` + - `signInUpPOST` can now return `SIGN_IN_UP_NOT_ALLOWED` +- ThirdPartyEmailPassword: + - Removed `getUserByThirdPartyInfo`, `getUsersByEmail`, `getUserById` + - `thirdPartyManuallyCreateOrUpdateUser`: + - now get a new `isVerified` param + - can return new statuses: `EMAIL_CHANGE_NOT_ALLOWED_ERROR`, `SIGN_IN_UP_NOT_ALLOWED` + - The `thirdPartySignInUp` override: + - now get a new `isVerified` param + - can return new status: `SIGN_IN_UP_NOT_ALLOWED` + - `email` was added to the input of `createResetPasswordToken` , `sendResetPasswordEmail`, `createResetPasswordLink` + - added an overrideable `createNewEmailPasswordRecipeUser` function that is called during email password sign up and in the “invitation link” flow + - added `consumePasswordResetToken` + - `updateEmailOrPassword` : + - now takes `recipeUserId` instead of `userId` + - can return the new `EMAIL_CHANGE_NOT_ALLOWED_ERROR` status + - added an overrideable `createNewEmailPasswordRecipeUser` function that is called during sign up and in the “invitation link” flow + - `emailPasswordSignIn`: + - returns new `recipeUserId` prop in the `status: OK` case + - `emailPasswordSignUp`: + - returns new `recipeUserId` prop in the `status: OK` case + - `emailPasswordSignInPOST`: + - can return status `SIGN_IN_NOT_ALLOWED` + - `emailPasswordSignUpPOST`: + - can return status `SIGN_UP_NOT_ALLOWED` + - `generatePasswordResetTokenPOST`: + - can now return `PASSWORD_RESET_NOT_ALLOWED` + - `passwordResetPOST`: + - now returns the `user` and the `email` whose password was reset + - can now return `PASSWORD_POLICY_VIOLATED_ERROR` + - `thirdPartySignInUpPOST` can now return `SIGN_IN_UP_NOT_ALLOWED` +- ThirdPartyPasswordless: + - Removed `getUserByThirdPartyInfo`, `getUsersByEmail`, `getUserByPhoneNumber`, `getUserById` + - `thirdPartyManuallyCreateOrUpdateUser`: + - gets a new `isVerified` param + - can return new statuses: `EMAIL_CHANGE_NOT_ALLOWED_ERROR`, `SIGN_IN_UP_NOT_ALLOWED` + - The `thirdPartySignInUp` override: + - gets a new `isVerified` param + - can return new status: `SIGN_IN_UP_NOT_ALLOWED` + - `updatePasswordlessUser`: + - now takes `recipeUserId` instead of `userId` + - can return `"EMAIL_CHANGE_NOT_ALLOWED_ERROR` and `PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR` statuses + - `thirdPartySignInUpPOST` can now return `SIGN_IN_UP_NOT_ALLOWED` + - `createCodePOST` and `consumeCodePOST` can now return `SIGN_IN_UP_NOT_ALLOWED` +- Multitenancy: + - `associateUserToTenant` can now return `ASSOCIATION_NOT_ALLOWED_ERROR` + - `associateUserToTenant` and `disassociateUserFromTenant` now take `RecipeUserId` instead of a string user id + +### Changes + +- Added `RecipeUserId` and a generic `User` class +- Added `getUser`, `listUsersByAccountInfo`, `convertToRecipeUserId` to the main exports +- Updated compilation target of typescript to ES2017 to make debugging easier. +- Added account-linking recipe + +### Migration guide + +#### New User structure + +We've added a generic `User` class instead of the old recipe specific ones. The mapping of old props to new in case you are not using account-linking: + +- `user.id` stays `user.id` (or `user.loginMethods[0].recipeUserId` in case you need `RecipeUserId`) +- `user.email` becomes `user.emails[0]` +- `user.phoneNumber` becomes `user.phoneNumbers[0]` +- `user.thirdParty` becomes `user.thirdParty[0]` +- `user.timeJoined` is still `user.timeJoined` +- `user.tenantIds` is still `user.tenantIds` + +#### RecipeUserId + +Some functions now require you to pass a `RecipeUserId` instead of a string user id. If you are using our auth recipes, you can find the recipeUserId as: `user.loginMethods[0].recipeUserId` (you'll need to worry about selecting the right login method after enabling account linking). Alternatively, if you already have a string user id you can convert it to a `RecipeUserId` using `supertokens.convertToRecipeUserId(userIdString)` + +#### Checking if a user signed up or signed in + +- In the passwordless consumeCode / social login signinup APIs, you can check if a user signed up by: + +``` + // Here res refers to the result the function/api functions mentioned above. + const isNewUser = res.createdNewRecipeUser && res.user.loginMethods.length === 1; +``` + +- In the emailpassword sign up API, you can check if a user signed up by: + +``` + const isNewUser = res.user.loginMethods.length === 1; +``` + +#### Changing user emails + +- We recommend that you check if the email change of a user is allowed, before calling the update function + - Check [here](https://supertokens.com/docs/thirdpartyemailpassword/common-customizations/change-email-post-login) for more information + +``` +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking"; +/// ... +app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { + let session = req.session!; + let email = req.body.email; + + // ... + if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, false))) { + // this can come here if you have enabled the account linking feature, and + // if there is a security risk in changing this user's email. + } + + // Update the email + let resp = await ThirdPartyEmailPassword.updateEmailOrPassword({ + recipeUserId: session.getRecipeUserId(), + email: email, + }); + // ... +}); +``` + ## [15.2.0] - 2023-09-11 ### Added diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index 20f2d5c45..586042019 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -1,4 +1,4 @@ { "_comment": "contains a list of core-driver interfaces branch names that this core supports", - "versions": ["3.0"] + "versions": ["4.0"] } diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e2ac6616a..000000000 --- a/docs/.nojekyll +++ /dev/null @@ -1 +0,0 @@ -TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/docs/assets/icons.css b/docs/assets/icons.css deleted file mode 100644 index 776a3562d..000000000 --- a/docs/assets/icons.css +++ /dev/null @@ -1,1043 +0,0 @@ -.tsd-kind-icon { - display: block; - position: relative; - padding-left: 20px; - text-indent: -20px; -} -.tsd-kind-icon:before { - content: ""; - display: inline-block; - vertical-align: middle; - width: 17px; - height: 17px; - margin: 0 3px 2px 0; - background-image: url(./icons.png); -} -@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { - .tsd-kind-icon:before { - background-image: url(./icons@2x.png); - background-size: 238px 204px; - } -} - -.tsd-signature.tsd-kind-icon:before { - background-position: 0 -153px; -} - -.tsd-kind-object-literal > .tsd-kind-icon:before { - background-position: 0px -17px; -} -.tsd-kind-object-literal.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -17px; -} -.tsd-kind-object-literal.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -17px; -} - -.tsd-kind-class > .tsd-kind-icon:before { - background-position: 0px -34px; -} -.tsd-kind-class.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -34px; -} -.tsd-kind-class.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -34px; -} - -.tsd-kind-class.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: 0px -51px; -} -.tsd-kind-class.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -17px -51px; -} -.tsd-kind-class.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -51px; -} - -.tsd-kind-interface > .tsd-kind-icon:before { - background-position: 0px -68px; -} -.tsd-kind-interface.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -68px; -} -.tsd-kind-interface.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -68px; -} - -.tsd-kind-interface.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: 0px -85px; -} -.tsd-kind-interface.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -17px -85px; -} -.tsd-kind-interface.tsd-has-type-parameter.tsd-is-private - > .tsd-kind-icon:before { - background-position: -34px -85px; -} - -.tsd-kind-namespace > .tsd-kind-icon:before { - background-position: 0px -102px; -} -.tsd-kind-namespace.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -102px; -} -.tsd-kind-namespace.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -102px; -} - -.tsd-kind-module > .tsd-kind-icon:before { - background-position: 0px -102px; -} -.tsd-kind-module.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -102px; -} -.tsd-kind-module.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -102px; -} - -.tsd-kind-enum > .tsd-kind-icon:before { - background-position: 0px -119px; -} -.tsd-kind-enum.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -119px; -} -.tsd-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -119px; -} - -.tsd-kind-enum-member > .tsd-kind-icon:before { - background-position: 0px -136px; -} -.tsd-kind-enum-member.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -136px; -} -.tsd-kind-enum-member.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -136px; -} - -.tsd-kind-signature > .tsd-kind-icon:before { - background-position: 0px -153px; -} -.tsd-kind-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -153px; -} -.tsd-kind-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -153px; -} - -.tsd-kind-type-alias > .tsd-kind-icon:before { - background-position: 0px -170px; -} -.tsd-kind-type-alias.tsd-is-protected > .tsd-kind-icon:before { - background-position: -17px -170px; -} -.tsd-kind-type-alias.tsd-is-private > .tsd-kind-icon:before { - background-position: -34px -170px; -} - -.tsd-kind-type-alias.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: 0px -187px; -} -.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -17px -187px; -} -.tsd-kind-type-alias.tsd-has-type-parameter.tsd-is-private - > .tsd-kind-icon:before { - background-position: -34px -187px; -} - -.tsd-kind-variable > .tsd-kind-icon:before { - background-position: -136px -0px; -} -.tsd-kind-variable.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -0px; -} -.tsd-kind-variable.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -0px; -} -.tsd-kind-variable.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-variable.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -0px; -} -.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -0px; -} -.tsd-kind-variable.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-variable.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -0px; -} -.tsd-kind-variable.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -0px; -} - -.tsd-kind-property > .tsd-kind-icon:before { - background-position: -136px -0px; -} -.tsd-kind-property.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -0px; -} -.tsd-kind-property.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-property.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -0px; -} -.tsd-kind-property.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -0px; -} -.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -0px; -} -.tsd-kind-property.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -0px; -} -.tsd-kind-property.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-property.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -0px; -} -.tsd-kind-property.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -0px; -} -.tsd-kind-property.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -0px; -} -.tsd-kind-property.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -0px; -} -.tsd-kind-property.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -0px; -} - -.tsd-kind-get-signature > .tsd-kind-icon:before { - background-position: -136px -17px; -} -.tsd-kind-get-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -17px; -} -.tsd-kind-get-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -17px; -} -.tsd-kind-get-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -17px; -} - -.tsd-kind-set-signature > .tsd-kind-icon:before { - background-position: -136px -34px; -} -.tsd-kind-set-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -34px; -} -.tsd-kind-set-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -34px; -} -.tsd-kind-set-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -34px; -} - -.tsd-kind-accessor > .tsd-kind-icon:before { - background-position: -136px -51px; -} -.tsd-kind-accessor.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -51px; -} -.tsd-kind-accessor.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -51px; -} -.tsd-kind-accessor.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -51px; -} - -.tsd-kind-function > .tsd-kind-icon:before { - background-position: -136px -68px; -} -.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -68px; -} -.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -68px; -} -.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -68px; -} -.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -68px; -} -.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -68px; -} -.tsd-kind-function.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -68px; -} -.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -68px; -} -.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-function.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -68px; -} -.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -68px; -} - -.tsd-kind-method > .tsd-kind-icon:before { - background-position: -136px -68px; -} -.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -68px; -} -.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -68px; -} -.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -68px; -} -.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -68px; -} -.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -68px; -} -.tsd-kind-method.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -68px; -} -.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { - background-position: -187px -68px; -} -.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-method.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -68px; -} -.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -68px; -} - -.tsd-kind-call-signature > .tsd-kind-icon:before { - background-position: -136px -68px; -} -.tsd-kind-call-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -68px; -} -.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -68px; -} -.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -68px; -} - -.tsd-kind-function.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: -136px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -153px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class - > .tsd-kind-icon:before { - background-position: -51px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum - > .tsd-kind-icon:before { - background-position: -170px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -85px; -} -.tsd-kind-function.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -85px; -} - -.tsd-kind-method.tsd-has-type-parameter > .tsd-kind-icon:before { - background-position: -136px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -153px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class - > .tsd-kind-icon:before { - background-position: -51px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum - > .tsd-kind-icon:before { - background-position: -170px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -85px; -} -.tsd-kind-method.tsd-has-type-parameter.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -85px; -} - -.tsd-kind-constructor > .tsd-kind-icon:before { - background-position: -136px -102px; -} -.tsd-kind-constructor.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -102px; -} -.tsd-kind-constructor.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -102px; -} -.tsd-kind-constructor.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -102px; -} - -.tsd-kind-constructor-signature > .tsd-kind-icon:before { - background-position: -136px -102px; -} -.tsd-kind-constructor-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -102px; -} -.tsd-kind-constructor-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -102px; -} -.tsd-kind-constructor-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -102px; -} - -.tsd-kind-index-signature > .tsd-kind-icon:before { - background-position: -136px -119px; -} -.tsd-kind-index-signature.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -119px; -} -.tsd-kind-index-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -119px; -} -.tsd-kind-index-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -119px; -} - -.tsd-kind-event > .tsd-kind-icon:before { - background-position: -136px -136px; -} -.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -136px; -} -.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -136px; -} -.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -136px; -} -.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { - background-position: -68px -136px; -} -.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { - background-position: -85px -136px; -} -.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -136px; -} -.tsd-kind-event.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -136px; -} -.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -136px; -} -.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { - background-position: -187px -136px; -} -.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -136px; -} -.tsd-kind-event.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -136px; -} -.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -136px; -} - -.tsd-is-static > .tsd-kind-icon:before { - background-position: -136px -153px; -} -.tsd-is-static.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -153px; -} -.tsd-is-static.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -153px; -} -.tsd-is-static.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -153px; -} -.tsd-is-static.tsd-parent-kind-class.tsd-is-inherited > .tsd-kind-icon:before { - background-position: -68px -153px; -} -.tsd-is-static.tsd-parent-kind-class.tsd-is-protected > .tsd-kind-icon:before { - background-position: -85px -153px; -} -.tsd-is-static.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -153px; -} -.tsd-is-static.tsd-parent-kind-class.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -153px; -} -.tsd-is-static.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -153px; -} -.tsd-is-static.tsd-parent-kind-enum.tsd-is-protected > .tsd-kind-icon:before { - background-position: -187px -153px; -} -.tsd-is-static.tsd-parent-kind-enum.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -153px; -} -.tsd-is-static.tsd-parent-kind-interface > .tsd-kind-icon:before { - background-position: -204px -153px; -} -.tsd-is-static.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -153px; -} - -.tsd-is-static.tsd-kind-function > .tsd-kind-icon:before { - background-position: -136px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -170px; -} -.tsd-is-static.tsd-kind-function.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -170px; -} - -.tsd-is-static.tsd-kind-method > .tsd-kind-icon:before { - background-position: -136px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -170px; -} -.tsd-is-static.tsd-kind-method.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -170px; -} - -.tsd-is-static.tsd-kind-call-signature > .tsd-kind-icon:before { - background-position: -136px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -153px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class - > .tsd-kind-icon:before { - background-position: -51px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum - > .tsd-kind-icon:before { - background-position: -170px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -170px; -} -.tsd-is-static.tsd-kind-call-signature.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -170px; -} - -.tsd-is-static.tsd-kind-event > .tsd-kind-icon:before { - background-position: -136px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-is-protected > .tsd-kind-icon:before { - background-position: -153px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-is-private > .tsd-kind-icon:before { - background-position: -119px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class > .tsd-kind-icon:before { - background-position: -51px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -68px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -85px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-protected.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -102px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-class.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum > .tsd-kind-icon:before { - background-position: -170px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-protected - > .tsd-kind-icon:before { - background-position: -187px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-enum.tsd-is-private - > .tsd-kind-icon:before { - background-position: -119px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface - > .tsd-kind-icon:before { - background-position: -204px -187px; -} -.tsd-is-static.tsd-kind-event.tsd-parent-kind-interface.tsd-is-inherited - > .tsd-kind-icon:before { - background-position: -221px -187px; -} diff --git a/docs/assets/icons.png b/docs/assets/icons.png deleted file mode 100644 index 3836d5fe4..000000000 Binary files a/docs/assets/icons.png and /dev/null differ diff --git a/docs/assets/icons@2x.png b/docs/assets/icons@2x.png deleted file mode 100644 index 5a209e2f6..000000000 Binary files a/docs/assets/icons@2x.png and /dev/null differ diff --git a/docs/assets/widgets.png b/docs/assets/widgets.png deleted file mode 100644 index c7380532a..000000000 Binary files a/docs/assets/widgets.png and /dev/null differ diff --git a/docs/assets/widgets@2x.png b/docs/assets/widgets@2x.png deleted file mode 100644 index 4bbbd5727..000000000 Binary files a/docs/assets/widgets@2x.png and /dev/null differ diff --git a/frontendDriverInterfaceSupported.json b/frontendDriverInterfaceSupported.json index 8c20b2117..18322b449 100644 --- a/frontendDriverInterfaceSupported.json +++ b/frontendDriverInterfaceSupported.json @@ -1,4 +1,4 @@ { "_comment": "contains a list of frontend-driver interfaces branch names that this core supports", - "versions": ["1.17"] + "versions": ["1.17", "1.18"] } diff --git a/lib/build/framework/awsLambda/framework.js b/lib/build/framework/awsLambda/framework.js index 0b2871b80..d9533f1c5 100644 --- a/lib/build/framework/awsLambda/framework.js +++ b/lib/build/framework/awsLambda/framework.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -47,20 +16,19 @@ const querystring_1 = require("querystring"); class AWSRequest extends request_1.BaseRequest { constructor(event) { super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - if (this.parsedUrlEncodedFormData === undefined) { - if (this.event.body === null || this.event.body === undefined) { + this.getFormData = async () => { + if (this.parsedUrlEncodedFormData === undefined) { + if (this.event.body === null || this.event.body === undefined) { + this.parsedUrlEncodedFormData = {}; + } else { + this.parsedUrlEncodedFormData = querystring_1.parse(this.event.body); + if (this.parsedUrlEncodedFormData === undefined) { this.parsedUrlEncodedFormData = {}; - } else { - this.parsedUrlEncodedFormData = querystring_1.parse(this.event.body); - if (this.parsedUrlEncodedFormData === undefined) { - this.parsedUrlEncodedFormData = {}; - } } } - return this.parsedUrlEncodedFormData; - }); + } + return this.parsedUrlEncodedFormData; + }; this.getKeyValueFromQuery = (key) => { if (this.event.queryStringParameters === undefined || this.event.queryStringParameters === null) { return undefined; @@ -71,20 +39,19 @@ class AWSRequest extends request_1.BaseRequest { } return value; }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - if (this.parsedJSONBody === undefined) { - if (this.event.body === null || this.event.body === undefined) { + this.getJSONBody = async () => { + if (this.parsedJSONBody === undefined) { + if (this.event.body === null || this.event.body === undefined) { + this.parsedJSONBody = {}; + } else { + this.parsedJSONBody = JSON.parse(this.event.body); + if (this.parsedJSONBody === undefined) { this.parsedJSONBody = {}; - } else { - this.parsedJSONBody = JSON.parse(this.event.body); - if (this.parsedJSONBody === undefined) { - this.parsedJSONBody = {}; - } } } - return this.parsedJSONBody; - }); + } + return this.parsedJSONBody; + }; this.getMethod = () => { let rawMethod = this.event.httpMethod; if (rawMethod !== undefined) { @@ -273,38 +240,37 @@ class AWSResponse extends response_1.BaseResponse { } exports.AWSResponse = AWSResponse; const middleware = (handler) => { - return (event, context, callback) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new AWSRequest(event); - let response = new AWSResponse(event); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); - try { - let result = yield supertokens.middleware(request, response, userContext); - if (result) { - return response.sendResponse(); - } - if (handler !== undefined) { - let handlerResult = yield handler(event, context, callback); - return response.sendResponse(handlerResult); - } - /** - * it reaches this point only if the API route was not exposed by - * the SDK and user didn't provide a handler - */ - response.setStatusCode(404); - response.sendJSONResponse({ - error: `The middleware couldn't serve the API path ${request.getOriginalURL()}, method: ${request.getMethod()}. If this is an unexpected behaviour, please create an issue here: https://github.com/supertokens/supertokens-node/issues`, - }); + return async (event, context, callback) => { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + let request = new AWSRequest(event); + let response = new AWSResponse(event); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + let result = await supertokens.middleware(request, response, userContext); + if (result) { return response.sendResponse(); - } catch (err) { - yield supertokens.errorHandler(err, request, response); - if (response.responseSet) { - return response.sendResponse(); - } - throw err; } - }); + if (handler !== undefined) { + let handlerResult = await handler(event, context, callback); + return response.sendResponse(handlerResult); + } + /** + * it reaches this point only if the API route was not exposed by + * the SDK and user didn't provide a handler + */ + response.setStatusCode(404); + response.sendJSONResponse({ + error: `The middleware couldn't serve the API path ${request.getOriginalURL()}, method: ${request.getMethod()}. If this is an unexpected behaviour, please create an issue here: https://github.com/supertokens/supertokens-node/issues`, + }); + return response.sendResponse(); + } catch (err) { + await supertokens.errorHandler(err, request, response); + if (response.responseSet) { + return response.sendResponse(); + } + throw err; + } + }; }; exports.middleware = middleware; exports.AWSWrapper = { diff --git a/lib/build/framework/express/framework.js b/lib/build/framework/express/framework.js index 783724e5e..f917472f1 100644 --- a/lib/build/framework/express/framework.js +++ b/lib/build/framework/express/framework.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -59,14 +28,13 @@ const supertokens_1 = __importDefault(require("../../supertokens")); class ExpressRequest extends request_1.BaseRequest { constructor(request) { super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - if (!this.formDataParserChecked) { - yield utils_2.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request); - this.formDataParserChecked = true; - } - return this.request.body; - }); + this.getFormData = async () => { + if (!this.formDataParserChecked) { + await utils_2.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request); + this.formDataParserChecked = true; + } + return this.request.body; + }; this.getKeyValueFromQuery = (key) => { if (this.request.query === undefined) { return undefined; @@ -77,14 +45,13 @@ class ExpressRequest extends request_1.BaseRequest { } return value; }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - if (!this.parserChecked) { - yield utils_2.assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request); - this.parserChecked = true; - } - return this.request.body; - }); + this.getJSONBody = async () => { + if (!this.parserChecked) { + await utils_2.assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request); + this.parserChecked = true; + } + return this.request.body; + }; this.getMethod = () => { return utils_1.normaliseHttpMethod(this.request.method); }; @@ -159,44 +126,42 @@ class ExpressResponse extends response_1.BaseResponse { } exports.ExpressResponse = ExpressResponse; const middleware = () => { - return (req, res, next) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens; - const request = new ExpressRequest(req); - const response = new ExpressResponse(res); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); - try { - supertokens = supertokens_1.default.getInstanceOrThrowError(); - const result = yield supertokens.middleware(request, response, userContext); - if (!result) { - return next(); - } - } catch (err) { - if (supertokens) { - try { - yield supertokens.errorHandler(err, request, response); - } catch (_a) { - next(err); - } - } else { + return async (req, res, next) => { + let supertokens; + const request = new ExpressRequest(req); + const response = new ExpressResponse(res); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + supertokens = supertokens_1.default.getInstanceOrThrowError(); + const result = await supertokens.middleware(request, response, userContext); + if (!result) { + return next(); + } + } catch (err) { + if (supertokens) { + try { + await supertokens.errorHandler(err, request, response); + } catch (_a) { next(err); } + } else { + next(err); } - }); + } + }; }; exports.middleware = middleware; const errorHandler = () => { - return (err, req, res, next) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new ExpressRequest(req); - let response = new ExpressResponse(res); - try { - yield supertokens.errorHandler(err, request, response); - } catch (err) { - return next(err); - } - }); + return async (err, req, res, next) => { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + let request = new ExpressRequest(req); + let response = new ExpressResponse(res); + try { + await supertokens.errorHandler(err, request, response); + } catch (err) { + return next(err); + } + }; }; exports.errorHandler = errorHandler; exports.ExpressWrapper = { diff --git a/lib/build/framework/fastify/framework.js b/lib/build/framework/fastify/framework.js index d2efd1c42..5bec9f553 100644 --- a/lib/build/framework/fastify/framework.js +++ b/lib/build/framework/fastify/framework.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -60,10 +29,9 @@ const constants_1 = require("../constants"); class FastifyRequest extends request_1.BaseRequest { constructor(request) { super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - return this.request.body; // NOTE: ask user to add require('fastify-formbody') - }); + this.getFormData = async () => { + return this.request.body; // NOTE: ask user to add require('fastify-formbody') + }; this.getKeyValueFromQuery = (key) => { if (this.request.query === undefined) { return undefined; @@ -74,10 +42,9 @@ class FastifyRequest extends request_1.BaseRequest { } return value; }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - return this.request.body; - }); + this.getJSONBody = async () => { + return this.request.body; + }; this.getMethod = () => { return utils_1.normaliseHttpMethod(this.request.method); }; @@ -197,30 +164,27 @@ class FastifyResponse extends response_1.BaseResponse { } exports.FastifyResponse = FastifyResponse; function plugin(fastify, _, done) { - fastify.addHook("preHandler", (req, reply) => - __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new FastifyRequest(req); - let response = new FastifyResponse(reply); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); - try { - yield supertokens.middleware(request, response, userContext); - } catch (err) { - yield supertokens.errorHandler(err, request, response); - } - }) - ); + fastify.addHook("preHandler", async (req, reply) => { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + let request = new FastifyRequest(req); + let response = new FastifyResponse(reply); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + await supertokens.middleware(request, response, userContext); + } catch (err) { + await supertokens.errorHandler(err, request, response); + } + }); done(); } plugin[Symbol.for("skip-override")] = true; const errorHandler = () => { - return (err, req, res) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new FastifyRequest(req); - let response = new FastifyResponse(res); - yield supertokens.errorHandler(err, request, response); - }); + return async (err, req, res) => { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + let request = new FastifyRequest(req); + let response = new FastifyResponse(res); + await supertokens.errorHandler(err, request, response); + }; }; exports.errorHandler = errorHandler; exports.FastifyWrapper = { diff --git a/lib/build/framework/hapi/framework.js b/lib/build/framework/hapi/framework.js index a2893bfcd..23a86baa8 100644 --- a/lib/build/framework/hapi/framework.js +++ b/lib/build/framework/hapi/framework.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -59,10 +28,9 @@ const supertokens_1 = __importDefault(require("../../supertokens")); class HapiRequest extends request_1.BaseRequest { constructor(request) { super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - return this.request.payload === undefined || this.request.payload === null ? {} : this.request.payload; - }); + this.getFormData = async () => { + return this.request.payload === undefined || this.request.payload === null ? {} : this.request.payload; + }; this.getKeyValueFromQuery = (key) => { if (this.request.query === undefined) { return undefined; @@ -73,10 +41,9 @@ class HapiRequest extends request_1.BaseRequest { } return value; }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - return this.request.payload === undefined || this.request.payload === null ? {} : this.request.payload; - }); + this.getJSONBody = async () => { + return this.request.payload === undefined || this.request.payload === null ? {} : this.request.payload; + }; this.getMethod = () => { return utils_1.normaliseHttpMethod(this.request.method); }; @@ -165,95 +132,89 @@ exports.HapiResponse = HapiResponse; const plugin = { name: "supertokens-hapi-middleware", version: "1.0.0", - register: function (server, _) { - return __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - server.ext("onPreHandler", (req, h) => - __awaiter(this, void 0, void 0, function* () { - let request = new HapiRequest(req); - let response = new HapiResponse(h); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); - let result = yield supertokens.middleware(request, response, userContext); - if (!result) { - return h.continue; - } - return response.sendResponse(); - }) - ); - server.ext("onPreResponse", (request, h) => - __awaiter(this, void 0, void 0, function* () { - (request.app.lazyHeaders || []).forEach(({ key, value, allowDuplicateKey }) => { - if (request.response.isBoom) { - request.response.output.headers[key] = value; - } else { - request.response.header(key, value, { append: allowDuplicateKey }); - } - }); - if (request.response.isBoom) { - let err = request.response.data || request.response; - let req = new HapiRequest(request); - let res = new HapiResponse(h); - if (err !== undefined && err !== null) { - try { - yield supertokens.errorHandler(err, req, res); - if (res.responseSet) { - let resObj = res.sendResponse(true); - (request.app.lazyHeaders || []).forEach(({ key, value, allowDuplicateKey }) => { - resObj.header(key, value, { append: allowDuplicateKey }); - }); - return resObj.takeover(); - } - return h.continue; - } catch (e) { - return h.continue; - } - } - } - return h.continue; - }) - ); - server.decorate("toolkit", "lazyHeaderBindings", function (h, key, value, allowDuplicateKey) { - const anyApp = h.request.app; - anyApp.lazyHeaders = anyApp.lazyHeaders || []; - if (value === undefined) { - anyApp.lazyHeaders = anyApp.lazyHeaders.filter( - (header) => header.key.toLowerCase() !== key.toLowerCase() - ); + register: async function (server, _) { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + server.ext("onPreHandler", async (req, h) => { + let request = new HapiRequest(req); + let response = new HapiResponse(h); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + let result = await supertokens.middleware(request, response, userContext); + if (!result) { + return h.continue; + } + return response.sendResponse(); + }); + server.ext("onPreResponse", async (request, h) => { + (request.app.lazyHeaders || []).forEach(({ key, value, allowDuplicateKey }) => { + if (request.response.isBoom) { + request.response.output.headers[key] = value; } else { - anyApp.lazyHeaders.push({ key, value, allowDuplicateKey }); + request.response.header(key, value, { append: allowDuplicateKey }); } }); - let supportedRoutes = []; - let methodsSupported = new Set(); - for (let i = 0; i < supertokens.recipeModules.length; i++) { - let apisHandled = supertokens.recipeModules[i].getAPIsHandled(); - for (let j = 0; j < apisHandled.length; j++) { - let api = apisHandled[j]; - if (!api.disabled) { - methodsSupported.add(api.method); + if (request.response.isBoom) { + let err = request.response.data || request.response; + let req = new HapiRequest(request); + let res = new HapiResponse(h); + if (err !== undefined && err !== null) { + try { + await supertokens.errorHandler(err, req, res); + if (res.responseSet) { + let resObj = res.sendResponse(true); + (request.app.lazyHeaders || []).forEach(({ key, value, allowDuplicateKey }) => { + resObj.header(key, value, { append: allowDuplicateKey }); + }); + return resObj.takeover(); + } + return h.continue; + } catch (e) { + return h.continue; } } } - /** - * Hapi requires that all API paths are registered before the server starts listening. - * When using multi-tenancy the tenant id is passed as part of the request path. Because - * this id is dynamic and unkown when starting the server, it is not possible for us to - * declare all APIs with the tenant id in the path. Because of this requests with tenant id - * in the path would give a 404. - * - * To solve this we use wildcards after the base path for all the requests. This will make - * sure that Hapi forwards requests to our handler which will in turn forward to the - * middleware. The middleware processes the full request URL so the logic will remain intact. - */ - supportedRoutes.push({ - path: `${supertokens.appInfo.apiBasePath.getAsStringDangerous()}/{path*}`, - method: [...methodsSupported], - handler: (_, h) => { - return h.continue; - }, - }); - server.route(supportedRoutes); + return h.continue; + }); + server.decorate("toolkit", "lazyHeaderBindings", function (h, key, value, allowDuplicateKey) { + const anyApp = h.request.app; + anyApp.lazyHeaders = anyApp.lazyHeaders || []; + if (value === undefined) { + anyApp.lazyHeaders = anyApp.lazyHeaders.filter( + (header) => header.key.toLowerCase() !== key.toLowerCase() + ); + } else { + anyApp.lazyHeaders.push({ key, value, allowDuplicateKey }); + } + }); + let supportedRoutes = []; + let methodsSupported = new Set(); + for (let i = 0; i < supertokens.recipeModules.length; i++) { + let apisHandled = supertokens.recipeModules[i].getAPIsHandled(); + for (let j = 0; j < apisHandled.length; j++) { + let api = apisHandled[j]; + if (!api.disabled) { + methodsSupported.add(api.method); + } + } + } + /** + * Hapi requires that all API paths are registered before the server starts listening. + * When using multi-tenancy the tenant id is passed as part of the request path. Because + * this id is dynamic and unkown when starting the server, it is not possible for us to + * declare all APIs with the tenant id in the path. Because of this requests with tenant id + * in the path would give a 404. + * + * To solve this we use wildcards after the base path for all the requests. This will make + * sure that Hapi forwards requests to our handler which will in turn forward to the + * middleware. The middleware processes the full request URL so the logic will remain intact. + */ + supportedRoutes.push({ + path: `${supertokens.appInfo.apiBasePath.getAsStringDangerous()}/{path*}`, + method: [...methodsSupported], + handler: (_, h) => { + return h.continue; + }, }); + server.route(supportedRoutes); }, }; exports.HapiWrapper = { diff --git a/lib/build/framework/koa/framework.js b/lib/build/framework/koa/framework.js index 57ed773f4..e4500d3ed 100644 --- a/lib/build/framework/koa/framework.js +++ b/lib/build/framework/koa/framework.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -59,13 +28,12 @@ const supertokens_1 = __importDefault(require("../../supertokens")); class KoaRequest extends request_1.BaseRequest { constructor(ctx) { super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - if (this.parsedUrlEncodedFormData === undefined) { - this.parsedUrlEncodedFormData = yield utils_2.parseURLEncodedFormData(this.ctx.req); - } - return this.parsedUrlEncodedFormData; - }); + this.getFormData = async () => { + if (this.parsedUrlEncodedFormData === undefined) { + this.parsedUrlEncodedFormData = await utils_2.parseURLEncodedFormData(this.ctx.req); + } + return this.parsedUrlEncodedFormData; + }; this.getKeyValueFromQuery = (key) => { if (this.ctx.query === undefined) { return undefined; @@ -76,13 +44,12 @@ class KoaRequest extends request_1.BaseRequest { } return value; }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - if (this.parsedJSONBody === undefined) { - this.parsedJSONBody = yield utils_2.parseJSONBodyFromRequest(this.ctx.req); - } - return this.parsedJSONBody === undefined ? {} : this.parsedJSONBody; - }); + this.getJSONBody = async () => { + if (this.parsedJSONBody === undefined) { + this.parsedJSONBody = await utils_2.parseJSONBodyFromRequest(this.ctx.req); + } + return this.parsedJSONBody === undefined ? {} : this.parsedJSONBody; + }; this.getMethod = () => { return utils_1.normaliseHttpMethod(this.ctx.request.method); }; @@ -164,21 +131,20 @@ class KoaResponse extends response_1.BaseResponse { } exports.KoaResponse = KoaResponse; const middleware = () => { - return (ctx, next) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new KoaRequest(ctx); - let response = new KoaResponse(ctx); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); - try { - let result = yield supertokens.middleware(request, response, userContext); - if (!result) { - return yield next(); - } - } catch (err) { - return yield supertokens.errorHandler(err, request, response); + return async (ctx, next) => { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + let request = new KoaRequest(ctx); + let response = new KoaResponse(ctx); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + let result = await supertokens.middleware(request, response, userContext); + if (!result) { + return await next(); } - }); + } catch (err) { + return await supertokens.errorHandler(err, request, response); + } + }; }; exports.middleware = middleware; exports.KoaWrapper = { diff --git a/lib/build/framework/loopback/framework.js b/lib/build/framework/loopback/framework.js index e359bcb48..cabc274cd 100644 --- a/lib/build/framework/loopback/framework.js +++ b/lib/build/framework/loopback/framework.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -59,14 +28,13 @@ const supertokens_1 = __importDefault(require("../../supertokens")); class LoopbackRequest extends request_1.BaseRequest { constructor(ctx) { super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - if (!this.formDataParserChecked) { - yield utils_2.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request); - this.formDataParserChecked = true; - } - return this.request.body; - }); + this.getFormData = async () => { + if (!this.formDataParserChecked) { + await utils_2.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request); + this.formDataParserChecked = true; + } + return this.request.body; + }; this.getKeyValueFromQuery = (key) => { if (this.request.query === undefined) { return undefined; @@ -77,14 +45,13 @@ class LoopbackRequest extends request_1.BaseRequest { } return value; }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - if (!this.parserChecked) { - yield utils_2.assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request); - this.parserChecked = true; - } - return this.request.body; - }); + this.getJSONBody = async () => { + if (!this.parserChecked) { + await utils_2.assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request); + this.parserChecked = true; + } + return this.request.body; + }; this.getMethod = () => { return utils_1.normaliseHttpMethod(this.request.method); }; @@ -148,22 +115,21 @@ class LoopbackResponse extends response_1.BaseResponse { } } exports.LoopbackResponse = LoopbackResponse; -const middleware = (ctx, next) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new LoopbackRequest(ctx); - let response = new LoopbackResponse(ctx); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); - try { - let result = yield supertokens.middleware(request, response, userContext); - if (!result) { - return yield next(); - } - return; - } catch (err) { - return yield supertokens.errorHandler(err, request, response); +const middleware = async (ctx, next) => { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + let request = new LoopbackRequest(ctx); + let response = new LoopbackResponse(ctx); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + let result = await supertokens.middleware(request, response, userContext); + if (!result) { + return await next(); } - }); + return; + } catch (err) { + return await supertokens.errorHandler(err, request, response); + } +}; exports.middleware = middleware; exports.LoopbackWrapper = { middleware: exports.middleware, diff --git a/lib/build/framework/utils.js b/lib/build/framework/utils.js index 3fbc45722..2e392b51c 100644 --- a/lib/build/framework/utils.js +++ b/lib/build/framework/utils.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -136,93 +105,54 @@ function getCharset(req) { return undefined; } } -function parseJSONBodyFromRequest(req) { - return __awaiter(this, void 0, void 0, function* () { - const encoding = getCharset(req) || "utf-8"; - if (!encoding.startsWith("utf-")) { - throw new Error(`unsupported charset ${encoding.toUpperCase()}`); - } - const buffer = yield getBody(inflation_1.default(req)); - const str = buffer.toString(encoding); - if (str.length === 0) { - return {}; - } - return JSON.parse(str); - }); +async function parseJSONBodyFromRequest(req) { + const encoding = getCharset(req) || "utf-8"; + if (!encoding.startsWith("utf-")) { + throw new Error(`unsupported charset ${encoding.toUpperCase()}`); + } + const buffer = await getBody(inflation_1.default(req)); + const str = buffer.toString(encoding); + if (str.length === 0) { + return {}; + } + return JSON.parse(str); } exports.parseJSONBodyFromRequest = parseJSONBodyFromRequest; -function parseURLEncodedFormData(req) { - return __awaiter(this, void 0, void 0, function* () { - const encoding = getCharset(req) || "utf-8"; - if (!encoding.startsWith("utf-")) { - throw new Error(`unsupported charset ${encoding.toUpperCase()}`); - } - const buffer = yield getBody(inflation_1.default(req)); - const str = buffer.toString(encoding); - let body = {}; - for (const [key, val] of new URLSearchParams(str).entries()) { - if (key in body) { - if (body[key] instanceof Array) { - body[key].push(val); - } else { - body[key] = [body[key], val]; - } +async function parseURLEncodedFormData(req) { + const encoding = getCharset(req) || "utf-8"; + if (!encoding.startsWith("utf-")) { + throw new Error(`unsupported charset ${encoding.toUpperCase()}`); + } + const buffer = await getBody(inflation_1.default(req)); + const str = buffer.toString(encoding); + let body = {}; + for (const [key, val] of new URLSearchParams(str).entries()) { + if (key in body) { + if (body[key] instanceof Array) { + body[key].push(val); } else { - body[key] = val; + body[key] = [body[key], val]; } + } else { + body[key] = val; } - return body; - }); + } + return body; } exports.parseURLEncodedFormData = parseURLEncodedFormData; -function assertThatBodyParserHasBeenUsedForExpressLikeRequest(method, request) { - return __awaiter(this, void 0, void 0, function* () { - // according to https://github.com/supertokens/supertokens-node/issues/33 - if (method === "post" || method === "put") { - if (typeof request.body === "string") { - try { - request.body = JSON.parse(request.body); - } catch (err) { - if (request.body === "") { - request.body = {}; - } else { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass a valid JSON input in the request body", - }); - } - } - } else if ( - request.body === undefined || - Buffer.isBuffer(request.body) || - (Object.keys(request.body).length === 0 && request.readable) - ) { - try { - // parsing it again to make sure that the request is parsed atleast once by a json parser - request.body = yield parseJSONBodyFromRequest(request); - } catch (_a) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass a valid JSON input in the request body", - }); - } - } - } - }); -} -exports.assertThatBodyParserHasBeenUsedForExpressLikeRequest = assertThatBodyParserHasBeenUsedForExpressLikeRequest; -function assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(request) { - return __awaiter(this, void 0, void 0, function* () { +async function assertThatBodyParserHasBeenUsedForExpressLikeRequest(method, request) { + // according to https://github.com/supertokens/supertokens-node/issues/33 + if (method === "post" || method === "put") { if (typeof request.body === "string") { try { - request.body = Object.fromEntries(new URLSearchParams(request.body).entries()); + request.body = JSON.parse(request.body); } catch (err) { if (request.body === "") { request.body = {}; } else { throw new error_1.default({ type: error_1.default.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass valid url encoded form in the request body", + message: "API input error: Please make sure to pass a valid JSON input in the request body", }); } } @@ -232,16 +162,47 @@ function assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(request) { (Object.keys(request.body).length === 0 && request.readable) ) { try { - // parsing it again to make sure that the request is parsed atleast once by a form data parser - request.body = yield parseURLEncodedFormData(request); + // parsing it again to make sure that the request is parsed atleast once by a json parser + request.body = await parseJSONBodyFromRequest(request); } catch (_a) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "API input error: Please make sure to pass a valid JSON input in the request body", + }); + } + } + } +} +exports.assertThatBodyParserHasBeenUsedForExpressLikeRequest = assertThatBodyParserHasBeenUsedForExpressLikeRequest; +async function assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(request) { + if (typeof request.body === "string") { + try { + request.body = Object.fromEntries(new URLSearchParams(request.body).entries()); + } catch (err) { + if (request.body === "") { + request.body = {}; + } else { throw new error_1.default({ type: error_1.default.BAD_INPUT_ERROR, message: "API input error: Please make sure to pass valid url encoded form in the request body", }); } } - }); + } else if ( + request.body === undefined || + Buffer.isBuffer(request.body) || + (Object.keys(request.body).length === 0 && request.readable) + ) { + try { + // parsing it again to make sure that the request is parsed atleast once by a form data parser + request.body = await parseURLEncodedFormData(request); + } catch (_a) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "API input error: Please make sure to pass valid url encoded form in the request body", + }); + } + } } exports.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest = assertFormDataBodyParserHasBeenUsedForExpressLikeRequest; function setHeaderForExpressLikeResponse(res, key, value, allowDuplicateKey) { diff --git a/lib/build/index.d.ts b/lib/build/index.d.ts index ffe197eec..4c093b659 100644 --- a/lib/build/index.d.ts +++ b/lib/build/index.d.ts @@ -1,9 +1,15 @@ // @ts-nocheck import SuperTokens from "./supertokens"; import SuperTokensError from "./error"; +import { User as UserType } from "./types"; +import { AccountInfo } from "./recipe/accountlinking/types"; +import RecipeUserId from "./recipeUserId"; +import { User } from "./user"; export default class SuperTokensWrapper { static init: typeof SuperTokens.init; static Error: typeof SuperTokensError; + static RecipeUserId: typeof RecipeUserId; + static User: typeof User; static getAllCORSHeaders(): string[]; static getUserCount(includeRecipeIds?: string[], tenantId?: string): Promise; static getUsersOldestFirst(input: { @@ -11,12 +17,11 @@ export default class SuperTokensWrapper { limit?: number; paginationToken?: string; includeRecipeIds?: string[]; - query?: object; + query?: { + [key: string]: string; + }; }): Promise<{ - users: { - recipeId: string; - user: any; - }[]; + users: UserType[]; nextPaginationToken?: string; }>; static getUsersNewestFirst(input: { @@ -24,19 +29,13 @@ export default class SuperTokensWrapper { limit?: number; paginationToken?: string; includeRecipeIds?: string[]; - query?: object; + query?: { + [key: string]: string; + }; }): Promise<{ - users: { - recipeId: string; - user: any; - }[]; + users: UserType[]; nextPaginationToken?: string; }>; - static deleteUser( - userId: string - ): Promise<{ - status: "OK"; - }>; static createUserIdMapping(input: { superTokensUserId: string; externalUserId: string; @@ -81,6 +80,21 @@ export default class SuperTokensWrapper { }): Promise<{ status: "OK" | "UNKNOWN_MAPPING_ERROR"; }>; + static getUser(userId: string, userContext?: any): Promise; + static listUsersByAccountInfo( + tenantId: string, + accountInfo: AccountInfo, + doUnionOfAccountInfo?: boolean, + userContext?: any + ): Promise; + static deleteUser( + userId: string, + removeAllLinkedAccounts?: boolean, + userContext?: any + ): Promise<{ + status: "OK"; + }>; + static convertToRecipeUserId(recipeUserId: string): RecipeUserId; static getRequestFromUserContext(userContext: any | undefined): import("./framework").BaseRequest | undefined; } export declare let init: typeof SuperTokens.init; @@ -93,5 +107,10 @@ export declare let createUserIdMapping: typeof SuperTokensWrapper.createUserIdMa export declare let getUserIdMapping: typeof SuperTokensWrapper.getUserIdMapping; export declare let deleteUserIdMapping: typeof SuperTokensWrapper.deleteUserIdMapping; export declare let updateOrDeleteUserIdMappingInfo: typeof SuperTokensWrapper.updateOrDeleteUserIdMappingInfo; +export declare let getUser: typeof SuperTokensWrapper.getUser; +export declare let listUsersByAccountInfo: typeof SuperTokensWrapper.listUsersByAccountInfo; +export declare let convertToRecipeUserId: typeof SuperTokensWrapper.convertToRecipeUserId; export declare let getRequestFromUserContext: typeof SuperTokensWrapper.getRequestFromUserContext; export declare let Error: typeof SuperTokensError; +export { default as RecipeUserId } from "./recipeUserId"; +export { User } from "./user"; diff --git a/lib/build/index.js b/lib/build/index.js index 3ce3ac81f..520f598a1 100644 --- a/lib/build/index.js +++ b/lib/build/index.js @@ -19,9 +19,12 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Error = exports.getRequestFromUserContext = exports.updateOrDeleteUserIdMappingInfo = exports.deleteUserIdMapping = exports.getUserIdMapping = exports.createUserIdMapping = exports.deleteUser = exports.getUsersNewestFirst = exports.getUsersOldestFirst = exports.getUserCount = exports.getAllCORSHeaders = exports.init = void 0; +exports.User = exports.RecipeUserId = exports.Error = exports.getRequestFromUserContext = exports.convertToRecipeUserId = exports.listUsersByAccountInfo = exports.getUser = exports.updateOrDeleteUserIdMappingInfo = exports.deleteUserIdMapping = exports.getUserIdMapping = exports.createUserIdMapping = exports.deleteUser = exports.getUsersNewestFirst = exports.getUsersOldestFirst = exports.getUserCount = exports.getAllCORSHeaders = exports.init = void 0; const supertokens_1 = __importDefault(require("./supertokens")); const error_1 = __importDefault(require("./error")); +const recipe_1 = __importDefault(require("./recipe/accountlinking/recipe")); +const recipeUserId_1 = __importDefault(require("./recipeUserId")); +const user_1 = require("./user"); // For Express class SuperTokensWrapper { static getAllCORSHeaders() { @@ -31,19 +34,18 @@ class SuperTokensWrapper { return supertokens_1.default.getInstanceOrThrowError().getUserCount(includeRecipeIds, tenantId); } static getUsersOldestFirst(input) { - return supertokens_1.default - .getInstanceOrThrowError() - .getUsers(Object.assign({ timeJoinedOrder: "ASC" }, input)); + return recipe_1.default + .getInstance() + .recipeInterfaceImpl.getUsers( + Object.assign(Object.assign({ timeJoinedOrder: "ASC" }, input), { userContext: undefined }) + ); } static getUsersNewestFirst(input) { - return supertokens_1.default - .getInstanceOrThrowError() - .getUsers(Object.assign({ timeJoinedOrder: "DESC" }, input)); - } - static deleteUser(userId) { - return supertokens_1.default.getInstanceOrThrowError().deleteUser({ - userId, - }); + return recipe_1.default + .getInstance() + .recipeInterfaceImpl.getUsers( + Object.assign(Object.assign({ timeJoinedOrder: "DESC" }, input), { userContext: undefined }) + ); } static createUserIdMapping(input) { return supertokens_1.default.getInstanceOrThrowError().createUserIdMapping(input); @@ -57,6 +59,30 @@ class SuperTokensWrapper { static updateOrDeleteUserIdMappingInfo(input) { return supertokens_1.default.getInstanceOrThrowError().updateOrDeleteUserIdMappingInfo(input); } + static async getUser(userId, userContext) { + return await recipe_1.default.getInstance().recipeInterfaceImpl.getUser({ + userId, + userContext: userContext === undefined ? {} : userContext, + }); + } + static async listUsersByAccountInfo(tenantId, accountInfo, doUnionOfAccountInfo = false, userContext) { + return await recipe_1.default.getInstance().recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo, + doUnionOfAccountInfo, + userContext: userContext === undefined ? {} : userContext, + }); + } + static async deleteUser(userId, removeAllLinkedAccounts = true, userContext) { + return await recipe_1.default.getInstance().recipeInterfaceImpl.deleteUser({ + userId, + removeAllLinkedAccounts, + userContext: userContext === undefined ? {} : userContext, + }); + } + static convertToRecipeUserId(recipeUserId) { + return new recipeUserId_1.default(recipeUserId); + } static getRequestFromUserContext(userContext) { return supertokens_1.default.getInstanceOrThrowError().getRequestFromUserContext(userContext); } @@ -64,6 +90,8 @@ class SuperTokensWrapper { exports.default = SuperTokensWrapper; SuperTokensWrapper.init = supertokens_1.default.init; SuperTokensWrapper.Error = error_1.default; +SuperTokensWrapper.RecipeUserId = recipeUserId_1.default; +SuperTokensWrapper.User = user_1.User; exports.init = SuperTokensWrapper.init; exports.getAllCORSHeaders = SuperTokensWrapper.getAllCORSHeaders; exports.getUserCount = SuperTokensWrapper.getUserCount; @@ -74,5 +102,22 @@ exports.createUserIdMapping = SuperTokensWrapper.createUserIdMapping; exports.getUserIdMapping = SuperTokensWrapper.getUserIdMapping; exports.deleteUserIdMapping = SuperTokensWrapper.deleteUserIdMapping; exports.updateOrDeleteUserIdMappingInfo = SuperTokensWrapper.updateOrDeleteUserIdMappingInfo; +exports.getUser = SuperTokensWrapper.getUser; +exports.listUsersByAccountInfo = SuperTokensWrapper.listUsersByAccountInfo; +exports.convertToRecipeUserId = SuperTokensWrapper.convertToRecipeUserId; exports.getRequestFromUserContext = SuperTokensWrapper.getRequestFromUserContext; exports.Error = SuperTokensWrapper.Error; +var recipeUserId_2 = require("./recipeUserId"); +Object.defineProperty(exports, "RecipeUserId", { + enumerable: true, + get: function () { + return __importDefault(recipeUserId_2).default; + }, +}); +var user_2 = require("./user"); +Object.defineProperty(exports, "User", { + enumerable: true, + get: function () { + return user_2.User; + }, +}); diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index 9b6436b37..adcc99506 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.superTokensNextWrapper = void 0; /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. @@ -48,44 +17,38 @@ exports.superTokensNextWrapper = void 0; */ const express_1 = require("./framework/express"); function next(request, response, resolve, reject) { - return function (middlewareError) { - return __awaiter(this, void 0, void 0, function* () { - if (middlewareError === undefined) { - return resolve(); + return async function (middlewareError) { + if (middlewareError === undefined) { + return resolve(); + } + await express_1.errorHandler()(middlewareError, request, response, (errorHandlerError) => { + if (errorHandlerError !== undefined) { + return reject(errorHandlerError); } - yield express_1.errorHandler()(middlewareError, request, response, (errorHandlerError) => { - if (errorHandlerError !== undefined) { - return reject(errorHandlerError); - } - // do nothing, error handler does not resolve the promise. - }); + // do nothing, error handler does not resolve the promise. }); }; } class NextJS { - static superTokensNextWrapper(middleware, request, response) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => - __awaiter(this, void 0, void 0, function* () { - try { - let callbackCalled = false; - const result = yield middleware((err) => { - callbackCalled = true; - next(request, response, resolve, reject)(err); - }); - if (!callbackCalled && !response.finished && !response.headersSent) { - return resolve(result); - } - } catch (err) { - yield express_1.errorHandler()(err, request, response, (errorHandlerError) => { - if (errorHandlerError !== undefined) { - return reject(errorHandlerError); - } - // do nothing, error handler does not resolve the promise. - }); + static async superTokensNextWrapper(middleware, request, response) { + return new Promise(async (resolve, reject) => { + try { + let callbackCalled = false; + const result = await middleware((err) => { + callbackCalled = true; + next(request, response, resolve, reject)(err); + }); + if (!callbackCalled && !response.finished && !response.headersSent) { + return resolve(result); + } + } catch (err) { + await express_1.errorHandler()(err, request, response, (errorHandlerError) => { + if (errorHandlerError !== undefined) { + return reject(errorHandlerError); } - }) - ); + // do nothing, error handler does not resolve the promise. + }); + } }); } } diff --git a/lib/build/processState.d.ts b/lib/build/processState.d.ts index 9decf8458..33ec77f3f 100644 --- a/lib/build/processState.d.ts +++ b/lib/build/processState.d.ts @@ -4,6 +4,10 @@ export declare enum PROCESS_STATE { CALLING_SERVICE_IN_GET_API_VERSION = 1, CALLING_SERVICE_IN_REQUEST_HELPER = 2, MULTI_JWKS_VALIDATION = 3, + IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS = 4, + IS_SIGN_UP_ALLOWED_CALLED = 5, + IS_SIGN_IN_ALLOWED_CALLED = 6, + IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED = 7, } export declare class ProcessState { history: PROCESS_STATE[]; diff --git a/lib/build/processState.js b/lib/build/processState.js index 847a97b15..a32e7187a 100644 --- a/lib/build/processState.js +++ b/lib/build/processState.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProcessState = exports.PROCESS_STATE = void 0; var PROCESS_STATE; @@ -52,6 +21,11 @@ var PROCESS_STATE; PROCESS_STATE[(PROCESS_STATE["CALLING_SERVICE_IN_GET_API_VERSION"] = 1)] = "CALLING_SERVICE_IN_GET_API_VERSION"; PROCESS_STATE[(PROCESS_STATE["CALLING_SERVICE_IN_REQUEST_HELPER"] = 2)] = "CALLING_SERVICE_IN_REQUEST_HELPER"; PROCESS_STATE[(PROCESS_STATE["MULTI_JWKS_VALIDATION"] = 3)] = "MULTI_JWKS_VALIDATION"; + PROCESS_STATE[(PROCESS_STATE["IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS"] = 4)] = + "IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS"; + PROCESS_STATE[(PROCESS_STATE["IS_SIGN_UP_ALLOWED_CALLED"] = 5)] = "IS_SIGN_UP_ALLOWED_CALLED"; + PROCESS_STATE[(PROCESS_STATE["IS_SIGN_IN_ALLOWED_CALLED"] = 6)] = "IS_SIGN_IN_ALLOWED_CALLED"; + PROCESS_STATE[(PROCESS_STATE["IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED"] = 7)] = "IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED"; })((PROCESS_STATE = exports.PROCESS_STATE || (exports.PROCESS_STATE = {}))); class ProcessState { constructor() { @@ -72,26 +46,25 @@ class ProcessState { this.reset = () => { this.history = []; }; - this.waitForEvent = (state, timeInMS = 7000) => - __awaiter(this, void 0, void 0, function* () { - let startTime = Date.now(); - return new Promise((resolve) => { - let actualThis = this; - function tryAndGet() { - let result = actualThis.getEventByLastEventByName(state); - if (result === undefined) { - if (Date.now() - startTime > timeInMS) { - resolve(undefined); - } else { - setTimeout(tryAndGet, 1000); - } + this.waitForEvent = async (state, timeInMS = 7000) => { + let startTime = Date.now(); + return new Promise((resolve) => { + let actualThis = this; + function tryAndGet() { + let result = actualThis.getEventByLastEventByName(state); + if (result === undefined) { + if (Date.now() - startTime > timeInMS) { + resolve(undefined); } else { - resolve(result); + setTimeout(tryAndGet, 1000); } + } else { + resolve(result); } - tryAndGet(); - }); + } + tryAndGet(); }); + }; } static getInstance() { if (ProcessState.instance === undefined) { diff --git a/lib/build/querier.js b/lib/build/querier.js index 7a855b9a5..6546e051b 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -35,37 +35,6 @@ var __importStar = __setModuleDefault(result, mod); return result; }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -97,47 +66,45 @@ class Querier { // we have rIdToCore so that recipes can force change the rId sent to core. This is a hack until the core is able // to support multiple rIds per API constructor(hosts, rIdToCore) { - this.getAPIVersion = () => - __awaiter(this, void 0, void 0, function* () { - var _a; - if (Querier.apiVersion !== undefined) { - return Querier.apiVersion; - } - processState_1.ProcessState.getInstance().addState( - processState_1.PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION - ); - let { body: response } = yield this.sendRequestHelper( - new normalisedURLPath_1.default("/apiversion"), - "GET", - (url) => - __awaiter(this, void 0, void 0, function* () { - let headers = {}; - if (Querier.apiKey !== undefined) { - headers = { - "api-key": Querier.apiKey, - }; - } - let response = yield cross_fetch_1.default(url, { - method: "GET", - headers, - }); - return response; - }), - ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 - ); - let cdiSupportedByServer = response.versions; - let supportedVersion = utils_1.getLargestVersionFromIntersection( - cdiSupportedByServer, - version_1.cdiSupported - ); - if (supportedVersion === undefined) { - throw Error( - "The running SuperTokens core version is not compatible with this NodeJS SDK. Please visit https://supertokens.io/docs/community/compatibility to find the right versions" - ); - } - Querier.apiVersion = supportedVersion; + this.getAPIVersion = async () => { + var _a; + if (Querier.apiVersion !== undefined) { return Querier.apiVersion; - }); + } + processState_1.ProcessState.getInstance().addState( + processState_1.PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION + ); + let { body: response } = await this.sendRequestHelper( + new normalisedURLPath_1.default("/apiversion"), + "GET", + async (url) => { + let headers = {}; + if (Querier.apiKey !== undefined) { + headers = { + "api-key": Querier.apiKey, + }; + } + let response = await cross_fetch_1.default(url, { + method: "GET", + headers, + }); + return response; + }, + ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ); + let cdiSupportedByServer = response.versions; + let supportedVersion = utils_1.getLargestVersionFromIntersection( + cdiSupportedByServer, + version_1.cdiSupported + ); + if (supportedVersion === undefined) { + throw Error( + "The running SuperTokens core version is not compatible with this NodeJS SDK. Please visit https://supertokens.io/docs/community/compatibility to find the right versions" + ); + } + Querier.apiVersion = supportedVersion; + return Querier.apiVersion; + }; this.getHostsAliveForTesting = () => { if (process.env.TEST_MODE !== "testing") { throw Error("calling testing function in non testing env"); @@ -145,239 +112,218 @@ class Querier { return Querier.hostsAliveForTesting; }; // path should start with "/" - this.sendPostRequest = (path, body) => - __awaiter(this, void 0, void 0, function* () { - var _b; - const { body: respBody } = yield this.sendRequestHelper( - path, - "POST", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { - "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", - }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - return cross_fetch_1.default(url, { - method: "POST", - body: body !== undefined ? JSON.stringify(body) : undefined, - headers, - }); - }), - ((_b = this.__hosts) === null || _b === void 0 ? void 0 : _b.length) || 0 - ); - return respBody; - }); + this.sendPostRequest = async (path, body) => { + var _a; + const { body: respBody } = await this.sendRequestHelper( + path, + "POST", + async (url) => { + let apiVersion = await this.getAPIVersion(); + let headers = { + "cdi-version": apiVersion, + "content-type": "application/json; charset=utf-8", + }; + if (Querier.apiKey !== undefined) { + headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); + } + return cross_fetch_1.default(url, { + method: "POST", + body: body !== undefined ? JSON.stringify(body) : undefined, + headers, + }); + }, + ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ); + return respBody; + }; // path should start with "/" - this.sendDeleteRequest = (path, body, params) => - __awaiter(this, void 0, void 0, function* () { - var _c; - const { body: respBody } = yield this.sendRequestHelper( - path, - "DELETE", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { - "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", - }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - const finalURL = new URL(url); - const searchParams = new URLSearchParams(params); - finalURL.search = searchParams.toString(); - return cross_fetch_1.default(finalURL.toString(), { - method: "DELETE", - body: body !== undefined ? JSON.stringify(body) : undefined, - headers, - }); - }), - ((_c = this.__hosts) === null || _c === void 0 ? void 0 : _c.length) || 0 - ); - return respBody; - }); + this.sendDeleteRequest = async (path, body, params) => { + var _a; + const { body: respBody } = await this.sendRequestHelper( + path, + "DELETE", + async (url) => { + let apiVersion = await this.getAPIVersion(); + let headers = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" }; + if (Querier.apiKey !== undefined) { + headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); + } + const finalURL = new URL(url); + const searchParams = new URLSearchParams(params); + finalURL.search = searchParams.toString(); + return cross_fetch_1.default(finalURL.toString(), { + method: "DELETE", + body: body !== undefined ? JSON.stringify(body) : undefined, + headers, + }); + }, + ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ); + return respBody; + }; // path should start with "/" - this.sendGetRequest = (path, params) => - __awaiter(this, void 0, void 0, function* () { - var _d; - const { body: respBody } = yield this.sendRequestHelper( - path, - "GET", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { "cdi-version": apiVersion }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - const finalURL = new URL(url); - const searchParams = new URLSearchParams( - Object.entries(params).filter(([_, value]) => value !== undefined) - ); - finalURL.search = searchParams.toString(); - return yield cross_fetch_1.default(finalURL.toString(), { - method: "GET", - headers, - }); - }), - ((_d = this.__hosts) === null || _d === void 0 ? void 0 : _d.length) || 0 - ); - return respBody; - }); - this.sendGetRequestWithResponseHeaders = (path, params) => - __awaiter(this, void 0, void 0, function* () { - var _e; - return yield this.sendRequestHelper( - path, - "GET", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { "cdi-version": apiVersion }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - const finalURL = new URL(url); - const searchParams = new URLSearchParams( - Object.entries(params).filter(([_, value]) => value !== undefined) - ); - finalURL.search = searchParams.toString(); - return yield cross_fetch_1.default(finalURL.toString(), { - method: "GET", - headers, - }); - }), - ((_e = this.__hosts) === null || _e === void 0 ? void 0 : _e.length) || 0 - ); - }); + this.sendGetRequest = async (path, params) => { + var _a; + const { body: respBody } = await this.sendRequestHelper( + path, + "GET", + async (url) => { + let apiVersion = await this.getAPIVersion(); + let headers = { "cdi-version": apiVersion }; + if (Querier.apiKey !== undefined) { + headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); + } + const finalURL = new URL(url); + const searchParams = new URLSearchParams( + Object.entries(params).filter(([_, value]) => value !== undefined) + ); + finalURL.search = searchParams.toString(); + return await cross_fetch_1.default(finalURL.toString(), { + method: "GET", + headers, + }); + }, + ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ); + return respBody; + }; + this.sendGetRequestWithResponseHeaders = async (path, params) => { + var _a; + return await this.sendRequestHelper( + path, + "GET", + async (url) => { + let apiVersion = await this.getAPIVersion(); + let headers = { "cdi-version": apiVersion }; + if (Querier.apiKey !== undefined) { + headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); + } + const finalURL = new URL(url); + const searchParams = new URLSearchParams( + Object.entries(params).filter(([_, value]) => value !== undefined) + ); + finalURL.search = searchParams.toString(); + return await cross_fetch_1.default(finalURL.toString(), { + method: "GET", + headers, + }); + }, + ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ); + }; // path should start with "/" - this.sendPutRequest = (path, body) => - __awaiter(this, void 0, void 0, function* () { - var _f; - const { body: respBody } = yield this.sendRequestHelper( - path, - "PUT", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { - "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", - }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - return cross_fetch_1.default(url, { - method: "PUT", - body: body !== undefined ? JSON.stringify(body) : undefined, - headers, - }); - }), - ((_f = this.__hosts) === null || _f === void 0 ? void 0 : _f.length) || 0 - ); - return respBody; - }); + this.sendPutRequest = async (path, body) => { + var _a; + const { body: respBody } = await this.sendRequestHelper( + path, + "PUT", + async (url) => { + let apiVersion = await this.getAPIVersion(); + let headers = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" }; + if (Querier.apiKey !== undefined) { + headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); + } + return cross_fetch_1.default(url, { + method: "PUT", + body: body !== undefined ? JSON.stringify(body) : undefined, + headers, + }); + }, + ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ); + return respBody; + }; // path should start with "/" - this.sendRequestHelper = (path, method, requestFunc, numberOfTries, retryInfoMap) => - __awaiter(this, void 0, void 0, function* () { - var _g; - if (this.__hosts === undefined) { - throw Error( - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); + this.sendRequestHelper = async (path, method, requestFunc, numberOfTries, retryInfoMap) => { + var _a; + if (this.__hosts === undefined) { + throw Error( + "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." + ); + } + if (numberOfTries === 0) { + throw Error("No SuperTokens core available to query"); + } + let currentDomain = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); + let currentBasePath = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); + const url = currentDomain + currentBasePath + path.getAsStringDangerous(); + const maxRetries = 5; + if (retryInfoMap === undefined) { + retryInfoMap = {}; + } + if (retryInfoMap[url] === undefined) { + retryInfoMap[url] = maxRetries; + } + Querier.lastTriedIndex++; + Querier.lastTriedIndex = Querier.lastTriedIndex % this.__hosts.length; + try { + processState_1.ProcessState.getInstance().addState( + processState_1.PROCESS_STATE.CALLING_SERVICE_IN_REQUEST_HELPER + ); + let response = await requestFunc(url); + if (process.env.TEST_MODE === "testing") { + Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); } - if (numberOfTries === 0) { - throw Error("No SuperTokens core available to query"); + if (response.status !== 200) { + throw response; } - let currentDomain = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); - let currentBasePath = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); - const url = currentDomain + currentBasePath + path.getAsStringDangerous(); - const maxRetries = 5; - if (retryInfoMap === undefined) { - retryInfoMap = {}; + if ( + (_a = response.headers.get("content-type")) === null || _a === void 0 + ? void 0 + : _a.startsWith("text") + ) { + return { body: await response.text(), headers: response.headers }; } - if (retryInfoMap[url] === undefined) { - retryInfoMap[url] = maxRetries; + return { body: await response.json(), headers: response.headers }; + } catch (err) { + if ( + err.message !== undefined && + (err.message.includes("Failed to fetch") || + err.message.includes("ECONNREFUSED") || + err.code === "ECONNREFUSED") + ) { + return await this.sendRequestHelper(path, method, requestFunc, numberOfTries - 1, retryInfoMap); } - Querier.lastTriedIndex++; - Querier.lastTriedIndex = Querier.lastTriedIndex % this.__hosts.length; - try { - processState_1.ProcessState.getInstance().addState( - processState_1.PROCESS_STATE.CALLING_SERVICE_IN_REQUEST_HELPER - ); - let response = yield requestFunc(url); - if (process.env.TEST_MODE === "testing") { - Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); - } - if (response.status !== 200) { - throw response; - } - if ( - (_g = response.headers.get("content-type")) === null || _g === void 0 - ? void 0 - : _g.startsWith("text") - ) { - return { body: yield response.text(), headers: response.headers }; - } - return { body: yield response.json(), headers: response.headers }; - } catch (err) { - if ( - err.message !== undefined && - (err.message.includes("Failed to fetch") || err.message.includes("ECONNREFUSED")) - ) { - return yield this.sendRequestHelper(path, method, requestFunc, numberOfTries - 1, retryInfoMap); - } - if (err instanceof cross_fetch_1.Response) { - if (err.status === constants_1.RATE_LIMIT_STATUS_CODE) { - const retriesLeft = retryInfoMap[url]; - if (retriesLeft > 0) { - retryInfoMap[url] = retriesLeft - 1; - const attemptsMade = maxRetries - retriesLeft; - const delay = 10 + 250 * attemptsMade; - yield new Promise((resolve) => setTimeout(resolve, delay)); - return yield this.sendRequestHelper( - path, - method, - requestFunc, - numberOfTries, - retryInfoMap - ); - } + if (err instanceof cross_fetch_1.Response) { + if (err.status === constants_1.RATE_LIMIT_STATUS_CODE) { + const retriesLeft = retryInfoMap[url]; + if (retriesLeft > 0) { + retryInfoMap[url] = retriesLeft - 1; + const attemptsMade = maxRetries - retriesLeft; + const delay = 10 + 250 * attemptsMade; + await new Promise((resolve) => setTimeout(resolve, delay)); + return await this.sendRequestHelper(path, method, requestFunc, numberOfTries, retryInfoMap); } - throw new Error( - "SuperTokens core threw an error for a " + - method + - " request to path: '" + - path.getAsStringDangerous() + - "' with status code: " + - err.status + - " and message: " + - (yield err.text()) - ); } - throw err; + throw new Error( + "SuperTokens core threw an error for a " + + method + + " request to path: '" + + path.getAsStringDangerous() + + "' with status code: " + + err.status + + " and message: " + + (await err.text()) + ); } - }); + throw err; + } + }; this.__hosts = hosts; this.rIdToCore = rIdToCore; } diff --git a/lib/build/recipe/accountlinking/index.d.ts b/lib/build/recipe/accountlinking/index.d.ts new file mode 100644 index 000000000..79ac077be --- /dev/null +++ b/lib/build/recipe/accountlinking/index.d.ts @@ -0,0 +1,150 @@ +// @ts-nocheck +import Recipe from "./recipe"; +import type { RecipeInterface, AccountInfoWithRecipeId } from "./types"; +import RecipeUserId from "../../recipeUserId"; +export default class Wrapper { + static init: typeof Recipe.init; + /** + * This is a function which is a combination of createPrimaryUser and + * linkAccounts where the input recipeUserId is either linked to a user that it can be + * linked to, or is made into a primary user. + * + * The output will be the user ID of the user that it was linked to, or it will be the + * same as the input recipeUserId if it was made into a primary user, or if there was + * no linking that happened. + */ + static createPrimaryUserIdOrLinkAccounts( + tenantId: string, + recipeUserId: RecipeUserId, + userContext?: any + ): Promise; + /** + * This function returns the primary user that the input recipe ID can be + * linked to. It can be used to determine which primary account the linking + * will happen to if the input recipe user ID was to be linked. + * + * If the function returns undefined, it means that there is no primary user + * that the input recipe ID can be linked to, and therefore it can be made + * into a primary user itself. + */ + static getPrimaryUserThatCanBeLinkedToRecipeUserId( + tenantId: string, + recipeUserId: RecipeUserId, + userContext?: any + ): Promise; + static canCreatePrimaryUser( + recipeUserId: RecipeUserId, + userContext?: any + ): Promise< + | { + status: "OK"; + wasAlreadyAPrimaryUser: boolean; + } + | { + status: + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + >; + static createPrimaryUser( + recipeUserId: RecipeUserId, + userContext?: any + ): Promise< + | { + status: "OK"; + user: import("../../types").User; + wasAlreadyAPrimaryUser: boolean; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + >; + static canLinkAccounts( + recipeUserId: RecipeUserId, + primaryUserId: string, + userContext?: any + ): Promise< + | { + status: "OK"; + accountsAlreadyLinked: boolean; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + description: string; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + | { + status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; + } + >; + static linkAccounts( + recipeUserId: RecipeUserId, + primaryUserId: string, + userContext?: any + ): Promise< + | { + status: "OK"; + accountsAlreadyLinked: boolean; + user: import("../../types").User; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + user: import("../../types").User; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + | { + status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; + } + >; + static unlinkAccount( + recipeUserId: RecipeUserId, + userContext?: any + ): Promise<{ + status: "OK"; + wasRecipeUserDeleted: boolean; + wasLinked: boolean; + }>; + static isSignUpAllowed( + tenantId: string, + newUser: AccountInfoWithRecipeId, + isVerified: boolean, + userContext?: any + ): Promise; + static isSignInAllowed(tenantId: string, recipeUserId: RecipeUserId, userContext?: any): Promise; + static isEmailChangeAllowed( + recipeUserId: RecipeUserId, + newEmail: string, + isVerified: boolean, + userContext?: any + ): Promise; +} +export declare const init: typeof Recipe.init; +export declare const canCreatePrimaryUser: typeof Wrapper.canCreatePrimaryUser; +export declare const createPrimaryUser: typeof Wrapper.createPrimaryUser; +export declare const canLinkAccounts: typeof Wrapper.canLinkAccounts; +export declare const linkAccounts: typeof Wrapper.linkAccounts; +export declare const unlinkAccount: typeof Wrapper.unlinkAccount; +export declare const createPrimaryUserIdOrLinkAccounts: typeof Wrapper.createPrimaryUserIdOrLinkAccounts; +export declare const getPrimaryUserThatCanBeLinkedToRecipeUserId: typeof Wrapper.getPrimaryUserThatCanBeLinkedToRecipeUserId; +export declare const isSignUpAllowed: typeof Wrapper.isSignUpAllowed; +export declare const isSignInAllowed: typeof Wrapper.isSignInAllowed; +export declare const isEmailChangeAllowed: typeof Wrapper.isEmailChangeAllowed; +export type { RecipeInterface }; diff --git a/lib/build/recipe/accountlinking/index.js b/lib/build/recipe/accountlinking/index.js new file mode 100644 index 000000000..fb878fbfc --- /dev/null +++ b/lib/build/recipe/accountlinking/index.js @@ -0,0 +1,142 @@ +"use strict"; +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEmailChangeAllowed = exports.isSignInAllowed = exports.isSignUpAllowed = exports.getPrimaryUserThatCanBeLinkedToRecipeUserId = exports.createPrimaryUserIdOrLinkAccounts = exports.unlinkAccount = exports.linkAccounts = exports.canLinkAccounts = exports.createPrimaryUser = exports.canCreatePrimaryUser = exports.init = void 0; +const recipe_1 = __importDefault(require("./recipe")); +const __1 = require("../.."); +class Wrapper { + /** + * This is a function which is a combination of createPrimaryUser and + * linkAccounts where the input recipeUserId is either linked to a user that it can be + * linked to, or is made into a primary user. + * + * The output will be the user ID of the user that it was linked to, or it will be the + * same as the input recipeUserId if it was made into a primary user, or if there was + * no linking that happened. + */ + static async createPrimaryUserIdOrLinkAccounts(tenantId, recipeUserId, userContext = {}) { + const user = await __1.getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + // Should never really come here unless a programming error happened in the app + throw new Error("Unknown recipeUserId"); + } + return await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user, + userContext, + }); + } + /** + * This function returns the primary user that the input recipe ID can be + * linked to. It can be used to determine which primary account the linking + * will happen to if the input recipe user ID was to be linked. + * + * If the function returns undefined, it means that there is no primary user + * that the input recipe ID can be linked to, and therefore it can be made + * into a primary user itself. + */ + static async getPrimaryUserThatCanBeLinkedToRecipeUserId(tenantId, recipeUserId, userContext = {}) { + const user = await __1.getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + // Should never really come here unless a programming error happened in the app + throw new Error("Unknown recipeUserId"); + } + return await recipe_1.default.getInstance().getPrimaryUserThatCanBeLinkedToRecipeUserId({ + tenantId, + user, + userContext, + }); + } + static async canCreatePrimaryUser(recipeUserId, userContext = {}) { + return await recipe_1.default.getInstance().recipeInterfaceImpl.canCreatePrimaryUser({ + recipeUserId, + userContext, + }); + } + static async createPrimaryUser(recipeUserId, userContext = {}) { + return await recipe_1.default.getInstance().recipeInterfaceImpl.createPrimaryUser({ + recipeUserId, + userContext, + }); + } + static async canLinkAccounts(recipeUserId, primaryUserId, userContext = {}) { + return await recipe_1.default.getInstance().recipeInterfaceImpl.canLinkAccounts({ + recipeUserId, + primaryUserId, + userContext, + }); + } + static async linkAccounts(recipeUserId, primaryUserId, userContext = {}) { + return await recipe_1.default.getInstance().recipeInterfaceImpl.linkAccounts({ + recipeUserId, + primaryUserId, + userContext, + }); + } + static async unlinkAccount(recipeUserId, userContext = {}) { + return await recipe_1.default.getInstance().recipeInterfaceImpl.unlinkAccount({ + recipeUserId, + userContext, + }); + } + static async isSignUpAllowed(tenantId, newUser, isVerified, userContext) { + return await recipe_1.default.getInstance().isSignUpAllowed({ + newUser, + isVerified, + tenantId, + userContext, + }); + } + static async isSignInAllowed(tenantId, recipeUserId, userContext = {}) { + const user = await __1.getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + // Should never really come here unless a programming error happened in the app + throw new Error("Unknown recipeUserId"); + } + return await recipe_1.default.getInstance().isSignInAllowed({ + user, + tenantId, + userContext, + }); + } + static async isEmailChangeAllowed(recipeUserId, newEmail, isVerified, userContext) { + const user = await __1.getUser(recipeUserId.getAsString(), userContext); + return await recipe_1.default.getInstance().isEmailChangeAllowed({ + user, + newEmail, + isVerified, + userContext, + }); + } +} +exports.default = Wrapper; +Wrapper.init = recipe_1.default.init; +exports.init = Wrapper.init; +exports.canCreatePrimaryUser = Wrapper.canCreatePrimaryUser; +exports.createPrimaryUser = Wrapper.createPrimaryUser; +exports.canLinkAccounts = Wrapper.canLinkAccounts; +exports.linkAccounts = Wrapper.linkAccounts; +exports.unlinkAccount = Wrapper.unlinkAccount; +exports.createPrimaryUserIdOrLinkAccounts = Wrapper.createPrimaryUserIdOrLinkAccounts; +exports.getPrimaryUserThatCanBeLinkedToRecipeUserId = Wrapper.getPrimaryUserThatCanBeLinkedToRecipeUserId; +exports.isSignUpAllowed = Wrapper.isSignUpAllowed; +exports.isSignInAllowed = Wrapper.isSignInAllowed; +exports.isEmailChangeAllowed = Wrapper.isEmailChangeAllowed; diff --git a/lib/build/recipe/accountlinking/recipe.d.ts b/lib/build/recipe/accountlinking/recipe.d.ts new file mode 100644 index 000000000..8c89d4643 --- /dev/null +++ b/lib/build/recipe/accountlinking/recipe.d.ts @@ -0,0 +1,99 @@ +// @ts-nocheck +import error from "../../error"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import normalisedURLPath from "../../normalisedURLPath"; +import RecipeModule from "../../recipeModule"; +import type { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, User } from "../../types"; +import type { TypeNormalisedInput, RecipeInterface, TypeInput, AccountInfoWithRecipeId } from "./types"; +import RecipeUserId from "../../recipeUserId"; +import { LoginMethod } from "../../user"; +export default class Recipe extends RecipeModule { + private static instance; + static RECIPE_ID: string; + config: TypeNormalisedInput; + recipeInterfaceImpl: RecipeInterface; + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + config: TypeInput | undefined, + _recipes: {}, + _ingredients: {} + ); + static init(config?: TypeInput): RecipeListFunction; + static getInstance(): Recipe; + getAPIsHandled(): APIHandled[]; + handleAPIRequest( + _id: string, + _tenantId: string, + _req: BaseRequest, + _response: BaseResponse, + _path: normalisedURLPath, + _method: HTTPMethod + ): Promise; + handleError(error: error, _request: BaseRequest, _response: BaseResponse): Promise; + getAllCORSHeaders(): string[]; + isErrorFromThisRecipe(err: any): err is error; + static reset(): void; + createPrimaryUserIdOrLinkAccounts: ({ + tenantId, + user, + userContext, + }: { + tenantId: string; + user: User; + userContext: any; + }) => Promise; + getPrimaryUserThatCanBeLinkedToRecipeUserId: ({ + tenantId, + user, + userContext, + }: { + tenantId: string; + user: User; + userContext: any; + }) => Promise; + isSignInAllowed: ({ + user, + tenantId, + userContext, + }: { + user: User; + tenantId: string; + userContext: any; + }) => Promise; + isSignUpAllowed: ({ + newUser, + isVerified, + tenantId, + userContext, + }: { + newUser: AccountInfoWithRecipeId; + isVerified: boolean; + tenantId: string; + userContext: any; + }) => Promise; + isSignInUpAllowedHelper: ({ + accountInfo, + isVerified, + tenantId, + isSignIn, + userContext, + }: { + accountInfo: AccountInfoWithRecipeId | LoginMethod; + isVerified: boolean; + tenantId: string; + isSignIn: boolean; + userContext: any; + }) => Promise; + isEmailChangeAllowed: (input: { + user?: User; + newEmail: string; + isVerified: boolean; + userContext: any; + }) => Promise; + verifyEmailForRecipeUserIfLinkedAccountsAreVerified: (input: { + user: User; + recipeUserId: RecipeUserId; + userContext: any; + }) => Promise; +} diff --git a/lib/build/recipe/accountlinking/recipe.js b/lib/build/recipe/accountlinking/recipe.js new file mode 100644 index 000000000..b13ab3527 --- /dev/null +++ b/lib/build/recipe/accountlinking/recipe.js @@ -0,0 +1,689 @@ +"use strict"; +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const recipeModule_1 = __importDefault(require("../../recipeModule")); +const utils_1 = require("./utils"); +const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); +const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); +const querier_1 = require("../../querier"); +const error_1 = __importDefault(require("../../error")); +const supertokens_1 = __importDefault(require("../../supertokens")); +const processState_1 = require("../../processState"); +const logger_1 = require("../../logger"); +const recipe_1 = __importDefault(require("../emailverification/recipe")); +class Recipe extends recipeModule_1.default { + constructor(recipeId, appInfo, config, _recipes, _ingredients) { + super(recipeId, appInfo); + // this function returns the user ID for which the session will be created. + this.createPrimaryUserIdOrLinkAccounts = async ({ tenantId, user, userContext }) => { + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts called"); + // TODO: fix this + if (user === undefined) { + // This can come here if the user is using session + email verification + // recipe with a user ID that is not known to supertokens. In this case, + // we do not allow linking for such users. + return user; + } + if (user.isPrimaryUser) { + return user; + } + // now we try and find a linking candidate. + let primaryUser = await this.getPrimaryUserThatCanBeLinkedToRecipeUserId({ + tenantId, + user: user, + userContext, + }); + if (primaryUser === undefined) { + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts user can become a primary user"); + // this means that this can become a primary user. + // we can use the 0 index cause this user is + // not a primary user. + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + user.loginMethods[0], + undefined, + tenantId, + userContext + ); + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logger_1.logDebugMessage( + "createPrimaryUserIdOrLinkAccounts not creating primary user because shouldAutomaticallyLink is false" + ); + return user; + } + if (shouldDoAccountLinking.shouldRequireVerification && !user.loginMethods[0].verified) { + logger_1.logDebugMessage( + "createPrimaryUserIdOrLinkAccounts not creating primary user because shouldRequireVerification is true but the login method is not verified" + ); + return user; + } + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts creating primary user"); + let createPrimaryUserResult = await this.recipeInterfaceImpl.createPrimaryUser({ + recipeUserId: user.loginMethods[0].recipeUserId, + userContext, + }); + if (createPrimaryUserResult.status === "OK") { + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts created primary user"); + return createPrimaryUserResult.user; + } + // status is "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" or "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" + // if it comes here, it means that the recipe + // user ID is already linked to another primary + // user id (race condition), or that some other + // primary user ID exists with the same email / phone number (again, race condition). + // So we do recursion here to try again. + return await this.createPrimaryUserIdOrLinkAccounts({ + tenantId, + user, + userContext, + }); + } else { + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts got linking candidate"); + if (primaryUser.id === user.id) { + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts user already linked"); + // This can only happen cause of a race condition cause we already check + // if the input recipeUserId is a primary user early on in the function. + return user; + } + // this means that we found a primary user ID which can be linked to this recipe user ID. So we try and link them. + // we can use the 0 index cause this user is + // not a primary user. + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + user.loginMethods[0], + primaryUser, + tenantId, + userContext + ); + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logger_1.logDebugMessage( + "createPrimaryUserIdOrLinkAccounts not linking because shouldAutomaticallyLink is false" + ); + return user; + } + if (shouldDoAccountLinking.shouldRequireVerification && !user.loginMethods[0].verified) { + logger_1.logDebugMessage( + "createPrimaryUserIdOrLinkAccounts not linking because shouldRequireVerification is true but the login method is not verified" + ); + return user; + } + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts linking"); + let linkAccountsResult = await this.recipeInterfaceImpl.linkAccounts({ + recipeUserId: user.loginMethods[0].recipeUserId, + primaryUserId: primaryUser.id, + userContext, + }); + if (linkAccountsResult.status === "OK") { + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts successfully linked"); + return linkAccountsResult.user; + } else if ( + linkAccountsResult.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + // this can happen cause of a race condition + // wherein the recipe user ID get's linked to + // some other primary user whilst this function is running. + // But this is OK, we can just return the primary user it is linked to + logger_1.logDebugMessage("createPrimaryUserIdOrLinkAccounts already linked to another user"); + return linkAccountsResult.user; + } else if (linkAccountsResult.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + logger_1.logDebugMessage( + "createPrimaryUserIdOrLinkAccounts linking failed because of a race condition" + ); + // this can be possible during a race condition wherein the primary user + // that we fetched somehow is no more a primary user. This can happen if + // the unlink function was called in parallel on that user. So we can just retry + return await this.createPrimaryUserIdOrLinkAccounts({ + tenantId, + user, + userContext, + }); + } else { + logger_1.logDebugMessage( + "createPrimaryUserIdOrLinkAccounts linking failed because of a race condition" + ); + // status is "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + // it can come here if the recipe user ID + // can't be linked to the primary user ID cause + // the email / phone number is associated with some other primary user ID. + // This can happen due to a race condition in which + // the email has changed from one primary user to another during this function call, + // or it can happen if the accounts to link table + // contains a user which this is supposed to + // be linked to, but we can't link it cause during the time the user was added to that table and now, some other primary user has + // got this email. + // So we try again, but without caring about + // the accounts to link table (cause they we will end up in an infinite recursion). + return await this.createPrimaryUserIdOrLinkAccounts({ + tenantId, + user, + userContext, + }); + } + } + }; + this.getPrimaryUserThatCanBeLinkedToRecipeUserId = async ({ tenantId, user, userContext }) => { + // first we check if this user itself is a + // primary user or not. If it is, we return that. + if (user.isPrimaryUser) { + return user; + } + // finally, we try and find a primary user based on + // the email / phone number / third party ID. + let users = await this.recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo: user.loginMethods[0], + doUnionOfAccountInfo: true, + userContext, + }); + logger_1.logDebugMessage( + `getPrimaryUserThatCanBeLinkedToRecipeUserId found ${users.length} matching users` + ); + let pUsers = users.filter((u) => u.isPrimaryUser); + logger_1.logDebugMessage( + `getPrimaryUserThatCanBeLinkedToRecipeUserId found ${pUsers.length} matching primary users` + ); + if (pUsers.length > 1) { + // this means that the new user has account info such that it's + // spread across multiple primary user IDs. In this case, even + // if we return one of them, it won't be able to be linked anyway + // cause if we did, it would mean 2 primary users would have the + // same account info. So we return undefined + /** + * this being said, with the current set of auth recipes, it should + * never come here - cause: + * ----> If the recipeuserid is a passwordless user, then it can have either a phone + * email or both. If it has just one of them, then anyway 2 primary users can't + * exist with the same phone number / email. If it has both, then the only way + * that it can have multiple primary users returned is if there is another passwordless + * primary user with the same phone number - which is not possible, cause phone + * numbers are unique across passwordless users. + * + * ----> If the input is a third party user, then it has third party info and an email. Now there can be able to primary user with the same email, but + * there can't be another thirdparty user with the same third party info (since that is unique). + * Nor can there an email password primary user with the same email along with another + * thirdparty primary user with the same email (since emails can't be the same across primary users). + * + * ----> If the input is an email password user, then it has an email. There can't be multiple primary users with the same email anyway. + */ + throw new Error("You found a bug. Please report it on github.com/supertokens/supertokens-node"); + } + return pUsers.length === 0 ? undefined : pUsers[0]; + }; + this.isSignInAllowed = async ({ user, tenantId, userContext }) => { + processState_1.ProcessState.getInstance().addState(processState_1.PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED); + if (user.isPrimaryUser || user.loginMethods[0].verified) { + return true; + } + return this.isSignInUpAllowedHelper({ + accountInfo: user.loginMethods[0], + isVerified: user.loginMethods[0].verified, + tenantId, + isSignIn: true, + userContext, + }); + }; + this.isSignUpAllowed = async ({ newUser, isVerified, tenantId, userContext }) => { + processState_1.ProcessState.getInstance().addState(processState_1.PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED); + if (newUser.email !== undefined && newUser.phoneNumber !== undefined) { + // we do this check cause below when we call listUsersByAccountInfo, + // we only pass in one of email or phone number + throw new Error("Please pass one of email or phone number, not both"); + } + return this.isSignInUpAllowedHelper({ + accountInfo: newUser, + isVerified, + tenantId, + userContext, + isSignIn: false, + }); + }; + this.isSignInUpAllowedHelper = async ({ accountInfo, isVerified, tenantId, isSignIn, userContext }) => { + processState_1.ProcessState.getInstance().addState( + processState_1.PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED + ); + // since this is a recipe level user, we have to do the following checks + // before allowing sign in. We do these checks cause sign in also attempts + // account linking: + // - If there is no primary user for this user's account info, then + // we check if any recipe user exist with the same info and it's not verified. If we + // find one, we disallow signing in cause when this user becomes a primary user, + // it may cause that other recipe user to be linked to this and if that recipe user + // is owned by an attacker, it will lead to an account take over case + // - If there exists another primary user, and if this user is not verified, we will + // disallow cause if after sign in, this user sends an email verification email + // to the email, then the primary user may click on it by mistake and get their account + // taken over. + // - If there exists another primary user, and that user's email is not verified, + // then we disallow sign in cause that primary user may be owned by an attacker + // and after this email is verified, it will link to that account causing account + // takeover. + // we find other accounts based on the email / phone number. + // we do not pass in third party info, or both email or phone + // cause we want to guarantee that the output array contains just one + // primary user. + let users = await this.recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo, + doUnionOfAccountInfo: true, + userContext, + }); + if (users.length === 0) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning true because no user with given account info" + ); + // this is a brand new email / phone number, so we allow sign up. + return true; + } + if (users.length === 1 && isSignIn) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning true because this is sign in and there is only a single user with the given account info" + ); + return true; + } + // now we check if there exists some primary user with the same email / phone number + // such that that info is not verified for that account. In this case, we do not allow + // sign up cause we cannot link this new account to that primary account yet (since + // the email / phone is unverified - this is to prevent an attack where an attacker + // might have access to the unverified account's primary user and we do not want to + // link this account to that one), and we can't make this a primary user either (since + // then there would be two primary users with the same email / phone number - which is + // not allowed..) + const primaryUsers = users.filter((u) => u.isPrimaryUser); + if (primaryUsers.length === 0) { + logger_1.logDebugMessage("isSignInUpAllowedHelper no primary user exists"); + // since there is no primary user, it means that this user, if signed up, will end up + // being the primary user. In this case, we check if any of the non primary user's + // are in an unverified state having the same account info, and if they are, then we + // disallow this sign up, cause if the user becomes the primary user, and then the other + // account which is unverified sends an email verification email, the legit user might + // click on the link and that account (which was unverified and could have been controlled + // by an attacker), will end up getting linked to this account. + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + accountInfo, + undefined, + tenantId, + userContext + ); + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning true because account linking is disabled" + ); + return true; + } + if (!shouldDoAccountLinking.shouldRequireVerification) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning true because dec does not require email verification" + ); + // the dev says they do not care about verification before account linking + // so we are OK with the risk mentioned above. + return true; + } + let shouldAllow = true; + for (let i = 0; i < users.length; i++) { + let currUser = users[i]; // all these are not primary users, so we can use + // loginMethods[0] to get the account info. + let thisIterationIsVerified = false; + if (accountInfo.email !== undefined) { + if ( + currUser.loginMethods[0].hasSameEmailAs(accountInfo.email) && + currUser.loginMethods[0].verified + ) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper found same email for another user and verified" + ); + thisIterationIsVerified = true; + } + } + if (accountInfo.phoneNumber !== undefined) { + if ( + currUser.loginMethods[0].hasSamePhoneNumberAs(accountInfo.phoneNumber) && + currUser.loginMethods[0].verified + ) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper found same phone number for another user and verified" + ); + thisIterationIsVerified = true; + } + } + if (!thisIterationIsVerified) { + // even if one of the users is not verified, we do not allow sign up (see why above). + // Sure, this allows attackers to create email password accounts with an email + // to block actual users from signing up, but that's ok, since those + // users will just see an email already exists error and then will try another + // login method. They can also still just go through the password reset flow + // and then gain access to their email password account (which can then be verified). + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning false cause one of the other recipe level users is not verified" + ); + shouldAllow = false; + break; + } + } + processState_1.ProcessState.getInstance().addState( + processState_1.PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS + ); + logger_1.logDebugMessage("isSignInUpAllowedHelper returning " + shouldAllow); + return shouldAllow; + } else { + if (primaryUsers.length > 1) { + throw new Error( + "You have found a bug. Please report to https://github.com/supertokens/supertokens-node/issues" + ); + } + let primaryUser = primaryUsers[0]; + logger_1.logDebugMessage("isSignInUpAllowedHelper primary user found"); + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + accountInfo, + primaryUser, + tenantId, + userContext + ); + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning true because account linking is disabled" + ); + return true; + } + if (!shouldDoAccountLinking.shouldRequireVerification) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning true because dec does not require email verification" + ); + // the dev says they do not care about verification before account linking + // so we can link this new user to the primary user post recipe user creation + // even if that user's email / phone number is not verified. + return true; + } + if (!isVerified) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning false because new user's email is not verified, and primary user with the same email was found." + ); + // this will exist early with a false here cause it means that + // if we come here, the newUser will be linked to the primary user post email + // verification. Whilst this seems OK, there is a risk that the actual user might + // click on the email verification link thinking that they it's for their existing + // (legit) account, and then the attacker (who signed up with email password maybe) + // will have access to the account - cause email verification will cause account linking. + // We do this AFTER calling shouldDoAutomaticAccountLinking cause + // in case email verification is not required, then linking should not be + // an issue anyway. + return false; + } + // we check for even if one is verified as opposed to all being unverified cause + // even if one is verified, we know that the email / phone number is owned by the + // primary account holder. + // we only check this for primary user and not other users in the users array cause + // they are all recipe users. The reason why we ignore them is cause, in normal + // situations, they should not exist cause: + // - if primary user was created first, then the recipe user creation would not + // be allowed via unverified means of login method (like email password). + // - if recipe user was created first, and is unverified, then the primary user + // sign up should not be possible either. + for (let i = 0; i < primaryUser.loginMethods.length; i++) { + let lM = primaryUser.loginMethods[i]; + if (lM.email !== undefined) { + if (lM.hasSameEmailAs(accountInfo.email) && lM.verified) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning true cause found same email for primary user and verified" + ); + return true; + } + } + if (lM.phoneNumber !== undefined) { + if (lM.hasSamePhoneNumberAs(accountInfo.phoneNumber) && lM.verified) { + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning true cause found same phone number for primary user and verified" + ); + return true; + } + } + } + logger_1.logDebugMessage( + "isSignInUpAllowedHelper returning false cause primary user does not have the same email or phone number that is verified" + //"isSignInUpAllowedHelper returning false because new user's email is not verified, and primary user with the same email was found." + ); + return false; + } + }; + this.isEmailChangeAllowed = async (input) => { + /** + * The purpose of this function is to check that if a recipe user ID's email + * can be changed or not. There are two conditions for when it can't be changed: + * - If the recipe user is a primary user, then we need to check that the new email + * doesn't belong to any other primary user. If it does, we disallow the change + * since multiple primary user's can't have the same account info. + * + * - If the recipe user is NOT a primary user, and if isVerified is false, then + * we check if there exists a primary user with the same email, and if it does + * we disallow the email change cause if this email is changed, and an email + * verification email is sent, then the primary user may end up clicking + * on the link by mistake, causing account linking to happen which can result + * in account take over if this recipe user is malicious. + */ + let user = input.user; + if (user === undefined) { + throw new Error("Passed in recipe user id does not exist"); + } + for (const tenantId of user.tenantIds) { + let existingUsersWithNewEmail = await this.recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId: user.tenantIds[0], + accountInfo: { + email: input.newEmail, + }, + doUnionOfAccountInfo: false, + userContext: input.userContext, + }); + let primaryUserForNewEmail = existingUsersWithNewEmail.filter((u) => u.isPrimaryUser); + if (primaryUserForNewEmail.length > 1) { + throw new Error("You found a bug. Please report it on github.com/supertokens/supertokens-node"); + } + if (user.isPrimaryUser) { + // this is condition one from the above comment. + if (primaryUserForNewEmail.length === 1 && primaryUserForNewEmail[0].id !== user.id) { + logger_1.logDebugMessage( + "isEmailChangeAllowed: returning false cause email change will lead to two primary users having same email" + ); + return false; + } + logger_1.logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is primary and new email doesn't belong to any other primary user` + ); + continue; + } else { + if (input.isVerified) { + logger_1.logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary and new email is verified` + ); + continue; + } + if (user.loginMethods[0].email === input.newEmail) { + logger_1.logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary and new email is same as the older one` + ); + continue; + } + if (primaryUserForNewEmail.length === 1) { + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + user.loginMethods[0], + primaryUserForNewEmail[0], + tenantId, + input.userContext + ); + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logger_1.logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary there exists a primary user exists with the new email, but the dev does not have account linking enabled.` + ); + continue; + } + if (!shouldDoAccountLinking.shouldRequireVerification) { + logger_1.logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary there exists a primary user exists with the new email, but the dev does not require email verification.` + ); + continue; + } + logger_1.logDebugMessage( + "isEmailChangeAllowed: returning false cause input user is not a primary there exists a primary user exists with the new email." + ); + return false; + } + logger_1.logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary no primary user exists with the new email` + ); + continue; + } + } + logger_1.logDebugMessage( + "isEmailChangeAllowed: returning true cause email change can happen on all tenants the user is part of" + ); + return true; + }; + this.verifyEmailForRecipeUserIfLinkedAccountsAreVerified = async (input) => { + try { + recipe_1.default.getInstanceOrThrowError(); + } catch (ignored) { + // if email verification recipe is not initialized, we do a no-op + return; + } + // This is just a helper function cause it's called in many places + // like during sign up, sign in and post linking accounts. + // This is not exposed to the developer as it's called in the relevant + // recipe functions. + // We do not do this in the core cause email verification is a different + // recipe. + // Finally, we only mark the email of this recipe user as verified and not + // the other recipe users in the primary user (if this user's email is verified), + // cause when those other users sign in, this function will be called for them anyway + if (input.user.isPrimaryUser) { + let recipeUserEmail = undefined; + let isAlreadyVerified = false; + input.user.loginMethods.forEach((lm) => { + if (lm.recipeUserId.getAsString() === input.recipeUserId.getAsString()) { + recipeUserEmail = lm.email; + isAlreadyVerified = lm.verified; + } + }); + if (recipeUserEmail !== undefined) { + if (isAlreadyVerified) { + return; + } + let shouldVerifyEmail = false; + input.user.loginMethods.forEach((lm) => { + if (lm.hasSameEmailAs(recipeUserEmail) && lm.verified) { + shouldVerifyEmail = true; + } + }); + if (shouldVerifyEmail) { + let resp = await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.createEmailVerificationToken({ + // While the token we create here is tenant specific, the verification status is not + // So we can use any tenantId the user is associated with here as long as we use the + // same in the verifyEmailUsingToken call + tenantId: input.user.tenantIds[0], + recipeUserId: input.recipeUserId, + email: recipeUserEmail, + userContext: input.userContext, + }); + if (resp.status === "OK") { + // we purposely pass in false below cause we don't want account + // linking to happen + await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ + // See comment about tenantId in the createEmailVerificationToken params + tenantId: input.user.tenantIds[0], + token: resp.token, + attemptAccountLinking: false, + userContext: input.userContext, + }); + } + } + } + } + }; + this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); + { + let builder = new supertokens_js_override_1.default( + recipeImplementation_1.default( + querier_1.Querier.getNewInstanceOrThrowError(recipeId), + this.config, + this + ) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + } + static init(config) { + return (appInfo) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe( + Recipe.RECIPE_ID, + appInfo, + config, + {}, + { + emailDelivery: undefined, + } + ); + return Recipe.instance; + } else { + throw new Error("AccountLinking recipe has already been initialised. Please check your code for bugs."); + } + }; + } + // we auto init the account linking recipe here cause we always require this + // to be initialized even if the user has not initialized it. + // The side effect of this is that if there are any APIs or errors specific to this recipe, + // those won't be handled by the supertokens middleware and error handler (cause this recipe + // is not in the recipeList). + static getInstance() { + if (Recipe.instance === undefined) { + Recipe.init()( + supertokens_1.default.getInstanceOrThrowError().appInfo, + supertokens_1.default.getInstanceOrThrowError().isInServerlessEnv + ); + } + return Recipe.instance; + } + getAPIsHandled() { + // APIs won't be added to the supertokens middleware cause we are auto initializing + // it in getInstance function + return []; + } + handleAPIRequest(_id, _tenantId, _req, _response, _path, _method) { + throw new Error("Should never come here"); + } + handleError(error, _request, _response) { + // Errors won't come here cause we are auto initializing + // it in getInstance function + throw error; + } + getAllCORSHeaders() { + return []; + } + isErrorFromThisRecipe(err) { + return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + } + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + Recipe.instance = undefined; + } +} +exports.default = Recipe; +Recipe.instance = undefined; +Recipe.RECIPE_ID = "accountlinking"; diff --git a/lib/build/recipe/accountlinking/recipeImplementation.d.ts b/lib/build/recipe/accountlinking/recipeImplementation.d.ts new file mode 100644 index 000000000..76484c9cd --- /dev/null +++ b/lib/build/recipe/accountlinking/recipeImplementation.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { RecipeInterface, TypeNormalisedInput } from "./types"; +import { Querier } from "../../querier"; +import type AccountLinkingRecipe from "./recipe"; +export default function getRecipeImplementation( + querier: Querier, + config: TypeNormalisedInput, + recipeInstance: AccountLinkingRecipe +): RecipeInterface; diff --git a/lib/build/recipe/accountlinking/recipeImplementation.js b/lib/build/recipe/accountlinking/recipeImplementation.js new file mode 100644 index 000000000..03043931b --- /dev/null +++ b/lib/build/recipe/accountlinking/recipeImplementation.js @@ -0,0 +1,163 @@ +"use strict"; +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const user_1 = require("../../user"); +function getRecipeImplementation(querier, config, recipeInstance) { + return { + getUsers: async function ({ timeJoinedOrder, limit, paginationToken, includeRecipeIds, query }) { + let includeRecipeIdsStr = undefined; + if (includeRecipeIds !== undefined) { + includeRecipeIdsStr = includeRecipeIds.join(","); + } + let response = await querier.sendGetRequest( + new normalisedURLPath_1.default("/users"), + Object.assign( + { + includeRecipeIds: includeRecipeIdsStr, + timeJoinedOrder: timeJoinedOrder, + limit: limit, + paginationToken: paginationToken, + }, + query + ) + ); + return { + users: response.users.map((u) => new user_1.User(u)), + nextPaginationToken: response.nextPaginationToken, + }; + }, + canCreatePrimaryUser: async function ({ recipeUserId }) { + return await querier.sendGetRequest( + new normalisedURLPath_1.default("/recipe/accountlinking/user/primary/check"), + { + recipeUserId: recipeUserId.getAsString(), + } + ); + }, + createPrimaryUser: async function ({ recipeUserId }) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/accountlinking/user/primary"), + { + recipeUserId: recipeUserId.getAsString(), + } + ); + if (response.status === "OK") { + response.user = new user_1.User(response.user); + } + return response; + }, + canLinkAccounts: async function ({ recipeUserId, primaryUserId }) { + let result = await querier.sendGetRequest( + new normalisedURLPath_1.default("/recipe/accountlinking/user/link/check"), + { + recipeUserId: recipeUserId.getAsString(), + primaryUserId, + } + ); + return result; + }, + linkAccounts: async function ({ recipeUserId, primaryUserId, userContext }) { + const accountsLinkingResult = await querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/accountlinking/user/link"), + { + recipeUserId: recipeUserId.getAsString(), + primaryUserId, + } + ); + if ( + ["OK", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"].includes( + accountsLinkingResult.status + ) + ) { + accountsLinkingResult.user = new user_1.User(accountsLinkingResult.user); + } + if (accountsLinkingResult.status === "OK") { + let user = accountsLinkingResult.user; + if (!accountsLinkingResult.accountsAlreadyLinked) { + await recipeInstance.verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: user, + recipeUserId, + userContext, + }); + const updatedUser = await this.getUser({ + userId: primaryUserId, + userContext, + }); + if (updatedUser === undefined) { + throw Error("this error should never be thrown"); + } + user = updatedUser; + let loginMethodInfo = user.loginMethods.find( + (u) => u.recipeUserId.getAsString() === recipeUserId.getAsString() + ); + if (loginMethodInfo === undefined) { + throw Error("this error should never be thrown"); + } + await config.onAccountLinked(user, loginMethodInfo, userContext); + } + accountsLinkingResult.user = user; + } + return accountsLinkingResult; + }, + unlinkAccount: async function ({ recipeUserId }) { + let accountsUnlinkingResult = await querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/accountlinking/user/unlink"), + { + recipeUserId: recipeUserId.getAsString(), + } + ); + return accountsUnlinkingResult; + }, + getUser: async function ({ userId }) { + let result = await querier.sendGetRequest(new normalisedURLPath_1.default("/user/id"), { + userId, + }); + if (result.status === "OK") { + return new user_1.User(result.user); + } + return undefined; + }, + listUsersByAccountInfo: async function ({ tenantId, accountInfo, doUnionOfAccountInfo }) { + var _a, _b; + let result = await querier.sendGetRequest( + new normalisedURLPath_1.default( + `${tenantId !== null && tenantId !== void 0 ? tenantId : "public"}/users/by-accountinfo` + ), + { + email: accountInfo.email, + phoneNumber: accountInfo.phoneNumber, + thirdPartyId: (_a = accountInfo.thirdParty) === null || _a === void 0 ? void 0 : _a.id, + thirdPartyUserId: (_b = accountInfo.thirdParty) === null || _b === void 0 ? void 0 : _b.userId, + doUnionOfAccountInfo, + } + ); + return result.users.map((u) => new user_1.User(u)); + }, + deleteUser: async function ({ userId, removeAllLinkedAccounts }) { + return await querier.sendPostRequest(new normalisedURLPath_1.default("/user/remove"), { + userId, + removeAllLinkedAccounts, + }); + }, + }; +} +exports.default = getRecipeImplementation; diff --git a/lib/build/recipe/accountlinking/types.d.ts b/lib/build/recipe/accountlinking/types.d.ts new file mode 100644 index 000000000..ee66ae675 --- /dev/null +++ b/lib/build/recipe/accountlinking/types.d.ts @@ -0,0 +1,189 @@ +// @ts-nocheck +import OverrideableBuilder from "supertokens-js-override"; +import type { User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; +export declare type TypeInput = { + onAccountLinked?: (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => Promise; + shouldDoAutomaticAccountLinking?: ( + newAccountInfo: AccountInfoWithRecipeId & { + recipeUserId?: RecipeUserId; + }, + user: User | undefined, + tenantId: string, + userContext: any + ) => Promise< + | { + shouldAutomaticallyLink: false; + } + | { + shouldAutomaticallyLink: true; + shouldRequireVerification: boolean; + } + >; + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + }; +}; +export declare type TypeNormalisedInput = { + onAccountLinked: (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => Promise; + shouldDoAutomaticAccountLinking: ( + newAccountInfo: AccountInfoWithRecipeId & { + recipeUserId?: RecipeUserId; + }, + user: User | undefined, + tenantId: string, + userContext: any + ) => Promise< + | { + shouldAutomaticallyLink: false; + } + | { + shouldAutomaticallyLink: true; + shouldRequireVerification: boolean; + } + >; + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + }; +}; +export declare type RecipeInterface = { + getUsers: (input: { + timeJoinedOrder: "ASC" | "DESC"; + limit?: number; + paginationToken?: string; + includeRecipeIds?: string[]; + query?: { + [key: string]: string; + }; + userContext: any; + }) => Promise<{ + users: User[]; + nextPaginationToken?: string; + }>; + canCreatePrimaryUser: (input: { + recipeUserId: RecipeUserId; + userContext: any; + }) => Promise< + | { + status: "OK"; + wasAlreadyAPrimaryUser: boolean; + } + | { + status: + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + >; + createPrimaryUser: (input: { + recipeUserId: RecipeUserId; + userContext: any; + }) => Promise< + | { + status: "OK"; + user: User; + wasAlreadyAPrimaryUser: boolean; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + >; + canLinkAccounts: (input: { + recipeUserId: RecipeUserId; + primaryUserId: string; + userContext: any; + }) => Promise< + | { + status: "OK"; + accountsAlreadyLinked: boolean; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + description: string; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + | { + status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; + } + >; + linkAccounts: (input: { + recipeUserId: RecipeUserId; + primaryUserId: string; + userContext: any; + }) => Promise< + | { + status: "OK"; + accountsAlreadyLinked: boolean; + user: User; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + user: User; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + | { + status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; + } + >; + unlinkAccount: (input: { + recipeUserId: RecipeUserId; + userContext: any; + }) => Promise<{ + status: "OK"; + wasRecipeUserDeleted: boolean; + wasLinked: boolean; + }>; + getUser: (input: { userId: string; userContext: any }) => Promise; + listUsersByAccountInfo: (input: { + tenantId: string; + accountInfo: AccountInfo; + doUnionOfAccountInfo: boolean; + userContext: any; + }) => Promise; + deleteUser: (input: { + userId: string; + removeAllLinkedAccounts: boolean; + userContext: any; + }) => Promise<{ + status: "OK"; + }>; +}; +export declare type AccountInfo = { + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; +}; +export declare type AccountInfoWithRecipeId = { + recipeId: "emailpassword" | "thirdparty" | "passwordless"; +} & AccountInfo; +export declare type RecipeLevelUser = { + tenantIds: string[]; + timeJoined: number; + recipeUserId: RecipeUserId; +} & AccountInfoWithRecipeId; diff --git a/lib/build/recipe/accountlinking/types.js b/lib/build/recipe/accountlinking/types.js new file mode 100644 index 000000000..8749b03ea --- /dev/null +++ b/lib/build/recipe/accountlinking/types.js @@ -0,0 +1,16 @@ +"use strict"; +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/accountlinking/utils.d.ts b/lib/build/recipe/accountlinking/utils.d.ts new file mode 100644 index 000000000..8e838c081 --- /dev/null +++ b/lib/build/recipe/accountlinking/utils.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +import type { NormalisedAppinfo } from "../../types"; +import type { TypeInput, TypeNormalisedInput } from "./types"; +export declare function validateAndNormaliseUserInput(_: NormalisedAppinfo, config?: TypeInput): TypeNormalisedInput; diff --git a/lib/build/recipe/accountlinking/utils.js b/lib/build/recipe/accountlinking/utils.js new file mode 100644 index 000000000..24c346b43 --- /dev/null +++ b/lib/build/recipe/accountlinking/utils.js @@ -0,0 +1,40 @@ +"use strict"; +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateAndNormaliseUserInput = void 0; +async function defaultOnAccountLinked() {} +async function defaultShouldDoAutomaticAccountLinking() { + return { + shouldAutomaticallyLink: false, + }; +} +function validateAndNormaliseUserInput(_, config) { + let onAccountLinked = + (config === null || config === void 0 ? void 0 : config.onAccountLinked) || defaultOnAccountLinked; + let shouldDoAutomaticAccountLinking = + (config === null || config === void 0 ? void 0 : config.shouldDoAutomaticAccountLinking) || + defaultShouldDoAutomaticAccountLinking; + let override = Object.assign( + { functions: (originalImplementation) => originalImplementation }, + config === null || config === void 0 ? void 0 : config.override + ); + return { + override, + onAccountLinked, + shouldDoAutomaticAccountLinking, + }; +} +exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/dashboard/api/analytics.js b/lib/build/recipe/dashboard/api/analytics.js index fc3776e70..f14cc300d 100644 --- a/lib/build/recipe/dashboard/api/analytics.js +++ b/lib/build/recipe/dashboard/api/analytics.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,69 +25,67 @@ const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath" const version_1 = require("../../../version"); const error_1 = __importDefault(require("../../../error")); const cross_fetch_1 = __importDefault(require("cross-fetch")); -function analyticsPost(_, ___, options, __) { - return __awaiter(this, void 0, void 0, function* () { - // If telemetry is disabled, dont send any event - if (!supertokens_1.default.getInstanceOrThrowError().telemetryEnabled) { - return { - status: "OK", - }; - } - const { email, dashboardVersion } = yield options.req.getJSONBody(); - if (email === undefined) { - throw new error_1.default({ - message: "Missing required property 'email'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (dashboardVersion === undefined) { - throw new error_1.default({ - message: "Missing required property 'dashboardVersion'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let telemetryId; - let numberOfUsers; - try { - let querier = querier_1.Querier.getNewInstanceOrThrowError(options.recipeId); - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/telemetry"), {}); - if (response.exists) { - telemetryId = response.telemetryId; - } - numberOfUsers = yield supertokens_1.default.getInstanceOrThrowError().getUserCount(); - } catch (_) { - // If either telemetry id API or user count fetch fails, no event should be sent - return { - status: "OK", - }; - } - const { apiDomain, websiteDomain, appName } = options.appInfo; - const data = { - websiteDomain: websiteDomain.getAsStringDangerous(), - apiDomain: apiDomain.getAsStringDangerous(), - appName, - sdk: "node", - sdkVersion: version_1.version, - telemetryId, - numberOfUsers, - email, - dashboardVersion, +async function analyticsPost(_, ___, options, __) { + // If telemetry is disabled, dont send any event + if (!supertokens_1.default.getInstanceOrThrowError().telemetryEnabled) { + return { + status: "OK", }; - try { - yield cross_fetch_1.default("https://api.supertokens.com/0/st/telemetry", { - method: "POST", - body: JSON.stringify(data), - headers: { - "api-version": "3", - "content-type": "application/json; charset=utf-8", - }, - }); - } catch (e) { - // Ignored + } + const { email, dashboardVersion } = await options.req.getJSONBody(); + if (email === undefined) { + throw new error_1.default({ + message: "Missing required property 'email'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (dashboardVersion === undefined) { + throw new error_1.default({ + message: "Missing required property 'dashboardVersion'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + let telemetryId; + let numberOfUsers; + try { + let querier = querier_1.Querier.getNewInstanceOrThrowError(options.recipeId); + let response = await querier.sendGetRequest(new normalisedURLPath_1.default("/telemetry"), {}); + if (response.exists) { + telemetryId = response.telemetryId; } + numberOfUsers = await supertokens_1.default.getInstanceOrThrowError().getUserCount(); + } catch (_) { + // If either telemetry id API or user count fetch fails, no event should be sent return { status: "OK", }; - }); + } + const { apiDomain, websiteDomain, appName } = options.appInfo; + const data = { + websiteDomain: websiteDomain.getAsStringDangerous(), + apiDomain: apiDomain.getAsStringDangerous(), + appName, + sdk: "node", + sdkVersion: version_1.version, + telemetryId, + numberOfUsers, + email, + dashboardVersion, + }; + try { + await cross_fetch_1.default("https://api.supertokens.com/0/st/telemetry", { + method: "POST", + body: JSON.stringify(data), + headers: { + "api-version": "3", + "content-type": "application/json; charset=utf-8", + }, + }); + } catch (e) { + // Ignored + } + return { + status: "OK", + }; } exports.default = analyticsPost; diff --git a/lib/build/recipe/dashboard/api/apiKeyProtector.js b/lib/build/recipe/dashboard/api/apiKeyProtector.js index 9d27c9d25..3437e153b 100644 --- a/lib/build/recipe/dashboard/api/apiKeyProtector.js +++ b/lib/build/recipe/dashboard/api/apiKeyProtector.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,32 +21,30 @@ Object.defineProperty(exports, "__esModule", { value: true }); */ const error_1 = __importDefault(require("../error")); const utils_1 = require("../utils"); -function apiKeyProtector(apiImplementation, tenantId, options, apiFunction, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let shouldAllowAccess = false; - try { - shouldAllowAccess = yield options.recipeImplementation.shouldAllowAccess({ - req: options.req, - config: options.config, - userContext, +async function apiKeyProtector(apiImplementation, tenantId, options, apiFunction, userContext) { + let shouldAllowAccess = false; + try { + shouldAllowAccess = await options.recipeImplementation.shouldAllowAccess({ + req: options.req, + config: options.config, + userContext, + }); + } catch (e) { + if (error_1.default.isErrorFromSuperTokens(e) && e.type === error_1.default.OPERATION_NOT_ALLOWED) { + options.res.setStatusCode(403); + options.res.sendJSONResponse({ + message: e.message, }); - } catch (e) { - if (error_1.default.isErrorFromSuperTokens(e) && e.type === error_1.default.OPERATION_NOT_ALLOWED) { - options.res.setStatusCode(403); - options.res.sendJSONResponse({ - message: e.message, - }); - return true; - } - throw e; - } - if (!shouldAllowAccess) { - utils_1.sendUnauthorisedAccess(options.res); return true; } - const response = yield apiFunction(apiImplementation, tenantId, options, userContext); - options.res.sendJSONResponse(response); + throw e; + } + if (!shouldAllowAccess) { + utils_1.sendUnauthorisedAccess(options.res); return true; - }); + } + const response = await apiFunction(apiImplementation, tenantId, options, userContext); + options.res.sendJSONResponse(response); + return true; } exports.default = apiKeyProtector; diff --git a/lib/build/recipe/dashboard/api/dashboard.js b/lib/build/recipe/dashboard/api/dashboard.js index 0ee40e726..8e1dd5170 100644 --- a/lib/build/recipe/dashboard/api/dashboard.js +++ b/lib/build/recipe/dashboard/api/dashboard.js @@ -13,49 +13,16 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); -function dashboard(apiImplementation, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.dashboardGET === undefined) { - return false; - } - const htmlString = yield apiImplementation.dashboardGET({ - options, - userContext, - }); - options.res.sendHTMLResponse(htmlString); - return true; +async function dashboard(apiImplementation, options, userContext) { + if (apiImplementation.dashboardGET === undefined) { + return false; + } + const htmlString = await apiImplementation.dashboardGET({ + options, + userContext, }); + options.res.sendHTMLResponse(htmlString); + return true; } exports.default = dashboard; diff --git a/lib/build/recipe/dashboard/api/implementation.js b/lib/build/recipe/dashboard/api/implementation.js index 730bcc1b0..e20eb31cd 100644 --- a/lib/build/recipe/dashboard/api/implementation.js +++ b/lib/build/recipe/dashboard/api/implementation.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -58,29 +27,28 @@ const utils_1 = require("../../../utils"); const constants_1 = require("../constants"); function getAPIImplementation() { return { - dashboardGET: function (input) { - return __awaiter(this, void 0, void 0, function* () { - const bundleBasePathString = yield input.options.recipeImplementation.getDashboardBundleLocation({ - userContext: input.userContext, - }); - const bundleDomain = - new normalisedURLDomain_1.default(bundleBasePathString).getAsStringDangerous() + - new normalisedURLPath_1.default(bundleBasePathString).getAsStringDangerous(); - let connectionURI = ""; - const superTokensInstance = supertokens_1.default.getInstanceOrThrowError(); - const authMode = input.options.config.authMode; - if (superTokensInstance.supertokens !== undefined) { - connectionURI = superTokensInstance.supertokens.connectionURI; - } - let isSearchEnabled = false; - const cdiVersion = yield querier_1.Querier.getNewInstanceOrThrowError( - input.options.recipeId - ).getAPIVersion(); - if (utils_1.maxVersion("2.20", cdiVersion) === cdiVersion) { - // Only enable search if CDI version is 2.20 or above - isSearchEnabled = true; - } - return ` + dashboardGET: async function (input) { + const bundleBasePathString = await input.options.recipeImplementation.getDashboardBundleLocation({ + userContext: input.userContext, + }); + const bundleDomain = + new normalisedURLDomain_1.default(bundleBasePathString).getAsStringDangerous() + + new normalisedURLPath_1.default(bundleBasePathString).getAsStringDangerous(); + let connectionURI = ""; + const superTokensInstance = supertokens_1.default.getInstanceOrThrowError(); + const authMode = input.options.config.authMode; + if (superTokensInstance.supertokens !== undefined) { + connectionURI = superTokensInstance.supertokens.connectionURI; + } + let isSearchEnabled = false; + const cdiVersion = await querier_1.Querier.getNewInstanceOrThrowError( + input.options.recipeId + ).getAPIVersion(); + if (utils_1.maxVersion("2.20", cdiVersion) === cdiVersion) { + // Only enable search if CDI version is 2.20 or above + isSearchEnabled = true; + } + return ` @@ -103,7 +71,6 @@ function getAPIImplementation() { `; - }); }, }; } diff --git a/lib/build/recipe/dashboard/api/listTenants.js b/lib/build/recipe/dashboard/api/listTenants.js index a6918eb8e..5f0e292e5 100644 --- a/lib/build/recipe/dashboard/api/listTenants.js +++ b/lib/build/recipe/dashboard/api/listTenants.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -37,27 +6,25 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); const multitenancy_1 = __importDefault(require("../../multitenancy")); -function listTenants(_, __, ___, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let tenantsRes = yield multitenancy_1.default.listAllTenants(userContext); - let finalTenants = []; - if (tenantsRes.status !== "OK") { - return tenantsRes; - } - for (let i = 0; i < tenantsRes.tenants.length; i++) { - let currentTenant = tenantsRes.tenants[i]; - let modifiedTenant = { - tenantId: currentTenant.tenantId, - emailPassword: currentTenant.emailPassword, - passwordless: currentTenant.passwordless, - thirdParty: currentTenant.thirdParty, - }; - finalTenants.push(modifiedTenant); - } - return { - status: "OK", - tenants: finalTenants, +async function listTenants(_, __, ___, userContext) { + let tenantsRes = await multitenancy_1.default.listAllTenants(userContext); + let finalTenants = []; + if (tenantsRes.status !== "OK") { + return tenantsRes; + } + for (let i = 0; i < tenantsRes.tenants.length; i++) { + let currentTenant = tenantsRes.tenants[i]; + let modifiedTenant = { + tenantId: currentTenant.tenantId, + emailPassword: currentTenant.emailPassword, + passwordless: currentTenant.passwordless, + thirdParty: currentTenant.thirdParty, }; - }); + finalTenants.push(modifiedTenant); + } + return { + status: "OK", + tenants: finalTenants, + }; } exports.default = listTenants; diff --git a/lib/build/recipe/dashboard/api/search/tagsGet.js b/lib/build/recipe/dashboard/api/search/tagsGet.js index 4a25b9dcc..977f7dc11 100644 --- a/lib/build/recipe/dashboard/api/search/tagsGet.js +++ b/lib/build/recipe/dashboard/api/search/tagsGet.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,10 +22,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.getSearchTags = void 0; const querier_1 = require("../../../../querier"); const normalisedURLPath_1 = __importDefault(require("../../../../normalisedURLPath")); -const getSearchTags = (_, ___, options, __) => - __awaiter(void 0, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(options.recipeId); - let tagsResponse = yield querier.sendGetRequest(new normalisedURLPath_1.default("/user/search/tags"), {}); - return tagsResponse; - }); +const getSearchTags = async (_, ___, options, __) => { + let querier = querier_1.Querier.getNewInstanceOrThrowError(options.recipeId); + let tagsResponse = await querier.sendGetRequest(new normalisedURLPath_1.default("/user/search/tags"), {}); + return tagsResponse; +}; exports.getSearchTags = getSearchTags; diff --git a/lib/build/recipe/dashboard/api/signIn.js b/lib/build/recipe/dashboard/api/signIn.js index 909b17fa4..9f5e59bde 100644 --- a/lib/build/recipe/dashboard/api/signIn.js +++ b/lib/build/recipe/dashboard/api/signIn.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -54,31 +23,26 @@ const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../../../error")); const querier_1 = require("../../../querier"); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); -function signIn(_, options, __) { - return __awaiter(this, void 0, void 0, function* () { - const { email, password } = yield options.req.getJSONBody(); - if (email === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'email'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (password === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'password'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - const signInResponse = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/dashboard/signin"), - { - email, - password, - } - ); - utils_1.send200Response(options.res, signInResponse); - return true; +async function signIn(_, options, __) { + const { email, password } = await options.req.getJSONBody(); + if (email === undefined) { + throw new error_1.default({ + message: "Missing required parameter 'email'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (password === undefined) { + throw new error_1.default({ + message: "Missing required parameter 'password'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); + const signInResponse = await querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/dashboard/signin"), { + email, + password, }); + utils_1.send200Response(options.res, signInResponse); + return true; } exports.default = signIn; diff --git a/lib/build/recipe/dashboard/api/signOut.js b/lib/build/recipe/dashboard/api/signOut.js index acc67eeff..0ae152417 100644 --- a/lib/build/recipe/dashboard/api/signOut.js +++ b/lib/build/recipe/dashboard/api/signOut.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,25 +22,21 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const querier_1 = require("../../../querier"); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); -function signOut(_, ___, options, __) { +async function signOut(_, ___, options, __) { var _a; - return __awaiter(this, void 0, void 0, function* () { - if (options.config.authMode === "api-key") { - utils_1.send200Response(options.res, { status: "OK" }); - } else { - const sessionIdFormAuthHeader = - (_a = options.req.getHeaderValue("authorization")) === null || _a === void 0 - ? void 0 - : _a.split(" ")[1]; - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - const sessionDeleteResponse = yield querier.sendDeleteRequest( - new normalisedURLPath_1.default("/recipe/dashboard/session"), - {}, - { sessionId: sessionIdFormAuthHeader } - ); - utils_1.send200Response(options.res, sessionDeleteResponse); - } - return true; - }); + if (options.config.authMode === "api-key") { + utils_1.send200Response(options.res, { status: "OK" }); + } else { + const sessionIdFormAuthHeader = + (_a = options.req.getHeaderValue("authorization")) === null || _a === void 0 ? void 0 : _a.split(" ")[1]; + let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); + const sessionDeleteResponse = await querier.sendDeleteRequest( + new normalisedURLPath_1.default("/recipe/dashboard/session"), + {}, + { sessionId: sessionIdFormAuthHeader } + ); + utils_1.send200Response(options.res, sessionDeleteResponse); + } + return true; } exports.default = signOut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userDelete.js b/lib/build/recipe/dashboard/api/userdetails/userDelete.js index 6596186fd..7840c2141 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userDelete.js +++ b/lib/build/recipe/dashboard/api/userdetails/userDelete.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -37,22 +6,25 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.userDelete = void 0; -const supertokens_1 = __importDefault(require("../../../../supertokens")); const error_1 = __importDefault(require("../../../../error")); -const userDelete = (_, ___, options, __) => - __awaiter(void 0, void 0, void 0, function* () { - const userId = options.req.getKeyValueFromQuery("userId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - yield supertokens_1.default.getInstanceOrThrowError().deleteUser({ - userId, +const __1 = require("../../../.."); +const userDelete = async (_, ___, options, __) => { + const userId = options.req.getKeyValueFromQuery("userId"); + let removeAllLinkedAccountsQueryValue = options.req.getKeyValueFromQuery("removeAllLinkedAccounts"); + if (removeAllLinkedAccountsQueryValue !== undefined) { + removeAllLinkedAccountsQueryValue = removeAllLinkedAccountsQueryValue.trim().toLowerCase(); + } + const removeAllLinkedAccounts = + removeAllLinkedAccountsQueryValue === undefined ? undefined : removeAllLinkedAccountsQueryValue === "true"; + if (userId === undefined || userId === "") { + throw new error_1.default({ + message: "Missing required parameter 'userId'", + type: error_1.default.BAD_INPUT_ERROR, }); - return { - status: "OK", - }; - }); + } + await __1.deleteUser(userId, removeAllLinkedAccounts); + return { + status: "OK", + }; +}; exports.userDelete = userDelete; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.d.ts b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.d.ts index ba6da64a6..f615e4ea2 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.d.ts @@ -1,3 +1,3 @@ // @ts-nocheck import { APIFunction } from "../../types"; -export declare const userEmailverifyGet: APIFunction; +export declare const userEmailVerifyGet: APIFunction; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.js b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.js index cfa913048..affe7a2ae 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.js +++ b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.js @@ -1,66 +1,39 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.userEmailverifyGet = void 0; +exports.userEmailVerifyGet = void 0; const error_1 = __importDefault(require("../../../../error")); -const recipe_1 = __importDefault(require("../../../emailverification/recipe")); const emailverification_1 = __importDefault(require("../../../emailverification")); -const userEmailverifyGet = (_, ___, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const req = options.req; - const userId = req.getKeyValueFromQuery("userId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (e) { - return { - status: "FEATURE_NOT_ENABLED_ERROR", - }; - } - const response = yield emailverification_1.default.isEmailVerified(userId, undefined, userContext); +const recipe_1 = __importDefault(require("../../../emailverification/recipe")); +const recipeUserId_1 = __importDefault(require("../../../../recipeUserId")); +const userEmailVerifyGet = async (_, ___, options, userContext) => { + const req = options.req; + const recipeUserId = req.getKeyValueFromQuery("recipeUserId"); + if (recipeUserId === undefined) { + throw new error_1.default({ + message: "Missing required parameter 'recipeUserId'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + try { + recipe_1.default.getInstanceOrThrowError(); + } catch (e) { return { - status: "OK", - isVerified: response, + status: "FEATURE_NOT_ENABLED_ERROR", }; - }); -exports.userEmailverifyGet = userEmailverifyGet; + } + const response = await emailverification_1.default.isEmailVerified( + new recipeUserId_1.default(recipeUserId), + undefined, + userContext + ); + return { + status: "OK", + isVerified: response, + }; +}; +exports.userEmailVerifyGet = userEmailVerifyGet; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js index 2551a2175..c7fb3b928 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js +++ b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,49 +8,53 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.userEmailVerifyPut = void 0; const error_1 = __importDefault(require("../../../../error")); const emailverification_1 = __importDefault(require("../../../emailverification")); -const userEmailVerifyPut = (_, tenantId, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - const verified = requestBody.verified; - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (verified === undefined || typeof verified !== "boolean") { - throw new error_1.default({ - message: "Required parameter 'verified' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); +const recipeUserId_1 = __importDefault(require("../../../../recipeUserId")); +const userEmailVerifyPut = async (_, tenantId, options, userContext) => { + const requestBody = await options.req.getJSONBody(); + const recipeUserId = requestBody.recipeUserId; + const verified = requestBody.verified; + if (recipeUserId === undefined || typeof recipeUserId !== "string") { + throw new error_1.default({ + message: "Required parameter 'recipeUserId' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (verified === undefined || typeof verified !== "boolean") { + throw new error_1.default({ + message: "Required parameter 'verified' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (verified) { + const tokenResponse = await emailverification_1.default.createEmailVerificationToken( + tenantId, + new recipeUserId_1.default(recipeUserId), + undefined, + userContext + ); + if (tokenResponse.status === "EMAIL_ALREADY_VERIFIED_ERROR") { + return { + status: "OK", + }; } - if (verified) { - const tokenResponse = yield emailverification_1.default.createEmailVerificationToken( - tenantId, - userId, - undefined, - userContext - ); - if (tokenResponse.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - return { - status: "OK", - }; - } - const verifyResponse = yield emailverification_1.default.verifyEmailUsingToken( - tenantId, - tokenResponse.token, - userContext - ); - if (verifyResponse.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { - // This should never happen because we consume the token immediately after creating it - throw new Error("Should not come here"); - } - } else { - yield emailverification_1.default.unverifyEmail(userId, undefined, userContext); + const verifyResponse = await emailverification_1.default.verifyEmailUsingToken( + tenantId, + tokenResponse.token, + userContext + ); + if (verifyResponse.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { + // This should never happen because we consume the token immediately after creating it + throw new Error("Should not come here"); } - return { - status: "OK", - }; - }); + } else { + await emailverification_1.default.unverifyEmail( + new recipeUserId_1.default(recipeUserId), + undefined, + userContext + ); + } + return { + status: "OK", + }; +}; exports.userEmailVerifyPut = userEmailVerifyPut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.js b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.js index c0b187706..6eed7be0d 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.js +++ b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,16 +8,29 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.userEmailVerifyTokenPost = void 0; const error_1 = __importDefault(require("../../../../error")); const emailverification_1 = __importDefault(require("../../../emailverification")); -const userEmailVerifyTokenPost = (_, tenantId, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - return yield emailverification_1.default.sendEmailVerificationEmail(tenantId, userId, undefined, userContext); - }); +const __1 = require("../../../.."); +const userEmailVerifyTokenPost = async (_, tenantId, options, userContext) => { + const requestBody = await options.req.getJSONBody(); + const recipeUserId = requestBody.recipeUserId; + if (recipeUserId === undefined || typeof recipeUserId !== "string") { + throw new error_1.default({ + message: "Required parameter 'recipeUserId' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + const user = await __1.getUser(recipeUserId, userContext); + if (!user) { + throw new error_1.default({ + message: "Unknown 'recipeUserId'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + return await emailverification_1.default.sendEmailVerificationEmail( + tenantId, + user.id, + __1.convertToRecipeUserId(recipeUserId), + undefined, + userContext + ); +}; exports.userEmailVerifyTokenPost = userEmailVerifyTokenPost; diff --git a/lib/build/recipe/dashboard/api/userdetails/userGet.js b/lib/build/recipe/dashboard/api/userdetails/userGet.js index 67b237620..9affeabb7 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userGet.js +++ b/lib/build/recipe/dashboard/api/userdetails/userGet.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -38,65 +7,42 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); exports.userGet = void 0; const error_1 = __importDefault(require("../../../../error")); -const utils_1 = require("../../utils"); const recipe_1 = __importDefault(require("../../../usermetadata/recipe")); const usermetadata_1 = __importDefault(require("../../../usermetadata")); -const userGet = (_, ___, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const userId = options.req.getKeyValueFromQuery("userId"); - const recipeId = options.req.getKeyValueFromQuery("recipeId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (recipeId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'recipeId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (!utils_1.isValidRecipeId(recipeId)) { - throw new error_1.default({ - message: "Invalid recipe id", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (!utils_1.isRecipeInitialised(recipeId)) { - return { - status: "RECIPE_NOT_INITIALISED", - }; - } - let user = (yield utils_1.getUserForRecipeId(userId, recipeId)).user; - if (user === undefined) { - return { - status: "NO_USER_FOUND_ERROR", - }; - } - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (_) { - user = Object.assign(Object.assign({}, user), { - firstName: "FEATURE_NOT_ENABLED", - lastName: "FEATURE_NOT_ENABLED", - }); - return { - status: "OK", - recipeId: recipeId, - user, - }; - } - const userMetaData = yield usermetadata_1.default.getUserMetadata(userId, userContext); - const { first_name, last_name } = userMetaData.metadata; - user = Object.assign(Object.assign({}, user), { - firstName: first_name === undefined ? "" : first_name, - lastName: last_name === undefined ? "" : last_name, +const __1 = require("../../../.."); +const userGet = async (_, ___, options, userContext) => { + const userId = options.req.getKeyValueFromQuery("userId"); + if (userId === undefined || userId === "") { + throw new error_1.default({ + message: "Missing required parameter 'userId'", + type: error_1.default.BAD_INPUT_ERROR, }); + } + let user = await __1.getUser(userId, userContext); + if (user === undefined) { + return { + status: "NO_USER_FOUND_ERROR", + }; + } + try { + recipe_1.default.getInstanceOrThrowError(); + } catch (_) { return { status: "OK", - recipeId: recipeId, - user, + user: Object.assign(Object.assign({}, user.toJson()), { + firstName: "FEATURE_NOT_ENABLED", + lastName: "FEATURE_NOT_ENABLED", + }), }; - }); + } + const userMetaData = await usermetadata_1.default.getUserMetadata(userId, userContext); + const { first_name, last_name } = userMetaData.metadata; + return { + status: "OK", + user: Object.assign(Object.assign({}, user.toJson()), { + firstName: first_name === undefined ? "" : first_name, + lastName: last_name === undefined ? "" : last_name, + }), + }; +}; exports.userGet = userGet; diff --git a/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.js b/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.js index 04f9dcb1f..b2e71cdce 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.js +++ b/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -40,26 +9,25 @@ exports.userMetaDataGet = void 0; const error_1 = __importDefault(require("../../../../error")); const recipe_1 = __importDefault(require("../../../usermetadata/recipe")); const usermetadata_1 = __importDefault(require("../../../usermetadata")); -const userMetaDataGet = (_, ___, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const userId = options.req.getKeyValueFromQuery("userId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (e) { - return { - status: "FEATURE_NOT_ENABLED_ERROR", - }; - } - const metaDataResponse = usermetadata_1.default.getUserMetadata(userId, userContext); +const userMetaDataGet = async (_, ___, options, userContext) => { + const userId = options.req.getKeyValueFromQuery("userId"); + if (userId === undefined || userId === "") { + throw new error_1.default({ + message: "Missing required parameter 'userId'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + try { + recipe_1.default.getInstanceOrThrowError(); + } catch (e) { return { - status: "OK", - data: (yield metaDataResponse).metadata, + status: "FEATURE_NOT_ENABLED_ERROR", }; - }); + } + const metaDataResponse = usermetadata_1.default.getUserMetadata(userId, userContext); + return { + status: "OK", + data: (await metaDataResponse).metadata, + }; +}; exports.userMetaDataGet = userMetaDataGet; diff --git a/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.js b/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.js index db731489a..0b77de89f 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.js +++ b/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -40,58 +9,57 @@ exports.userMetadataPut = void 0; const recipe_1 = __importDefault(require("../../../usermetadata/recipe")); const usermetadata_1 = __importDefault(require("../../../usermetadata")); const error_1 = __importDefault(require("../../../../error")); -const userMetadataPut = (_, ___, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - const data = requestBody.data; - // This is to throw an error early in case the recipe has not been initialised - recipe_1.default.getInstanceOrThrowError(); - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); +const userMetadataPut = async (_, ___, options, userContext) => { + const requestBody = await options.req.getJSONBody(); + const userId = requestBody.userId; + const data = requestBody.data; + // This is to throw an error early in case the recipe has not been initialised + recipe_1.default.getInstanceOrThrowError(); + if (userId === undefined || typeof userId !== "string") { + throw new error_1.default({ + message: "Required parameter 'userId' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (data === undefined || typeof data !== "string") { + throw new error_1.default({ + message: "Required parameter 'data' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + // Make sure that data is a valid JSON, this will throw + try { + let parsedData = JSON.parse(data); + if (typeof parsedData !== "object") { + throw new Error(); } - if (data === undefined || typeof data !== "string") { - throw new error_1.default({ - message: "Required parameter 'data' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); + if (Array.isArray(parsedData)) { + throw new Error(); } - // Make sure that data is a valid JSON, this will throw - try { - let parsedData = JSON.parse(data); - if (typeof parsedData !== "object") { - throw new Error(); - } - if (Array.isArray(parsedData)) { - throw new Error(); - } - if (parsedData === null) { - throw new Error(); - } - } catch (e) { - throw new error_1.default({ - message: "'data' must be a valid JSON body", - type: error_1.default.BAD_INPUT_ERROR, - }); + if (parsedData === null) { + throw new Error(); } - /** - * This API is meant to set the user metadata of a user. We delete the existing data - * before updating it because we want to make sure that shallow merging does not result - * in the data being incorrect - * - * For example if the old data is {test: "test", test2: "test2"} and the user wants to delete - * test2 from the data simply calling updateUserMetadata with {test: "test"} would not remove - * test2 because of shallow merging. - * - * Removing first ensures that the final data is exactly what the user wanted it to be - */ - yield usermetadata_1.default.clearUserMetadata(userId, userContext); - yield usermetadata_1.default.updateUserMetadata(userId, JSON.parse(data), userContext); - return { - status: "OK", - }; - }); + } catch (e) { + throw new error_1.default({ + message: "'data' must be a valid JSON body", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + /** + * This API is meant to set the user metadata of a user. We delete the existing data + * before updating it because we want to make sure that shallow merging does not result + * in the data being incorrect + * + * For example if the old data is {test: "test", test2: "test2"} and the user wants to delete + * test2 from the data simply calling updateUserMetadata with {test: "test"} would not remove + * test2 because of shallow merging. + * + * Removing first ensures that the final data is exactly what the user wanted it to be + */ + await usermetadata_1.default.clearUserMetadata(userId, userContext); + await usermetadata_1.default.updateUserMetadata(userId, JSON.parse(data), userContext); + return { + status: "OK", + }; +}; exports.userMetadataPut = userMetadataPut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js b/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js index e43b3c046..53a9afa91 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js +++ b/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -42,102 +11,83 @@ const recipe_1 = __importDefault(require("../../../emailpassword/recipe")); const emailpassword_1 = __importDefault(require("../../../emailpassword")); const recipe_2 = __importDefault(require("../../../thirdpartyemailpassword/recipe")); const thirdpartyemailpassword_1 = __importDefault(require("../../../thirdpartyemailpassword")); -const constants_1 = require("../../../emailpassword/constants"); -const userPasswordPut = (_, tenantId, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - const newPassword = requestBody.newPassword; - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (newPassword === undefined || typeof newPassword !== "string") { - throw new error_1.default({ - message: "Required parameter 'newPassword' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let recipeToUse; +const recipeUserId_1 = __importDefault(require("../../../../recipeUserId")); +const userPasswordPut = async (_, tenantId, options, userContext) => { + const requestBody = await options.req.getJSONBody(); + const recipeUserId = requestBody.recipeUserId; + const newPassword = requestBody.newPassword; + if (recipeUserId === undefined || typeof recipeUserId !== "string") { + throw new error_1.default({ + message: "Required parameter 'recipeUserId' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (newPassword === undefined || typeof newPassword !== "string") { + throw new error_1.default({ + message: "Required parameter 'newPassword' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + let recipeToUse; + try { + recipe_1.default.getInstanceOrThrowError(); + recipeToUse = "emailpassword"; + } catch (_) {} + if (recipeToUse === undefined) { try { - recipe_1.default.getInstanceOrThrowError(); - recipeToUse = "emailpassword"; + recipe_2.default.getInstanceOrThrowError(); + recipeToUse = "thirdpartyemailpassword"; } catch (_) {} - if (recipeToUse === undefined) { - try { - recipe_2.default.getInstanceOrThrowError(); - recipeToUse = "thirdpartyemailpassword"; - } catch (_) {} - } - if (recipeToUse === undefined) { - // This means that neither emailpassword or thirdpartyemailpassword is initialised + } + if (recipeToUse === undefined) { + // This means that neither emailpassword or thirdpartyemailpassword is initialised + throw new Error("Should never come here"); + } + if (recipeToUse === "emailpassword") { + const updateResponse = await emailpassword_1.default.updateEmailOrPassword({ + recipeUserId: new recipeUserId_1.default(recipeUserId), + password: newPassword, + tenantIdForPasswordPolicy: tenantId, + userContext, + }); + if ( + updateResponse.status === "UNKNOWN_USER_ID_ERROR" || + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + // Techincally it can but its an edge case so we assume that it wont throw new Error("Should never come here"); - } - if (recipeToUse === "emailpassword") { - let passwordFormFields = recipe_1.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_PASSWORD_ID); - let passwordValidationError = yield passwordFormFields[0].validate(newPassword, tenantId); - if (passwordValidationError !== undefined) { - return { - status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, - }; - } - const passwordResetToken = yield emailpassword_1.default.createResetPasswordToken( - tenantId, - userId, - userContext - ); - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { - // Techincally it can but its an edge case so we assume that it wont - throw new Error("Should never come here"); - } - const passwordResetResponse = yield emailpassword_1.default.resetPasswordUsingToken( - tenantId, - passwordResetToken.token, - newPassword, - userContext - ); - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } - return { - status: "OK", - }; - } - let passwordFormFields = recipe_2.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_PASSWORD_ID); - let passwordValidationError = yield passwordFormFields[0].validate(newPassword, tenantId); - if (passwordValidationError !== undefined) { + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { return { status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, + error: updateResponse.failureReason, }; } - const passwordResetToken = yield thirdpartyemailpassword_1.default.createResetPasswordToken( - tenantId, - userId, - userContext - ); - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { - // Techincally it can but its an edge case so we assume that it wont - throw new Error("Should never come here"); - } - const passwordResetResponse = yield thirdpartyemailpassword_1.default.resetPasswordUsingToken( - tenantId, - passwordResetToken.token, - newPassword, - userContext - ); - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } return { status: "OK", }; + } + const updateResponse = await thirdpartyemailpassword_1.default.updateEmailOrPassword({ + recipeUserId: new recipeUserId_1.default(recipeUserId), + password: newPassword, + tenantIdForPasswordPolicy: tenantId, + userContext, }); + if ( + updateResponse.status === "UNKNOWN_USER_ID_ERROR" || + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + // Techincally it can but its an edge case so we assume that it wont + throw new Error("Should never come here"); + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + return { + status: "INVALID_PASSWORD_ERROR", + error: updateResponse.failureReason, + }; + } + return { + status: "OK", + }; +}; exports.userPasswordPut = userPasswordPut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts index 6bc884816..737bae4d1 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts @@ -17,6 +17,14 @@ declare type Response = | { status: "INVALID_PHONE_ERROR"; error: string; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + error: string; + } + | { + status: "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + error: string; }; export declare const userPut: ( _: APIInterface, diff --git a/lib/build/recipe/dashboard/api/userdetails/userPut.js b/lib/build/recipe/dashboard/api/userdetails/userPut.js index af1a8aaae..642c427fb 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userPut.js +++ b/lib/build/recipe/dashboard/api/userdetails/userPut.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -51,333 +20,369 @@ const recipe_5 = __importDefault(require("../../../usermetadata/recipe")); const usermetadata_1 = __importDefault(require("../../../usermetadata")); const constants_1 = require("../../../emailpassword/constants"); const utils_2 = require("../../../passwordless/utils"); -const updateEmailForRecipeId = (recipeId, userId, email, tenantId, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - if (recipeId === "emailpassword") { - let emailFormFields = recipe_1.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_EMAIL_ID); - let validationError = yield emailFormFields[0].validate(email, tenantId); - if (validationError !== undefined) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - const emailUpdateResponse = yield emailpassword_1.default.updateEmailOrPassword({ - userId, - email, - userContext, - }); - if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } +const recipeUserId_1 = __importDefault(require("../../../../recipeUserId")); +const updateEmailForRecipeId = async (recipeId, recipeUserId, email, tenantId, userContext) => { + if (recipeId === "emailpassword") { + let emailFormFields = recipe_1.default + .getInstanceOrThrowError() + .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_EMAIL_ID); + let validationError = await emailFormFields[0].validate(email, tenantId); + if (validationError !== undefined) { return { - status: "OK", + status: "INVALID_EMAIL_ERROR", + error: validationError, }; } - if (recipeId === "thirdpartyemailpassword") { - let emailFormFields = recipe_2.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_EMAIL_ID); - let validationError = yield emailFormFields[0].validate(email, tenantId); - if (validationError !== undefined) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - const emailUpdateResponse = yield thirdpartyemailpassword_1.default.updateEmailOrPassword({ - userId, - email, - userContext, - }); - if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } + const emailUpdateResponse = await emailpassword_1.default.updateEmailOrPassword({ + recipeUserId, + email, + userContext, + }); + if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { + return { + status: "EMAIL_ALREADY_EXISTS_ERROR", + }; + } else if (emailUpdateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { return { - status: "OK", + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR", + reason: emailUpdateResponse.reason, }; + } else if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") { + throw new Error("Should never come here"); } - if (recipeId === "passwordless") { - let isValidEmail = true; - let validationError = ""; - const passwordlessConfig = recipe_3.default.getInstanceOrThrowError().config; - if (passwordlessConfig.contactMethod === "PHONE") { - const validationResult = yield utils_2.defaultValidateEmail(email); - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } else { - const validationResult = yield passwordlessConfig.validateEmailAddress(email, tenantId); - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } - if (!isValidEmail) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - const updateResult = yield passwordless_1.default.updateUser({ - userId, - email, - userContext, - }); - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - if (updateResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } + return { + status: "OK", + }; + } + if (recipeId === "thirdpartyemailpassword") { + let emailFormFields = recipe_2.default + .getInstanceOrThrowError() + .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_EMAIL_ID); + let validationError = await emailFormFields[0].validate(email, tenantId); + if (validationError !== undefined) { return { - status: "OK", + status: "INVALID_EMAIL_ERROR", + error: validationError, }; } - if (recipeId === "thirdpartypasswordless") { - let isValidEmail = true; - let validationError = ""; - const passwordlessConfig = recipe_4.default.getInstanceOrThrowError().passwordlessRecipe.config; - if (passwordlessConfig.contactMethod === "PHONE") { - const validationResult = yield utils_2.defaultValidateEmail(email); - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } else { - const validationResult = yield passwordlessConfig.validateEmailAddress(email, tenantId); - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } - if (!isValidEmail) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - const updateResult = yield thirdpartypasswordless_1.default.updatePasswordlessUser({ - userId, - email, - userContext, - }); - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - if (updateResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } + const emailUpdateResponse = await thirdpartyemailpassword_1.default.updateEmailOrPassword({ + recipeUserId, + email, + userContext, + }); + if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { return { - status: "OK", + status: "EMAIL_ALREADY_EXISTS_ERROR", }; + } else if (emailUpdateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + return { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR", + reason: emailUpdateResponse.reason, + }; + } else if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") { + throw new Error("Should never come here"); } - /** - * If it comes here then the user is a third party user in which case the UI should not have allowed this - */ - throw new Error("Should never come here"); - }); -const updatePhoneForRecipeId = (recipeId, userId, phone, tenantId, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - if (recipeId === "passwordless") { - let isValidPhone = true; - let validationError = ""; - const passwordlessConfig = recipe_3.default.getInstanceOrThrowError().config; - if (passwordlessConfig.contactMethod === "EMAIL") { - const validationResult = yield utils_2.defaultValidatePhoneNumber(phone); - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } else { - const validationResult = yield passwordlessConfig.validatePhoneNumber(phone, tenantId); - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } - if (!isValidPhone) { - return { - status: "INVALID_PHONE_ERROR", - error: validationError, - }; - } - const updateResult = yield passwordless_1.default.updateUser({ - userId, - phoneNumber: phone, - userContext, - }); - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); + return { + status: "OK", + }; + } + if (recipeId === "passwordless") { + let isValidEmail = true; + let validationError = ""; + const passwordlessConfig = recipe_3.default.getInstanceOrThrowError().config; + if (passwordlessConfig.contactMethod === "PHONE") { + const validationResult = await utils_2.defaultValidateEmail(email); + if (validationResult !== undefined) { + isValidEmail = false; + validationError = validationResult; } - if (updateResult.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { - return { - status: "PHONE_ALREADY_EXISTS_ERROR", - }; + } else { + const validationResult = await passwordlessConfig.validateEmailAddress(email, tenantId); + if (validationResult !== undefined) { + isValidEmail = false; + validationError = validationResult; } + } + if (!isValidEmail) { return { - status: "OK", + status: "INVALID_EMAIL_ERROR", + error: validationError, }; } - if (recipeId === "thirdpartypasswordless") { - let isValidPhone = true; - let validationError = ""; - const passwordlessConfig = recipe_4.default.getInstanceOrThrowError().passwordlessRecipe.config; - if (passwordlessConfig.contactMethod === "EMAIL") { - const validationResult = yield utils_2.defaultValidatePhoneNumber(phone); - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } else { - const validationResult = yield passwordlessConfig.validatePhoneNumber(phone, tenantId); - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } - if (!isValidPhone) { - return { - status: "INVALID_PHONE_ERROR", - error: validationError, - }; - } - const updateResult = yield thirdpartypasswordless_1.default.updatePasswordlessUser({ - userId, - phoneNumber: phone, - userContext, - }); - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - if (updateResult.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { - return { - status: "PHONE_ALREADY_EXISTS_ERROR", - }; - } + const updateResult = await passwordless_1.default.updateUser({ + recipeUserId, + email, + userContext, + }); + if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { + throw new Error("Should never come here"); + } + if (updateResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { return { - status: "OK", + status: "EMAIL_ALREADY_EXISTS_ERROR", }; } - /** - * If it comes here then the user is a not a passwordless user in which case the UI should not have allowed this - */ - throw new Error("Should never come here"); - }); -const userPut = (_, tenantId, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - const recipeId = requestBody.recipeId; - const firstName = requestBody.firstName; - const lastName = requestBody.lastName; - const email = requestBody.email; - const phone = requestBody.phone; - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); + if ( + updateResult.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" || + updateResult.status === "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" + ) { + return { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR", + reason: updateResult.reason, + }; } - if (recipeId === undefined || typeof recipeId !== "string") { - throw new error_1.default({ - message: "Required parameter 'recipeId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); + return { + status: "OK", + }; + } + if (recipeId === "thirdpartypasswordless") { + let isValidEmail = true; + let validationError = ""; + const passwordlessConfig = recipe_4.default.getInstanceOrThrowError().passwordlessRecipe.config; + if (passwordlessConfig.contactMethod === "PHONE") { + const validationResult = await utils_2.defaultValidateEmail(email); + if (validationResult !== undefined) { + isValidEmail = false; + validationError = validationResult; + } + } else { + const validationResult = await passwordlessConfig.validateEmailAddress(email, tenantId); + if (validationResult !== undefined) { + isValidEmail = false; + validationError = validationResult; + } } - if (!utils_1.isValidRecipeId(recipeId)) { - throw new error_1.default({ - message: "Invalid recipe id", - type: error_1.default.BAD_INPUT_ERROR, - }); + if (!isValidEmail) { + return { + status: "INVALID_EMAIL_ERROR", + error: validationError, + }; } - if (firstName === undefined || typeof firstName !== "string") { - throw new error_1.default({ - message: "Required parameter 'firstName' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); + const updateResult = await thirdpartypasswordless_1.default.updatePasswordlessUser({ + recipeUserId, + email, + userContext, + }); + if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { + throw new Error("Should never come here"); } - if (lastName === undefined || typeof lastName !== "string") { - throw new error_1.default({ - message: "Required parameter 'lastName' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); + if (updateResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { + return { + status: "EMAIL_ALREADY_EXISTS_ERROR", + }; } - if (email === undefined || typeof email !== "string") { - throw new error_1.default({ - message: "Required parameter 'email' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); + return { + status: "OK", + }; + } + /** + * If it comes here then the user is a third party user in which case the UI should not have allowed this + */ + throw new Error("Should never come here"); +}; +const updatePhoneForRecipeId = async (recipeId, recipeUserId, phone, tenantId, userContext) => { + if (recipeId === "passwordless") { + let isValidPhone = true; + let validationError = ""; + const passwordlessConfig = recipe_3.default.getInstanceOrThrowError().config; + if (passwordlessConfig.contactMethod === "EMAIL") { + const validationResult = await utils_2.defaultValidatePhoneNumber(phone); + if (validationResult !== undefined) { + isValidPhone = false; + validationError = validationResult; + } + } else { + const validationResult = await passwordlessConfig.validatePhoneNumber(phone, tenantId); + if (validationResult !== undefined) { + isValidPhone = false; + validationError = validationResult; + } } - if (phone === undefined || typeof phone !== "string") { - throw new error_1.default({ - message: "Required parameter 'phone' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); + if (!isValidPhone) { + return { + status: "INVALID_PHONE_ERROR", + error: validationError, + }; } - let userResponse = yield utils_1.getUserForRecipeId(userId, recipeId); - if (userResponse.user === undefined || userResponse.recipe === undefined) { + const updateResult = await passwordless_1.default.updateUser({ + recipeUserId, + phoneNumber: phone, + userContext, + }); + if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { throw new Error("Should never come here"); } - if (firstName.trim() !== "" || lastName.trim() !== "") { - let isRecipeInitialised = false; - try { - recipe_5.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) { - // no op + if (updateResult.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { + return { + status: "PHONE_ALREADY_EXISTS_ERROR", + }; + } + if (updateResult.status === "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR") { + return { + status: updateResult.status, + reason: updateResult.reason, + }; + } + return { + status: "OK", + }; + } + if (recipeId === "thirdpartypasswordless") { + let isValidPhone = true; + let validationError = ""; + const passwordlessConfig = recipe_4.default.getInstanceOrThrowError().passwordlessRecipe.config; + if (passwordlessConfig.contactMethod === "EMAIL") { + const validationResult = await utils_2.defaultValidatePhoneNumber(phone); + if (validationResult !== undefined) { + isValidPhone = false; + validationError = validationResult; } - if (isRecipeInitialised) { - let metaDataUpdate = {}; - if (firstName.trim() !== "") { - metaDataUpdate["first_name"] = firstName.trim(); - } - if (lastName.trim() !== "") { - metaDataUpdate["last_name"] = lastName.trim(); - } - yield usermetadata_1.default.updateUserMetadata(userId, metaDataUpdate, userContext); + } else { + const validationResult = await passwordlessConfig.validatePhoneNumber(phone, tenantId); + if (validationResult !== undefined) { + isValidPhone = false; + validationError = validationResult; } } - if (email.trim() !== "") { - const emailUpdateResponse = yield updateEmailForRecipeId( - userResponse.recipe, - userId, - email.trim(), - tenantId, - userContext - ); - if (emailUpdateResponse.status !== "OK") { - return emailUpdateResponse; - } + if (!isValidPhone) { + return { + status: "INVALID_PHONE_ERROR", + error: validationError, + }; } - if (phone.trim() !== "") { - const phoneUpdateResponse = yield updatePhoneForRecipeId( - userResponse.recipe, - userId, - phone.trim(), - tenantId, - userContext - ); - if (phoneUpdateResponse.status !== "OK") { - return phoneUpdateResponse; - } + const updateResult = await thirdpartypasswordless_1.default.updatePasswordlessUser({ + recipeUserId, + phoneNumber: phone, + userContext, + }); + if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { + throw new Error("Should never come here"); + } + if (updateResult.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { + return { + status: "PHONE_ALREADY_EXISTS_ERROR", + }; } return { status: "OK", }; - }); + } + /** + * If it comes here then the user is a not a passwordless user in which case the UI should not have allowed this + */ + throw new Error("Should never come here"); +}; +const userPut = async (_, tenantId, options, userContext) => { + const requestBody = await options.req.getJSONBody(); + const recipeUserId = requestBody.recipeUserId; + const recipeId = requestBody.recipeId; + const firstName = requestBody.firstName; + const lastName = requestBody.lastName; + const email = requestBody.email; + const phone = requestBody.phone; + if (recipeUserId === undefined || typeof recipeUserId !== "string") { + throw new error_1.default({ + message: "Required parameter 'recipeUserId' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (recipeId === undefined || typeof recipeId !== "string") { + throw new error_1.default({ + message: "Required parameter 'recipeId' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (!utils_1.isValidRecipeId(recipeId)) { + throw new error_1.default({ + message: "Invalid recipe id", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (firstName === undefined || typeof firstName !== "string") { + throw new error_1.default({ + message: "Required parameter 'firstName' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (lastName === undefined || typeof lastName !== "string") { + throw new error_1.default({ + message: "Required parameter 'lastName' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (email === undefined || typeof email !== "string") { + throw new error_1.default({ + message: "Required parameter 'email' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + if (phone === undefined || typeof phone !== "string") { + throw new error_1.default({ + message: "Required parameter 'phone' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + let userResponse = await utils_1.getUserForRecipeId(new recipeUserId_1.default(recipeUserId), recipeId); + if (userResponse.user === undefined || userResponse.recipe === undefined) { + throw new Error("Should never come here"); + } + if (firstName.trim() !== "" || lastName.trim() !== "") { + let isRecipeInitialised = false; + try { + recipe_5.default.getInstanceOrThrowError(); + isRecipeInitialised = true; + } catch (_) { + // no op + } + if (isRecipeInitialised) { + let metaDataUpdate = {}; + if (firstName.trim() !== "") { + metaDataUpdate["first_name"] = firstName.trim(); + } + if (lastName.trim() !== "") { + metaDataUpdate["last_name"] = lastName.trim(); + } + await usermetadata_1.default.updateUserMetadata(userResponse.user.id, metaDataUpdate, userContext); + } + } + if (email.trim() !== "") { + const emailUpdateResponse = await updateEmailForRecipeId( + userResponse.recipe, + new recipeUserId_1.default(recipeUserId), + email.trim(), + tenantId, + userContext + ); + if (emailUpdateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + return { + error: emailUpdateResponse.reason, + status: emailUpdateResponse.status, + }; + } + if (emailUpdateResponse.status !== "OK") { + return emailUpdateResponse; + } + } + if (phone.trim() !== "") { + const phoneUpdateResponse = await updatePhoneForRecipeId( + userResponse.recipe, + new recipeUserId_1.default(recipeUserId), + phone.trim(), + tenantId, + userContext + ); + if (phoneUpdateResponse.status === "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR") { + return { + error: phoneUpdateResponse.reason, + status: phoneUpdateResponse.status, + }; + } + if (phoneUpdateResponse.status !== "OK") { + return phoneUpdateResponse; + } + } + return { + status: "OK", + }; +}; exports.userPut = userPut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.js b/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.js index b49fb12ff..2528cf4ff 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.js +++ b/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,44 +8,38 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.userSessionsGet = void 0; const error_1 = __importDefault(require("../../../../error")); const session_1 = __importDefault(require("../../../session")); -const userSessionsGet = (_, ___, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const userId = options.req.getKeyValueFromQuery("userId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - const response = yield session_1.default.getAllSessionHandlesForUser(userId, undefined, userContext); - let sessions = []; - let sessionInfoPromises = []; - for (let i = 0; i < response.length; i++) { - sessionInfoPromises.push( - new Promise((res, rej) => - __awaiter(void 0, void 0, void 0, function* () { - try { - const sessionResponse = yield session_1.default.getSessionInformation( - response[i], - userContext - ); - if (sessionResponse !== undefined) { - const accessTokenPayload = sessionResponse.customClaimsInAccessTokenPayload; - delete sessionResponse.customClaimsInAccessTokenPayload; - sessions[i] = Object.assign(Object.assign({}, sessionResponse), { accessTokenPayload }); - } - res(); - } catch (e) { - rej(e); - } - }) - ) - ); - } - yield Promise.all(sessionInfoPromises); - return { - status: "OK", - sessions, - }; - }); +const userSessionsGet = async (_, ___, options, userContext) => { + const userId = options.req.getKeyValueFromQuery("userId"); + if (userId === undefined || userId === "") { + throw new error_1.default({ + message: "Missing required parameter 'userId'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + const response = await session_1.default.getAllSessionHandlesForUser(userId, undefined, undefined, userContext); + let sessions = []; + let sessionInfoPromises = []; + for (let i = 0; i < response.length; i++) { + sessionInfoPromises.push( + new Promise(async (res, rej) => { + try { + const sessionResponse = await session_1.default.getSessionInformation(response[i], userContext); + if (sessionResponse !== undefined) { + const accessTokenPayload = sessionResponse.customClaimsInAccessTokenPayload; + delete sessionResponse.customClaimsInAccessTokenPayload; + sessions[i] = Object.assign(Object.assign({}, sessionResponse), { accessTokenPayload }); + } + res(); + } catch (e) { + rej(e); + } + }) + ); + } + await Promise.all(sessionInfoPromises); + return { + status: "OK", + sessions, + }; +}; exports.userSessionsGet = userSessionsGet; diff --git a/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.js b/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.js index 449bc9fd6..51ab7f94a 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.js +++ b/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,19 +8,18 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.userSessionsPost = void 0; const error_1 = __importDefault(require("../../../../error")); const session_1 = __importDefault(require("../../../session")); -const userSessionsPost = (_, ___, options, userContext) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const sessionHandles = requestBody.sessionHandles; - if (sessionHandles === undefined || !Array.isArray(sessionHandles)) { - throw new error_1.default({ - message: "Required parameter 'sessionHandles' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - yield session_1.default.revokeMultipleSessions(sessionHandles, userContext); - return { - status: "OK", - }; - }); +const userSessionsPost = async (_, ___, options, userContext) => { + const requestBody = await options.req.getJSONBody(); + const sessionHandles = requestBody.sessionHandles; + if (sessionHandles === undefined || !Array.isArray(sessionHandles)) { + throw new error_1.default({ + message: "Required parameter 'sessionHandles' is missing or has an invalid type", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + await session_1.default.revokeMultipleSessions(sessionHandles, userContext); + return { + status: "OK", + }; +}; exports.userSessionsPost = userSessionsPost; diff --git a/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.d.ts b/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.d.ts new file mode 100644 index 000000000..8d94598ea --- /dev/null +++ b/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.d.ts @@ -0,0 +1,12 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from "../../types"; +declare type Response = { + status: "OK"; +}; +export declare const userUnlink: ( + _: APIInterface, + ___: string, + options: APIOptions, + userContext: any +) => Promise; +export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.js b/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.js new file mode 100644 index 000000000..11283fce1 --- /dev/null +++ b/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.js @@ -0,0 +1,25 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.userUnlink = void 0; +const error_1 = __importDefault(require("../../../../error")); +const accountlinking_1 = __importDefault(require("../../../accountlinking")); +const recipeUserId_1 = __importDefault(require("../../../../recipeUserId")); +const userUnlink = async (_, ___, options, userContext) => { + const recipeUserId = options.req.getKeyValueFromQuery("recipeUserId"); + if (recipeUserId === undefined) { + throw new error_1.default({ + message: "Required field recipeUserId is missing", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + await accountlinking_1.default.unlinkAccount(new recipeUserId_1.default(recipeUserId), userContext); + return { + status: "OK", + }; +}; +exports.userUnlink = userUnlink; diff --git a/lib/build/recipe/dashboard/api/usersCountGet.js b/lib/build/recipe/dashboard/api/usersCountGet.js index 4c032bb59..20b359227 100644 --- a/lib/build/recipe/dashboard/api/usersCountGet.js +++ b/lib/build/recipe/dashboard/api/usersCountGet.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -51,13 +20,11 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); const supertokens_1 = __importDefault(require("../../../supertokens")); -function usersCountGet(_, tenantId, __, ___) { - return __awaiter(this, void 0, void 0, function* () { - const count = yield supertokens_1.default.getInstanceOrThrowError().getUserCount(undefined, tenantId); - return { - status: "OK", - count, - }; - }); +async function usersCountGet(_, tenantId, __, ___) { + const count = await supertokens_1.default.getInstanceOrThrowError().getUserCount(undefined, tenantId); + return { + status: "OK", + count, + }; } exports.default = usersCountGet; diff --git a/lib/build/recipe/dashboard/api/usersGet.d.ts b/lib/build/recipe/dashboard/api/usersGet.d.ts index 379b01c5f..4a07467ad 100644 --- a/lib/build/recipe/dashboard/api/usersGet.d.ts +++ b/lib/build/recipe/dashboard/api/usersGet.d.ts @@ -1,33 +1,9 @@ // @ts-nocheck -import { APIInterface, APIOptions } from "../types"; +import { APIInterface, APIOptions, UserWithFirstAndLastName } from "../types"; export declare type Response = { status: "OK"; nextPaginationToken?: string; - users: { - recipeId: string; - user: { - id: string; - timeJoined: number; - firstName?: string; - lastName?: string; - tenantIds: string[]; - } & ( - | { - email: string; - } - | { - email: string; - thirdParty: { - id: string; - userId: string; - }; - } - | { - email?: string; - phoneNumber?: string; - } - ); - }[]; + users: UserWithFirstAndLastName[]; }; export default function usersGet( _: APIInterface, diff --git a/lib/build/recipe/dashboard/api/usersGet.js b/lib/build/recipe/dashboard/api/usersGet.js index 1f2e91b1c..79494b162 100644 --- a/lib/build/recipe/dashboard/api/usersGet.js +++ b/lib/build/recipe/dashboard/api/usersGet.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -38,113 +7,113 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); exports.getSearchParamsFromURL = void 0; const error_1 = __importDefault(require("../../../error")); -const supertokens_1 = __importDefault(require("../../../supertokens")); +const __1 = require("../../.."); const recipe_1 = __importDefault(require("../../usermetadata/recipe")); const usermetadata_1 = __importDefault(require("../../usermetadata")); -function usersGet(_, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const req = options.req; - const limit = options.req.getKeyValueFromQuery("limit"); - if (limit === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'limit'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let timeJoinedOrder = req.getKeyValueFromQuery("timeJoinedOrder"); - if (timeJoinedOrder === undefined) { - timeJoinedOrder = "DESC"; - } - if (timeJoinedOrder !== "ASC" && timeJoinedOrder !== "DESC") { - throw new error_1.default({ - message: "Invalid value recieved for 'timeJoinedOrder'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let paginationToken = options.req.getKeyValueFromQuery("paginationToken"); - const query = getSearchParamsFromURL(options.req.getOriginalURL()); - let usersResponse = yield supertokens_1.default.getInstanceOrThrowError().getUsers({ - query, - timeJoinedOrder: timeJoinedOrder, - limit: parseInt(limit), - paginationToken, - tenantId, +async function usersGet(_, tenantId, options, userContext) { + const req = options.req; + const limit = options.req.getKeyValueFromQuery("limit"); + if (limit === undefined) { + throw new error_1.default({ + message: "Missing required parameter 'limit'", + type: error_1.default.BAD_INPUT_ERROR, }); - // If the UserMetaData recipe has been initialised, fetch first and last name - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (e) { - // Recipe has not been initialised, return without first name and last name - return { - status: "OK", - users: usersResponse.users, - nextPaginationToken: usersResponse.nextPaginationToken, - }; - } - let updatedUsersArray = []; - let metaDataFetchPromises = []; - for (let i = 0; i < usersResponse.users.length; i++) { - const userObj = usersResponse.users[i]; - metaDataFetchPromises.push( - () => - new Promise((resolve, reject) => - __awaiter(this, void 0, void 0, function* () { - try { - const userMetaDataResponse = yield usermetadata_1.default.getUserMetadata( - userObj.user.id, - userContext - ); - const { first_name, last_name } = userMetaDataResponse.metadata; - updatedUsersArray[i] = { - recipeId: userObj.recipeId, - user: Object.assign(Object.assign({}, userObj.user), { - firstName: first_name, - lastName: last_name, - }), - }; - resolve(true); - } catch (e) { - // Something went wrong when fetching user meta data - reject(e); - } - }) - ) - ); - } - let promiseArrayStartPosition = 0; - let batchSize = 5; - while (promiseArrayStartPosition < metaDataFetchPromises.length) { - /** - * We want to query only 5 in parallel at a time - * - * First we check if the the array has enough elements to iterate - * promiseArrayStartPosition + 4 (5 elements including current) - */ - let promiseArrayEndPosition = promiseArrayStartPosition + (batchSize - 1); - // If the end position is higher than the arrays length, we need to adjust it - if (promiseArrayEndPosition >= metaDataFetchPromises.length) { - /** - * For example if the array has 7 elements [A, B, C, D, E, F, G], when you run - * the second batch [startPosition = 5], this will result in promiseArrayEndPosition - * to be equal to 6 [5 + ((7 - 1) - 5)] and will then iterate over indexes [5] and [6] - */ - promiseArrayEndPosition = - promiseArrayStartPosition + (metaDataFetchPromises.length - 1 - promiseArrayStartPosition); - } - let promisesToCall = []; - for (let j = promiseArrayStartPosition; j <= promiseArrayEndPosition; j++) { - promisesToCall.push(metaDataFetchPromises[j]); - } - yield Promise.all(promisesToCall.map((p) => p())); - promiseArrayStartPosition += batchSize; - } - usersResponse = Object.assign(Object.assign({}, usersResponse), { users: updatedUsersArray }); + } + let timeJoinedOrder = req.getKeyValueFromQuery("timeJoinedOrder"); + if (timeJoinedOrder === undefined) { + timeJoinedOrder = "DESC"; + } + if (timeJoinedOrder !== "ASC" && timeJoinedOrder !== "DESC") { + throw new error_1.default({ + message: "Invalid value recieved for 'timeJoinedOrder'", + type: error_1.default.BAD_INPUT_ERROR, + }); + } + let paginationToken = options.req.getKeyValueFromQuery("paginationToken"); + const query = getSearchParamsFromURL(options.req.getOriginalURL()); + let usersResponse = + timeJoinedOrder === "DESC" + ? await __1.getUsersNewestFirst({ + tenantId, + query, + limit: parseInt(limit), + paginationToken, + }) + : await __1.getUsersOldestFirst({ + tenantId, + query, + limit: parseInt(limit), + paginationToken, + }); + // If the UserMetaData recipe has been initialised, fetch first and last name + try { + recipe_1.default.getInstanceOrThrowError(); + } catch (e) { + // Recipe has not been initialised, return without first name and last name return { status: "OK", - users: usersResponse.users, + users: usersResponse.users.map((u) => u.toJson()), nextPaginationToken: usersResponse.nextPaginationToken, }; - }); + } + let updatedUsersArray = []; + let metaDataFetchPromises = []; + for (let i = 0; i < usersResponse.users.length; i++) { + const userObj = usersResponse.users[i].toJson(); + metaDataFetchPromises.push( + () => + new Promise(async (resolve, reject) => { + try { + const userMetaDataResponse = await usermetadata_1.default.getUserMetadata( + userObj.id, + userContext + ); + const { first_name, last_name } = userMetaDataResponse.metadata; + updatedUsersArray[i] = Object.assign(Object.assign({}, userObj), { + firstName: first_name, + lastName: last_name, + }); + resolve(true); + } catch (e) { + // Something went wrong when fetching user meta data + reject(e); + } + }) + ); + } + let promiseArrayStartPosition = 0; + let batchSize = 5; + while (promiseArrayStartPosition < metaDataFetchPromises.length) { + /** + * We want to query only 5 in parallel at a time + * + * First we check if the the array has enough elements to iterate + * promiseArrayStartPosition + 4 (5 elements including current) + */ + let promiseArrayEndPosition = promiseArrayStartPosition + (batchSize - 1); + // If the end position is higher than the arrays length, we need to adjust it + if (promiseArrayEndPosition >= metaDataFetchPromises.length) { + /** + * For example if the array has 7 elements [A, B, C, D, E, F, G], when you run + * the second batch [startPosition = 5], this will result in promiseArrayEndPosition + * to be equal to 6 [5 + ((7 - 1) - 5)] and will then iterate over indexes [5] and [6] + */ + promiseArrayEndPosition = + promiseArrayStartPosition + (metaDataFetchPromises.length - 1 - promiseArrayStartPosition); + } + let promisesToCall = []; + for (let j = promiseArrayStartPosition; j <= promiseArrayEndPosition; j++) { + promisesToCall.push(metaDataFetchPromises[j]); + } + await Promise.all(promisesToCall.map((p) => p())); + promiseArrayStartPosition += batchSize; + } + usersResponse = Object.assign(Object.assign({}, usersResponse), { users: updatedUsersArray }); + return { + status: "OK", + users: usersResponse.users, + nextPaginationToken: usersResponse.nextPaginationToken, + }; } exports.default = usersGet; function getSearchParamsFromURL(path) { diff --git a/lib/build/recipe/dashboard/api/validateKey.js b/lib/build/recipe/dashboard/api/validateKey.js index 6e5730bc0..4e1df7946 100644 --- a/lib/build/recipe/dashboard/api/validateKey.js +++ b/lib/build/recipe/dashboard/api/validateKey.js @@ -13,50 +13,17 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../utils"); -function validateKey(_, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const input = { req: options.req, config: options.config, userContext }; - if (yield utils_1.validateApiKey(input)) { - options.res.sendJSONResponse({ - status: "OK", - }); - } else { - utils_1.sendUnauthorisedAccess(options.res); - } - return true; - }); +async function validateKey(_, options, userContext) { + const input = { req: options.req, config: options.config, userContext }; + if (await utils_1.validateApiKey(input)) { + options.res.sendJSONResponse({ + status: "OK", + }); + } else { + utils_1.sendUnauthorisedAccess(options.res); + } + return true; } exports.default = validateKey; diff --git a/lib/build/recipe/dashboard/constants.d.ts b/lib/build/recipe/dashboard/constants.d.ts index 0ef826f56..58c53c2fc 100644 --- a/lib/build/recipe/dashboard/constants.d.ts +++ b/lib/build/recipe/dashboard/constants.d.ts @@ -14,3 +14,4 @@ export declare const USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token export declare const SEARCH_TAGS_API = "/api/search/tags"; export declare const DASHBOARD_ANALYTICS_API = "/api/analytics"; export declare const TENANTS_LIST_API = "/api/tenants/list"; +export declare const UNLINK_USER = "/api/user/unlink"; diff --git a/lib/build/recipe/dashboard/constants.js b/lib/build/recipe/dashboard/constants.js index ea41c846b..d551fd218 100644 --- a/lib/build/recipe/dashboard/constants.js +++ b/lib/build/recipe/dashboard/constants.js @@ -14,7 +14,7 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.TENANTS_LIST_API = exports.DASHBOARD_ANALYTICS_API = exports.SEARCH_TAGS_API = exports.USER_EMAIL_VERIFY_TOKEN_API = exports.USER_PASSWORD_API = exports.USER_SESSIONS_API = exports.USER_METADATA_API = exports.USER_EMAIL_VERIFY_API = exports.USER_API = exports.USERS_COUNT_API = exports.USERS_LIST_GET_API = exports.VALIDATE_KEY_API = exports.SIGN_OUT_API = exports.SIGN_IN_API = exports.DASHBOARD_API = void 0; +exports.UNLINK_USER = exports.TENANTS_LIST_API = exports.DASHBOARD_ANALYTICS_API = exports.SEARCH_TAGS_API = exports.USER_EMAIL_VERIFY_TOKEN_API = exports.USER_PASSWORD_API = exports.USER_SESSIONS_API = exports.USER_METADATA_API = exports.USER_EMAIL_VERIFY_API = exports.USER_API = exports.USERS_COUNT_API = exports.USERS_LIST_GET_API = exports.VALIDATE_KEY_API = exports.SIGN_OUT_API = exports.SIGN_IN_API = exports.DASHBOARD_API = void 0; exports.DASHBOARD_API = "/dashboard"; exports.SIGN_IN_API = "/api/signin"; exports.SIGN_OUT_API = "/api/signout"; @@ -30,3 +30,4 @@ exports.USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token"; exports.SEARCH_TAGS_API = "/api/search/tags"; exports.DASHBOARD_ANALYTICS_API = "/api/analytics"; exports.TENANTS_LIST_API = "/api/tenants/list"; +exports.UNLINK_USER = "/api/user/unlink"; diff --git a/lib/build/recipe/dashboard/recipe.d.ts b/lib/build/recipe/dashboard/recipe.d.ts index 7bfa03944..1747e1482 100644 --- a/lib/build/recipe/dashboard/recipe.d.ts +++ b/lib/build/recipe/dashboard/recipe.d.ts @@ -3,7 +3,7 @@ import RecipeModule from "../../recipeModule"; import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import error from "../../error"; export default class Recipe extends RecipeModule { private static instance; diff --git a/lib/build/recipe/dashboard/recipe.js b/lib/build/recipe/dashboard/recipe.js index 21048b788..6a8011b5c 100644 --- a/lib/build/recipe/dashboard/recipe.js +++ b/lib/build/recipe/dashboard/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -79,6 +48,7 @@ const signOut_1 = __importDefault(require("./api/signOut")); const tagsGet_1 = require("./api/search/tagsGet"); const analytics_1 = __importDefault(require("./api/analytics")); const listTenants_1 = __importDefault(require("./api/listTenants")); +const userUnlinkGet_1 = require("./api/userdetails/userUnlinkGet"); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); @@ -251,89 +221,97 @@ class Recipe extends recipeModule_1.default { disabled: false, method: "get", }, + { + id: constants_1.UNLINK_USER, + pathWithoutApiBasePath: new normalisedURLPath_1.default( + utils_1.getApiPathWithDashboardBase(constants_1.UNLINK_USER) + ), + disabled: false, + method: "get", + }, ]; }; - this.handleAPIRequest = (id, tenantId, req, res, __, ___, userContext) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - isInServerlessEnv: this.isInServerlessEnv, - appInfo: this.getAppInfo(), - }; - // For these APIs we dont need API key validation - if (id === constants_1.DASHBOARD_API) { - return yield dashboard_1.default(this.apiImpl, options, userContext); + this.handleAPIRequest = async (id, tenantId, req, res, __, ___, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + isInServerlessEnv: this.isInServerlessEnv, + appInfo: this.getAppInfo(), + }; + // For these APIs we dont need API key validation + if (id === constants_1.DASHBOARD_API) { + return await dashboard_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.SIGN_IN_API) { + return await signIn_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.VALIDATE_KEY_API) { + return await validateKey_1.default(this.apiImpl, options, userContext); + } + // Do API key validation for the remaining APIs + let apiFunction; + if (id === constants_1.USERS_LIST_GET_API) { + apiFunction = usersGet_1.default; + } else if (id === constants_1.USERS_COUNT_API) { + apiFunction = usersCountGet_1.default; + } else if (id === constants_1.USER_API) { + if (req.getMethod() === "get") { + apiFunction = userGet_1.userGet; + } + if (req.getMethod() === "delete") { + apiFunction = userDelete_1.userDelete; + } + if (req.getMethod() === "put") { + apiFunction = userPut_1.userPut; } - if (id === constants_1.SIGN_IN_API) { - return yield signIn_1.default(this.apiImpl, options, userContext); + } else if (id === constants_1.USER_EMAIL_VERIFY_API) { + if (req.getMethod() === "get") { + apiFunction = userEmailVerifyGet_1.userEmailVerifyGet; } - if (id === constants_1.VALIDATE_KEY_API) { - return yield validateKey_1.default(this.apiImpl, options, userContext); + if (req.getMethod() === "put") { + apiFunction = userEmailVerifyPut_1.userEmailVerifyPut; } - // Do API key validation for the remaining APIs - let apiFunction; - if (id === constants_1.USERS_LIST_GET_API) { - apiFunction = usersGet_1.default; - } else if (id === constants_1.USERS_COUNT_API) { - apiFunction = usersCountGet_1.default; - } else if (id === constants_1.USER_API) { - if (req.getMethod() === "get") { - apiFunction = userGet_1.userGet; - } - if (req.getMethod() === "delete") { - apiFunction = userDelete_1.userDelete; - } - if (req.getMethod() === "put") { - apiFunction = userPut_1.userPut; - } - } else if (id === constants_1.USER_EMAIL_VERIFY_API) { - if (req.getMethod() === "get") { - apiFunction = userEmailVerifyGet_1.userEmailverifyGet; - } - if (req.getMethod() === "put") { - apiFunction = userEmailVerifyPut_1.userEmailVerifyPut; - } - } else if (id === constants_1.USER_METADATA_API) { - if (req.getMethod() === "get") { - apiFunction = userMetadataGet_1.userMetaDataGet; - } - if (req.getMethod() === "put") { - apiFunction = userMetadataPut_1.userMetadataPut; - } - } else if (id === constants_1.USER_SESSIONS_API) { - if (req.getMethod() === "get") { - apiFunction = userSessionsGet_1.userSessionsGet; - } - if (req.getMethod() === "post") { - apiFunction = userSessionsPost_1.userSessionsPost; - } - } else if (id === constants_1.USER_PASSWORD_API) { - apiFunction = userPasswordPut_1.userPasswordPut; - } else if (id === constants_1.USER_EMAIL_VERIFY_TOKEN_API) { - apiFunction = userEmailVerifyTokenPost_1.userEmailVerifyTokenPost; - } else if (id === constants_1.SEARCH_TAGS_API) { - apiFunction = tagsGet_1.getSearchTags; - } else if (id === constants_1.SIGN_OUT_API) { - apiFunction = signOut_1.default; - } else if (id === constants_1.DASHBOARD_ANALYTICS_API && req.getMethod() === "post") { - apiFunction = analytics_1.default; - } else if (id === constants_1.TENANTS_LIST_API) { - apiFunction = listTenants_1.default; + } else if (id === constants_1.USER_METADATA_API) { + if (req.getMethod() === "get") { + apiFunction = userMetadataGet_1.userMetaDataGet; } - // If the id doesnt match any APIs return false - if (apiFunction === undefined) { - return false; + if (req.getMethod() === "put") { + apiFunction = userMetadataPut_1.userMetadataPut; } - return yield apiKeyProtector_1.default(this.apiImpl, tenantId, options, apiFunction, userContext); - }); - this.handleError = (err, _, __) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); + } else if (id === constants_1.USER_SESSIONS_API) { + if (req.getMethod() === "get") { + apiFunction = userSessionsGet_1.userSessionsGet; + } + if (req.getMethod() === "post") { + apiFunction = userSessionsPost_1.userSessionsPost; + } + } else if (id === constants_1.USER_PASSWORD_API) { + apiFunction = userPasswordPut_1.userPasswordPut; + } else if (id === constants_1.USER_EMAIL_VERIFY_TOKEN_API) { + apiFunction = userEmailVerifyTokenPost_1.userEmailVerifyTokenPost; + } else if (id === constants_1.SEARCH_TAGS_API) { + apiFunction = tagsGet_1.getSearchTags; + } else if (id === constants_1.SIGN_OUT_API) { + apiFunction = signOut_1.default; + } else if (id === constants_1.DASHBOARD_ANALYTICS_API && req.getMethod() === "post") { + apiFunction = analytics_1.default; + } else if (id === constants_1.TENANTS_LIST_API) { + apiFunction = listTenants_1.default; + } else if (id === constants_1.UNLINK_USER) { + apiFunction = userUnlinkGet_1.userUnlink; + } + // If the id doesnt match any APIs return false + if (apiFunction === undefined) { + return false; + } + return await apiKeyProtector_1.default(this.apiImpl, tenantId, options, apiFunction, userContext); + }; + this.handleError = async (err, _, __) => { + throw err; + }; this.getAllCORSHeaders = () => { return []; }; diff --git a/lib/build/recipe/dashboard/recipeImplementation.js b/lib/build/recipe/dashboard/recipeImplementation.js index c04b5fdec..d31e33aa2 100644 --- a/lib/build/recipe/dashboard/recipeImplementation.js +++ b/lib/build/recipe/dashboard/recipeImplementation.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -60,69 +29,65 @@ const constants_1 = require("./constants"); const utils_2 = require("./utils"); function getRecipeImplementation() { return { - getDashboardBundleLocation: function () { - return __awaiter(this, void 0, void 0, function* () { - return `https://cdn.jsdelivr.net/gh/supertokens/dashboard@v${version_1.dashboardVersion}/build/`; - }); + getDashboardBundleLocation: async function () { + return `https://cdn.jsdelivr.net/gh/supertokens/dashboard@v${version_1.dashboardVersion}/build/`; }, - shouldAllowAccess: function (input) { + shouldAllowAccess: async function (input) { var _a; - return __awaiter(this, void 0, void 0, function* () { - // For cases where we're not using the API key, the JWT is being used; we allow their access by default - if (!input.config.apiKey) { - // make the check for the API endpoint here with querier - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - const authHeaderValue = - (_a = input.req.getHeaderValue("authorization")) === null || _a === void 0 - ? void 0 - : _a.split(" ")[1]; - const sessionVerificationResponse = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/dashboard/session/verify"), - { - sessionId: authHeaderValue, - } - ); - if (sessionVerificationResponse.status !== "OK") { + // For cases where we're not using the API key, the JWT is being used; we allow their access by default + if (!input.config.apiKey) { + // make the check for the API endpoint here with querier + let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); + const authHeaderValue = + (_a = input.req.getHeaderValue("authorization")) === null || _a === void 0 + ? void 0 + : _a.split(" ")[1]; + const sessionVerificationResponse = await querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/dashboard/session/verify"), + { + sessionId: authHeaderValue, + } + ); + if (sessionVerificationResponse.status !== "OK") { + return false; + } + // For all non GET requests we also want to check if the user is allowed to perform this operation + if (utils_1.normaliseHttpMethod(input.req.getMethod()) !== "get") { + // We dont want to block the analytics API + if (input.req.getOriginalURL().endsWith(constants_1.DASHBOARD_ANALYTICS_API)) { + return true; + } + // We do not want to block the sign out request + if (input.req.getOriginalURL().endsWith(constants_1.SIGN_OUT_API)) { + return true; + } + const admins = input.config.admins; + if (admins === undefined) { + return true; + } + if (admins.length === 0) { + logger_1.logDebugMessage( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ); + throw new error_1.default(); + } + const userEmail = sessionVerificationResponse.email; + if (userEmail === undefined || typeof userEmail !== "string") { + logger_1.logDebugMessage( + "User Dashboard: Returning Unauthorised because no email was returned from the core. Should never come here" + ); return false; } - // For all non GET requests we also want to check if the user is allowed to perform this operation - if (utils_1.normaliseHttpMethod(input.req.getMethod()) !== "get") { - // We dont want to block the analytics API - if (input.req.getOriginalURL().endsWith(constants_1.DASHBOARD_ANALYTICS_API)) { - return true; - } - // We do not want to block the sign out request - if (input.req.getOriginalURL().endsWith(constants_1.SIGN_OUT_API)) { - return true; - } - const admins = input.config.admins; - if (admins === undefined) { - return true; - } - if (admins.length === 0) { - logger_1.logDebugMessage( - "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" - ); - throw new error_1.default(); - } - const userEmail = sessionVerificationResponse.email; - if (userEmail === undefined || typeof userEmail !== "string") { - logger_1.logDebugMessage( - "User Dashboard: Returning Unauthorised because no email was returned from the core. Should never come here" - ); - return false; - } - if (!admins.includes(userEmail)) { - logger_1.logDebugMessage( - "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" - ); - throw new error_1.default(); - } + if (!admins.includes(userEmail)) { + logger_1.logDebugMessage( + "User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin" + ); + throw new error_1.default(); } - return true; } - return yield utils_2.validateApiKey(input); - }); + return true; + } + return await utils_2.validateApiKey(input); }, }; } diff --git a/lib/build/recipe/dashboard/types.d.ts b/lib/build/recipe/dashboard/types.d.ts index 3778f7734..def30a57a 100644 --- a/lib/build/recipe/dashboard/types.d.ts +++ b/lib/build/recipe/dashboard/types.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; -import { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo } from "../../types"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import { NormalisedAppinfo, User } from "../../types"; export declare type TypeInput = { apiKey?: string; admins?: string[]; @@ -49,25 +49,7 @@ export declare type APIFunction = ( ) => Promise; export declare type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless"; export declare type AuthMode = "api-key" | "email-password"; -declare type CommonUserInformation = { - id: string; - timeJoined: number; - firstName: string; - lastName: string; - tenantIds: string[]; +export declare type UserWithFirstAndLastName = User & { + firstName?: string; + lastName?: string; }; -export declare type EmailPasswordUser = CommonUserInformation & { - email: string; -}; -export declare type ThirdPartyUser = CommonUserInformation & { - email: string; - thirdParty: { - id: string; - userId: string; - }; -}; -export declare type PasswordlessUser = CommonUserInformation & { - email?: string; - phone?: string; -}; -export {}; diff --git a/lib/build/recipe/dashboard/utils.d.ts b/lib/build/recipe/dashboard/utils.d.ts index 46510227d..15877f8ca 100644 --- a/lib/build/recipe/dashboard/utils.d.ts +++ b/lib/build/recipe/dashboard/utils.d.ts @@ -1,21 +1,15 @@ // @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import { - EmailPasswordUser, - PasswordlessUser, - RecipeIdForUser, - ThirdPartyUser, - TypeInput, - TypeNormalisedInput, -} from "./types"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import { RecipeIdForUser, TypeInput, TypeNormalisedInput, UserWithFirstAndLastName } from "./types"; +import RecipeUserId from "../../recipeUserId"; export declare function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput; export declare function sendUnauthorisedAccess(res: BaseResponse): void; export declare function isValidRecipeId(recipeId: string): recipeId is RecipeIdForUser; export declare function getUserForRecipeId( - userId: string, + recipeUserId: RecipeUserId, recipeId: string ): Promise<{ - user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined; + user: UserWithFirstAndLastName | undefined; recipe: | "emailpassword" | "thirdparty" diff --git a/lib/build/recipe/dashboard/utils.js b/lib/build/recipe/dashboard/utils.js index 78661380b..ca68fa91b 100644 --- a/lib/build/recipe/dashboard/utils.js +++ b/lib/build/recipe/dashboard/utils.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,16 +22,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.getApiPathWithDashboardBase = exports.validateApiKey = exports.isRecipeInitialised = exports.getUserForRecipeId = exports.isValidRecipeId = exports.sendUnauthorisedAccess = exports.validateAndNormaliseUserInput = void 0; const utils_1 = require("../../utils"); const constants_1 = require("./constants"); -const recipe_1 = __importDefault(require("../emailpassword/recipe")); -const recipe_2 = __importDefault(require("../thirdparty/recipe")); -const recipe_3 = __importDefault(require("../passwordless/recipe")); -const emailpassword_1 = __importDefault(require("../emailpassword")); -const thirdparty_1 = __importDefault(require("../thirdparty")); -const passwordless_1 = __importDefault(require("../passwordless")); -const thirdpartyemailpassword_1 = __importDefault(require("../thirdpartyemailpassword")); -const recipe_4 = __importDefault(require("../thirdpartyemailpassword/recipe")); -const thirdpartypasswordless_1 = __importDefault(require("../thirdpartypasswordless")); -const recipe_5 = __importDefault(require("../thirdpartypasswordless/recipe")); +const recipe_1 = __importDefault(require("../accountlinking/recipe")); +const recipe_2 = __importDefault(require("../emailpassword/recipe")); +const recipe_3 = __importDefault(require("../thirdparty/recipe")); +const recipe_4 = __importDefault(require("../passwordless/recipe")); +const recipe_5 = __importDefault(require("../thirdpartyemailpassword/recipe")); +const recipe_6 = __importDefault(require("../thirdpartypasswordless/recipe")); const logger_1 = require("../../logger"); function validateAndNormaliseUserInput(config) { let override = Object.assign( @@ -97,132 +62,139 @@ function isValidRecipeId(recipeId) { return recipeId === "emailpassword" || recipeId === "thirdparty" || recipeId === "passwordless"; } exports.isValidRecipeId = isValidRecipeId; -function getUserForRecipeId(userId, recipeId) { - return __awaiter(this, void 0, void 0, function* () { - let user; - let recipe; - if (recipeId === recipe_1.default.RECIPE_ID) { +async function getUserForRecipeId(recipeUserId, recipeId) { + let userResponse = await _getUserForRecipeId(recipeUserId, recipeId); + let user = undefined; + if (userResponse.user !== undefined) { + user = Object.assign(Object.assign({}, userResponse.user), { firstName: "", lastName: "" }); + } + return { + user, + recipe: userResponse.recipe, + }; +} +exports.getUserForRecipeId = getUserForRecipeId; +async function _getUserForRecipeId(recipeUserId, recipeId) { + let recipe; + const user = await recipe_1.default.getInstance().recipeInterfaceImpl.getUser({ + userId: recipeUserId.getAsString(), + userContext: {}, + }); + if (user === undefined) { + return { + user: undefined, + recipe: undefined, + }; + } + const loginMethod = user.loginMethods.find( + (m) => m.recipeId === recipeId && m.recipeUserId.getAsString() === recipeUserId.getAsString() + ); + if (loginMethod === undefined) { + return { + user: undefined, + recipe: undefined, + }; + } + if (recipeId === recipe_2.default.RECIPE_ID) { + try { + // we detect if this recipe has been init or not.. + recipe_2.default.getInstanceOrThrowError(); + recipe = "emailpassword"; + } catch (e) { + // No - op + } + if (recipe === undefined) { try { - const userResponse = yield emailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "emailpassword"; - } + // we detect if this recipe has been init or not.. + recipe_5.default.getInstanceOrThrowError(); + recipe = "thirdpartyemailpassword"; } catch (e) { // No - op } - if (user === undefined) { - try { - const userResponse = yield thirdpartyemailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === recipe_2.default.RECIPE_ID) { + } + } else if (recipeId === recipe_3.default.RECIPE_ID) { + try { + recipe_3.default.getInstanceOrThrowError(); + recipe = "thirdparty"; + } catch (e) { + // No - op + } + if (recipe === undefined) { try { - const userResponse = yield thirdparty_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdparty"; - } + // we detect if this recipe has been init or not.. + recipe_5.default.getInstanceOrThrowError(); + recipe = "thirdpartyemailpassword"; } catch (e) { // No - op } - if (user === undefined) { - try { - const userResponse = yield thirdpartyemailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - if (user === undefined) { - try { - const userResponse = yield thirdpartypasswordless_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === recipe_3.default.RECIPE_ID) { + } + if (recipe === undefined) { try { - const userResponse = yield passwordless_1.default.getUserById({ - userId, - }); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "passwordless"; - } + recipe_6.default.getInstanceOrThrowError(); + recipe = "thirdpartypasswordless"; } catch (e) { // No - op } - if (user === undefined) { - try { - const userResponse = yield thirdpartypasswordless_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } + } + } else if (recipeId === recipe_4.default.RECIPE_ID) { + try { + recipe_4.default.getInstanceOrThrowError(); + recipe = "passwordless"; + } catch (e) { + // No - op + } + if (recipe === undefined) { + try { + recipe_6.default.getInstanceOrThrowError(); + recipe = "thirdpartypasswordless"; + } catch (e) { + // No - op } } - return { - user, - recipe, - }; - }); + } + return { + user, + recipe, + }; } -exports.getUserForRecipeId = getUserForRecipeId; function isRecipeInitialised(recipeId) { let isRecipeInitialised = false; if (recipeId === "emailpassword") { try { - recipe_1.default.getInstanceOrThrowError(); + recipe_2.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} if (!isRecipeInitialised) { try { - recipe_4.default.getInstanceOrThrowError(); + recipe_5.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} } } else if (recipeId === "passwordless") { try { - recipe_3.default.getInstanceOrThrowError(); + recipe_4.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} if (!isRecipeInitialised) { try { - recipe_5.default.getInstanceOrThrowError(); + recipe_6.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} } } else if (recipeId === "thirdparty") { try { - recipe_2.default.getInstanceOrThrowError(); + recipe_3.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} if (!isRecipeInitialised) { try { - recipe_4.default.getInstanceOrThrowError(); + recipe_5.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} } if (!isRecipeInitialised) { try { - recipe_5.default.getInstanceOrThrowError(); + recipe_6.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} } @@ -230,17 +202,15 @@ function isRecipeInitialised(recipeId) { return isRecipeInitialised; } exports.isRecipeInitialised = isRecipeInitialised; -function validateApiKey(input) { - return __awaiter(this, void 0, void 0, function* () { - let apiKeyHeaderValue = input.req.getHeaderValue("authorization"); - // We receieve the api key as `Bearer API_KEY`, this retrieves just the key - apiKeyHeaderValue = - apiKeyHeaderValue === null || apiKeyHeaderValue === void 0 ? void 0 : apiKeyHeaderValue.split(" ")[1]; - if (apiKeyHeaderValue === undefined) { - return false; - } - return apiKeyHeaderValue === input.config.apiKey; - }); +async function validateApiKey(input) { + let apiKeyHeaderValue = input.req.getHeaderValue("authorization"); + // We receieve the api key as `Bearer API_KEY`, this retrieves just the key + apiKeyHeaderValue = + apiKeyHeaderValue === null || apiKeyHeaderValue === void 0 ? void 0 : apiKeyHeaderValue.split(" ")[1]; + if (apiKeyHeaderValue === undefined) { + return false; + } + return apiKeyHeaderValue === input.config.apiKey; } exports.validateApiKey = validateApiKey; function getApiPathWithDashboardBase(path) { diff --git a/lib/build/recipe/emailpassword/api/emailExists.js b/lib/build/recipe/emailpassword/api/emailExists.js index 059db0319..b76ac08c7 100644 --- a/lib/build/recipe/emailpassword/api/emailExists.js +++ b/lib/build/recipe/emailpassword/api/emailExists.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,27 +21,25 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -function emailExists(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/47#issue-751571692 - if (apiImplementation.emailExistsGET === undefined) { - return false; - } - let email = options.req.getKeyValueFromQuery("email"); - if (email === undefined || typeof email !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the email as a GET param", - }); - } - let result = yield apiImplementation.emailExistsGET({ - email, - tenantId, - options, - userContext, +async function emailExists(apiImplementation, tenantId, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/47#issue-751571692 + if (apiImplementation.emailExistsGET === undefined) { + return false; + } + let email = options.req.getKeyValueFromQuery("email"); + if (email === undefined || typeof email !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the email as a GET param", }); - utils_1.send200Response(options.res, result); - return true; + } + let result = await apiImplementation.emailExistsGET({ + email, + tenantId, + options, + userContext, }); + utils_1.send200Response(options.res, result); + return true; } exports.default = emailExists; diff --git a/lib/build/recipe/emailpassword/api/generatePasswordResetToken.js b/lib/build/recipe/emailpassword/api/generatePasswordResetToken.js index 90c591fa1..c9de5389d 100644 --- a/lib/build/recipe/emailpassword/api/generatePasswordResetToken.js +++ b/lib/build/recipe/emailpassword/api/generatePasswordResetToken.js @@ -13,60 +13,27 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const utils_2 = require("./utils"); -function generatePasswordResetToken(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 - if (apiImplementation.generatePasswordResetTokenPOST === undefined) { - return false; - } - // step 1 - let formFields = yield utils_2.validateFormFieldsOrThrowError( - options.config.resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm, - (yield options.req.getJSONBody()).formFields, - tenantId - ); - let result = yield apiImplementation.generatePasswordResetTokenPOST({ - formFields, - tenantId, - options, - userContext, - }); - utils_1.send200Response(options.res, result); - return true; +async function generatePasswordResetToken(apiImplementation, tenantId, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 + if (apiImplementation.generatePasswordResetTokenPOST === undefined) { + return false; + } + // step 1 + let formFields = await utils_2.validateFormFieldsOrThrowError( + options.config.resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm, + (await options.req.getJSONBody()).formFields, + tenantId + ); + let result = await apiImplementation.generatePasswordResetTokenPOST({ + formFields, + tenantId, + options, + userContext, }); + utils_1.send200Response(options.res, result); + return true; } exports.default = generatePasswordResetToken; diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 4be718c00..06fd3603e 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -38,118 +7,569 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const logger_1 = require("../../../logger"); const session_1 = __importDefault(require("../../session")); -const utils_1 = require("../utils"); +const __1 = require("../../../"); +const recipe_1 = __importDefault(require("../../accountlinking/recipe")); +const recipe_2 = __importDefault(require("../../emailverification/recipe")); +const recipeUserId_1 = __importDefault(require("../../../recipeUserId")); function getAPIImplementation() { return { - emailExistsGET: function ({ email, tenantId, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield options.recipeImplementation.getUserByEmail({ email, tenantId, userContext }); - return { - status: "OK", - exists: user !== undefined, - }; - }); + emailExistsGET: async function ({ email, tenantId }) { + // even if the above returns true, we still need to check if there + // exists an email password user with the same email cause the function + // above does not check for that. + let users = await __1.listUsersByAccountInfo( + tenantId, + { + email, + }, + false + ); + let emailPasswordUserExists = + users.find((u) => { + return ( + u.loginMethods.find((lm) => lm.recipeId === "emailpassword" && lm.hasSameEmailAs(email)) !== + undefined + ); + }) !== undefined; + return { + status: "OK", + exists: emailPasswordUserExists, + }; }, - generatePasswordResetTokenPOST: function ({ formFields, tenantId, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let email = formFields.filter((f) => f.id === "email")[0].value; - let user = yield options.recipeImplementation.getUserByEmail({ email, tenantId, userContext }); - if (user === undefined) { + generatePasswordResetTokenPOST: async function ({ formFields, tenantId, options, userContext }) { + const email = formFields.filter((f) => f.id === "email")[0].value; + // this function will be reused in different parts of the flow below.. + async function generateAndSendPasswordResetToken(primaryUserId, recipeUserId) { + // the user ID here can be primary or recipe level. + let response = await options.recipeImplementation.createResetPasswordToken({ + tenantId, + userId: recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString(), + email, + userContext, + }); + if (response.status === "UNKNOWN_USER_ID_ERROR") { + logger_1.logDebugMessage( + `Password reset email not sent, unknown user id: ${ + recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString() + }` + ); return { status: "OK", }; } - let response = yield options.recipeImplementation.createResetPasswordToken({ - userId: user.id, + let passwordResetLink = + options.appInfo.websiteDomain.getAsStringDangerous() + + options.appInfo.websiteBasePath.getAsStringDangerous() + + "/reset-password?token=" + + response.token + + "&rid=" + + options.recipeId; + logger_1.logDebugMessage(`Sending password reset email to ${email}`); + await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ tenantId, + type: "PASSWORD_RESET", + user: { + id: primaryUserId, + recipeUserId, + email, + }, + passwordResetLink, userContext, }); - if (response.status === "UNKNOWN_USER_ID_ERROR") { - logger_1.logDebugMessage(`Password reset email not sent, unknown user id: ${user.id}`); + return { + status: "OK", + }; + } + /** + * check if primaryUserId is linked with this email + */ + let users = await __1.listUsersByAccountInfo( + tenantId, + { + email, + }, + false + ); + // we find the recipe user ID of the email password account from the user's list + // for later use. + let emailPasswordAccount = undefined; + for (let i = 0; i < users.length; i++) { + let emailPasswordAccountTmp = users[i].loginMethods.find( + (l) => l.recipeId === "emailpassword" && l.hasSameEmailAs(email) + ); + if (emailPasswordAccountTmp !== undefined) { + emailPasswordAccount = emailPasswordAccountTmp; + break; + } + } + // we find the primary user ID from the user's list for later use. + let primaryUserAssociatedWithEmail = users.find((u) => u.isPrimaryUser); + // first we check if there even exists a primary user that has the input email + // if not, then we do the regular flow for password reset. + if (primaryUserAssociatedWithEmail === undefined) { + if (emailPasswordAccount === undefined) { + logger_1.logDebugMessage(`Password reset email not sent, unknown user email: ${email}`); return { status: "OK", }; } - let passwordResetLink = utils_1.getPasswordResetLink({ - appInfo: options.appInfo, - token: response.token, - recipeId: options.recipeId, + return await generateAndSendPasswordResetToken( + emailPasswordAccount.recipeUserId.getAsString(), + emailPasswordAccount.recipeUserId + ); + } + let shouldDoAccountLinkingResponse = await recipe_1.default + .getInstance() + .config.shouldDoAutomaticAccountLinking( + emailPasswordAccount !== undefined + ? emailPasswordAccount + : { + recipeId: "emailpassword", + email, + }, + primaryUserAssociatedWithEmail, tenantId, - }); - logger_1.logDebugMessage(`Sending password reset email to ${email}`); - yield options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORD_RESET", - user, - passwordResetLink, + userContext + ); + // Now we need to check that if there exists any email password user at all + // for the input email. If not, then it implies that when the token is consumed, + // then we will create a new user - so we should only generate the token if + // the criteria for the new user is met. + if (emailPasswordAccount === undefined) { + // this means that there is no email password user that exists for the input email. + // So we check for the sign up condition and only go ahead if that condition is + // met. + // But first we must check if account linking is enabled at all - cause if it's + // not, then the new email password user that will be created in password reset + // code consume cannot be linked to the primary user - therefore, we should + // not generate a password reset token + if (!shouldDoAccountLinkingResponse.shouldAutomaticallyLink) { + logger_1.logDebugMessage( + `Password reset email not sent, since email password user didn't exist, and account linking not enabled` + ); + return { + status: "OK", + }; + } + let isSignUpAllowed = await recipe_1.default.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "emailpassword", + email, + }, + isVerified: true, tenantId, userContext, }); + if (isSignUpAllowed) { + // notice that we pass in the primary user ID here. This means that + // we will be creating a new email password account when the token + // is consumed and linking it to this primary user. + return await generateAndSendPasswordResetToken(primaryUserAssociatedWithEmail.id, undefined); + } else { + logger_1.logDebugMessage( + `Password reset email not sent, isSignUpAllowed returned false for email: ${email}` + ); + return { + status: "OK", + }; + } + } + // At this point, we know that some email password user exists with this email + // and also some primary user ID exist. We now need to find out if they are linked + // together or not. If they are linked together, then we can just generate the token + // else we check for more security conditions (since we will be linking them post token generation) + let areTheTwoAccountsLinked = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.recipeUserId.getAsString() === emailPasswordAccount.recipeUserId.getAsString(); + }) !== undefined; + if (areTheTwoAccountsLinked) { + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } + // Here we know that the two accounts are NOT linked. We now need to check for an + // extra security measure here to make sure that the input email in the primary user + // is verified, and if not, we need to make sure that there is no other email / phone number + // associated with the primary user account. If there is, then we do not proceed. + /* + This security measure helps prevent the following attack: + An attacker has email A and they create an account using TP and it doesn't matter if A is verified or not. Now they create another account using EP with email A and verifies it. Both these accounts are linked. Now the attacker changes the email for EP recipe to B which makes the EP account unverified, but it's still linked. + + If the real owner of B tries to signup using EP, it will say that the account already exists so they may try to reset password which should be denied because then they will end up getting access to attacker's account and verify the EP account. + + The problem with this situation is if the EP account is verified, it will allow further sign-ups with email B which will also be linked to this primary account (that the attacker had created with email A). + + It is important to realize that the attacker had created another account with A because if they hadn't done that, then they wouldn't have access to this account after the real user resets the password which is why it is important to check there is another non-EP account linked to the primary such that the email is not the same as B. + + Exception to the above is that, if there is a third recipe account linked to the above two accounts and has B as verified, then we should allow reset password token generation because user has already proven that the owns the email B + */ + // But first, this only matters it the user cares about checking for email verification status.. + if (!shouldDoAccountLinkingResponse.shouldAutomaticallyLink) { + // here we will go ahead with the token generation cause + // even when the token is consumed, we will not be linking the accounts + // so no need to check for anything + return await generateAndSendPasswordResetToken( + emailPasswordAccount.recipeUserId.getAsString(), + emailPasswordAccount.recipeUserId + ); + } + if (!shouldDoAccountLinkingResponse.shouldRequireVerification) { + // the checks below are related to email verification, and if the user + // does not care about that, then we should just continue with token generation + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } + // Now we start the required security checks. First we check if the primary user + // it has just one linked account. And if that's true, then we continue + // cause then there is no scope for account takeover + if (primaryUserAssociatedWithEmail.loginMethods.length === 1) { + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } + // Next we check if there is any login method in which the input email is verified. + // If that is the case, then it's proven that the user owns the email and we can + // trust linking of the email password account. + let emailVerified = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.hasSameEmailAs(email) && lm.verified; + }) !== undefined; + if (emailVerified) { + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } + // finally, we check if the primary user has any other email / phone number + // associated with this account - and if it does, then it means that + // there is a risk of account takeover, so we do not allow the token to be generated + let hasOtherEmailOrPhone = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + // we do the extra undefined check below cause + // hasSameEmailAs returns false if the lm.email is undefined, and + // we want to check that the email is different as opposed to email + // not existing in lm. + return (lm.email !== undefined && !lm.hasSameEmailAs(email)) || lm.phoneNumber !== undefined; + }) !== undefined; + if (hasOtherEmailOrPhone) { return { - status: "OK", + status: "PASSWORD_RESET_NOT_ALLOWED", + reason: + "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)", }; - }); + } else { + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } }, - passwordResetPOST: function ({ formFields, token, tenantId, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let newPassword = formFields.filter((f) => f.id === "password")[0].value; - let response = yield options.recipeImplementation.resetPasswordUsingToken({ - token, - newPassword, - tenantId, + passwordResetPOST: async function ({ formFields, token, tenantId, options, userContext }) { + async function markEmailAsVerified(recipeUserId, email) { + const emailVerificationInstance = recipe_2.default.getInstance(); + if (emailVerificationInstance) { + const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( + { + tenantId, + recipeUserId, + email, + userContext, + } + ); + if (tokenResponse.status === "OK") { + await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ + tenantId, + token: tokenResponse.token, + attemptAccountLinking: false, + // we anyway do account linking in this API after this function is + // called. + userContext, + }); + } + } + } + async function doUpdatePassword(recipeUserId) { + let updateResponse = await options.recipeImplementation.updateEmailOrPassword({ + tenantIdForPasswordPolicy: tenantId, + // we can treat userIdForWhomTokenWasGenerated as a recipe user id cause + // whenever this function is called, + recipeUserId, + password: newPassword, userContext, }); - return response; + if ( + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + throw new Error("This should never come here because we are not updating the email"); + } else if (updateResponse.status === "UNKNOWN_USER_ID_ERROR") { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + return { + status: "PASSWORD_POLICY_VIOLATED_ERROR", + failureReason: updateResponse.failureReason, + }; + } else { + // status: "OK" + return { + status: "OK", + user: existingUser, + email: emailForWhomTokenWasGenerated, + }; + } + } + let newPassword = formFields.filter((f) => f.id === "password")[0].value; + let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ + token, + tenantId, + userContext, }); - }, - signInPOST: function ({ formFields, tenantId, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; - let response = yield options.recipeImplementation.signIn({ email, password, tenantId, userContext }); - if (response.status === "WRONG_CREDENTIALS_ERROR") { - return response; + if (tokenConsumptionResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { + return tokenConsumptionResponse; + } + let userIdForWhomTokenWasGenerated = tokenConsumptionResponse.userId; + let emailForWhomTokenWasGenerated = tokenConsumptionResponse.email; + let existingUser = await __1.getUser(tokenConsumptionResponse.userId, userContext); + if (existingUser === undefined) { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + // Also note that this being undefined doesn't mean that the email password + // user does not exist, but it means that there is no reicpe or primary user + // for whom the token was generated. + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } + // We start by checking if the existingUser is a primary user or not. If it is, + // then we will try and create a new email password user and link it to the primary user (if required) + if (existingUser.isPrimaryUser) { + // If this user contains an email password account for whom the token was generated, + // then we update that user's password. + let emailPasswordUserIsLinkedToExistingUser = + existingUser.loginMethods.find((lm) => { + // we check based on user ID and not email because the only time + // the primary user ID is used for token generation is if the email password + // user did not exist - in which case the value of emailPasswordUserExists will + // resolve to false anyway, and that's what we want. + // there is an edge case where if the email password recipe user was created + // after the password reset token generation, and it was linked to the + // primary user id (userIdForWhomTokenWasGenerated), in this case, + // we still don't allow password update, cause the user should try again + // and the token should be regenerated for the right recipe user. + return ( + lm.recipeUserId.getAsString() === userIdForWhomTokenWasGenerated && + lm.recipeId === "emailpassword" + ); + }) !== undefined; + if (emailPasswordUserIsLinkedToExistingUser) { + return doUpdatePassword(new recipeUserId_1.default(userIdForWhomTokenWasGenerated)); + } else { + // this means that the existingUser does not have an emailpassword user associated + // with it. It could now mean that no emailpassword user exists, or it could mean that + // the the ep user exists, but it's not linked to the current account. + // If no ep user doesn't exists, we will create one, and link it to the existing account. + // If ep user exists, then it means there is some race condition cause + // then the token should have been generated for that user instead of the primary user, + // and it shouldn't have come into this branch. So we can simply send a password reset + // invalid error and the user can try again. + // NOTE: We do not ask the dev if we should do account linking or not here + // cause we already have asked them this when generating an password reset token. + // In the edge case that the dev changes account linking allowance from true to false + // when it comes here, only a new recipe user id will be created and not linked + // cause createPrimaryUserIdOrLinkAccounts will disallow linking. This doesn't + // really cause any security issue. + let createUserResponse = await options.recipeImplementation.createNewRecipeUser({ + tenantId, + email: tokenConsumptionResponse.email, + password: newPassword, + userContext, + }); + if (createUserResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // this means that the user already existed and we can just return an invalid + // token (see the above comment) + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } else { + // we mark the email as verified because password reset also requires + // access to the email to work.. This has a good side effect that + // any other login method with the same email in existingAccount will also get marked + // as verified. + await markEmailAsVerified( + createUserResponse.user.loginMethods[0].recipeUserId, + tokenConsumptionResponse.email + ); + const updatedUser = await __1.getUser(createUserResponse.user.id, userContext); + if (updatedUser === undefined) { + throw new Error("Should never happen - user deleted after during password reset"); + } + createUserResponse.user = updatedUser; + // Now we try and link the accounts. The function below will try and also + // create a primary user of the new account, and if it does that, it's OK.. + // But in most cases, it will end up linking to existing account since the + // email is shared. + let linkedToUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: createUserResponse.user, + userContext, + }); + if (linkedToUser.id !== existingUser.id) { + // this means that the account we just linked to + // was not the one we had expected to link it to. This can happen + // due to some race condition or the other.. Either way, this + // is not an issue and we can just return OK + } + return { + status: "OK", + email: tokenConsumptionResponse.email, + user: linkedToUser, + }; + } } - let user = response.user; - let session = yield session_1.default.createNewSession( - options.req, - options.res, - tenantId, - user.id, - {}, - {}, - userContext - ); + } else { + // This means that the existing user is not a primary account, which implies that + // it must be a non linked email password account. In this case, we simply update the password. + // Linking to an existing account will be done after the user goes through the email + // verification flow once they log in (if applicable). + return doUpdatePassword(new recipeUserId_1.default(userIdForWhomTokenWasGenerated)); + } + }, + signInPOST: async function ({ formFields, tenantId, options, userContext }) { + let email = formFields.filter((f) => f.id === "email")[0].value; + let password = formFields.filter((f) => f.id === "password")[0].value; + let response = await options.recipeImplementation.signIn({ email, password, tenantId, userContext }); + if (response.status === "WRONG_CREDENTIALS_ERROR") { + return response; + } + let emailPasswordRecipeUser = response.user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.hasSameEmailAs(email) + ); + if (emailPasswordRecipeUser === undefined) { + // this can happen cause of some race condition, but it's not a big deal. + throw new Error("Race condition error - please call this API again"); + } + // Here we do this check after sign in is done cause: + // - We first want to check if the credentials are correct first or not + // - The above recipe function marks the email as verified if other linked users + // with the same email are verified. The function below checks for the email verification + // so we want to call it only once this is up to date, + let isSignInAllowed = await recipe_1.default.getInstance().isSignInAllowed({ + user: response.user, + tenantId, + userContext, + }); + if (!isSignInAllowed) { return { - status: "OK", - session, - user, + status: "SIGN_IN_NOT_ALLOWED", + reason: + "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)", }; + } + // the above sign in recipe function does not do account linking - so we do it here. + response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, }); + let session = await session_1.default.createNewSession( + options.req, + options.res, + tenantId, + emailPasswordRecipeUser.recipeUserId, + {}, + {}, + userContext + ); + return { + status: "OK", + session, + user: response.user, + }; }, - signUpPOST: function ({ formFields, tenantId, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; - let response = yield options.recipeImplementation.signUp({ email, password, tenantId, userContext }); - if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return response; + signUpPOST: async function ({ formFields, tenantId, options, userContext }) { + let email = formFields.filter((f) => f.id === "email")[0].value; + let password = formFields.filter((f) => f.id === "password")[0].value; + // Here we do this check because if the input email already exists with a primary user, + // then we do not allow sign up, cause even though we do not link this and the existing + // account right away, and we send an email verification link, the user + // may click on it by mistake assuming it's for their existing account - resulting + // in account take over. In this case, we return an EMAIL_ALREADY_EXISTS_ERROR + // and if the user goes through the forgot password flow, it will create + // an account there and it will work fine cause there the email is also verified. + let isSignUpAllowed = await recipe_1.default.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "emailpassword", + email, + }, + isVerified: false, + tenantId, + userContext, + }); + if (!isSignUpAllowed) { + const conflictingUsers = await recipe_1.default + .getInstance() + .recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo: { + email, + }, + doUnionOfAccountInfo: false, + userContext, + }); + if ( + conflictingUsers.some((u) => + u.loginMethods.some((lm) => lm.recipeId === "emailpassword" && lm.hasSameEmailAs(email)) + ) + ) { + return { + status: "EMAIL_ALREADY_EXISTS_ERROR", + }; } - let user = response.user; - let session = yield session_1.default.createNewSession( - options.req, - options.res, - tenantId, - user.id, - {}, - {}, - userContext - ); return { - status: "OK", - session, - user, + status: "SIGN_UP_NOT_ALLOWED", + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", }; + } + // this function also does account linking + let response = await options.recipeImplementation.signUp({ + tenantId, + email, + password, + userContext, }); + if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { + return response; + } + let emailPasswordRecipeUser = response.user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.hasSameEmailAs(email) + ); + if (emailPasswordRecipeUser === undefined) { + // this can happen cause of some race condition, but it's not a big deal. + throw new Error("Race condition error - please call this API again"); + } + let session = await session_1.default.createNewSession( + options.req, + options.res, + tenantId, + emailPasswordRecipeUser.recipeUserId, + {}, + {}, + userContext + ); + return { + status: "OK", + session, + user: response.user, + }; }, }; } diff --git a/lib/build/recipe/emailpassword/api/passwordReset.js b/lib/build/recipe/emailpassword/api/passwordReset.js index 9fc50b87b..6a4e3969e 100644 --- a/lib/build/recipe/emailpassword/api/passwordReset.js +++ b/lib/build/recipe/emailpassword/api/passwordReset.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,47 +22,62 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const utils_2 = require("./utils"); const error_1 = __importDefault(require("../error")); -function passwordReset(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 - if (apiImplementation.passwordResetPOST === undefined) { - return false; - } - // step 1 - let formFields = yield utils_2.validateFormFieldsOrThrowError( - options.config.resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm, - (yield options.req.getJSONBody()).formFields, - tenantId - ); - let token = (yield options.req.getJSONBody()).token; - if (token === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the password reset token", - }); - } - if (typeof token !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "The password reset token must be a string", - }); - } - let result = yield apiImplementation.passwordResetPOST({ - formFields, - token, - tenantId, - options, - userContext, +async function passwordReset(apiImplementation, tenantId, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 + if (apiImplementation.passwordResetPOST === undefined) { + return false; + } + // step 1: We need to do this here even though the update emailpassword recipe function would do this cause: + // - we want to throw this error before consuming the token, so that the user can try again + // - there is a case in the api impl where we create a new user, and we want to assign + // a password that meets the password policy. + let formFields = await utils_2.validateFormFieldsOrThrowError( + options.config.resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm, + (await options.req.getJSONBody()).formFields, + tenantId + ); + let token = (await options.req.getJSONBody()).token; + if (token === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the password reset token", }); - utils_1.send200Response( - options.res, - result.status === "OK" - ? { - status: "OK", - } - : result - ); - return true; + } + if (typeof token !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "The password reset token must be a string", + }); + } + let result = await apiImplementation.passwordResetPOST({ + formFields, + token, + tenantId, + options, + userContext, }); + if (result.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + // this error will be caught by the recipe error handler, just + // like it's done in the validateFormFieldsOrThrowError function above. + throw new error_1.default({ + type: error_1.default.FIELD_ERROR, + payload: [ + { + id: "password", + error: result.failureReason, + }, + ], + message: "Error in input formFields", + }); + } + utils_1.send200Response( + options.res, + result.status === "OK" + ? { + status: "OK", + } + : result + ); + return true; } exports.default = passwordReset; diff --git a/lib/build/recipe/emailpassword/api/signin.js b/lib/build/recipe/emailpassword/api/signin.js index 0351b9064..235d8f0c6 100644 --- a/lib/build/recipe/emailpassword/api/signin.js +++ b/lib/build/recipe/emailpassword/api/signin.js @@ -13,67 +13,34 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const utils_2 = require("./utils"); -function signInAPI(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/20#issuecomment-710346362 - if (apiImplementation.signInPOST === undefined) { - return false; - } - // step 1 - let formFields = yield utils_2.validateFormFieldsOrThrowError( - options.config.signInFeature.formFields, - (yield options.req.getJSONBody()).formFields, - tenantId - ); - let result = yield apiImplementation.signInPOST({ - formFields, - tenantId, - options, - userContext, - }); - if (result.status === "OK") { - utils_1.send200Response(options.res, { - status: "OK", - user: result.user, - }); - } else { - utils_1.send200Response(options.res, result); - } - return true; +async function signInAPI(apiImplementation, tenantId, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/20#issuecomment-710346362 + if (apiImplementation.signInPOST === undefined) { + return false; + } + // step 1 + let formFields = await utils_2.validateFormFieldsOrThrowError( + options.config.signInFeature.formFields, + (await options.req.getJSONBody()).formFields, + tenantId + ); + let result = await apiImplementation.signInPOST({ + formFields, + tenantId, + options, + userContext, }); + if (result.status === "OK") { + utils_1.send200Response( + options.res, + Object.assign({ status: "OK" }, utils_1.getBackwardsCompatibleUserInfo(options.req, result)) + ); + } else { + utils_1.send200Response(options.res, result); + } + return true; } exports.default = signInAPI; diff --git a/lib/build/recipe/emailpassword/api/signup.js b/lib/build/recipe/emailpassword/api/signup.js index fa66ca4c9..d6468cf21 100644 --- a/lib/build/recipe/emailpassword/api/signup.js +++ b/lib/build/recipe/emailpassword/api/signup.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,44 +22,44 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const utils_2 = require("./utils"); const error_1 = __importDefault(require("../error")); -function signUpAPI(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/21#issuecomment-710423536 - if (apiImplementation.signUpPOST === undefined) { - return false; - } - // step 1 - let formFields = yield utils_2.validateFormFieldsOrThrowError( - options.config.signUpFeature.formFields, - (yield options.req.getJSONBody()).formFields, - tenantId +async function signUpAPI(apiImplementation, tenantId, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/21#issuecomment-710423536 + if (apiImplementation.signUpPOST === undefined) { + return false; + } + // step 1 + let formFields = await utils_2.validateFormFieldsOrThrowError( + options.config.signUpFeature.formFields, + (await options.req.getJSONBody()).formFields, + tenantId + ); + let result = await apiImplementation.signUpPOST({ + formFields, + tenantId, + options, + userContext: userContext, + }); + if (result.status === "OK") { + utils_1.send200Response( + options.res, + Object.assign({ status: "OK" }, utils_1.getBackwardsCompatibleUserInfo(options.req, result)) ); - let result = yield apiImplementation.signUpPOST({ - formFields, - tenantId, - options, - userContext: userContext, + } else if (result.status === "GENERAL_ERROR") { + utils_1.send200Response(options.res, result); + } else if (result.status === "EMAIL_ALREADY_EXISTS_ERROR") { + throw new error_1.default({ + type: error_1.default.FIELD_ERROR, + payload: [ + { + id: "email", + error: "This email already exists. Please sign in instead.", + }, + ], + message: "Error in input formFields", }); - if (result.status === "OK") { - utils_1.send200Response(options.res, { - status: "OK", - user: result.user, - }); - } else if (result.status === "GENERAL_ERROR") { - utils_1.send200Response(options.res, result); - } else { - throw new error_1.default({ - type: error_1.default.FIELD_ERROR, - payload: [ - { - id: "email", - error: "This email already exists. Please sign in instead.", - }, - ], - message: "Error in input formFields", - }); - } - return true; - }); + } else { + utils_1.send200Response(options.res, result); + } + return true; } exports.default = signUpAPI; diff --git a/lib/build/recipe/emailpassword/api/utils.js b/lib/build/recipe/emailpassword/api/utils.js index e1164872d..84f7a4f7f 100644 --- a/lib/build/recipe/emailpassword/api/utils.js +++ b/lib/build/recipe/emailpassword/api/utils.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,42 +8,40 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.validateFormFieldsOrThrowError = void 0; const error_1 = __importDefault(require("../error")); const constants_1 = require("../constants"); -function validateFormFieldsOrThrowError(configFormFields, formFieldsRaw, tenantId) { - return __awaiter(this, void 0, void 0, function* () { - // first we check syntax ---------------------------- - if (formFieldsRaw === undefined) { - throw newBadRequestError("Missing input param: formFields"); +async function validateFormFieldsOrThrowError(configFormFields, formFieldsRaw, tenantId) { + // first we check syntax ---------------------------- + if (formFieldsRaw === undefined) { + throw newBadRequestError("Missing input param: formFields"); + } + if (!Array.isArray(formFieldsRaw)) { + throw newBadRequestError("formFields must be an array"); + } + let formFields = []; + for (let i = 0; i < formFieldsRaw.length; i++) { + let curr = formFieldsRaw[i]; + if (typeof curr !== "object" || curr === null) { + throw newBadRequestError("All elements of formFields must be an object"); } - if (!Array.isArray(formFieldsRaw)) { - throw newBadRequestError("formFields must be an array"); + if (typeof curr.id !== "string" || curr.value === undefined) { + throw newBadRequestError("All elements of formFields must contain an 'id' and 'value' field"); } - let formFields = []; - for (let i = 0; i < formFieldsRaw.length; i++) { - let curr = formFieldsRaw[i]; - if (typeof curr !== "object" || curr === null) { - throw newBadRequestError("All elements of formFields must be an object"); - } - if (typeof curr.id !== "string" || curr.value === undefined) { - throw newBadRequestError("All elements of formFields must contain an 'id' and 'value' field"); + if (curr.id === constants_1.FORM_FIELD_EMAIL_ID || curr.id === constants_1.FORM_FIELD_PASSWORD_ID) { + if (typeof curr.value !== "string") { + throw newBadRequestError("The value of formFields with id = " + curr.id + " must be a string"); } - if (curr.id === constants_1.FORM_FIELD_EMAIL_ID || curr.id === constants_1.FORM_FIELD_PASSWORD_ID) { - if (typeof curr.value !== "string") { - throw newBadRequestError("The value of formFields with id = " + curr.id + " must be a string"); - } - } - formFields.push(curr); } - // we trim the email: https://github.com/supertokens/supertokens-core/issues/99 - formFields = formFields.map((field) => { - if (field.id === constants_1.FORM_FIELD_EMAIL_ID) { - return Object.assign(Object.assign({}, field), { value: field.value.trim() }); - } - return field; - }); - // then run validators through them----------------------- - yield validateFormOrThrowError(formFields, configFormFields, tenantId); - return formFields; + formFields.push(curr); + } + // we trim the email: https://github.com/supertokens/supertokens-core/issues/99 + formFields = formFields.map((field) => { + if (field.id === constants_1.FORM_FIELD_EMAIL_ID) { + return Object.assign(Object.assign({}, field), { value: field.value.trim() }); + } + return field; }); + // then run validators through them----------------------- + await validateFormOrThrowError(formFields, configFormFields, tenantId); + return formFields; } exports.validateFormFieldsOrThrowError = validateFormFieldsOrThrowError; function newBadRequestError(message) { @@ -85,41 +52,39 @@ function newBadRequestError(message) { } // We check that the number of fields in input and config form field is the same. // We check that each item in the config form field is also present in the input form field -function validateFormOrThrowError(inputs, configFormFields, tenantId) { - return __awaiter(this, void 0, void 0, function* () { - let validationErrors = []; - if (configFormFields.length !== inputs.length) { - throw newBadRequestError("Are you sending too many / too few formFields?"); - } - // Loop through all form fields. - for (let i = 0; i < configFormFields.length; i++) { - const field = configFormFields[i]; - // Find corresponding input value. - const input = inputs.find((i) => i.id === field.id); - // Absent or not optional empty field - if (input === undefined || (input.value === "" && !field.optional)) { +async function validateFormOrThrowError(inputs, configFormFields, tenantId) { + let validationErrors = []; + if (configFormFields.length !== inputs.length) { + throw newBadRequestError("Are you sending too many / too few formFields?"); + } + // Loop through all form fields. + for (let i = 0; i < configFormFields.length; i++) { + const field = configFormFields[i]; + // Find corresponding input value. + const input = inputs.find((i) => i.id === field.id); + // Absent or not optional empty field + if (input === undefined || (input.value === "" && !field.optional)) { + validationErrors.push({ + error: "Field is not optional", + id: field.id, + }); + } else { + // Otherwise, use validate function. + const error = await field.validate(input.value, tenantId); + // If error, add it. + if (error !== undefined) { validationErrors.push({ - error: "Field is not optional", + error, id: field.id, }); - } else { - // Otherwise, use validate function. - const error = yield field.validate(input.value, tenantId); - // If error, add it. - if (error !== undefined) { - validationErrors.push({ - error, - id: field.id, - }); - } } } - if (validationErrors.length !== 0) { - throw new error_1.default({ - type: error_1.default.FIELD_ERROR, - payload: validationErrors, - message: "Error in input formFields", - }); - } - }); + } + if (validationErrors.length !== 0) { + throw new error_1.default({ + type: error_1.default.FIELD_ERROR, + payload: validationErrors, + message: "Error in input formFields", + }); + } } diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts index 3f9e44647..4ee0e54df 100644 --- a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,13 +1,12 @@ // @ts-nocheck -import { TypeEmailPasswordEmailDeliveryInput, RecipeInterface } from "../../../types"; +import { TypeEmailPasswordEmailDeliveryInput } from "../../../types"; import { NormalisedAppinfo } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { - private recipeInterfaceImpl; private isInServerlessEnv; private appInfo; - constructor(recipeInterfaceImpl: RecipeInterface, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean); + constructor(appInfo: NormalisedAppinfo, isInServerlessEnv: boolean); sendEmail: ( input: TypeEmailPasswordEmailDeliveryInput & { userContext: any; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js index 6cd37e689..8d799d719 100644 --- a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js @@ -1,68 +1,27 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const passwordResetFunctions_1 = require("../../../passwordResetFunctions"); class BackwardCompatibilityService { - constructor(recipeInterfaceImpl, appInfo, isInServerlessEnv) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - let user = yield this.recipeInterfaceImpl.getUserById({ - userId: input.user.id, - userContext: input.userContext, - }); - if (user === undefined) { - throw Error("this should never come here"); + constructor(appInfo, isInServerlessEnv) { + this.sendEmail = async (input) => { + // we add this here cause the user may have overridden the sendEmail function + // to change the input email and if we don't do this, the input email + // will get reset by the getUserById call above. + try { + if (!this.isInServerlessEnv) { + passwordResetFunctions_1 + .createAndSendEmailUsingSupertokensService(this.appInfo, input.user, input.passwordResetLink) + .catch((_) => {}); + } else { + // see https://github.com/supertokens/supertokens-node/pull/135 + await passwordResetFunctions_1.createAndSendEmailUsingSupertokensService( + this.appInfo, + input.user, + input.passwordResetLink + ); } - // we add this here cause the user may have overridden the sendEmail function - // to change the input email and if we don't do this, the input email - // will get reset by the getUserById call above. - user.email = input.user.email; - try { - if (!this.isInServerlessEnv) { - passwordResetFunctions_1 - .createAndSendEmailUsingSupertokensService(this.appInfo, user, input.passwordResetLink) - .catch((_) => {}); - } else { - // see https://github.com/supertokens/supertokens-node/pull/135 - yield passwordResetFunctions_1.createAndSendEmailUsingSupertokensService( - this.appInfo, - user, - input.passwordResetLink - ); - } - } catch (_) {} - }); - this.recipeInterfaceImpl = recipeInterfaceImpl; + } catch (_) {} + }; this.isInServerlessEnv = isInServerlessEnv; this.appInfo = appInfo; } diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.js b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.js index e917fb0fe..dedcbe33f 100644 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.js +++ b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -41,13 +10,12 @@ const supertokens_js_override_1 = __importDefault(require("supertokens-js-overri const serviceImplementation_1 = require("./serviceImplementation"); class SMTPService { constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - let content = yield this.serviceImpl.getContent(input); - yield this.serviceImpl.sendRawEmail( - Object.assign(Object.assign({}, content), { userContext: input.userContext }) - ); - }); + this.sendEmail = async (input) => { + let content = await this.serviceImpl.getContent(input); + await this.serviceImpl.sendRawEmail( + Object.assign(Object.assign({}, content), { userContext: input.userContext }) + ); + }; const transporter = nodemailer_1.createTransport({ host: config.smtpSettings.host, port: config.smtpSettings.port, diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.js b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.js index f4ebb5266..bc0d7525c 100644 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.js +++ b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -54,29 +23,25 @@ exports.getServiceImplementation = void 0; const passwordReset_1 = __importDefault(require("../passwordReset")); function getServiceImplementation(transporter, from) { return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (input.isHtml) { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }); + sendRawEmail: async function (input) { + if (input.isHtml) { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + html: input.body, + }); + } else { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + text: input.body, + }); + } }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return passwordReset_1.default(input); - }); + getContent: async function (input) { + return passwordReset_1.default(input); }, }; } diff --git a/lib/build/recipe/emailpassword/index.d.ts b/lib/build/recipe/emailpassword/index.d.ts index 699bdfb5c..8a5678f9b 100644 --- a/lib/build/recipe/emailpassword/index.d.ts +++ b/lib/build/recipe/emailpassword/index.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, User, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; +import { RecipeInterface, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init: typeof Recipe.init; static Error: typeof SuperTokensError; @@ -13,7 +14,8 @@ export default class Wrapper { ): Promise< | { status: "OK"; - user: User; + user: import("../../types").User; + recipeUserId: RecipeUserId; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -27,17 +29,28 @@ export default class Wrapper { ): Promise< | { status: "OK"; - user: User; + user: import("../../types").User; + recipeUserId: RecipeUserId; } | { status: "WRONG_CREDENTIALS_ERROR"; } >; - static getUserById(userId: string, userContext?: any): Promise; - static getUserByEmail(tenantId: string, email: string, userContext?: any): Promise; + /** + * We do not make email optional here cause we want to + * allow passing in primaryUserId. If we make email optional, + * and if the user provides a primaryUserId, then it may result in two problems: + * - there is no recipeUserId = input primaryUserId, in this case, + * this function will throw an error + * - There is a recipe userId = input primaryUserId, but that recipe has no email, + * or has wrong email compared to what the user wanted to generate a reset token for. + * + * And we want to allow primaryUserId being passed in. + */ static createResetPasswordToken( tenantId: string, userId: string, + email: string, userContext?: any ): Promise< | { @@ -53,17 +66,38 @@ export default class Wrapper { token: string, newPassword: string, userContext?: any + ): Promise< + | { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; + } + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "PASSWORD_POLICY_VIOLATED_ERROR"; + failureReason: string; + } + >; + static consumePasswordResetToken( + tenantId: string, + token: string, + userContext?: any ): Promise< | { status: "OK"; - userId?: string | undefined; + email: string; + userId: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; } >; static updateEmailOrPassword(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext?: any; @@ -71,7 +105,11 @@ export default class Wrapper { tenantIdForPasswordPolicy?: string; }): Promise< | { - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR"; + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; @@ -81,6 +119,7 @@ export default class Wrapper { static createResetPasswordLink( tenantId: string, userId: string, + email: string, userContext?: any ): Promise< | { @@ -94,6 +133,7 @@ export default class Wrapper { static sendResetPasswordEmail( tenantId: string, userId: string, + email: string, userContext?: any ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR"; @@ -108,12 +148,11 @@ export declare let init: typeof Recipe.init; export declare let Error: typeof SuperTokensError; export declare let signUp: typeof Wrapper.signUp; export declare let signIn: typeof Wrapper.signIn; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByEmail: typeof Wrapper.getUserByEmail; export declare let createResetPasswordToken: typeof Wrapper.createResetPasswordToken; export declare let resetPasswordUsingToken: typeof Wrapper.resetPasswordUsingToken; +export declare let consumePasswordResetToken: typeof Wrapper.consumePasswordResetToken; export declare let updateEmailOrPassword: typeof Wrapper.updateEmailOrPassword; -export type { RecipeInterface, User, APIOptions, APIInterface }; +export type { RecipeInterface, APIOptions, APIInterface }; export declare let createResetPasswordLink: typeof Wrapper.createResetPasswordLink; export declare let sendResetPasswordEmail: typeof Wrapper.sendResetPasswordEmail; export declare let sendEmail: typeof Wrapper.sendEmail; diff --git a/lib/build/recipe/emailpassword/index.js b/lib/build/recipe/emailpassword/index.js index 96181eb23..564796686 100644 --- a/lib/build/recipe/emailpassword/index.js +++ b/lib/build/recipe/emailpassword/index.js @@ -13,48 +13,19 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendEmail = exports.sendResetPasswordEmail = exports.createResetPasswordLink = exports.updateEmailOrPassword = exports.resetPasswordUsingToken = exports.createResetPasswordToken = exports.getUserByEmail = exports.getUserById = exports.signIn = exports.signUp = exports.Error = exports.init = void 0; +exports.sendEmail = exports.sendResetPasswordEmail = exports.createResetPasswordLink = exports.updateEmailOrPassword = exports.consumePasswordResetToken = exports.resetPasswordUsingToken = exports.createResetPasswordToken = exports.signIn = exports.signUp = exports.Error = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); const utils_1 = require("./utils"); +const __1 = require("../.."); class Wrapper { static signUp(tenantId, email, password, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.signUp({ @@ -72,37 +43,50 @@ class Wrapper { userContext: userContext === undefined ? {} : userContext, }); } - static getUserById(userId, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ + /** + * We do not make email optional here cause we want to + * allow passing in primaryUserId. If we make email optional, + * and if the user provides a primaryUserId, then it may result in two problems: + * - there is no recipeUserId = input primaryUserId, in this case, + * this function will throw an error + * - There is a recipe userId = input primaryUserId, but that recipe has no email, + * or has wrong email compared to what the user wanted to generate a reset token for. + * + * And we want to allow primaryUserId being passed in. + */ + static createResetPasswordToken(tenantId, userId, email, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - static getUserByEmail(tenantId, email, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ email, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, userContext: userContext === undefined ? {} : userContext, }); } - static createResetPasswordToken(tenantId, userId, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ - userId, - tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + static async resetPasswordUsingToken(tenantId, token, newPassword, userContext) { + const consumeResp = await Wrapper.consumePasswordResetToken(tenantId, token, userContext); + if (consumeResp.status !== "OK") { + return consumeResp; + } + return await Wrapper.updateEmailOrPassword({ + recipeUserId: new recipeUserId_1.default(consumeResp.userId), + email: consumeResp.email, + password: newPassword, + tenantIdForPasswordPolicy: tenantId, + userContext, }); } - static resetPasswordUsingToken(tenantId, token, newPassword, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + static consumePasswordResetToken(tenantId, token, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, - newPassword, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, userContext: userContext === undefined ? {} : userContext, }); } static updateEmailOrPassword(input) { + var _a; return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword( - Object.assign(Object.assign({ userContext: {} }, input), { + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, tenantIdForPasswordPolicy: input.tenantIdForPasswordPolicy === undefined ? constants_1.DEFAULT_TENANT_ID @@ -110,51 +94,57 @@ class Wrapper { }) ); } - static createResetPasswordLink(tenantId, userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let token = yield exports.createResetPasswordToken(tenantId, userId, userContext); - if (token.status === "UNKNOWN_USER_ID_ERROR") { - return token; - } - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return { - status: "OK", - link: utils_1.getPasswordResetLink({ - appInfo: recipeInstance.getAppInfo(), - recipeId: recipeInstance.getRecipeId(), - token: token.token, - tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - }), - }; - }); + static async createResetPasswordLink(tenantId, userId, email, userContext = {}) { + let token = await exports.createResetPasswordToken(tenantId, userId, email, userContext); + if (token.status === "UNKNOWN_USER_ID_ERROR") { + return token; + } + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return { + status: "OK", + link: utils_1.getPasswordResetLink({ + appInfo: recipeInstance.getAppInfo(), + recipeId: recipeInstance.getRecipeId(), + token: token.token, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + }), + }; } - static sendResetPasswordEmail(tenantId, userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let link = yield exports.createResetPasswordLink(tenantId, userId, userContext); - if (link.status === "UNKNOWN_USER_ID_ERROR") { - return link; - } - yield exports.sendEmail({ - passwordResetLink: link.link, - type: "PASSWORD_RESET", - user: yield exports.getUserById(userId, userContext), - tenantId, - userContext, - }); - return { - status: "OK", - }; + static async sendResetPasswordEmail(tenantId, userId, email, userContext = {}) { + const user = await __1.getUser(userId, userContext); + if (!user) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + const loginMethod = user.loginMethods.find((m) => m.recipeId === "emailpassword" && m.hasSameEmailAs(email)); + if (!loginMethod) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + let link = await exports.createResetPasswordLink(tenantId, userId, email, userContext); + if (link.status === "UNKNOWN_USER_ID_ERROR") { + return link; + } + await exports.sendEmail({ + passwordResetLink: link.link, + type: "PASSWORD_RESET", + user: { + id: user.id, + recipeUserId: loginMethod.recipeUserId, + email: loginMethod.email, + }, + tenantId, + userContext, }); + return { + status: "OK", + }; } - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - let recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return yield recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign(Object.assign({ userContext: {} }, input), { - tenantId: input.tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : input.tenantId, - }) - ); - }); + static async sendEmail(input) { + let recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( + Object.assign(Object.assign({ userContext: {} }, input), { + tenantId: input.tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : input.tenantId, + }) + ); } } exports.default = Wrapper; @@ -164,10 +154,9 @@ exports.init = Wrapper.init; exports.Error = Wrapper.Error; exports.signUp = Wrapper.signUp; exports.signIn = Wrapper.signIn; -exports.getUserById = Wrapper.getUserById; -exports.getUserByEmail = Wrapper.getUserByEmail; exports.createResetPasswordToken = Wrapper.createResetPasswordToken; exports.resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; +exports.consumePasswordResetToken = Wrapper.consumePasswordResetToken; exports.updateEmailOrPassword = Wrapper.updateEmailOrPassword; exports.createResetPasswordLink = Wrapper.createResetPasswordLink; exports.sendResetPasswordEmail = Wrapper.sendResetPasswordEmail; diff --git a/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts b/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts index 38959a2d7..e7ed30fe1 100644 --- a/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts +++ b/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts @@ -1,8 +1,10 @@ // @ts-nocheck -import { User } from "./types"; import { NormalisedAppinfo } from "../../types"; export declare function createAndSendEmailUsingSupertokensService( appInfo: NormalisedAppinfo, - user: User, + user: { + id: string; + email: string; + }, passwordResetURLWithToken: string ): Promise; diff --git a/lib/build/recipe/emailpassword/passwordResetFunctions.js b/lib/build/recipe/emailpassword/passwordResetFunctions.js index 2a9b7dae8..04a611730 100644 --- a/lib/build/recipe/emailpassword/passwordResetFunctions.js +++ b/lib/build/recipe/emailpassword/passwordResetFunctions.js @@ -13,62 +13,29 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAndSendEmailUsingSupertokensService = void 0; const utils_1 = require("../../utils"); -function createAndSendEmailUsingSupertokensService(appInfo, user, passwordResetURLWithToken) { - return __awaiter(this, void 0, void 0, function* () { - // related issue: https://github.com/supertokens/supertokens-node/issues/38 - if (process.env.TEST_MODE === "testing") { - return; +async function createAndSendEmailUsingSupertokensService(appInfo, user, passwordResetURLWithToken) { + // related issue: https://github.com/supertokens/supertokens-node/issues/38 + if (process.env.TEST_MODE === "testing") { + return; + } + await utils_1.postWithFetch( + "https://api.supertokens.io/0/st/auth/password/reset", + { + "api-version": "0", + "content-type": "application/json; charset=utf-8", + }, + { + email: user.email, + appName: appInfo.appName, + passwordResetURL: passwordResetURLWithToken, + }, + { + successLog: `Password reset email sent to ${user.email}`, + errorLogHeader: "Error sending password reset email", } - yield utils_1.postWithFetch( - "https://api.supertokens.io/0/st/auth/password/reset", - { - "api-version": "0", - "content-type": "application/json; charset=utf-8", - }, - { - email: user.email, - appName: appInfo.appName, - passwordResetURL: passwordResetURLWithToken, - }, - { - successLog: `Password reset email sent to ${user.email}`, - errorLogHeader: "Error sending password reset email", - } - ); - }); + ); } exports.createAndSendEmailUsingSupertokensService = createAndSendEmailUsingSupertokensService; diff --git a/lib/build/recipe/emailpassword/recipe.d.ts b/lib/build/recipe/emailpassword/recipe.d.ts index 64080f92e..d68aafab8 100644 --- a/lib/build/recipe/emailpassword/recipe.d.ts +++ b/lib/build/recipe/emailpassword/recipe.d.ts @@ -4,10 +4,9 @@ import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from ". import { NormalisedAppinfo, APIHandled, HTTPMethod, RecipeListFunction } from "../../types"; import STError from "./error"; import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypeEmailPasswordEmailDeliveryInput } from "./types"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; export default class Recipe extends RecipeModule { private static instance; static RECIPE_ID: string; @@ -41,5 +40,4 @@ export default class Recipe extends RecipeModule { handleError: (err: STError, _request: BaseRequest, response: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; isErrorFromThisRecipe: (err: any) => err is STError; - getEmailForUserId: GetEmailForUserIdFunc; } diff --git a/lib/build/recipe/emailpassword/recipe.js b/lib/build/recipe/emailpassword/recipe.js index 8a6cf5a55..80e801b57 100644 --- a/lib/build/recipe/emailpassword/recipe.js +++ b/lib/build/recipe/emailpassword/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -61,13 +30,11 @@ const generatePasswordResetToken_1 = __importDefault(require("./api/generatePass const passwordReset_1 = __importDefault(require("./api/passwordReset")); const utils_2 = require("../../utils"); const emailExists_1 = __importDefault(require("./api/emailExists")); -const recipe_1 = __importDefault(require("../emailverification/recipe")); const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); const implementation_1 = __importDefault(require("./api/implementation")); const querier_1 = require("../../querier"); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); -const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { super(recipeId, appInfo); @@ -108,66 +75,50 @@ class Recipe extends recipeModule_1.default { }, ]; }; - this.handleAPIRequest = (id, tenantId, req, res, _path, _method, userContext) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.SIGN_UP_API) { - return yield signup_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.SIGN_IN_API) { - return yield signin_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.GENERATE_PASSWORD_RESET_TOKEN_API) { - return yield generatePasswordResetToken_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.PASSWORD_RESET_API) { - return yield passwordReset_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.SIGNUP_EMAIL_EXISTS_API) { - return yield emailExists_1.default(this.apiImpl, tenantId, options, userContext); - } - return false; - }); - this.handleError = (err, _request, response) => - __awaiter(this, void 0, void 0, function* () { - if (err.fromRecipe === Recipe.RECIPE_ID) { - if (err.type === error_1.default.FIELD_ERROR) { - return utils_2.send200Response(response, { - status: "FIELD_ERROR", - formFields: err.payload, - }); - } else { - throw err; - } + this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + emailDelivery: this.emailDelivery, + appInfo: this.getAppInfo(), + }; + if (id === constants_1.SIGN_UP_API) { + return await signup_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.SIGN_IN_API) { + return await signin_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.GENERATE_PASSWORD_RESET_TOKEN_API) { + return await generatePasswordResetToken_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.PASSWORD_RESET_API) { + return await passwordReset_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.SIGNUP_EMAIL_EXISTS_API) { + return await emailExists_1.default(this.apiImpl, tenantId, options, userContext); + } + return false; + }; + this.handleError = async (err, _request, response) => { + if (err.fromRecipe === Recipe.RECIPE_ID) { + if (err.type === error_1.default.FIELD_ERROR) { + return utils_2.send200Response(response, { + status: "FIELD_ERROR", + formFields: err.payload, + }); } else { throw err; } - }); + } else { + throw err; + } + }; this.getAllCORSHeaders = () => { return []; }; this.isErrorFromThisRecipe = (err) => { return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; - // extra instance functions below............... - this.getEmailForUserId = (userId, userContext) => - __awaiter(this, void 0, void 0, function* () { - let userInfo = yield this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }); this.isInServerlessEnv = isInServerlessEnv; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); { @@ -190,16 +141,8 @@ class Recipe extends recipeModule_1.default { */ this.emailDelivery = ingredients.emailDelivery === undefined - ? new emaildelivery_1.default( - this.config.getEmailDeliveryConfig(this.recipeInterfaceImpl, this.isInServerlessEnv) - ) + ? new emaildelivery_1.default(this.config.getEmailDeliveryConfig(this.isInServerlessEnv)) : ingredients.emailDelivery; - postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = recipe_1.default.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); } static getInstanceOrThrowError() { if (Recipe.instance !== undefined) { diff --git a/lib/build/recipe/emailpassword/recipeImplementation.js b/lib/build/recipe/emailpassword/recipeImplementation.js index 5ab61cd8b..f42372b28 100644 --- a/lib/build/recipe/emailpassword/recipeImplementation.js +++ b/lib/build/recipe/emailpassword/recipeImplementation.js @@ -1,192 +1,166 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +const recipe_1 = __importDefault(require("../accountlinking/recipe")); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const __1 = require("../.."); const constants_1 = require("./constants"); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_2 = require("../multitenancy/constants"); +const user_1 = require("../../user"); function getRecipeInterface(querier, getEmailPasswordConfig) { return { - signUp: function ({ email, password, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default( - `/${tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId}/recipe/signup` - ), - { - email, - password, - } - ); - if (response.status === "OK") { - return response; - } else { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } + signUp: async function ({ email, password, tenantId, userContext }) { + const response = await this.createNewRecipeUser({ + email, + password, + tenantId, + userContext, }); - }, - signIn: function ({ email, password, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default( - `/${tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId}/recipe/signin` - ), - { - email, - password, - } - ); - if (response.status === "OK") { - return response; - } else { - return { - status: "WRONG_CREDENTIALS_ERROR", - }; - } + if (response.status !== "OK") { + return response; + } + let updatedUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, }); + return { + status: "OK", + user: updatedUser, + recipeUserId: response.recipeUserId, + }; }, - getUserById: function ({ userId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user"), { - userId, - }); - if (response.status === "OK") { - return Object.assign({}, response.user); - } else { - return undefined; + createNewRecipeUser: async function (input) { + const resp = await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${input.tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : input.tenantId}/recipe/signup` + ), + { + email: input.email, + password: input.password, } - }); + ); + if (resp.status === "OK") { + resp.user = new user_1.User(resp.user); + resp.recipeUserId = new recipeUserId_1.default(resp.recipeUserId); + } + return resp; + // we do not do email verification here cause it's a new user and email password + // users are always initially unverified. }, - getUserByEmail: function ({ email, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default( - `/${tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId}/recipe/user` - ), - { - email, - } + signIn: async function ({ email, password, tenantId, userContext }) { + const response = await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId}/recipe/signin` + ), + { + email, + password, + } + ); + if (response.status === "OK") { + response.user = new user_1.User(response.user); + response.recipeUserId = new recipeUserId_1.default(response.recipeUserId); + const loginMethod = response.user.loginMethods.find( + (lm) => lm.recipeUserId.getAsString() === response.recipeUserId.getAsString() ); - if (response.status === "OK") { - return Object.assign({}, response.user); - } else { - return undefined; + if (!loginMethod.verified) { + await recipe_1.default.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: response.user, + recipeUserId: response.recipeUserId, + userContext, + }); + // Unlike in the sign up recipe function, we do not do account linking here + // cause we do not want sign in to change the potentially user ID of a user + // due to linking when this function is called by the dev in their API - + // for example in their update password API. If we did account linking + // then we would have to ask the dev to also change the session + // in such API calls. + // In the case of sign up, since we are creating a new user, it's fine + // to link there since there is no user id change really from the dev's + // point of view who is calling the sign up recipe function. + // We do this so that we get the updated user (in case the above + // function updated the verification status) and can return that + response.user = await __1.getUser(response.recipeUserId.getAsString(), userContext); } - }); + } + return response; }, - createResetPasswordToken: function ({ userId, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default( - `/${ - tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId - }/recipe/user/password/reset/token` - ), - { - userId, - } - ); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; + createResetPasswordToken: async function ({ userId, email, tenantId }) { + // the input user ID can be a recipe or a primary user ID. + return await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId + }/recipe/user/password/reset/token` + ), + { + userId, + email, } - }); + ); }, - resetPasswordUsingToken: function ({ token, newPassword, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default( - `/${ - tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId - }/recipe/user/password/reset` - ), - { - method: "token", - token, - newPassword, - } - ); - return response; - }); + consumePasswordResetToken: async function ({ token, tenantId }) { + return await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId + }/recipe/user/password/reset/token/consume` + ), + { + method: "token", + token, + } + ); }, - updateEmailOrPassword: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (input.applyPasswordPolicy || input.applyPasswordPolicy === undefined) { - let formFields = getEmailPasswordConfig().signUpFeature.formFields; - if (input.password !== undefined) { - const passwordField = formFields.filter( - (el) => el.id === constants_1.FORM_FIELD_PASSWORD_ID - )[0]; - const error = yield passwordField.validate(input.password, input.tenantIdForPasswordPolicy); - if (error !== undefined) { - return { - status: "PASSWORD_POLICY_VIOLATED_ERROR", - failureReason: error, - }; - } + updateEmailOrPassword: async function (input) { + if (input.applyPasswordPolicy || input.applyPasswordPolicy === undefined) { + let formFields = getEmailPasswordConfig().signUpFeature.formFields; + if (input.password !== undefined) { + const passwordField = formFields.filter((el) => el.id === constants_1.FORM_FIELD_PASSWORD_ID)[0]; + const error = await passwordField.validate(input.password, input.tenantIdForPasswordPolicy); + if (error !== undefined) { + return { + status: "PASSWORD_POLICY_VIOLATED_ERROR", + failureReason: error, + }; } } - let response = yield querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/user"), { - userId: input.userId, + } + // We do not check for AccountLinking.isEmailChangeAllowed here cause + // that may return false if the user's email is not verified, and this + // function should not fail due to lack of email verification - since it's + // really up to the developer to decide what should be the pre condition for + // a change in email. The check for email verification should actually go in + // an update email API (post login update). + let response = await querier.sendPutRequest( + new normalisedURLPath_1.default(`${input.tenantIdForPasswordPolicy}/recipe/user`), + { + recipeUserId: input.recipeUserId.getAsString(), email: input.email, password: input.password, - }); - if (response.status === "OK") { - return { - status: "OK", - }; - } else if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } else { + } + ); + if (response.status === "OK") { + const user = await __1.getUser(input.recipeUserId.getAsString(), input.userContext); + if (user === undefined) { + // This means that the user was deleted between the put and get requests return { status: "UNKNOWN_USER_ID_ERROR", }; } - }); + await recipe_1.default.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user, + recipeUserId: input.recipeUserId, + userContext: input.userContext, + }); + } + return response; }, }; } diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index 891459939..734c7a5c6 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; import { @@ -7,12 +7,12 @@ import { TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export declare type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; signInFeature: TypeNormalisedInputSignIn; getEmailDeliveryConfig: ( - recipeImpl: RecipeInterface, isInServerlessEnv: boolean ) => EmailDeliveryTypeInputWithService; resetPasswordUsingTokenFeature: TypeNormalisedInputResetPasswordUsingTokenFeature; @@ -51,12 +51,6 @@ export declare type TypeNormalisedInputResetPasswordUsingTokenFeature = { formFieldsForGenerateTokenForm: NormalisedFormField[]; formFieldsForPasswordResetForm: NormalisedFormField[]; }; -export declare type User = { - id: string; - email: string; - timeJoined: number; - tenantIds: string[]; -}; export declare type TypeInput = { signUpFeature?: TypeInputSignUp; emailDelivery?: EmailDeliveryTypeInput; @@ -78,6 +72,22 @@ export declare type RecipeInterface = { | { status: "OK"; user: User; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } + >; + createNewRecipeUser(input: { + email: string; + password: string; + tenantId: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -92,15 +102,20 @@ export declare type RecipeInterface = { | { status: "OK"; user: User; + recipeUserId: RecipeUserId; } | { status: "WRONG_CREDENTIALS_ERROR"; } >; - getUserById(input: { userId: string; userContext: any }): Promise; - getUserByEmail(input: { email: string; tenantId: string; userContext: any }): Promise; + /** + * We pass in the email as well to this function cause the input userId + * may not be associated with an emailpassword account. In this case, we + * need to know which email to use to create an emailpassword account later on. + */ createResetPasswordToken(input: { userId: string; + email: string; tenantId: string; userContext: any; }): Promise< @@ -112,26 +127,22 @@ export declare type RecipeInterface = { status: "UNKNOWN_USER_ID_ERROR"; } >; - resetPasswordUsingToken(input: { + consumePasswordResetToken(input: { token: string; - newPassword: string; tenantId: string; userContext: any; }): Promise< | { status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; + email: string; + userId: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; } >; updateEmailOrPassword(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext: any; @@ -141,6 +152,10 @@ export declare type RecipeInterface = { | { status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string; @@ -186,6 +201,10 @@ export declare type APIInterface = { | { status: "OK"; } + | { + status: "PASSWORD_RESET_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); passwordResetPOST: @@ -202,11 +221,16 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - userId?: string; + email: string; + user: User; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; } + | { + status: "PASSWORD_POLICY_VIOLATED_ERROR"; + failureReason: string; + } | GeneralErrorResponse >); signInPOST: @@ -225,6 +249,10 @@ export declare type APIInterface = { user: User; session: SessionContainerInterface; } + | { + status: "SIGN_IN_NOT_ALLOWED"; + reason: string; + } | { status: "WRONG_CREDENTIALS_ERROR"; } @@ -246,6 +274,10 @@ export declare type APIInterface = { user: User; session: SessionContainerInterface; } + | { + status: "SIGN_UP_NOT_ALLOWED"; + reason: string; + } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } @@ -256,6 +288,7 @@ export declare type TypeEmailPasswordPasswordResetEmailDeliveryInput = { type: "PASSWORD_RESET"; user: { id: string; + recipeUserId: RecipeUserId | undefined; email: string; }; passwordResetLink: string; diff --git a/lib/build/recipe/emailpassword/utils.js b/lib/build/recipe/emailpassword/utils.js index 30c3fc21d..823085fe1 100644 --- a/lib/build/recipe/emailpassword/utils.js +++ b/lib/build/recipe/emailpassword/utils.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -68,19 +37,18 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { }, config === null || config === void 0 ? void 0 : config.override ); - function getEmailDeliveryConfig(recipeImpl, isInServerlessEnv) { + function getEmailDeliveryConfig(isInServerlessEnv) { var _a; let emailService = (_a = config === null || config === void 0 ? void 0 : config.emailDelivery) === null || _a === void 0 ? void 0 : _a.service; /** - * following code is for backward compatibility. - * if user has not passed emailService config, we use the default - * createAndSendEmailUsingSupertokensService implementation which calls our supertokens API + * If the user has not passed even that config, we use the default + * createAndSendCustomEmail implementation which calls our supertokens API */ if (emailService === undefined) { - emailService = new backwardCompatibility_1.default(recipeImpl, appInfo, isInServerlessEnv); + emailService = new backwardCompatibility_1.default(appInfo, isInServerlessEnv); } return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.emailDelivery), { /** @@ -201,52 +169,46 @@ function validateAndNormaliseSignupConfig(_, __, config) { formFields, }; } -function defaultValidator(_) { - return __awaiter(this, void 0, void 0, function* () { - return undefined; - }); +async function defaultValidator(_) { + return undefined; } -function defaultPasswordValidator(value) { - return __awaiter(this, void 0, void 0, function* () { - // length >= 8 && < 100 - // must have a number and a character - // as per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 - if (typeof value !== "string") { - return "Development bug: Please make sure the password field yields a string"; - } - if (value.length < 8) { - return "Password must contain at least 8 characters, including a number"; - } - if (value.length >= 100) { - return "Password's length must be lesser than 100 characters"; - } - if (value.match(/^.*[A-Za-z]+.*$/) === null) { - return "Password must contain at least one alphabet"; - } - if (value.match(/^.*[0-9]+.*$/) === null) { - return "Password must contain at least one number"; - } - return undefined; - }); +async function defaultPasswordValidator(value) { + // length >= 8 && < 100 + // must have a number and a character + // as per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 + if (typeof value !== "string") { + return "Development bug: Please make sure the password field yields a string"; + } + if (value.length < 8) { + return "Password must contain at least 8 characters, including a number"; + } + if (value.length >= 100) { + return "Password's length must be lesser than 100 characters"; + } + if (value.match(/^.*[A-Za-z]+.*$/) === null) { + return "Password must contain at least one alphabet"; + } + if (value.match(/^.*[0-9]+.*$/) === null) { + return "Password must contain at least one number"; + } + return undefined; } exports.defaultPasswordValidator = defaultPasswordValidator; -function defaultEmailValidator(value) { - return __awaiter(this, void 0, void 0, function* () { - // We check if the email syntax is correct - // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 - // Regex from https://stackoverflow.com/a/46181/3867175 - if (typeof value !== "string") { - return "Development bug: Please make sure the email field yields a string"; - } - if ( - value.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) === null - ) { - return "Email is invalid"; - } - return undefined; - }); +async function defaultEmailValidator(value) { + // We check if the email syntax is correct + // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 + // Regex from https://stackoverflow.com/a/46181/3867175 + if (typeof value !== "string") { + return "Development bug: Please make sure the email field yields a string"; + } + if ( + value.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ) === null + ) { + return "Email is invalid"; + } + return undefined; } exports.defaultEmailValidator = defaultEmailValidator; function getPasswordResetLink(input) { diff --git a/lib/build/recipe/emailverification/api/emailVerify.js b/lib/build/recipe/emailverification/api/emailVerify.js index 8501f4f57..2df3bd417 100644 --- a/lib/build/recipe/emailverification/api/emailVerify.js +++ b/lib/build/recipe/emailverification/api/emailVerify.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,63 +22,64 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); const session_1 = __importDefault(require("../../session")); -function emailVerify(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let result; - if (utils_1.normaliseHttpMethod(options.req.getMethod()) === "post") { - // Logic according to Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 - if (apiImplementation.verifyEmailPOST === undefined) { - return false; - } - let token = (yield options.req.getJSONBody()).token; - if (token === undefined || token === null) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the email verification token", - }); - } - if (typeof token !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "The email verification token must be a string", - }); - } - const session = yield session_1.default.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [], sessionRequired: false }, - userContext - ); - let response = yield apiImplementation.verifyEmailPOST({ - token, - tenantId, - options, - session, - userContext, +async function emailVerify(apiImplementation, tenantId, options, userContext) { + let result; + if (utils_1.normaliseHttpMethod(options.req.getMethod()) === "post") { + // Logic according to Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 + if (apiImplementation.verifyEmailPOST === undefined) { + return false; + } + let token = (await options.req.getJSONBody()).token; + if (token === undefined || token === null) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the email verification token", }); - if (response.status === "OK") { - result = { status: "OK" }; - } else { - result = response; - } - } else { - if (apiImplementation.isEmailVerifiedGET === undefined) { - return false; - } - const session = yield session_1.default.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [] }, - userContext - ); - result = yield apiImplementation.isEmailVerifiedGET({ - options, - session: session, - userContext, + } + if (typeof token !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "The email verification token must be a string", }); } - utils_1.send200Response(options.res, result); - return true; - }); + const session = await session_1.default.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [], sessionRequired: false }, + userContext + ); + let response = await apiImplementation.verifyEmailPOST({ + token, + tenantId, + options, + session, + userContext, + }); + if (response.status === "OK") { + // if there is a new session, it will be + // automatically added to the response by the createNewSession function call + // inside the verifyEmailPOST function. + result = { status: "OK" }; + } else { + result = response; + } + } else { + if (apiImplementation.isEmailVerifiedGET === undefined) { + return false; + } + const session = await session_1.default.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [] }, + userContext + ); + result = await apiImplementation.isEmailVerifiedGET({ + options, + session, + userContext, + }); + } + utils_1.send200Response(options.res, result); + return true; } exports.default = emailVerify; diff --git a/lib/build/recipe/emailverification/api/generateEmailVerifyToken.js b/lib/build/recipe/emailverification/api/generateEmailVerifyToken.js index 9b4cb0d87..32ed210ee 100644 --- a/lib/build/recipe/emailverification/api/generateEmailVerifyToken.js +++ b/lib/build/recipe/emailverification/api/generateEmailVerifyToken.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,25 +21,23 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const session_1 = __importDefault(require("../../session")); -function generateEmailVerifyToken(apiImplementation, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 - if (apiImplementation.generateEmailVerifyTokenPOST === undefined) { - return false; - } - const session = yield session_1.default.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [] }, - userContext - ); - const result = yield apiImplementation.generateEmailVerifyTokenPOST({ - options, - session: session, - userContext, - }); - utils_1.send200Response(options.res, result); - return true; +async function generateEmailVerifyToken(apiImplementation, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 + if (apiImplementation.generateEmailVerifyTokenPOST === undefined) { + return false; + } + const session = await session_1.default.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [] }, + userContext + ); + const result = await apiImplementation.generateEmailVerifyTokenPOST({ + options, + session: session, + userContext, }); + utils_1.send200Response(options.res, result); + return true; } exports.default = generateEmailVerifyToken; diff --git a/lib/build/recipe/emailverification/api/implementation.js b/lib/build/recipe/emailverification/api/implementation.js index 38513a21b..f75f28545 100644 --- a/lib/build/recipe/emailverification/api/implementation.js +++ b/lib/build/recipe/emailverification/api/implementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -43,136 +12,186 @@ const error_1 = __importDefault(require("../../session/error")); const utils_1 = require("../utils"); function getAPIInterface() { return { - verifyEmailPOST: function ({ token, tenantId, options, session, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const res = yield options.recipeImplementation.verifyEmailUsingToken({ tenantId, token, userContext }); - if (res.status === "OK" && session !== undefined) { - try { - yield session.fetchAndSetClaim(emailVerificationClaim_1.EmailVerificationClaim, userContext); - } catch (err) { - // This should never happen, since we've just set the status above. - if (err.message === "UNKNOWN_USER_ID") { - logger_1.logDebugMessage( - "verifyEmailPOST: Returning UNAUTHORISED because the user id provided is unknown" - ); - throw new error_1.default({ - type: error_1.default.UNAUTHORISED, - message: "Unknown User ID provided", - }); - } - throw err; - } - } - return res; + verifyEmailPOST: async function ({ token, tenantId, options, session, userContext }) { + const verifyTokenResponse = await options.recipeImplementation.verifyEmailUsingToken({ + token, + tenantId, + attemptAccountLinking: true, + userContext, }); + if (verifyTokenResponse.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { + return verifyTokenResponse; + } + // status: "OK" + let newSession = await recipe_1.default + .getInstanceOrThrowError() + .updateSessionIfRequiredPostEmailVerification({ + req: options.req, + res: options.res, + session, + recipeUserIdWhoseEmailGotVerified: verifyTokenResponse.user.recipeUserId, + userContext, + }); + return { + status: "OK", + user: verifyTokenResponse.user, + newSession, + }; }, - isEmailVerifiedGET: function ({ userContext, session }) { - return __awaiter(this, void 0, void 0, function* () { - if (session === undefined) { - throw new Error("Session is undefined. Should not come here."); - } - try { - yield session.fetchAndSetClaim(emailVerificationClaim_1.EmailVerificationClaim, userContext); - } catch (err) { - if (err.message === "UNKNOWN_USER_ID") { - logger_1.logDebugMessage( - "isEmailVerifiedGET: Returning UNAUTHORISED because the user id provided is unknown" - ); - throw new error_1.default({ - type: error_1.default.UNAUTHORISED, - message: "Unknown User ID provided", + isEmailVerifiedGET: async function ({ userContext, session, options }) { + // In this API, we will check if the session's recipe user id's email is verified or not. + const emailInfo = await recipe_1.default + .getInstanceOrThrowError() + .getEmailForRecipeUserId(undefined, session.getRecipeUserId(), userContext); + if (emailInfo.status === "OK") { + const isVerified = await options.recipeImplementation.isEmailVerified({ + recipeUserId: session.getRecipeUserId(), + email: emailInfo.email, + userContext, + }); + if (isVerified) { + // here we do the same things we do for post email verification + // cause email verification could happen in a different browser + // whilst the first browser is polling this API - in this case, + // we want to have the same effect to the session as if the + // email was opened on the original browser itself. + let newSession = await recipe_1.default + .getInstanceOrThrowError() + .updateSessionIfRequiredPostEmailVerification({ + req: options.req, + res: options.res, + session, + recipeUserIdWhoseEmailGotVerified: session.getRecipeUserId(), + userContext, }); + return { + status: "OK", + isVerified: true, + newSession, + }; + } else { + if ((await session.getClaimValue(emailVerificationClaim_1.EmailVerificationClaim)) !== false) { + await session.setClaimValue( + emailVerificationClaim_1.EmailVerificationClaim, + false, + userContext + ); } - throw err; - } - const isVerified = yield session.getClaimValue( - emailVerificationClaim_1.EmailVerificationClaim, - userContext - ); - if (isVerified === undefined) { - throw new Error("Should never come here: EmailVerificationClaim failed to set value"); + return { + status: "OK", + isVerified: false, + }; } + } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { + // We consider people without email addresses as validated return { status: "OK", - isVerified, + isVerified: true, }; - }); + } else { + // this means that the user ID is not known to supertokens. This could + // happen if the current session's user ID is not an auth user, + // or if it belong to a recipe user ID that got deleted. Either way, + // we logout the user. + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Unknown User ID provided", + }); + } }, - generateEmailVerifyTokenPOST: function ({ options, userContext, session }) { - return __awaiter(this, void 0, void 0, function* () { - if (session === undefined) { - throw new Error("Session is undefined. Should not come here."); - } - const userId = session.getUserId(); - const tenantId = session.getTenantId(); - const emailInfo = yield recipe_1.default + generateEmailVerifyTokenPOST: async function ({ options, userContext, session }) { + // In this API, we generate the email verification token for session's recipe user ID. + const tenantId = session.getTenantId(); + const emailInfo = await recipe_1.default + .getInstanceOrThrowError() + .getEmailForRecipeUserId(undefined, session.getRecipeUserId(), userContext); + if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { + logger_1.logDebugMessage( + `Email verification email not sent to user ${session + .getRecipeUserId() + .getAsString()} because it doesn't have an email address.` + ); + // this can happen if the user ID was found, but it has no email. In this + // case, we treat it as a success case. + let newSession = await recipe_1.default .getInstanceOrThrowError() - .getEmailForUserId(userId, userContext); - if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { + .updateSessionIfRequiredPostEmailVerification({ + req: options.req, + res: options.res, + session, + recipeUserIdWhoseEmailGotVerified: session.getRecipeUserId(), + userContext, + }); + return { + status: "EMAIL_ALREADY_VERIFIED_ERROR", + newSession, + }; + } else if (emailInfo.status === "OK") { + let response = await options.recipeImplementation.createEmailVerificationToken({ + recipeUserId: session.getRecipeUserId(), + email: emailInfo.email, + tenantId, + userContext, + }); + // In case the email is already verified, we do the same thing + // as what happens in the verifyEmailPOST API post email verification (cause maybe the session is outdated). + if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { logger_1.logDebugMessage( - `Email verification email not sent to user ${userId} because it doesn't have an email address.` + `Email verification email not sent to user ${session + .getRecipeUserId() + .getAsString()} because it is already verified.` ); + let newSession = await recipe_1.default + .getInstanceOrThrowError() + .updateSessionIfRequiredPostEmailVerification({ + req: options.req, + res: options.res, + session, + recipeUserIdWhoseEmailGotVerified: session.getRecipeUserId(), + userContext, + }); return { status: "EMAIL_ALREADY_VERIFIED_ERROR", + newSession, }; - } else if (emailInfo.status === "OK") { - let response = yield options.recipeImplementation.createEmailVerificationToken({ - userId, - email: emailInfo.email, - tenantId, - userContext, - }); - if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - if ((yield session.getClaimValue(emailVerificationClaim_1.EmailVerificationClaim)) !== true) { - // this can happen if the email was verified in another browser - // and this session is still outdated - and the user has not - // called the get email verification API yet. - yield session.fetchAndSetClaim( - emailVerificationClaim_1.EmailVerificationClaim, - userContext - ); - } - logger_1.logDebugMessage( - `Email verification email not sent to ${emailInfo.email} because it is already verified.` - ); - return response; - } - if ((yield session.getClaimValue(emailVerificationClaim_1.EmailVerificationClaim)) !== false) { - // this can happen if the email was unverified in another browser - // and this session is still outdated - and the user has not - // called the get email verification API yet. - yield session.fetchAndSetClaim(emailVerificationClaim_1.EmailVerificationClaim, userContext); - } - let emailVerifyLink = utils_1.getEmailVerifyLink({ - appInfo: options.appInfo, - token: response.token, - recipeId: options.recipeId, - tenantId, - }); - logger_1.logDebugMessage(`Sending email verification email to ${emailInfo}`); - yield options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "EMAIL_VERIFICATION", - user: { - id: userId, - email: emailInfo.email, - }, - emailVerifyLink, - tenantId, - userContext, - }); - return { - status: "OK", - }; - } else { - logger_1.logDebugMessage( - "generateEmailVerifyTokenPOST: Returning UNAUTHORISED because the user id provided is unknown" - ); - throw new error_1.default({ - type: error_1.default.UNAUTHORISED, - message: "Unknown User ID provided", - }); } - }); + if ((await session.getClaimValue(emailVerificationClaim_1.EmailVerificationClaim)) !== false) { + // this can happen if the email was unverified in another browser + // and this session is still outdated - and the user has not + // called the get email verification API yet. + await session.fetchAndSetClaim(emailVerificationClaim_1.EmailVerificationClaim, userContext); + } + let emailVerifyLink = utils_1.getEmailVerifyLink({ + appInfo: options.appInfo, + token: response.token, + recipeId: options.recipeId, + tenantId, + }); + logger_1.logDebugMessage(`Sending email verification email to ${emailInfo}`); + await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: "EMAIL_VERIFICATION", + user: { + id: session.getUserId(), + recipeUserId: session.getRecipeUserId(), + email: emailInfo.email, + }, + emailVerifyLink, + tenantId, + userContext, + }); + return { + status: "OK", + }; + } else { + // this means that the user ID is not known to supertokens. This could + // happen if the current session's user ID is not an auth user, + // or if it belong to a recipe user ID that got deleted. Either way, + // we logout the user. + logger_1.logDebugMessage( + "generateEmailVerifyTokenPOST: Returning UNAUTHORISED because the user id provided is unknown" + ); + throw new error_1.default({ type: error_1.default.UNAUTHORISED, message: "Unknown User ID provided" }); + } }, }; } diff --git a/lib/build/recipe/emailverification/emailVerificationClaim.js b/lib/build/recipe/emailverification/emailVerificationClaim.js index 0d86f5075..451c7c303 100644 --- a/lib/build/recipe/emailverification/emailVerificationClaim.js +++ b/lib/build/recipe/emailverification/emailVerificationClaim.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -46,23 +15,21 @@ class EmailVerificationClaimClass extends claims_1.BooleanClaim { constructor() { super({ key: "st-ev", - fetchValue(userId, _, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipe = recipe_1.default.getInstanceOrThrowError(); - let emailInfo = yield recipe.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - return recipe.recipeInterfaceImpl.isEmailVerified({ - userId, - email: emailInfo.email, - userContext, - }); - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // We consider people without email addresses as validated - return true; - } else { - throw new Error("UNKNOWN_USER_ID"); - } - }); + async fetchValue(_userId, recipeUserId, __tenantId, userContext) { + const recipe = recipe_1.default.getInstanceOrThrowError(); + let emailInfo = await recipe.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + if (emailInfo.status === "OK") { + return recipe.recipeInterfaceImpl.isEmailVerified({ + recipeUserId, + email: emailInfo.email, + userContext, + }); + } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { + // We consider people without email addresses as validated + return true; + } else { + throw new Error("UNKNOWN_USER_ID"); + } }, defaultMaxAgeInSeconds: 300, }); diff --git a/lib/build/recipe/emailverification/emailVerificationFunctions.d.ts b/lib/build/recipe/emailverification/emailVerificationFunctions.d.ts index 0dd678d38..2eb1c0427 100644 --- a/lib/build/recipe/emailverification/emailVerificationFunctions.d.ts +++ b/lib/build/recipe/emailverification/emailVerificationFunctions.d.ts @@ -1,8 +1,8 @@ // @ts-nocheck -import { User } from "./types"; +import { UserEmailInfo } from "./types"; import { NormalisedAppinfo } from "../../types"; export declare function createAndSendEmailUsingSupertokensService( appInfo: NormalisedAppinfo, - user: User, + user: UserEmailInfo, emailVerifyURLWithToken: string ): Promise; diff --git a/lib/build/recipe/emailverification/emailVerificationFunctions.js b/lib/build/recipe/emailverification/emailVerificationFunctions.js index 849ee5da6..ded4afd5a 100644 --- a/lib/build/recipe/emailverification/emailVerificationFunctions.js +++ b/lib/build/recipe/emailverification/emailVerificationFunctions.js @@ -13,61 +13,28 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAndSendEmailUsingSupertokensService = void 0; const utils_1 = require("../../utils"); -function createAndSendEmailUsingSupertokensService(appInfo, user, emailVerifyURLWithToken) { - return __awaiter(this, void 0, void 0, function* () { - if (process.env.TEST_MODE === "testing") { - return; +async function createAndSendEmailUsingSupertokensService(appInfo, user, emailVerifyURLWithToken) { + if (process.env.TEST_MODE === "testing") { + return; + } + await utils_1.postWithFetch( + "https://api.supertokens.io/0/st/auth/email/verify", + { + "api-version": "0", + "content-type": "application/json; charset=utf-8", + }, + { + email: user.email, + appName: appInfo.appName, + emailVerifyURL: emailVerifyURLWithToken, + }, + { + successLog: `Email sent to ${user.email}`, + errorLogHeader: "Error sending verification email", } - yield utils_1.postWithFetch( - "https://api.supertokens.io/0/st/auth/email/verify", - { - "api-version": "0", - "content-type": "application/json; charset=utf-8", - }, - { - email: user.email, - appName: appInfo.appName, - emailVerifyURL: emailVerifyURLWithToken, - }, - { - successLog: `Email sent to ${user.email}`, - errorLogHeader: "Error sending verification email", - } - ); - }); + ); } exports.createAndSendEmailUsingSupertokensService = createAndSendEmailUsingSupertokensService; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.js index 7867c3be3..7fc3cc6f6 100644 --- a/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.js @@ -1,56 +1,24 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const emailVerificationFunctions_1 = require("../../../emailVerificationFunctions"); class BackwardCompatibilityService { constructor(appInfo, isInServerlessEnv) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - try { - if (!this.isInServerlessEnv) { - emailVerificationFunctions_1 - .createAndSendEmailUsingSupertokensService(this.appInfo, input.user, input.emailVerifyLink) - .catch((_) => {}); - } else { - // see https://github.com/supertokens/supertokens-node/pull/135 - yield emailVerificationFunctions_1.createAndSendEmailUsingSupertokensService( - this.appInfo, - input.user, - input.emailVerifyLink - ); - } - } catch (_) {} - }); + this.sendEmail = async (input) => { + try { + if (!this.isInServerlessEnv) { + emailVerificationFunctions_1 + .createAndSendEmailUsingSupertokensService(this.appInfo, input.user, input.emailVerifyLink) + .catch((_) => {}); + } else { + // see https://github.com/supertokens/supertokens-node/pull/135 + await emailVerificationFunctions_1.createAndSendEmailUsingSupertokensService( + this.appInfo, + input.user, + input.emailVerifyLink + ); + } + } catch (_) {} + }; this.appInfo = appInfo; this.isInServerlessEnv = isInServerlessEnv; } diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.js b/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.js index e917fb0fe..dedcbe33f 100644 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.js +++ b/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -41,13 +10,12 @@ const supertokens_js_override_1 = __importDefault(require("supertokens-js-overri const serviceImplementation_1 = require("./serviceImplementation"); class SMTPService { constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - let content = yield this.serviceImpl.getContent(input); - yield this.serviceImpl.sendRawEmail( - Object.assign(Object.assign({}, content), { userContext: input.userContext }) - ); - }); + this.sendEmail = async (input) => { + let content = await this.serviceImpl.getContent(input); + await this.serviceImpl.sendRawEmail( + Object.assign(Object.assign({}, content), { userContext: input.userContext }) + ); + }; const transporter = nodemailer_1.createTransport({ host: config.smtpSettings.host, port: config.smtpSettings.port, diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.js b/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.js index 8efb6167a..3160ed2a7 100644 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.js +++ b/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -54,29 +23,25 @@ exports.getServiceImplementation = void 0; const emailVerify_1 = __importDefault(require("./emailVerify")); function getServiceImplementation(transporter, from) { return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (input.isHtml) { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }); + sendRawEmail: async function (input) { + if (input.isHtml) { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + html: input.body, + }); + } else { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + text: input.body, + }); + } }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return emailVerify_1.default(input); - }); + getContent: async function (input) { + return emailVerify_1.default(input); }, }; } diff --git a/lib/build/recipe/emailverification/index.d.ts b/lib/build/recipe/emailverification/index.d.ts index 58be90168..fed350183 100644 --- a/lib/build/recipe/emailverification/index.d.ts +++ b/lib/build/recipe/emailverification/index.d.ts @@ -1,14 +1,21 @@ // @ts-nocheck import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, APIOptions, APIInterface, User, TypeEmailVerificationEmailDeliveryInput } from "./types"; +import { + RecipeInterface, + APIOptions, + APIInterface, + UserEmailInfo, + TypeEmailVerificationEmailDeliveryInput, +} from "./types"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init: typeof Recipe.init; static Error: typeof SuperTokensError; static EmailVerificationClaim: import("./emailVerificationClaim").EmailVerificationClaimClass; static createEmailVerificationToken( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, email?: string, userContext?: any ): Promise< @@ -22,7 +29,7 @@ export default class Wrapper { >; static createEmailVerificationLink( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, email?: string, userContext?: any ): Promise< @@ -37,6 +44,7 @@ export default class Wrapper { static sendEmailVerificationEmail( tenantId: string, userId: string, + recipeUserId: RecipeUserId, email?: string, userContext?: any ): Promise< @@ -50,27 +58,28 @@ export default class Wrapper { static verifyEmailUsingToken( tenantId: string, token: string, + attemptAccountLinking?: boolean, userContext?: any ): Promise< | { status: "OK"; - user: User; + user: UserEmailInfo; } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; } >; - static isEmailVerified(userId: string, email?: string, userContext?: any): Promise; + static isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext?: any): Promise; static revokeEmailVerificationTokens( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, email?: string, userContext?: any ): Promise<{ status: string; }>; static unverifyEmail( - userId: string, + recipeUserId: RecipeUserId, email?: string, userContext?: any ): Promise<{ @@ -91,6 +100,6 @@ export declare let verifyEmailUsingToken: typeof Wrapper.verifyEmailUsingToken; export declare let isEmailVerified: typeof Wrapper.isEmailVerified; export declare let revokeEmailVerificationTokens: typeof Wrapper.revokeEmailVerificationTokens; export declare let unverifyEmail: typeof Wrapper.unverifyEmail; -export type { RecipeInterface, APIOptions, APIInterface, User }; +export type { RecipeInterface, APIOptions, APIInterface, UserEmailInfo }; export declare let sendEmail: typeof Wrapper.sendEmail; export { EmailVerificationClaim } from "./emailVerificationClaim"; diff --git a/lib/build/recipe/emailverification/index.js b/lib/build/recipe/emailverification/index.js index c9d1958c5..9f81c6832 100644 --- a/lib/build/recipe/emailverification/index.js +++ b/lib/build/recipe/emailverification/index.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,178 +25,167 @@ const error_1 = __importDefault(require("./error")); const emailVerificationClaim_1 = require("./emailVerificationClaim"); const utils_1 = require("./utils"); class Wrapper { - static createEmailVerificationToken(tenantId, userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - if (email === undefined) { - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - return yield recipeInstance.recipeInterfaceImpl.createEmailVerificationToken({ - userId, - email: email, - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static createEmailVerificationLink(tenantId, userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - const appInfo = recipeInstance.getAppInfo(); - let emailVerificationToken = yield exports.createEmailVerificationToken( - tenantId, - userId, - email, - userContext - ); - if (emailVerificationToken.status === "EMAIL_ALREADY_VERIFIED_ERROR") { + static async createEmailVerificationToken(tenantId, recipeUserId, email, userContext = {}) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + if (email === undefined) { + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + if (emailInfo.status === "OK") { + email = emailInfo.email; + } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { return { status: "EMAIL_ALREADY_VERIFIED_ERROR", }; + } else { + throw new global.Error("Unknown User ID provided without email"); } + } + return await recipeInstance.recipeInterfaceImpl.createEmailVerificationToken({ + recipeUserId, + email: email, + tenantId, + userContext, + }); + } + static async createEmailVerificationLink(tenantId, recipeUserId, email, userContext = {}) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + const appInfo = recipeInstance.getAppInfo(); + let emailVerificationToken = await exports.createEmailVerificationToken( + tenantId, + recipeUserId, + email, + userContext + ); + if (emailVerificationToken.status === "EMAIL_ALREADY_VERIFIED_ERROR") { return { - status: "OK", - link: utils_1.getEmailVerifyLink({ - appInfo, - token: emailVerificationToken.token, - recipeId: recipeInstance.getRecipeId(), - tenantId, - }), + status: "EMAIL_ALREADY_VERIFIED_ERROR", }; - }); + } + return { + status: "OK", + link: utils_1.getEmailVerifyLink({ + appInfo, + token: emailVerificationToken.token, + recipeId: recipeInstance.getRecipeId(), + tenantId, + }), + }; } - static sendEmailVerificationEmail(tenantId, userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (email === undefined) { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - let emailVerificationLink = yield this.createEmailVerificationLink(tenantId, userId, email, userContext); - if (emailVerificationLink.status === "EMAIL_ALREADY_VERIFIED_ERROR") { + static async sendEmailVerificationEmail(tenantId, userId, recipeUserId, email, userContext = {}) { + if (email === undefined) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + if (emailInfo.status === "OK") { + email = emailInfo.email; + } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { return { status: "EMAIL_ALREADY_VERIFIED_ERROR", }; + } else { + throw new global.Error("Unknown User ID provided without email"); } - yield exports.sendEmail({ - type: "EMAIL_VERIFICATION", - user: { - id: userId, - email: email, - }, - emailVerifyLink: emailVerificationLink.link, - tenantId, - }); + } + let emailVerificationLink = await this.createEmailVerificationLink(tenantId, recipeUserId, email, userContext); + if (emailVerificationLink.status === "EMAIL_ALREADY_VERIFIED_ERROR") { return { - status: "OK", + status: "EMAIL_ALREADY_VERIFIED_ERROR", }; + } + await exports.sendEmail({ + type: "EMAIL_VERIFICATION", + user: { + id: userId, + recipeUserId: recipeUserId, + email: email, + }, + emailVerifyLink: emailVerificationLink.link, + tenantId, }); + return { + status: "OK", + }; } - static verifyEmailUsingToken(tenantId, token, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ - token, - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); + static async verifyEmailUsingToken(tenantId, token, attemptAccountLinking = true, userContext = {}) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ + token, + tenantId, + attemptAccountLinking, + userContext, }); } - static isEmailVerified(userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - if (email === undefined) { - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - return true; - } else { - throw new global.Error("Unknown User ID provided without email"); - } + static async isEmailVerified(recipeUserId, email, userContext = {}) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + if (email === undefined) { + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + if (emailInfo.status === "OK") { + email = emailInfo.email; + } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { + return true; + } else { + throw new global.Error("Unknown User ID provided without email"); } - return yield recipeInstance.recipeInterfaceImpl.isEmailVerified({ - userId, - email: email, - userContext: userContext === undefined ? {} : userContext, - }); + } + return await recipeInstance.recipeInterfaceImpl.isEmailVerified({ + recipeUserId, + email, + userContext, }); } - static revokeEmailVerificationTokens(tenantId, userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - // If the dev wants to delete the tokens for an old email address of the user they can pass the address - // but redeeming those tokens would have no effect on isEmailVerified called without the old address - // so in general that is not necessary either. - if (email === undefined) { - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // This only happens for phone based passwordless users (or if the user added a custom getEmailForUserId) - // We can return OK here, since there is no way to create an email verification token - // if getEmailForUserId returns EMAIL_DOES_NOT_EXIST_ERROR. - return { - status: "OK", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } + static async revokeEmailVerificationTokens(tenantId, recipeUserId, email, userContext = {}) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + // If the dev wants to delete the tokens for an old email address of the user they can pass the address + // but redeeming those tokens would have no effect on isEmailVerified called without the old address + // so in general that is not necessary either. + if (email === undefined) { + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + if (emailInfo.status === "OK") { + email = emailInfo.email; + } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { + // This only happens for phone based passwordless users (or if the user added a custom getEmailForUserId) + // We can return OK here, since there is no way to create an email verification token + // if getEmailForUserId returns EMAIL_DOES_NOT_EXIST_ERROR. + return { + status: "OK", + }; + } else { + throw new global.Error("Unknown User ID provided without email"); } - return yield recipeInstance.recipeInterfaceImpl.revokeEmailVerificationTokens({ - userId, - email: email, - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); + } + return await recipeInstance.recipeInterfaceImpl.revokeEmailVerificationTokens({ + recipeUserId, + email: email, + tenantId, + userContext, }); } - static unverifyEmail(userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - if (email === undefined) { - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // Here we are returning OK since that's how it used to work, but a later call to isVerified will still return true - return { - status: "OK", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } + static async unverifyEmail(recipeUserId, email, userContext = {}) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + if (email === undefined) { + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + if (emailInfo.status === "OK") { + email = emailInfo.email; + } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { + // Here we are returning OK since that's how it used to work, but a later call to isVerified will still return true + return { + status: "OK", + }; + } else { + throw new global.Error("Unknown User ID provided without email"); } - return yield recipeInstance.recipeInterfaceImpl.unverifyEmail({ - userId, - email: email, - userContext: userContext === undefined ? {} : userContext, - }); + } + return await recipeInstance.recipeInterfaceImpl.unverifyEmail({ + recipeUserId, + email, + userContext, }); } - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - let recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return yield recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign(Object.assign({ userContext: {} }, input), { tenantId: input.tenantId }) - ); - }); + static async sendEmail(input) { + var _a; + let recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } } exports.default = Wrapper; diff --git a/lib/build/recipe/emailverification/recipe.d.ts b/lib/build/recipe/emailverification/recipe.d.ts index c18ce8b93..94132508c 100644 --- a/lib/build/recipe/emailverification/recipe.d.ts +++ b/lib/build/recipe/emailverification/recipe.d.ts @@ -1,12 +1,14 @@ // @ts-nocheck import RecipeModule from "../../recipeModule"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, GetEmailForUserIdFunc } from "./types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, GetEmailForRecipeUserIdFunc } from "./types"; import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; import STError from "./error"; import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypeEmailVerificationEmailDeliveryInput } from "./types"; +import { SessionContainerInterface } from "../session/types"; +import RecipeUserId from "../../recipeUserId"; export default class Recipe extends RecipeModule { private static instance; static RECIPE_ID: string; @@ -15,7 +17,6 @@ export default class Recipe extends RecipeModule { apiImpl: APIInterface; isInServerlessEnv: boolean; emailDelivery: EmailDeliveryIngredient; - getEmailForUserIdFuncsFromOtherRecipes: GetEmailForUserIdFunc[]; constructor( recipeId: string, appInfo: NormalisedAppinfo, @@ -42,6 +43,13 @@ export default class Recipe extends RecipeModule { handleError: (err: STError, _: BaseRequest, __: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; isErrorFromThisRecipe: (err: any) => err is STError; - getEmailForUserId: GetEmailForUserIdFunc; - addGetEmailForUserIdFunc: (func: GetEmailForUserIdFunc) => void; + getEmailForRecipeUserId: GetEmailForRecipeUserIdFunc; + getPrimaryUserIdForRecipeUser: (recipeUserId: RecipeUserId, userContext: any) => Promise; + updateSessionIfRequiredPostEmailVerification: (input: { + req: BaseRequest; + res: BaseResponse; + session: SessionContainerInterface | undefined; + recipeUserIdWhoseEmailGotVerified: RecipeUserId; + userContext: any; + }) => Promise; } diff --git a/lib/build/recipe/emailverification/recipe.js b/lib/build/recipe/emailverification/recipe.js index 501788a5e..96faa4eb0 100644 --- a/lib/build/recipe/emailverification/recipe.js +++ b/lib/build/recipe/emailverification/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -65,10 +34,13 @@ const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); const recipe_1 = __importDefault(require("../session/recipe")); const emailVerificationClaim_1 = require("./emailVerificationClaim"); +const error_2 = __importDefault(require("../session/error")); +const session_1 = __importDefault(require("../session")); +const __1 = require("../.."); +const logger_1 = require("../../logger"); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { super(recipeId, appInfo); - this.getEmailForUserIdFuncsFromOtherRecipes = []; // abstract instance functions below............... this.getAPIsHandled = () => { return [ @@ -94,60 +66,209 @@ class Recipe extends recipeModule_1.default { }, ]; }; - this.handleAPIRequest = (id, tenantId, req, res, _, __, userContext) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.GENERATE_EMAIL_VERIFY_TOKEN_API) { - return yield generateEmailVerifyToken_1.default(this.apiImpl, options, userContext); - } else { - return yield emailVerify_1.default(this.apiImpl, tenantId, options, userContext); - } - }); - this.handleError = (err, _, __) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); + this.handleAPIRequest = async (id, tenantId, req, res, _, __, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + emailDelivery: this.emailDelivery, + appInfo: this.getAppInfo(), + }; + if (id === constants_1.GENERATE_EMAIL_VERIFY_TOKEN_API) { + return await generateEmailVerifyToken_1.default(this.apiImpl, options, userContext); + } else { + return await emailVerify_1.default(this.apiImpl, tenantId, options, userContext); + } + }; + this.handleError = async (err, _, __) => { + throw err; + }; this.getAllCORSHeaders = () => { return []; }; this.isErrorFromThisRecipe = (err) => { return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; - this.getEmailForUserId = (userId, userContext) => - __awaiter(this, void 0, void 0, function* () { - if (this.config.getEmailForUserId !== undefined) { - const userRes = yield this.config.getEmailForUserId(userId, userContext); - if (userRes.status !== "UNKNOWN_USER_ID_ERROR") { - return userRes; + this.getEmailForRecipeUserId = async (user, recipeUserId, userContext) => { + if (this.config.getEmailForRecipeUserId !== undefined) { + const userRes = await this.config.getEmailForRecipeUserId(recipeUserId, userContext); + if (userRes.status !== "UNKNOWN_USER_ID_ERROR") { + return userRes; + } + } + if (user === undefined) { + user = await __1.getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + } + } + for (let i = 0; i < user.loginMethods.length; i++) { + let currLM = user.loginMethods[i]; + if (currLM.recipeUserId.getAsString() === recipeUserId.getAsString()) { + if (currLM.email !== undefined) { + return { + email: currLM.email, + status: "OK", + }; + } else { + return { + status: "EMAIL_DOES_NOT_EXIST_ERROR", + }; } } - for (const getEmailForUserId of this.getEmailForUserIdFuncsFromOtherRecipes) { - const res = yield getEmailForUserId(userId, userContext); - if (res.status !== "UNKNOWN_USER_ID_ERROR") { - return res; + } + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + }; + this.getPrimaryUserIdForRecipeUser = async (recipeUserId, userContext) => { + // We extract this into its own function like this cause we want to make sure that + // this recipe does not get the email of the user ID from the getUser function. + // In fact, there is a test "email verification recipe uses getUser function only in getEmailForRecipeUserId" + // which makes sure that this function is only called in 3 places in this recipe: + // - this function + // - getEmailForRecipeUserId function (above) + // - after verification to get the updated user in verifyEmailUsingToken + // We want to isolate the result of calling this function as much as possible + // so that the consumer of the getUser function does not read the email + // from the primaryUser. Hence, this function only returns the string ID + // and nothing else from the primaryUser. + let primaryUser = await __1.getUser(recipeUserId.getAsString(), userContext); + if (primaryUser === undefined) { + // This can come here if the user is using session + email verification + // recipe with a user ID that is not known to supertokens. In this case, + // we do not allow linking for such users. + return recipeUserId.getAsString(); + } + return primaryUser === null || primaryUser === void 0 ? void 0 : primaryUser.id; + }; + this.updateSessionIfRequiredPostEmailVerification = async (input) => { + let primaryUserId = await this.getPrimaryUserIdForRecipeUser( + input.recipeUserIdWhoseEmailGotVerified, + input.userContext + ); + // if a session exists in the API, then we can update the session + // claim related to email verification + if (input.session !== undefined) { + logger_1.logDebugMessage("updateSessionIfRequiredPostEmailVerification got session"); + // Due to linking, we will have to correct the current + // session's user ID. There are four cases here: + // --> (Case 1) User signed up and did email verification and the new account + // became a primary user (user ID no change) + // --> (Case 2) User signed up and did email verification and the new account got linked + // to another primary user (user ID change) + // --> (Case 3) This is post login account linking, in which the account that got verified + // got linked to the session's account (user ID of account has changed to the session's user ID) + // --> (Case 4) This is post login account linking, in which the account that got verified + // got linked to ANOTHER primary account (user ID of account has changed to a different user ID != session.getUserId, but + // we should ignore this since it will result in the user's session changing.) + if ( + input.session.getRecipeUserId().getAsString() === + input.recipeUserIdWhoseEmailGotVerified.getAsString() + ) { + logger_1.logDebugMessage( + "updateSessionIfRequiredPostEmailVerification the session belongs to the verified user" + ); + // this means that the session's login method's account is the + // one that just got verified and that we are NOT doing post login + // account linking. So this is only for (Case 1) and (Case 2) + if (input.session.getUserId() === primaryUserId) { + logger_1.logDebugMessage( + "updateSessionIfRequiredPostEmailVerification the session userId matches the primary user id, so we are only refreshing the claim" + ); + // if the session's primary user ID is equal to the + // primary user ID that the account was linked to, then + // this means that the new account became a primary user (Case 1) + // We also have the sub cases here that the account that just + // got verified was already linked to the session's primary user ID, + // but either way, we don't need to change any user ID. + // In this case, all we do is to update the emailverification claim if it's + // not already set to true (it is ok to assume true cause this function + // is only called when the email is verified). + if ( + (await input.session.getClaimValue(emailVerificationClaim_1.EmailVerificationClaim)) !== + true + ) { + try { + // EmailVerificationClaim will be based on the recipeUserId + // and not the primary user ID. + await input.session.fetchAndSetClaim( + emailVerificationClaim_1.EmailVerificationClaim, + input.userContext + ); + } catch (err) { + // This should never happen, since we've just set the status above. + if (err.message === "UNKNOWN_USER_ID") { + throw new error_2.default({ + type: error_2.default.UNAUTHORISED, + message: "Unknown User ID provided", + }); + } + throw err; + } + } + return; + } else { + logger_1.logDebugMessage( + "updateSessionIfRequiredPostEmailVerification the session user id doesn't match the primary user id, so we are revoking all sessions and creating a new one" + ); + // if the session's primary user ID is NOT equal to the + // primary user ID that the account that it was linked to, then + // this means that the new account got linked to another primary user (Case 2) + // In this case, we need to update the session's user ID by creating + // a new session + // Revoke all session belonging to session.getRecipeUserId() + // We do not really need to do this, but we do it anyway.. no harm. + await session_1.default.revokeAllSessionsForUser( + input.recipeUserIdWhoseEmailGotVerified.getAsString(), + false, + input.session.getTenantId(), + input.userContext + ); + // create a new session and return that.. + return await session_1.default.createNewSession( + input.req, + input.res, + input.session.getTenantId(), + input.session.getRecipeUserId(), + {}, + {}, + input.userContext + ); } + } else { + logger_1.logDebugMessage( + "updateSessionIfRequiredPostEmailVerification the verified user doesn't match the session" + ); + // this means that the session's login method's account was NOT the + // one that just got verified and that we ARE doing post login + // account linking. So this is only for (Case 3) and (Case 4) + // In both case 3 and case 4, we do not want to change anything in the + // current session in terms of user ID or email verification claim (since + // both of these refer to the current logged in user and not the newly + // linked user's account). + return undefined; } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }); - this.addGetEmailForUserIdFunc = (func) => { - this.getEmailForUserIdFuncsFromOtherRecipes.push(func); + } else { + logger_1.logDebugMessage("updateSessionIfRequiredPostEmailVerification got no session"); + // the session is updated when the is email verification GET API is called + // so we don't do anything in this API. + return undefined; + } }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); this.isInServerlessEnv = isInServerlessEnv; { let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId)) + recipeImplementation_1.default( + querier_1.Querier.getNewInstanceOrThrowError(recipeId), + this.getEmailForRecipeUserId + ) ); this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); } diff --git a/lib/build/recipe/emailverification/recipeImplementation.d.ts b/lib/build/recipe/emailverification/recipeImplementation.d.ts index 6a2182ed3..625b1087e 100644 --- a/lib/build/recipe/emailverification/recipeImplementation.d.ts +++ b/lib/build/recipe/emailverification/recipeImplementation.d.ts @@ -1,4 +1,8 @@ // @ts-nocheck import { RecipeInterface } from "./"; import { Querier } from "../../querier"; -export default function getRecipeInterface(querier: Querier): RecipeInterface; +import { GetEmailForRecipeUserIdFunc } from "./types"; +export default function getRecipeInterface( + querier: Querier, + getEmailForRecipeUserId: GetEmailForRecipeUserIdFunc +): RecipeInterface; diff --git a/lib/build/recipe/emailverification/recipeImplementation.js b/lib/build/recipe/emailverification/recipeImplementation.js index ed7e24bf6..55beaa84f 100644 --- a/lib/build/recipe/emailverification/recipeImplementation.js +++ b/lib/build/recipe/emailverification/recipeImplementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -37,85 +6,96 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeInterface(querier) { +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); +const __1 = require("../.."); +function getRecipeInterface(querier, getEmailForRecipeUserId) { return { - createEmailVerificationToken: function ({ userId, email, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/user/email/verify/token`), - { - userId, - email, - } - ); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; + createEmailVerificationToken: async function ({ recipeUserId, email, tenantId }) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${tenantId}/recipe/user/email/verify/token`), + { + userId: recipeUserId.getAsString(), + email, } - }); + ); + if (response.status === "OK") { + return { + status: "OK", + token: response.token, + }; + } else { + return { + status: "EMAIL_ALREADY_VERIFIED_ERROR", + }; + } }, - verifyEmailUsingToken: function ({ token, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/user/email/verify`), - { - method: "token", - token, - } - ); - if (response.status === "OK") { - return { - status: "OK", - user: { - id: response.userId, - email: response.email, - }, - }; - } else { - return { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR", - }; + verifyEmailUsingToken: async function ({ token, attemptAccountLinking, tenantId, userContext }) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${tenantId}/recipe/user/email/verify`), + { + method: "token", + token, } - }); - }, - isEmailVerified: function ({ userId, email }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/user/email/verify"), - { - userId, - email, + ); + if (response.status === "OK") { + const recipeUserId = new recipeUserId_1.default(response.userId); + if (attemptAccountLinking) { + // TODO: this should ideally come from the api response + const updatedUser = await __1.getUser(recipeUserId.getAsString()); + if (updatedUser) { + // before attempting this, we must check that the email that got verified + // from the ID is the one that is currently associated with the ID (since + // email verification can be done for any combination of (user id, email) + // and not necessarily the email that is currently associated with the ID) + let emailInfo = await getEmailForRecipeUserId(updatedUser, recipeUserId, userContext); + if (emailInfo.status === "OK" && emailInfo.email === response.email) { + // we do this here to prevent cyclic dependencies. + // TODO: Fix this. + let AccountLinking = require("../accountlinking/recipe").default.getInstance(); + await AccountLinking.createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: updatedUser, + userContext, + }); + } } - ); - return response.isVerified; - }); + } + return { + status: "OK", + user: { + recipeUserId, + email: response.email, + }, + }; + } else { + return { + status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR", + }; + } }, - revokeEmailVerificationTokens: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/user/email/verify/token/remove`), - { - userId: input.userId, - email: input.email, - } - ); - return { status: "OK" }; + isEmailVerified: async function ({ recipeUserId, email }) { + let response = await querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user/email/verify"), { + userId: recipeUserId.getAsString(), + email, }); + return response.isVerified; }, - unverifyEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/user/email/verify/remove"), { - userId: input.userId, + revokeEmailVerificationTokens: async function (input) { + await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/user/email/verify/token/remove`), + { + userId: input.recipeUserId.getAsString(), email: input.email, - }); - return { status: "OK" }; + } + ); + return { status: "OK" }; + }, + unverifyEmail: async function (input) { + await querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/user/email/verify/remove"), { + userId: input.recipeUserId.getAsString(), + email: input.email, }); + return { status: "OK" }; }, }; } diff --git a/lib/build/recipe/emailverification/types.d.ts b/lib/build/recipe/emailverification/types.d.ts index f85e650c5..0c7aa6824 100644 --- a/lib/build/recipe/emailverification/types.d.ts +++ b/lib/build/recipe/emailverification/types.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { TypeInput as EmailDeliveryTypeInput, @@ -8,11 +8,13 @@ import { import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; import { SessionContainerInterface } from "../session/types"; +import RecipeUserId from "../../recipeUserId"; +import { User } from "../../types"; export declare type TypeInput = { mode: "REQUIRED" | "OPTIONAL"; emailDelivery?: EmailDeliveryTypeInput; - getEmailForUserId?: ( - userId: string, + getEmailForRecipeUserId?: ( + recipeUserId: RecipeUserId, userContext: any ) => Promise< | { @@ -36,8 +38,8 @@ export declare type TypeNormalisedInput = { getEmailDeliveryConfig: ( isInServerlessEnv: boolean ) => EmailDeliveryTypeInputWithService; - getEmailForUserId?: ( - userId: string, + getEmailForRecipeUserId?: ( + recipeUserId: RecipeUserId, userContext: any ) => Promise< | { @@ -56,13 +58,13 @@ export declare type TypeNormalisedInput = { apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; }; }; -export declare type User = { - id: string; +export declare type UserEmailInfo = { + recipeUserId: RecipeUserId; email: string; }; export declare type RecipeInterface = { createEmailVerificationToken(input: { - userId: string; + recipeUserId: RecipeUserId; email: string; tenantId: string; userContext: any; @@ -77,20 +79,21 @@ export declare type RecipeInterface = { >; verifyEmailUsingToken(input: { token: string; + attemptAccountLinking: boolean; tenantId: string; userContext: any; }): Promise< | { status: "OK"; - user: User; + user: UserEmailInfo; } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; } >; - isEmailVerified(input: { userId: string; email: string; userContext: any }): Promise; + isEmailVerified(input: { recipeUserId: RecipeUserId; email: string; userContext: any }): Promise; revokeEmailVerificationTokens(input: { - userId: string; + recipeUserId: RecipeUserId; email: string; tenantId: string; userContext: any; @@ -98,7 +101,7 @@ export declare type RecipeInterface = { status: "OK"; }>; unverifyEmail(input: { - userId: string; + recipeUserId: RecipeUserId; email: string; userContext: any; }): Promise<{ @@ -127,7 +130,8 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - user: User; + user: UserEmailInfo; + newSession?: SessionContainerInterface; } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; @@ -144,6 +148,7 @@ export declare type APIInterface = { | { status: "OK"; isVerified: boolean; + newSession?: SessionContainerInterface; } | GeneralErrorResponse >); @@ -155,7 +160,11 @@ export declare type APIInterface = { session: SessionContainerInterface; }) => Promise< | { - status: "EMAIL_ALREADY_VERIFIED_ERROR" | "OK"; + status: "OK"; + } + | { + status: "EMAIL_ALREADY_VERIFIED_ERROR"; + newSession?: SessionContainerInterface; } | GeneralErrorResponse >); @@ -164,13 +173,15 @@ export declare type TypeEmailVerificationEmailDeliveryInput = { type: "EMAIL_VERIFICATION"; user: { id: string; + recipeUserId: RecipeUserId; email: string; }; emailVerifyLink: string; tenantId: string; }; -export declare type GetEmailForUserIdFunc = ( - userId: string, +export declare type GetEmailForRecipeUserIdFunc = ( + user: User | undefined, + recipeUserId: RecipeUserId, userContext: any ) => Promise< | { diff --git a/lib/build/recipe/emailverification/utils.js b/lib/build/recipe/emailverification/utils.js index d3072cf03..b273a003e 100644 --- a/lib/build/recipe/emailverification/utils.js +++ b/lib/build/recipe/emailverification/utils.js @@ -57,7 +57,7 @@ function validateAndNormaliseUserInput(_, appInfo, config) { } return { mode: config.mode, - getEmailForUserId: config.getEmailForUserId, + getEmailForRecipeUserId: config.getEmailForRecipeUserId, override, getEmailDeliveryConfig, }; diff --git a/lib/build/recipe/jwt/api/getJWKS.js b/lib/build/recipe/jwt/api/getJWKS.js index 3ca3746cd..d4d77a9eb 100644 --- a/lib/build/recipe/jwt/api/getJWKS.js +++ b/lib/build/recipe/jwt/api/getJWKS.js @@ -13,55 +13,22 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); -function getJWKS(apiImplementation, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.getJWKSGET === undefined) { - return false; - } - let result = yield apiImplementation.getJWKSGET({ - options, - userContext, - }); - if ("status" in result && result.status === "GENERAL_ERROR") { - utils_1.send200Response(options.res, result); - } else { - options.res.setHeader("Access-Control-Allow-Origin", "*", false); - utils_1.send200Response(options.res, result); - } - return true; +async function getJWKS(apiImplementation, options, userContext) { + if (apiImplementation.getJWKSGET === undefined) { + return false; + } + let result = await apiImplementation.getJWKSGET({ + options, + userContext, }); + if ("status" in result && result.status === "GENERAL_ERROR") { + utils_1.send200Response(options.res, result); + } else { + options.res.setHeader("Access-Control-Allow-Origin", "*", false); + utils_1.send200Response(options.res, result); + } + return true; } exports.default = getJWKS; diff --git a/lib/build/recipe/jwt/api/implementation.js b/lib/build/recipe/jwt/api/implementation.js index 3685f0152..39f048519 100644 --- a/lib/build/recipe/jwt/api/implementation.js +++ b/lib/build/recipe/jwt/api/implementation.js @@ -13,48 +13,15 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getAPIImplementation() { return { - getJWKSGET: function ({ options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const resp = yield options.recipeImplementation.getJWKS({ userContext }); - if (resp.validityInSeconds !== undefined) { - options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false); - } - return resp; - }); + getJWKSGET: async function ({ options, userContext }) { + const resp = await options.recipeImplementation.getJWKS({ userContext }); + if (resp.validityInSeconds !== undefined) { + options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false); + } + return resp; }, }; } diff --git a/lib/build/recipe/jwt/index.js b/lib/build/recipe/jwt/index.js index 01f4ee357..646e50f44 100644 --- a/lib/build/recipe/jwt/index.js +++ b/lib/build/recipe/jwt/index.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,21 +22,17 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.getJWKS = exports.createJWT = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); class Wrapper { - static createJWT(payload, validitySeconds, useStaticSigningKey, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createJWT({ - payload, - validitySeconds, - useStaticSigningKey, - userContext: userContext === undefined ? {} : userContext, - }); + static async createJWT(payload, validitySeconds, useStaticSigningKey, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createJWT({ + payload, + validitySeconds, + useStaticSigningKey, + userContext: userContext === undefined ? {} : userContext, }); } - static getJWKS(userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, - }); + static async getJWKS(userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ + userContext: userContext === undefined ? {} : userContext, }); } } diff --git a/lib/build/recipe/jwt/recipe.d.ts b/lib/build/recipe/jwt/recipe.d.ts index d7d48bd35..f15875f2a 100644 --- a/lib/build/recipe/jwt/recipe.d.ts +++ b/lib/build/recipe/jwt/recipe.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; @@ -18,12 +18,12 @@ export default class Recipe extends RecipeModule { static reset(): void; getAPIsHandled(): APIHandled[]; handleAPIRequest: ( - _: string, - ____: string | undefined, + _id: string, + _tenantId: string | undefined, req: BaseRequest, res: BaseResponse, - __: normalisedURLPath, - ___: HTTPMethod, + _path: normalisedURLPath, + _method: HTTPMethod, userContext: any ) => Promise; handleError(error: error, _: BaseRequest, __: BaseResponse): Promise; diff --git a/lib/build/recipe/jwt/recipe.js b/lib/build/recipe/jwt/recipe.js index e64d695d7..2b1b48575 100644 --- a/lib/build/recipe/jwt/recipe.js +++ b/lib/build/recipe/jwt/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -63,26 +32,17 @@ const supertokens_js_override_1 = __importDefault(require("supertokens-js-overri class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); - this.handleAPIRequest = ( - _, - ____, // TODO tenantId - req, - res, - __, - ___, - userContext - ) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - }; - return yield getJWKS_1.default(this.apiImpl, options, userContext); - }); + this.handleAPIRequest = async (_id, _tenantId, req, res, _path, _method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + }; + return await getJWKS_1.default(this.apiImpl, options, userContext); + }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); this.isInServerlessEnv = isInServerlessEnv; { diff --git a/lib/build/recipe/jwt/recipeImplementation.js b/lib/build/recipe/jwt/recipeImplementation.js index 3eee19cd9..033ed8467 100644 --- a/lib/build/recipe/jwt/recipeImplementation.js +++ b/lib/build/recipe/jwt/recipeImplementation.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -54,50 +23,46 @@ const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const defaultJWKSMaxAge = 60; // This corresponds to the dynamicSigningKeyOverlapMS in the core function getRecipeInterface(querier, config, appInfo) { return { - createJWT: function ({ payload, validitySeconds, useStaticSigningKey }) { - return __awaiter(this, void 0, void 0, function* () { - if (validitySeconds === undefined) { - // If the user does not provide a validity to this function and the config validity is also undefined, use 100 years (in seconds) - validitySeconds = config.jwtValiditySeconds; - } - let response = yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/jwt"), { - payload: payload !== null && payload !== void 0 ? payload : {}, - validity: validitySeconds, - useStaticSigningKey: useStaticSigningKey !== false, - algorithm: "RS256", - jwksDomain: appInfo.apiDomain.getAsStringDangerous(), - }); - if (response.status === "OK") { - return { - status: "OK", - jwt: response.jwt, - }; - } else { - return { - status: "UNSUPPORTED_ALGORITHM_ERROR", - }; - } + createJWT: async function ({ payload, validitySeconds, useStaticSigningKey }) { + if (validitySeconds === undefined) { + // If the user does not provide a validity to this function and the config validity is also undefined, use 100 years (in seconds) + validitySeconds = config.jwtValiditySeconds; + } + let response = await querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/jwt"), { + payload: payload !== null && payload !== void 0 ? payload : {}, + validity: validitySeconds, + useStaticSigningKey: useStaticSigningKey !== false, + algorithm: "RS256", + jwksDomain: appInfo.apiDomain.getAsStringDangerous(), }); + if (response.status === "OK") { + return { + status: "OK", + jwt: response.jwt, + }; + } else { + return { + status: "UNSUPPORTED_ALGORITHM_ERROR", + }; + } }, - getJWKS: function () { - return __awaiter(this, void 0, void 0, function* () { - const { body, headers } = yield querier.sendGetRequestWithResponseHeaders( - new normalisedURLPath_1.default("/.well-known/jwks.json"), - {} - ); - let validityInSeconds = defaultJWKSMaxAge; - const cacheControl = headers.get("Cache-Control"); - if (cacheControl) { - const maxAgeHeader = cacheControl.match(/,?\s*max-age=(\d+)(?:,|$)/); - if (maxAgeHeader !== null) { - validityInSeconds = Number.parseInt(maxAgeHeader[1]); - if (!Number.isSafeInteger(validityInSeconds)) { - validityInSeconds = defaultJWKSMaxAge; - } + getJWKS: async function () { + const { body, headers } = await querier.sendGetRequestWithResponseHeaders( + new normalisedURLPath_1.default("/.well-known/jwks.json"), + {} + ); + let validityInSeconds = defaultJWKSMaxAge; + const cacheControl = headers.get("Cache-Control"); + if (cacheControl) { + const maxAgeHeader = cacheControl.match(/,?\s*max-age=(\d+)(?:,|$)/); + if (maxAgeHeader !== null) { + validityInSeconds = Number.parseInt(maxAgeHeader[1]); + if (!Number.isSafeInteger(validityInSeconds)) { + validityInSeconds = defaultJWKSMaxAge; } } - return Object.assign({ validityInSeconds }, body); - }); + } + return Object.assign({ validityInSeconds }, body); }, }; } diff --git a/lib/build/recipe/jwt/types.d.ts b/lib/build/recipe/jwt/types.d.ts index 72650b54d..337815a43 100644 --- a/lib/build/recipe/jwt/types.d.ts +++ b/lib/build/recipe/jwt/types.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { GeneralErrorResponse } from "../../types"; export declare type JsonWebKey = { diff --git a/lib/build/recipe/multitenancy/allowedDomainsClaim.js b/lib/build/recipe/multitenancy/allowedDomainsClaim.js index e09f0f62d..adecb2009 100644 --- a/lib/build/recipe/multitenancy/allowedDomainsClaim.js +++ b/lib/build/recipe/multitenancy/allowedDomainsClaim.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -46,14 +15,12 @@ class AllowedDomainsClaimClass extends claims_1.PrimitiveArrayClaim { constructor() { super({ key: "st-t-dmns", - fetchValue(_, tenantId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipe = recipe_1.default.getInstanceOrThrowError(); - if (recipe.getAllowedDomainsForTenantId === undefined) { - return undefined; // User did not provide a function to get allowed domains, but is using a validator. So we don't allow any domains by default - } - return yield recipe.getAllowedDomainsForTenantId(tenantId, userContext); - }); + async fetchValue(_userId, _recipeUserId, tenantId, userContext) { + const recipe = recipe_1.default.getInstanceOrThrowError(); + if (recipe.getAllowedDomainsForTenantId === undefined) { + return undefined; // User did not provide a function to get allowed domains, but is using a validator. So we don't allow any domains by default + } + return await recipe.getAllowedDomainsForTenantId(tenantId, userContext); }, defaultMaxAgeInSeconds: 3600, }); diff --git a/lib/build/recipe/multitenancy/api/implementation.js b/lib/build/recipe/multitenancy/api/implementation.js index f015409f8..0fad3facd 100644 --- a/lib/build/recipe/multitenancy/api/implementation.js +++ b/lib/build/recipe/multitenancy/api/implementation.js @@ -1,91 +1,58 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const configUtils_1 = require("../../thirdparty/providers/configUtils"); function getAPIInterface() { return { - loginMethodsGET: function ({ tenantId, clientType, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const tenantConfigRes = yield options.recipeImplementation.getTenant({ - tenantId, - userContext, - }); - if (tenantConfigRes === undefined) { - throw new Error("Tenant not found"); - } - const providerInputsFromStatic = options.staticThirdPartyProviders; - const providerConfigsFromCore = tenantConfigRes.thirdParty.providers; - const mergedProviders = configUtils_1.mergeProvidersFromCoreAndStatic( - providerConfigsFromCore, - providerInputsFromStatic - ); - const finalProviderList = []; - for (const providerInput of mergedProviders) { - try { - const providerInstance = yield configUtils_1.findAndCreateProviderInstance( - mergedProviders, - providerInput.config.thirdPartyId, - clientType, - userContext - ); - if (providerInstance === undefined) { - throw new Error("should never come here"); // because creating instance from the merged provider list itself - } - finalProviderList.push({ - id: providerInstance.id, - name: providerInstance.config.name, - }); - } catch (err) { - if (err.type === "CLIENT_TYPE_NOT_FOUND_ERROR") { - continue; - } - throw err; + loginMethodsGET: async function ({ tenantId, clientType, options, userContext }) { + const tenantConfigRes = await options.recipeImplementation.getTenant({ + tenantId, + userContext, + }); + if (tenantConfigRes === undefined) { + throw new Error("Tenant not found"); + } + const providerInputsFromStatic = options.staticThirdPartyProviders; + const providerConfigsFromCore = tenantConfigRes.thirdParty.providers; + const mergedProviders = configUtils_1.mergeProvidersFromCoreAndStatic( + providerConfigsFromCore, + providerInputsFromStatic + ); + const finalProviderList = []; + for (const providerInput of mergedProviders) { + try { + const providerInstance = await configUtils_1.findAndCreateProviderInstance( + mergedProviders, + providerInput.config.thirdPartyId, + clientType, + userContext + ); + if (providerInstance === undefined) { + throw new Error("should never come here"); // because creating instance from the merged provider list itself } + finalProviderList.push({ + id: providerInstance.id, + name: providerInstance.config.name, + }); + } catch (err) { + if (err.type === "CLIENT_TYPE_NOT_FOUND_ERROR") { + continue; + } + throw err; } - return { - status: "OK", - emailPassword: { - enabled: tenantConfigRes.emailPassword.enabled, - }, - passwordless: { - enabled: tenantConfigRes.passwordless.enabled, - }, - thirdParty: { - enabled: tenantConfigRes.thirdParty.enabled, - providers: finalProviderList, - }, - }; - }); + } + return { + status: "OK", + emailPassword: { + enabled: tenantConfigRes.emailPassword.enabled, + }, + passwordless: { + enabled: tenantConfigRes.passwordless.enabled, + }, + thirdParty: { + enabled: tenantConfigRes.thirdParty.enabled, + providers: finalProviderList, + }, + }; }, }; } diff --git a/lib/build/recipe/multitenancy/api/loginMethods.js b/lib/build/recipe/multitenancy/api/loginMethods.js index dbaba76be..2a009f786 100644 --- a/lib/build/recipe/multitenancy/api/loginMethods.js +++ b/lib/build/recipe/multitenancy/api/loginMethods.js @@ -13,48 +13,15 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); -function loginMethodsAPI(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.loginMethodsGET === undefined) { - return false; - } - const clientType = options.req.getKeyValueFromQuery("clientType"); - const result = yield apiImplementation.loginMethodsGET({ tenantId, clientType, options, userContext }); - utils_1.send200Response(options.res, result); - return true; - }); +async function loginMethodsAPI(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.loginMethodsGET === undefined) { + return false; + } + const clientType = options.req.getKeyValueFromQuery("clientType"); + const result = await apiImplementation.loginMethodsGET({ tenantId, clientType, options, userContext }); + utils_1.send200Response(options.res, result); + return true; } exports.default = loginMethodsAPI; diff --git a/lib/build/recipe/multitenancy/index.d.ts b/lib/build/recipe/multitenancy/index.d.ts index d8b8fddc7..0bd0e09d0 100644 --- a/lib/build/recipe/multitenancy/index.d.ts +++ b/lib/build/recipe/multitenancy/index.d.ts @@ -3,6 +3,7 @@ import Recipe from "./recipe"; import { RecipeInterface, APIOptions, APIInterface } from "./types"; import { ProviderConfig } from "../thirdparty/types"; import { AllowedDomainsClaim } from "./allowedDomainsClaim"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init: typeof Recipe.init; static createOrUpdateTenant( @@ -89,7 +90,7 @@ export default class Wrapper { }>; static associateUserToTenant( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, userContext?: any ): Promise< | { @@ -103,10 +104,14 @@ export default class Wrapper { | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"; } + | { + status: "ASSOCIATION_NOT_ALLOWED_ERROR"; + reason: string; + } >; static disassociateUserFromTenant( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, userContext?: any ): Promise<{ status: "OK"; diff --git a/lib/build/recipe/multitenancy/index.js b/lib/build/recipe/multitenancy/index.js index 4d1b70484..c96023c92 100644 --- a/lib/build/recipe/multitenancy/index.js +++ b/lib/build/recipe/multitenancy/index.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -60,81 +29,65 @@ Object.defineProperty(exports, "AllowedDomainsClaim", { }, }); class Wrapper { - static createOrUpdateTenant(tenantId, config, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.createOrUpdateTenant({ - tenantId, - config, - userContext: userContext === undefined ? {} : userContext, - }); + static async createOrUpdateTenant(tenantId, config, userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.createOrUpdateTenant({ + tenantId, + config, + userContext: userContext === undefined ? {} : userContext, }); } - static deleteTenant(tenantId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.deleteTenant({ - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); + static async deleteTenant(tenantId, userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.deleteTenant({ + tenantId, + userContext: userContext === undefined ? {} : userContext, }); } - static getTenant(tenantId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.getTenant({ - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); + static async getTenant(tenantId, userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.getTenant({ + tenantId, + userContext: userContext === undefined ? {} : userContext, }); } - static listAllTenants(userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.listAllTenants({ - userContext: userContext === undefined ? {} : userContext, - }); + static async listAllTenants(userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.listAllTenants({ + userContext: userContext === undefined ? {} : userContext, }); } - static createOrUpdateThirdPartyConfig(tenantId, config, skipValidation, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.createOrUpdateThirdPartyConfig({ - tenantId, - config, - skipValidation, - userContext: userContext === undefined ? {} : userContext, - }); + static async createOrUpdateThirdPartyConfig(tenantId, config, skipValidation, userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.createOrUpdateThirdPartyConfig({ + tenantId, + config, + skipValidation, + userContext: userContext === undefined ? {} : userContext, }); } - static deleteThirdPartyConfig(tenantId, thirdPartyId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.deleteThirdPartyConfig({ - tenantId, - thirdPartyId, - userContext: userContext === undefined ? {} : userContext, - }); + static async deleteThirdPartyConfig(tenantId, thirdPartyId, userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.deleteThirdPartyConfig({ + tenantId, + thirdPartyId, + userContext: userContext === undefined ? {} : userContext, }); } - static associateUserToTenant(tenantId, userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.associateUserToTenant({ - tenantId, - userId, - userContext: userContext === undefined ? {} : userContext, - }); + static async associateUserToTenant(tenantId, recipeUserId, userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.associateUserToTenant({ + tenantId, + recipeUserId, + userContext: userContext === undefined ? {} : userContext, }); } - static disassociateUserFromTenant(tenantId, userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return recipeInstance.recipeInterfaceImpl.disassociateUserFromTenant({ - tenantId, - userId, - userContext: userContext === undefined ? {} : userContext, - }); + static async disassociateUserFromTenant(tenantId, recipeUserId, userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return recipeInstance.recipeInterfaceImpl.disassociateUserFromTenant({ + tenantId, + recipeUserId, + userContext: userContext === undefined ? {} : userContext, }); } } diff --git a/lib/build/recipe/multitenancy/recipe.js b/lib/build/recipe/multitenancy/recipe.js index fe7537aec..646cfd476 100644 --- a/lib/build/recipe/multitenancy/recipe.js +++ b/lib/build/recipe/multitenancy/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -78,26 +47,24 @@ class Recipe extends recipeModule_1.default { }, ]; }; - this.handleAPIRequest = (id, tenantId, req, res, _, __, userContext) => - __awaiter(this, void 0, void 0, function* () { - let options = { - recipeImplementation: this.recipeInterfaceImpl, - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - req, - res, - staticThirdPartyProviders: this.staticThirdPartyProviders, - }; - if (id === constants_1.LOGIN_METHODS_API) { - return yield loginMethods_1.default(this.apiImpl, tenantId, options, userContext); - } - throw new Error("should never come here"); - }); - this.handleError = (err, _, __) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); + this.handleAPIRequest = async (id, tenantId, req, res, _, __, userContext) => { + let options = { + recipeImplementation: this.recipeInterfaceImpl, + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + req, + res, + staticThirdPartyProviders: this.staticThirdPartyProviders, + }; + if (id === constants_1.LOGIN_METHODS_API) { + return await loginMethods_1.default(this.apiImpl, tenantId, options, userContext); + } + throw new Error("should never come here"); + }; + this.handleError = async (err, _, __) => { + throw err; + }; this.getAllCORSHeaders = () => { return []; }; diff --git a/lib/build/recipe/multitenancy/recipeImplementation.js b/lib/build/recipe/multitenancy/recipeImplementation.js index 37c72db34..f070db1b2 100644 --- a/lib/build/recipe/multitenancy/recipeImplementation.js +++ b/lib/build/recipe/multitenancy/recipeImplementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -40,116 +9,96 @@ const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const constants_1 = require("./constants"); function getRecipeInterface(querier) { return { - getTenantId: function ({ tenantIdFromFrontend }) { - return __awaiter(this, void 0, void 0, function* () { - return tenantIdFromFrontend; - }); - }, - createOrUpdateTenant: function ({ tenantId, config }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPutRequest( - new normalisedURLPath_1.default(`/recipe/multitenancy/tenant`), - Object.assign({ tenantId }, config) - ); - return response; - }); + getTenantId: async function ({ tenantIdFromFrontend }) { + return tenantIdFromFrontend; }, - deleteTenant: function ({ tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/recipe/multitenancy/tenant/remove`), - { - tenantId, - } - ); - return response; - }); + createOrUpdateTenant: async function ({ tenantId, config }) { + let response = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/multitenancy/tenant`), + Object.assign({ tenantId }, config) + ); + return response; }, - getTenant: function ({ tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default( - `/${ - tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId - }/recipe/multitenancy/tenant` - ), - {} - ); - if (response.status === "TENANT_NOT_FOUND_ERROR") { - return undefined; + deleteTenant: async function ({ tenantId }) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/multitenancy/tenant/remove`), + { + tenantId, } - return response; - }); + ); + return response; }, - listAllTenants: function () { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/recipe/multitenancy/tenant/list`), - {} - ); - return response; - }); + getTenant: async function ({ tenantId }) { + let response = await querier.sendGetRequest( + new normalisedURLPath_1.default( + `/${tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId}/recipe/multitenancy/tenant` + ), + {} + ); + if (response.status === "TENANT_NOT_FOUND_ERROR") { + return undefined; + } + return response; + }, + listAllTenants: async function () { + let response = await querier.sendGetRequest( + new normalisedURLPath_1.default(`/recipe/multitenancy/tenant/list`), + {} + ); + return response; }, - createOrUpdateThirdPartyConfig: function ({ tenantId, config, skipValidation }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPutRequest( - new normalisedURLPath_1.default( - `/${ - tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId - }/recipe/multitenancy/config/thirdparty` - ), - { - config, - skipValidation, - } - ); - return response; - }); + createOrUpdateThirdPartyConfig: async function ({ tenantId, config, skipValidation }) { + let response = await querier.sendPutRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/config/thirdparty` + ), + { + config, + skipValidation, + } + ); + return response; }, - deleteThirdPartyConfig: function ({ tenantId, thirdPartyId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default( - `/${ - tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId - }/recipe/multitenancy/config/thirdparty/remove` - ), - { - thirdPartyId, - } - ); - return response; - }); + deleteThirdPartyConfig: async function ({ tenantId, thirdPartyId }) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/config/thirdparty/remove` + ), + { + thirdPartyId, + } + ); + return response; }, - associateUserToTenant: function ({ tenantId, userId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default( - `/${ - tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId - }/recipe/multitenancy/tenant/user` - ), - { - userId, - } - ); - return response; - }); + associateUserToTenant: async function ({ tenantId, recipeUserId }) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/tenant/user` + ), + { + recipeUserId: recipeUserId.getAsString(), + } + ); + return response; }, - disassociateUserFromTenant: function ({ tenantId, userId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default( - `/${ - tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId - }/recipe/multitenancy/tenant/user/remove` - ), - { - userId, - } - ); - return response; - }); + disassociateUserFromTenant: async function ({ tenantId, recipeUserId }) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default( + `/${ + tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId + }/recipe/multitenancy/tenant/user/remove` + ), + { + recipeUserId: recipeUserId.getAsString(), + } + ); + return response; }, }; } diff --git a/lib/build/recipe/multitenancy/types.d.ts b/lib/build/recipe/multitenancy/types.d.ts index 23efbd196..f9bbcb2ca 100644 --- a/lib/build/recipe/multitenancy/types.d.ts +++ b/lib/build/recipe/multitenancy/types.d.ts @@ -3,6 +3,7 @@ import { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { ProviderConfig, ProviderInput } from "../thirdparty/types"; import { GeneralErrorResponse } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export declare type TypeInput = { getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; override?: { @@ -109,7 +110,7 @@ export declare type RecipeInterface = { }>; associateUserToTenant: (input: { tenantId: string; - userId: string; + recipeUserId: RecipeUserId; userContext: any; }) => Promise< | { @@ -123,10 +124,14 @@ export declare type RecipeInterface = { | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"; } + | { + status: "ASSOCIATION_NOT_ALLOWED_ERROR"; + reason: string; + } >; disassociateUserFromTenant: (input: { tenantId: string; - userId: string; + recipeUserId: RecipeUserId; userContext: any; }) => Promise<{ status: "OK"; diff --git a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js index 9e90a5e36..000d7f489 100644 --- a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js +++ b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. * @@ -46,25 +15,23 @@ Object.defineProperty(exports, "__esModule", { value: true }); * under the License. */ const utils_1 = require("../../../utils"); -function getOpenIdDiscoveryConfiguration(apiImplementation, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.getOpenIdDiscoveryConfigurationGET === undefined) { - return false; - } - let result = yield apiImplementation.getOpenIdDiscoveryConfigurationGET({ - options, - userContext, - }); - if (result.status === "OK") { - options.res.setHeader("Access-Control-Allow-Origin", "*", false); - utils_1.send200Response(options.res, { - issuer: result.issuer, - jwks_uri: result.jwks_uri, - }); - } else { - utils_1.send200Response(options.res, result); - } - return true; +async function getOpenIdDiscoveryConfiguration(apiImplementation, options, userContext) { + if (apiImplementation.getOpenIdDiscoveryConfigurationGET === undefined) { + return false; + } + let result = await apiImplementation.getOpenIdDiscoveryConfigurationGET({ + options, + userContext, }); + if (result.status === "OK") { + options.res.setHeader("Access-Control-Allow-Origin", "*", false); + utils_1.send200Response(options.res, { + issuer: result.issuer, + jwks_uri: result.jwks_uri, + }); + } else { + utils_1.send200Response(options.res, result); + } + return true; } exports.default = getOpenIdDiscoveryConfiguration; diff --git a/lib/build/recipe/openid/api/implementation.js b/lib/build/recipe/openid/api/implementation.js index 81609c3b2..a29e99348 100644 --- a/lib/build/recipe/openid/api/implementation.js +++ b/lib/build/recipe/openid/api/implementation.js @@ -1,42 +1,9 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getAPIImplementation() { return { - getOpenIdDiscoveryConfigurationGET: function ({ options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - return yield options.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); - }); + getOpenIdDiscoveryConfigurationGET: async function ({ options, userContext }) { + return await options.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); }, }; } diff --git a/lib/build/recipe/openid/recipe.d.ts b/lib/build/recipe/openid/recipe.d.ts index 058c6ea59..ca59ef755 100644 --- a/lib/build/recipe/openid/recipe.d.ts +++ b/lib/build/recipe/openid/recipe.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import STError from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; diff --git a/lib/build/recipe/openid/recipe.js b/lib/build/recipe/openid/recipe.js index d113689c3..03827ba3f 100644 --- a/lib/build/recipe/openid/recipe.js +++ b/lib/build/recipe/openid/recipe.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -74,29 +43,27 @@ class OpenIdRecipe extends recipeModule_1.default { ...this.jwtRecipe.getAPIsHandled(), ]; }; - this.handleAPIRequest = (id, tenantId, req, response, path, method, userContext) => - __awaiter(this, void 0, void 0, function* () { - let apiOptions = { - recipeImplementation: this.recipeImplementation, - config: this.config, - recipeId: this.getRecipeId(), - req, - res: response, - }; - if (id === constants_1.GET_DISCOVERY_CONFIG_URL) { - return yield getOpenIdDiscoveryConfiguration_1.default(this.apiImpl, apiOptions, userContext); - } else { - return this.jwtRecipe.handleAPIRequest(id, tenantId, req, response, path, method, userContext); - } - }); - this.handleError = (error, request, response) => - __awaiter(this, void 0, void 0, function* () { - if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) { - throw error; - } else { - return yield this.jwtRecipe.handleError(error, request, response); - } - }); + this.handleAPIRequest = async (id, tenantId, req, response, path, method, userContext) => { + let apiOptions = { + recipeImplementation: this.recipeImplementation, + config: this.config, + recipeId: this.getRecipeId(), + req, + res: response, + }; + if (id === constants_1.GET_DISCOVERY_CONFIG_URL) { + return await getOpenIdDiscoveryConfiguration_1.default(this.apiImpl, apiOptions, userContext); + } else { + return this.jwtRecipe.handleAPIRequest(id, tenantId, req, response, path, method, userContext); + } + }; + this.handleError = async (error, request, response) => { + if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) { + throw error; + } else { + return await this.jwtRecipe.handleError(error, request, response); + } + }; this.getAllCORSHeaders = () => { return [...this.jwtRecipe.getAllCORSHeaders()]; }; diff --git a/lib/build/recipe/openid/recipeImplementation.js b/lib/build/recipe/openid/recipeImplementation.js index 53c193eda..0edb22162 100644 --- a/lib/build/recipe/openid/recipeImplementation.js +++ b/lib/build/recipe/openid/recipeImplementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -40,37 +9,31 @@ const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const constants_1 = require("../jwt/constants"); function getRecipeInterface(config, jwtRecipeImplementation) { return { - getOpenIdDiscoveryConfiguration: function () { - return __awaiter(this, void 0, void 0, function* () { - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); - let jwks_uri = - config.issuerDomain.getAsStringDangerous() + - config.issuerPath - .appendPath(new normalisedURLPath_1.default(constants_1.GET_JWKS_API)) - .getAsStringDangerous(); - return { - status: "OK", - issuer, - jwks_uri, - }; - }); + getOpenIdDiscoveryConfiguration: async function () { + let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); + let jwks_uri = + config.issuerDomain.getAsStringDangerous() + + config.issuerPath + .appendPath(new normalisedURLPath_1.default(constants_1.GET_JWKS_API)) + .getAsStringDangerous(); + return { + status: "OK", + issuer, + jwks_uri, + }; }, - createJWT: function ({ payload, validitySeconds, useStaticSigningKey, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - payload = payload === undefined || payload === null ? {} : payload; - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); - return yield jwtRecipeImplementation.createJWT({ - payload: Object.assign({ iss: issuer }, payload), - useStaticSigningKey, - validitySeconds, - userContext, - }); + createJWT: async function ({ payload, validitySeconds, useStaticSigningKey, userContext }) { + payload = payload === undefined || payload === null ? {} : payload; + let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); + return await jwtRecipeImplementation.createJWT({ + payload: Object.assign({ iss: issuer }, payload), + useStaticSigningKey, + validitySeconds, + userContext, }); }, - getJWKS: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield jwtRecipeImplementation.getJWKS(input); - }); + getJWKS: async function (input) { + return await jwtRecipeImplementation.getJWKS(input); }, }; } diff --git a/lib/build/recipe/openid/types.d.ts b/lib/build/recipe/openid/types.d.ts index 30af4d0b1..ccb4e6561 100644 --- a/lib/build/recipe/openid/types.d.ts +++ b/lib/build/recipe/openid/types.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLDomain from "../../normalisedURLDomain"; import NormalisedURLPath from "../../normalisedURLPath"; import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface, JsonWebKey } from "../jwt/types"; diff --git a/lib/build/recipe/passwordless/api/consumeCode.js b/lib/build/recipe/passwordless/api/consumeCode.js index 9af5c7827..2857aa964 100644 --- a/lib/build/recipe/passwordless/api/consumeCode.js +++ b/lib/build/recipe/passwordless/api/consumeCode.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,64 +21,63 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -function consumeCode(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.consumeCodePOST === undefined) { - return false; - } - const body = yield options.req.getJSONBody(); - const preAuthSessionId = body.preAuthSessionId; - const linkCode = body.linkCode; - const deviceId = body.deviceId; - const userInputCode = body.userInputCode; - if (preAuthSessionId === undefined) { +async function consumeCode(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.consumeCodePOST === undefined) { + return false; + } + const body = await options.req.getJSONBody(); + const preAuthSessionId = body.preAuthSessionId; + const linkCode = body.linkCode; + const deviceId = body.deviceId; + const userInputCode = body.userInputCode; + if (preAuthSessionId === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide preAuthSessionId", + }); + } + if (deviceId !== undefined || userInputCode !== undefined) { + if (linkCode !== undefined) { throw new error_1.default({ type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide preAuthSessionId", + message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", }); } - if (deviceId !== undefined || userInputCode !== undefined) { - if (linkCode !== undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", - }); - } - if (deviceId === undefined || userInputCode === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide both deviceId and userInputCode", - }); - } - } else if (linkCode === undefined) { + if (deviceId === undefined || userInputCode === undefined) { throw new error_1.default({ type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", + message: "Please provide both deviceId and userInputCode", }); } - let result = yield apiImplementation.consumeCodePOST( - deviceId !== undefined - ? { - deviceId, - userInputCode, - preAuthSessionId, - tenantId, - options, - userContext, - } - : { - linkCode, - options, - preAuthSessionId, - tenantId, - userContext, - } - ); - if (result.status === "OK") { - delete result.session; - } - utils_1.send200Response(options.res, result); - return true; - }); + } else if (linkCode === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", + }); + } + let result = await apiImplementation.consumeCodePOST( + deviceId !== undefined + ? { + deviceId, + userInputCode, + preAuthSessionId, + tenantId, + options, + userContext, + } + : { + linkCode, + options, + preAuthSessionId, + tenantId, + userContext, + } + ); + if (result.status === "OK") { + result = Object.assign(Object.assign({}, result), utils_1.getBackwardsCompatibleUserInfo(options.req, result)); + delete result.session; + } + utils_1.send200Response(options.res, result); + return true; } exports.default = consumeCode; diff --git a/lib/build/recipe/passwordless/api/createCode.js b/lib/build/recipe/passwordless/api/createCode.js index 587bb43fe..6dc7a4828 100644 --- a/lib/build/recipe/passwordless/api/createCode.js +++ b/lib/build/recipe/passwordless/api/createCode.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,75 +22,73 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); const max_1 = __importDefault(require("libphonenumber-js/max")); -function createCode(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.createCodePOST === undefined) { - return false; - } - const body = yield options.req.getJSONBody(); - let email = body.email; - let phoneNumber = body.phoneNumber; - if ((email !== undefined && phoneNumber !== undefined) || (email === undefined && phoneNumber === undefined)) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide exactly one of email or phoneNumber", - }); - } - if (email === undefined && options.config.contactMethod === "EMAIL") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: 'Please provide an email since you have set the contactMethod to "EMAIL"', +async function createCode(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.createCodePOST === undefined) { + return false; + } + const body = await options.req.getJSONBody(); + let email = body.email; + let phoneNumber = body.phoneNumber; + if ((email !== undefined && phoneNumber !== undefined) || (email === undefined && phoneNumber === undefined)) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide exactly one of email or phoneNumber", + }); + } + if (email === undefined && options.config.contactMethod === "EMAIL") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: 'Please provide an email since you have set the contactMethod to "EMAIL"', + }); + } + if (phoneNumber === undefined && options.config.contactMethod === "PHONE") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: 'Please provide a phoneNumber since you have set the contactMethod to "PHONE"', + }); + } + // normalise and validate format of input + if ( + email !== undefined && + (options.config.contactMethod === "EMAIL" || options.config.contactMethod === "EMAIL_OR_PHONE") + ) { + email = email.trim(); + const validateError = await options.config.validateEmailAddress(email, tenantId); + if (validateError !== undefined) { + utils_1.send200Response(options.res, { + status: "GENERAL_ERROR", + message: validateError, }); + return true; } - if (phoneNumber === undefined && options.config.contactMethod === "PHONE") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: 'Please provide a phoneNumber since you have set the contactMethod to "PHONE"', + } + if ( + phoneNumber !== undefined && + (options.config.contactMethod === "PHONE" || options.config.contactMethod === "EMAIL_OR_PHONE") + ) { + const validateError = await options.config.validatePhoneNumber(phoneNumber, tenantId); + if (validateError !== undefined) { + utils_1.send200Response(options.res, { + status: "GENERAL_ERROR", + message: validateError, }); + return true; } - // normalise and validate format of input - if ( - email !== undefined && - (options.config.contactMethod === "EMAIL" || options.config.contactMethod === "EMAIL_OR_PHONE") - ) { - email = email.trim(); - const validateError = yield options.config.validateEmailAddress(email, tenantId); - if (validateError !== undefined) { - utils_1.send200Response(options.res, { - status: "GENERAL_ERROR", - message: validateError, - }); - return true; - } - } - if ( - phoneNumber !== undefined && - (options.config.contactMethod === "PHONE" || options.config.contactMethod === "EMAIL_OR_PHONE") - ) { - const validateError = yield options.config.validatePhoneNumber(phoneNumber, tenantId); - if (validateError !== undefined) { - utils_1.send200Response(options.res, { - status: "GENERAL_ERROR", - message: validateError, - }); - return true; - } - const parsedPhoneNumber = max_1.default(phoneNumber); - if (parsedPhoneNumber === undefined) { - // this can come here if the user has provided their own impl of validatePhoneNumber and - // the phone number is valid according to their impl, but not according to the libphonenumber-js lib. - phoneNumber = phoneNumber.trim(); - } else { - phoneNumber = parsedPhoneNumber.format("E.164"); - } + const parsedPhoneNumber = max_1.default(phoneNumber); + if (parsedPhoneNumber === undefined) { + // this can come here if the user has provided their own impl of validatePhoneNumber and + // the phone number is valid according to their impl, but not according to the libphonenumber-js lib. + phoneNumber = phoneNumber.trim(); + } else { + phoneNumber = parsedPhoneNumber.format("E.164"); } - let result = yield apiImplementation.createCodePOST( - email !== undefined - ? { email, tenantId, options, userContext } - : { phoneNumber: phoneNumber, tenantId, options, userContext } - ); - utils_1.send200Response(options.res, result); - return true; - }); + } + let result = await apiImplementation.createCodePOST( + email !== undefined + ? { email, tenantId, options, userContext } + : { phoneNumber: phoneNumber, tenantId, options, userContext } + ); + utils_1.send200Response(options.res, result); + return true; } exports.default = createCode; diff --git a/lib/build/recipe/passwordless/api/emailExists.js b/lib/build/recipe/passwordless/api/emailExists.js index e2ac25961..312816062 100644 --- a/lib/build/recipe/passwordless/api/emailExists.js +++ b/lib/build/recipe/passwordless/api/emailExists.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,26 +21,24 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -function emailExists(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.emailExistsGET === undefined) { - return false; - } - let email = options.req.getKeyValueFromQuery("email"); - if (email === undefined || typeof email !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the email as a GET param", - }); - } - let result = yield apiImplementation.emailExistsGET({ - email, - tenantId, - options, - userContext, +async function emailExists(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.emailExistsGET === undefined) { + return false; + } + let email = options.req.getKeyValueFromQuery("email"); + if (email === undefined || typeof email !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the email as a GET param", }); - utils_1.send200Response(options.res, result); - return true; + } + let result = await apiImplementation.emailExistsGET({ + email, + tenantId, + options, + userContext, }); + utils_1.send200Response(options.res, result); + return true; } exports.default = emailExists; diff --git a/lib/build/recipe/passwordless/api/implementation.js b/lib/build/recipe/passwordless/api/implementation.js index 3cdeee719..abdee27bc 100644 --- a/lib/build/recipe/passwordless/api/implementation.js +++ b/lib/build/recipe/passwordless/api/implementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -37,285 +6,406 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); const logger_1 = require("../../../logger"); -const recipe_1 = __importDefault(require("../../emailverification/recipe")); +const recipe_1 = __importDefault(require("../../accountlinking/recipe")); const session_1 = __importDefault(require("../../session")); +const __1 = require("../../.."); function getAPIImplementation() { return { - consumeCodePOST: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield input.options.recipeImplementation.consumeCode( - "deviceId" in input - ? { - preAuthSessionId: input.preAuthSessionId, - deviceId: input.deviceId, - userInputCode: input.userInputCode, - tenantId: input.tenantId, - userContext: input.userContext, - } - : { - preAuthSessionId: input.preAuthSessionId, - linkCode: input.linkCode, - tenantId: input.tenantId, - userContext: input.userContext, - } - ); - if (response.status !== "OK") { - return response; - } - let user = response.user; - if (user.email !== undefined) { - const emailVerificationInstance = recipe_1.default.getInstance(); - if (emailVerificationInstance) { - const tokenResponse = yield emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( - { - userId: user.id, - email: user.email, - tenantId: input.tenantId, - userContext: input.userContext, - } - ); - if (tokenResponse.status === "OK") { - yield emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ - token: tokenResponse.token, - tenantId: input.tenantId, - userContext: input.userContext, - }); - } - } + consumeCodePOST: async function (input) { + const deviceInfo = await input.options.recipeImplementation.listCodesByPreAuthSessionId({ + tenantId: input.tenantId, + preAuthSessionId: input.preAuthSessionId, + userContext: input.userContext, + }); + if (!deviceInfo) { + return { + status: "RESTART_FLOW_ERROR", + }; + } + let existingUsers = await __1.listUsersByAccountInfo( + input.tenantId, + { + phoneNumber: deviceInfo.phoneNumber, + email: deviceInfo.email, + }, + false, + input.userContext + ); + existingUsers = existingUsers.filter((u) => + u.loginMethods.some( + (m) => + m.recipeId === "passwordless" && + (m.hasSameEmailAs(deviceInfo.email) || m.hasSamePhoneNumberAs(m.phoneNumber)) + ) + ); + if (existingUsers.length === 0) { + let isSignUpAllowed = await recipe_1.default.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "passwordless", + email: deviceInfo.email, + phoneNumber: deviceInfo.phoneNumber, + }, + isVerified: true, + tenantId: input.tenantId, + userContext: input.userContext, + }); + if (!isSignUpAllowed) { + // On the frontend, this should show a UI of asking the user + // to login using a different method. + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)", + }; } - const session = yield session_1.default.createNewSession( - input.options.req, - input.options.res, - input.tenantId, - user.id, - {}, - {}, - input.userContext + } else if (existingUsers.length > 1) { + throw new Error( + "You have found a bug. Please report it on https://github.com/supertokens/supertokens-node/issues" ); - return { - status: "OK", - createdNewUser: response.createdNewUser, + } + let response = await input.options.recipeImplementation.consumeCode( + "deviceId" in input + ? { + preAuthSessionId: input.preAuthSessionId, + deviceId: input.deviceId, + userInputCode: input.userInputCode, + tenantId: input.tenantId, + userContext: input.userContext, + } + : { + preAuthSessionId: input.preAuthSessionId, + linkCode: input.linkCode, + tenantId: input.tenantId, + userContext: input.userContext, + } + ); + if (response.status !== "OK") { + return response; + } + let loginMethod = response.user.loginMethods.find( + (m) => + m.recipeId === "passwordless" && + (m.hasSameEmailAs(deviceInfo.email) || m.hasSamePhoneNumberAs(m.phoneNumber)) + ); + if (loginMethod === undefined) { + throw new Error("Should never come here"); + } + if (existingUsers.length > 0) { + // Here we do this check after sign in is done cause: + // - We first want to check if the credentials are correct first or not + // - The above recipe function marks the email as verified + // - Even though the above call to signInUp is state changing (it changes the email + // of the user), it's OK to do this check here cause the isSignInAllowed checks + // conditions related to account linking + let isSignInAllowed = await recipe_1.default.getInstance().isSignInAllowed({ user: response.user, - session, - }; - }); + tenantId: input.tenantId, + userContext: input.userContext, + }); + if (!isSignInAllowed) { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_003)", + }; + } + // we do account linking only during sign in here cause during sign up, + // the recipe function above does account linking for us. + response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId: input.tenantId, + user: response.user, + userContext: input.userContext, + }); + } + const session = await session_1.default.createNewSession( + input.options.req, + input.options.res, + input.tenantId, + loginMethod.recipeUserId, + {}, + {}, + input.userContext + ); + return { + status: "OK", + createdNewRecipeUser: response.createdNewRecipeUser, + user: response.user, + session, + }; }, - createCodePOST: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield input.options.recipeImplementation.createCode( - "email" in input - ? { - userContext: input.userContext, - email: input.email, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : yield input.options.config.getCustomUserInputCode( - input.tenantId, - input.userContext - ), - tenantId: input.tenantId, - } - : { - userContext: input.userContext, - phoneNumber: input.phoneNumber, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : yield input.options.config.getCustomUserInputCode( - input.tenantId, - input.userContext - ), - tenantId: input.tenantId, - } - ); - // now we send the email / text message. - let magicLink = undefined; - let userInputCode = undefined; - const flowType = input.options.config.flowType; - if (flowType === "MAGIC_LINK" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - magicLink = - input.options.appInfo.websiteDomain.getAsStringDangerous() + - input.options.appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - input.options.recipeId + - "&preAuthSessionId=" + - response.preAuthSessionId + - "&tenantId=" + - input.tenantId + - "#" + - response.linkCode; + createCodePOST: async function (input) { + const accountInfo = {}; + if ("email" in input) { + accountInfo.email = input.email; + } + if ("phoneNumber" in input) { + accountInfo.email = input.phoneNumber; + } + let existingUsers = await __1.listUsersByAccountInfo(input.tenantId, accountInfo, false, input.userContext); + existingUsers = existingUsers.filter((u) => + u.loginMethods.some( + (m) => + m.recipeId === "passwordless" && + (m.hasSameEmailAs(accountInfo.email) || m.hasSamePhoneNumberAs(accountInfo.phoneNumber)) + ) + ); + if (existingUsers.length === 0) { + let isSignUpAllowed = await recipe_1.default.getInstance().isSignUpAllowed({ + newUser: Object.assign({ recipeId: "passwordless" }, accountInfo), + isVerified: true, + tenantId: input.tenantId, + userContext: input.userContext, + }); + if (!isSignUpAllowed) { + // On the frontend, this should show a UI of asking the user + // to login using a different method. + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)", + }; } - if (flowType === "USER_INPUT_CODE" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - userInputCode = response.userInputCode; + } else if (existingUsers.length === 1) { + let loginMethod = existingUsers[0].loginMethods.find( + (m) => + m.recipeId === "passwordless" && + (m.hasSameEmailAs(accountInfo.email) || m.hasSamePhoneNumberAs(accountInfo.phoneNumber)) + ); + if (loginMethod === undefined) { + throw new Error("Should never come here"); } - // we don't do something special for serverless env here - // cause we want to wait for service's reply since it can show - // a UI error message for if sending an SMS / email failed or not. - if ( - input.options.config.contactMethod === "PHONE" || - (input.options.config.contactMethod === "EMAIL_OR_PHONE" && "phoneNumber" in input) - ) { - logger_1.logDebugMessage(`Sending passwordless login SMS to ${input.phoneNumber}`); - yield input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ - type: "PASSWORDLESS_LOGIN", - codeLifetime: response.codeLifetime, - phoneNumber: input.phoneNumber, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - tenantId: input.tenantId, - userContext: input.userContext, - }); - } else { - logger_1.logDebugMessage(`Sending passwordless login email to ${input.email}`); - yield input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORDLESS_LOGIN", - email: input.email, - codeLifetime: response.codeLifetime, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - tenantId: input.tenantId, - userContext: input.userContext, - }); + let isSignInAllowed = await recipe_1.default.getInstance().isSignInAllowed({ + user: existingUsers[0], + tenantId: input.tenantId, + userContext: input.userContext, + }); + if (!isSignInAllowed) { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_003)", + }; } - return { - status: "OK", - deviceId: response.deviceId, - flowType: input.options.config.flowType, + } else { + throw new Error( + "You have found a bug. Please report it on https://github.com/supertokens/supertokens-node/issues" + ); + } + let response = await input.options.recipeImplementation.createCode( + "email" in input + ? { + userContext: input.userContext, + email: input.email, + userInputCode: + input.options.config.getCustomUserInputCode === undefined + ? undefined + : await input.options.config.getCustomUserInputCode( + input.tenantId, + input.userContext + ), + tenantId: input.tenantId, + } + : { + userContext: input.userContext, + phoneNumber: input.phoneNumber, + userInputCode: + input.options.config.getCustomUserInputCode === undefined + ? undefined + : await input.options.config.getCustomUserInputCode( + input.tenantId, + input.userContext + ), + tenantId: input.tenantId, + } + ); + // now we send the email / text message. + let magicLink = undefined; + let userInputCode = undefined; + const flowType = input.options.config.flowType; + if (flowType === "MAGIC_LINK" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { + magicLink = + input.options.appInfo.websiteDomain.getAsStringDangerous() + + input.options.appInfo.websiteBasePath.getAsStringDangerous() + + "/verify" + + "?rid=" + + input.options.recipeId + + "&preAuthSessionId=" + + response.preAuthSessionId + + "&tenantId=" + + input.tenantId + + "#" + + response.linkCode; + } + if (flowType === "USER_INPUT_CODE" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { + userInputCode = response.userInputCode; + } + // we don't do something special for serverless env here + // cause we want to wait for service's reply since it can show + // a UI error message for if sending an SMS / email failed or not. + if ( + input.options.config.contactMethod === "PHONE" || + (input.options.config.contactMethod === "EMAIL_OR_PHONE" && "phoneNumber" in input) + ) { + logger_1.logDebugMessage(`Sending passwordless login SMS to ${input.phoneNumber}`); + await input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ + type: "PASSWORDLESS_LOGIN", + codeLifetime: response.codeLifetime, + phoneNumber: input.phoneNumber, preAuthSessionId: response.preAuthSessionId, - }; - }); - }, - emailExistsGET: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield input.options.recipeImplementation.getUserByEmail({ + urlWithLinkCode: magicLink, + userInputCode, + tenantId: input.tenantId, userContext: input.userContext, + }); + } else { + logger_1.logDebugMessage(`Sending passwordless login email to ${input.email}`); + await input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: "PASSWORDLESS_LOGIN", email: input.email, + codeLifetime: response.codeLifetime, + preAuthSessionId: response.preAuthSessionId, + urlWithLinkCode: magicLink, + userInputCode, tenantId: input.tenantId, + userContext: input.userContext, }); - return { - exists: response !== undefined, - status: "OK", - }; - }); + } + return { + status: "OK", + deviceId: response.deviceId, + flowType: input.options.config.flowType, + preAuthSessionId: response.preAuthSessionId, + }; }, - phoneNumberExistsGET: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield input.options.recipeImplementation.getUserByPhoneNumber({ - userContext: input.userContext, + emailExistsGET: async function (input) { + let users = await __1.listUsersByAccountInfo( + input.tenantId, + { + email: input.email, + // tenantId: input.tenantId, + }, + false, + input.userContext + ); + return { + exists: users.length > 0, + status: "OK", + }; + }, + phoneNumberExistsGET: async function (input) { + let users = await __1.listUsersByAccountInfo( + input.tenantId, + { phoneNumber: input.phoneNumber, - tenantId: input.tenantId, - }); + // tenantId: input.tenantId, + }, + false, + input.userContext + ); + return { + exists: users.length > 0, + status: "OK", + }; + }, + resendCodePOST: async function (input) { + let deviceInfo = await input.options.recipeImplementation.listCodesByDeviceId({ + userContext: input.userContext, + deviceId: input.deviceId, + tenantId: input.tenantId, + }); + if (deviceInfo === undefined) { return { - exists: response !== undefined, - status: "OK", + status: "RESTART_FLOW_ERROR", }; - }); - }, - resendCodePOST: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let deviceInfo = yield input.options.recipeImplementation.listCodesByDeviceId({ + } + if ( + (input.options.config.contactMethod === "PHONE" && deviceInfo.phoneNumber === undefined) || + (input.options.config.contactMethod === "EMAIL" && deviceInfo.email === undefined) + ) { + return { + status: "RESTART_FLOW_ERROR", + }; + } + let numberOfTriesToCreateNewCode = 0; + while (true) { + numberOfTriesToCreateNewCode++; + let response = await input.options.recipeImplementation.createNewCodeForDevice({ userContext: input.userContext, deviceId: input.deviceId, + userInputCode: + input.options.config.getCustomUserInputCode === undefined + ? undefined + : await input.options.config.getCustomUserInputCode(input.tenantId, input.userContext), tenantId: input.tenantId, }); - if (deviceInfo === undefined) { - return { - status: "RESTART_FLOW_ERROR", - }; - } - if ( - (input.options.config.contactMethod === "PHONE" && deviceInfo.phoneNumber === undefined) || - (input.options.config.contactMethod === "EMAIL" && deviceInfo.email === undefined) - ) { - return { - status: "RESTART_FLOW_ERROR", - }; + if (response.status === "USER_INPUT_CODE_ALREADY_USED_ERROR") { + if (numberOfTriesToCreateNewCode >= 3) { + // we retry 3 times. + return { + status: "GENERAL_ERROR", + message: "Failed to generate a one time code. Please try again", + }; + } + continue; } - let numberOfTriesToCreateNewCode = 0; - while (true) { - numberOfTriesToCreateNewCode++; - let response = yield input.options.recipeImplementation.createNewCodeForDevice({ - userContext: input.userContext, - deviceId: input.deviceId, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : yield input.options.config.getCustomUserInputCode(input.tenantId, input.userContext), - tenantId: input.tenantId, - }); - if (response.status === "USER_INPUT_CODE_ALREADY_USED_ERROR") { - if (numberOfTriesToCreateNewCode >= 3) { - // we retry 3 times. - return { - status: "GENERAL_ERROR", - message: "Failed to generate a one time code. Please try again", - }; - } - continue; + if (response.status === "OK") { + let magicLink = undefined; + let userInputCode = undefined; + const flowType = input.options.config.flowType; + if (flowType === "MAGIC_LINK" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { + magicLink = + input.options.appInfo.websiteDomain.getAsStringDangerous() + + input.options.appInfo.websiteBasePath.getAsStringDangerous() + + "/verify" + + "?rid=" + + input.options.recipeId + + "&preAuthSessionId=" + + response.preAuthSessionId + + "&tenantId=" + + input.tenantId + + "#" + + response.linkCode; } - if (response.status === "OK") { - let magicLink = undefined; - let userInputCode = undefined; - const flowType = input.options.config.flowType; - if (flowType === "MAGIC_LINK" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - magicLink = - input.options.appInfo.websiteDomain.getAsStringDangerous() + - input.options.appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - input.options.recipeId + - "&preAuthSessionId=" + - response.preAuthSessionId + - "&tenantId=" + - input.tenantId + - "#" + - response.linkCode; - } - if (flowType === "USER_INPUT_CODE" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - userInputCode = response.userInputCode; - } - // we don't do something special for serverless env here - // cause we want to wait for service's reply since it can show - // a UI error message for if sending an SMS / email failed or not. - if ( - input.options.config.contactMethod === "PHONE" || - (input.options.config.contactMethod === "EMAIL_OR_PHONE" && - deviceInfo.phoneNumber !== undefined) - ) { - logger_1.logDebugMessage(`Sending passwordless login SMS to ${input.phoneNumber}`); - yield input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ - type: "PASSWORDLESS_LOGIN", - codeLifetime: response.codeLifetime, - phoneNumber: deviceInfo.phoneNumber, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - tenantId: input.tenantId, - userContext: input.userContext, - }); - } else { - logger_1.logDebugMessage(`Sending passwordless login email to ${input.email}`); - yield input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORDLESS_LOGIN", - email: deviceInfo.email, - codeLifetime: response.codeLifetime, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - tenantId: input.tenantId, - userContext: input.userContext, - }); - } + if (flowType === "USER_INPUT_CODE" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { + userInputCode = response.userInputCode; + } + // we don't do something special for serverless env here + // cause we want to wait for service's reply since it can show + // a UI error message for if sending an SMS / email failed or not. + if ( + input.options.config.contactMethod === "PHONE" || + (input.options.config.contactMethod === "EMAIL_OR_PHONE" && + deviceInfo.phoneNumber !== undefined) + ) { + logger_1.logDebugMessage(`Sending passwordless login SMS to ${input.phoneNumber}`); + await input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ + type: "PASSWORDLESS_LOGIN", + codeLifetime: response.codeLifetime, + phoneNumber: deviceInfo.phoneNumber, + preAuthSessionId: response.preAuthSessionId, + urlWithLinkCode: magicLink, + userInputCode, + tenantId: input.tenantId, + userContext: input.userContext, + }); + } else { + logger_1.logDebugMessage(`Sending passwordless login email to ${input.email}`); + await input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: "PASSWORDLESS_LOGIN", + email: deviceInfo.email, + codeLifetime: response.codeLifetime, + preAuthSessionId: response.preAuthSessionId, + urlWithLinkCode: magicLink, + userInputCode, + tenantId: input.tenantId, + userContext: input.userContext, + }); } - return { - status: response.status, - }; } - }); + return { + status: response.status, + }; + } }, }; } diff --git a/lib/build/recipe/passwordless/api/phoneNumberExists.js b/lib/build/recipe/passwordless/api/phoneNumberExists.js index 16ffe0a56..887522533 100644 --- a/lib/build/recipe/passwordless/api/phoneNumberExists.js +++ b/lib/build/recipe/passwordless/api/phoneNumberExists.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,26 +21,24 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -function phoneNumberExists(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.phoneNumberExistsGET === undefined) { - return false; - } - let phoneNumber = options.req.getKeyValueFromQuery("phoneNumber"); - if (phoneNumber === undefined || typeof phoneNumber !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the phoneNumber as a GET param", - }); - } - let result = yield apiImplementation.phoneNumberExistsGET({ - phoneNumber, - tenantId, - options, - userContext, +async function phoneNumberExists(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.phoneNumberExistsGET === undefined) { + return false; + } + let phoneNumber = options.req.getKeyValueFromQuery("phoneNumber"); + if (phoneNumber === undefined || typeof phoneNumber !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the phoneNumber as a GET param", }); - utils_1.send200Response(options.res, result); - return true; + } + let result = await apiImplementation.phoneNumberExistsGET({ + phoneNumber, + tenantId, + options, + userContext, }); + utils_1.send200Response(options.res, result); + return true; } exports.default = phoneNumberExists; diff --git a/lib/build/recipe/passwordless/api/resendCode.js b/lib/build/recipe/passwordless/api/resendCode.js index 69714dc8e..27b1d6699 100644 --- a/lib/build/recipe/passwordless/api/resendCode.js +++ b/lib/build/recipe/passwordless/api/resendCode.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,35 +21,33 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -function resendCode(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.resendCodePOST === undefined) { - return false; - } - const body = yield options.req.getJSONBody(); - const preAuthSessionId = body.preAuthSessionId; - const deviceId = body.deviceId; - if (preAuthSessionId === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide preAuthSessionId", - }); - } - if (deviceId === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide a deviceId", - }); - } - let result = yield apiImplementation.resendCodePOST({ - deviceId, - preAuthSessionId, - tenantId, - options, - userContext, +async function resendCode(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.resendCodePOST === undefined) { + return false; + } + const body = await options.req.getJSONBody(); + const preAuthSessionId = body.preAuthSessionId; + const deviceId = body.deviceId; + if (preAuthSessionId === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide preAuthSessionId", + }); + } + if (deviceId === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide a deviceId", }); - utils_1.send200Response(options.res, result); - return true; + } + let result = await apiImplementation.resendCodePOST({ + deviceId, + preAuthSessionId, + tenantId, + options, + userContext, }); + utils_1.send200Response(options.res, result); + return true; } exports.default = resendCode; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.js index 57164988c..6442d001d 100644 --- a/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.js @@ -1,88 +1,54 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../../../utils"); -function createAndSendEmailUsingSupertokensService(input) { - return __awaiter(this, void 0, void 0, function* () { - if (process.env.TEST_MODE === "testing") { - return; - } - const result = yield utils_1.postWithFetch( - "https://api.supertokens.io/0/st/auth/passwordless/login", - { - "api-version": "0", - "content-type": "application/json; charset=utf-8", - }, - { - email: input.email, - appName: input.appInfo.appName, - codeLifetime: input.codeLifetime, - urlWithLinkCode: input.urlWithLinkCode, - userInputCode: input.userInputCode, - }, - { - successLog: `Email sent to ${input.email}`, - errorLogHeader: "Error sending passwordless login email", - } - ); - if ("error" in result) { - throw result.error; +async function createAndSendEmailUsingSupertokensService(input) { + if (process.env.TEST_MODE === "testing") { + return; + } + const result = await utils_1.postWithFetch( + "https://api.supertokens.io/0/st/auth/passwordless/login", + { + "api-version": "0", + "content-type": "application/json; charset=utf-8", + }, + { + email: input.email, + appName: input.appInfo.appName, + codeLifetime: input.codeLifetime, + urlWithLinkCode: input.urlWithLinkCode, + userInputCode: input.userInputCode, + }, + { + successLog: `Email sent to ${input.email}`, + errorLogHeader: "Error sending passwordless login email", } - if (result.resp && result.resp.status >= 400) { - if (result.resp.body.err) { - /** - * if the error is thrown from API, the response object - * will be of type `{err: string}` - */ - throw new Error(result.resp.body.err); - } else { - throw new Error(`Request failed with status code ${result.resp.status}`); - } + ); + if ("error" in result) { + throw result.error; + } + if (result.resp && result.resp.status >= 400) { + if (result.resp.body.err) { + /** + * if the error is thrown from API, the response object + * will be of type `{err: string}` + */ + throw new Error(result.resp.body.err); + } else { + throw new Error(`Request failed with status code ${result.resp.status}`); } - }); + } } class BackwardCompatibilityService { constructor(appInfo) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - yield createAndSendEmailUsingSupertokensService({ - appInfo: this.appInfo, - email: input.email, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - }); + this.sendEmail = async (input) => { + await createAndSendEmailUsingSupertokensService({ + appInfo: this.appInfo, + email: input.email, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + codeLifetime: input.codeLifetime, }); + }; this.appInfo = appInfo; } } diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.js b/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.js index e917fb0fe..dedcbe33f 100644 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.js +++ b/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -41,13 +10,12 @@ const supertokens_js_override_1 = __importDefault(require("supertokens-js-overri const serviceImplementation_1 = require("./serviceImplementation"); class SMTPService { constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - let content = yield this.serviceImpl.getContent(input); - yield this.serviceImpl.sendRawEmail( - Object.assign(Object.assign({}, content), { userContext: input.userContext }) - ); - }); + this.sendEmail = async (input) => { + let content = await this.serviceImpl.getContent(input); + await this.serviceImpl.sendRawEmail( + Object.assign(Object.assign({}, content), { userContext: input.userContext }) + ); + }; const transporter = nodemailer_1.createTransport({ host: config.smtpSettings.host, port: config.smtpSettings.port, diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.js b/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.js index dfffb9e5d..abb00642c 100644 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.js +++ b/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -54,29 +23,25 @@ exports.getServiceImplementation = void 0; const passwordlessLogin_1 = __importDefault(require("./passwordlessLogin")); function getServiceImplementation(transporter, from) { return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (input.isHtml) { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }); + sendRawEmail: async function (input) { + if (input.isHtml) { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + html: input.body, + }); + } else { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + text: input.body, + }); + } }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return passwordlessLogin_1.default(input); - }); + getContent: async function (input) { + return passwordlessLogin_1.default(input); }, }; } diff --git a/lib/build/recipe/passwordless/index.d.ts b/lib/build/recipe/passwordless/index.d.ts index 94cb0f4f9..e8b9ea035 100644 --- a/lib/build/recipe/passwordless/index.d.ts +++ b/lib/build/recipe/passwordless/index.d.ts @@ -3,12 +3,12 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; import { RecipeInterface, - User, APIOptions, APIInterface, TypePasswordlessEmailDeliveryInput, TypePasswordlessSmsDeliveryInput, } from "./types"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init: typeof Recipe.init; static Error: typeof SuperTokensError; @@ -73,8 +73,9 @@ export default class Wrapper { ): Promise< | { status: "OK"; - createdNewUser: boolean; - user: User; + createdNewRecipeUser: boolean; + user: import("../../types").User; + recipeUserId: RecipeUserId; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -85,21 +86,24 @@ export default class Wrapper { status: "RESTART_FLOW_ERROR"; } >; - static getUserById(input: { userId: string; userContext?: any }): Promise; - static getUserByEmail(input: { email: string; tenantId: string; userContext?: any }): Promise; - static getUserByPhoneNumber(input: { - phoneNumber: string; - tenantId: string; - userContext?: any; - }): Promise; static updateUser(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; userContext?: any; - }): Promise<{ - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; + }): Promise< + | { + status: + | "OK" + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; static revokeAllCodes( input: | { @@ -169,8 +173,9 @@ export default class Wrapper { } ): Promise<{ status: string; - createdNewUser: boolean; - user: User; + createdNewRecipeUser: boolean; + recipeUserId: RecipeUserId; + user: import("../../types").User; }>; static sendEmail( input: TypePasswordlessEmailDeliveryInput & { @@ -187,9 +192,6 @@ export declare let init: typeof Recipe.init; export declare let Error: typeof SuperTokensError; export declare let createCode: typeof Wrapper.createCode; export declare let consumeCode: typeof Wrapper.consumeCode; -export declare let getUserByEmail: typeof Wrapper.getUserByEmail; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByPhoneNumber: typeof Wrapper.getUserByPhoneNumber; export declare let listCodesByDeviceId: typeof Wrapper.listCodesByDeviceId; export declare let listCodesByEmail: typeof Wrapper.listCodesByEmail; export declare let listCodesByPhoneNumber: typeof Wrapper.listCodesByPhoneNumber; @@ -200,6 +202,6 @@ export declare let revokeAllCodes: typeof Wrapper.revokeAllCodes; export declare let revokeCode: typeof Wrapper.revokeCode; export declare let createMagicLink: typeof Wrapper.createMagicLink; export declare let signInUp: typeof Wrapper.signInUp; -export type { RecipeInterface, User, APIOptions, APIInterface }; +export type { RecipeInterface, APIOptions, APIInterface }; export declare let sendEmail: typeof Wrapper.sendEmail; export declare let sendSms: typeof Wrapper.sendSms; diff --git a/lib/build/recipe/passwordless/index.js b/lib/build/recipe/passwordless/index.js index 9b30c18fa..fc5b789da 100644 --- a/lib/build/recipe/passwordless/index.js +++ b/lib/build/recipe/passwordless/index.js @@ -13,80 +13,39 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendSms = exports.sendEmail = exports.signInUp = exports.createMagicLink = exports.revokeCode = exports.revokeAllCodes = exports.updateUser = exports.createNewCodeForDevice = exports.listCodesByPreAuthSessionId = exports.listCodesByPhoneNumber = exports.listCodesByEmail = exports.listCodesByDeviceId = exports.getUserByPhoneNumber = exports.getUserById = exports.getUserByEmail = exports.consumeCode = exports.createCode = exports.Error = exports.init = void 0; +exports.sendSms = exports.sendEmail = exports.signInUp = exports.createMagicLink = exports.revokeCode = exports.revokeAllCodes = exports.updateUser = exports.createNewCodeForDevice = exports.listCodesByPreAuthSessionId = exports.listCodesByPhoneNumber = exports.listCodesByEmail = exports.listCodesByDeviceId = exports.consumeCode = exports.createCode = exports.Error = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); class Wrapper { static createCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createCode(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createCode( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static createNewCodeForDevice(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createNewCodeForDevice(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static consumeCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.consumeCode(Object.assign({ userContext: {} }, input)); - } - static getUserById(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getUserById(Object.assign({ userContext: {} }, input)); - } - static getUserByEmail(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getUserByEmail( - Object.assign(Object.assign({ userContext: {} }, input), { tenantId: input.tenantId }) - ); - } - static getUserByPhoneNumber(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getUserByPhoneNumber( - Object.assign(Object.assign({ userContext: {} }, input), { tenantId: input.tenantId }) - ); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static updateUser(input) { return recipe_1.default @@ -94,54 +53,84 @@ class Wrapper { .recipeInterfaceImpl.updateUser(Object.assign({ userContext: {} }, input)); } static revokeAllCodes(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeAllCodes(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static revokeCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeCode(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static listCodesByEmail(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByEmail(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static listCodesByPhoneNumber(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByPhoneNumber(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static listCodesByDeviceId(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByDeviceId(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static listCodesByPreAuthSessionId(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByPreAuthSessionId(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static createMagicLink(input) { - return recipe_1.default.getInstanceOrThrowError().createMagicLink(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().createMagicLink( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static signInUp(input) { - return recipe_1.default.getInstanceOrThrowError().signInUp(Object.assign({ userContext: {} }, input)); - } - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .emailDelivery.ingredientInterfaceImpl.sendEmail(Object.assign({ userContext: {} }, input)); - }); - } - static sendSms(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .smsDelivery.ingredientInterfaceImpl.sendSms(Object.assign({ userContext: {} }, input)); - }); + var _a; + return recipe_1.default.getInstanceOrThrowError().signInUp( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); + } + static async sendEmail(input) { + var _a; + return await recipe_1.default.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); + } + static async sendSms(input) { + var _a; + return await recipe_1.default.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } } exports.default = Wrapper; @@ -151,9 +140,6 @@ exports.init = Wrapper.init; exports.Error = Wrapper.Error; exports.createCode = Wrapper.createCode; exports.consumeCode = Wrapper.consumeCode; -exports.getUserByEmail = Wrapper.getUserByEmail; -exports.getUserById = Wrapper.getUserById; -exports.getUserByPhoneNumber = Wrapper.getUserByPhoneNumber; exports.listCodesByDeviceId = Wrapper.listCodesByDeviceId; exports.listCodesByEmail = Wrapper.listCodesByEmail; exports.listCodesByPhoneNumber = Wrapper.listCodesByPhoneNumber; diff --git a/lib/build/recipe/passwordless/recipe.d.ts b/lib/build/recipe/passwordless/recipe.d.ts index 51deab078..4019d514a 100644 --- a/lib/build/recipe/passwordless/recipe.d.ts +++ b/lib/build/recipe/passwordless/recipe.d.ts @@ -4,11 +4,10 @@ import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from ". import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; import STError from "./error"; import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypePasswordlessEmailDeliveryInput, TypePasswordlessSmsDeliveryInput } from "./types"; import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; export default class Recipe extends RecipeModule { private static instance; static RECIPE_ID: string; @@ -71,8 +70,8 @@ export default class Recipe extends RecipeModule { } ) => Promise<{ status: string; - createdNewUser: boolean; - user: import("./types").User; + createdNewRecipeUser: boolean; + recipeUserId: import("../..").RecipeUserId; + user: import("../../types").User; }>; - getEmailForUserId: GetEmailForUserIdFunc; } diff --git a/lib/build/recipe/passwordless/recipe.js b/lib/build/recipe/passwordless/recipe.js index fff815b69..28828fb58 100644 --- a/lib/build/recipe/passwordless/recipe.js +++ b/lib/build/recipe/passwordless/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -54,7 +23,6 @@ const recipeModule_1 = __importDefault(require("../../recipeModule")); const error_1 = __importDefault(require("./error")); const utils_1 = require("./utils"); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const recipe_1 = __importDefault(require("../emailverification/recipe")); const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); const implementation_1 = __importDefault(require("./api/implementation")); const querier_1 = require("../../querier"); @@ -67,7 +35,6 @@ const resendCode_1 = __importDefault(require("./api/resendCode")); const constants_1 = require("./constants"); const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); const smsdelivery_1 = __importDefault(require("../../ingredients/smsdelivery")); -const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { super(recipeId, appInfo); @@ -106,35 +73,33 @@ class Recipe extends recipeModule_1.default { }, ]; }; - this.handleAPIRequest = (id, tenantId, req, res, _, __, userContext) => - __awaiter(this, void 0, void 0, function* () { - const options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - smsDelivery: this.smsDelivery, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.CONSUME_CODE_API) { - return yield consumeCode_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.CREATE_CODE_API) { - return yield createCode_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.DOES_EMAIL_EXIST_API) { - return yield emailExists_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.DOES_PHONE_NUMBER_EXIST_API) { - return yield phoneNumberExists_1.default(this.apiImpl, tenantId, options, userContext); - } else { - return yield resendCode_1.default(this.apiImpl, tenantId, options, userContext); - } - }); - this.handleError = (err, _, __) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); + this.handleAPIRequest = async (id, tenantId, req, res, _, __, userContext) => { + const options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + emailDelivery: this.emailDelivery, + smsDelivery: this.smsDelivery, + appInfo: this.getAppInfo(), + }; + if (id === constants_1.CONSUME_CODE_API) { + return await consumeCode_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.CREATE_CODE_API) { + return await createCode_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.DOES_EMAIL_EXIST_API) { + return await emailExists_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.DOES_PHONE_NUMBER_EXIST_API) { + return await phoneNumberExists_1.default(this.apiImpl, tenantId, options, userContext); + } else { + return await resendCode_1.default(this.apiImpl, tenantId, options, userContext); + } + }; + this.handleError = async (err, _, __) => { + throw err; + }; this.getAllCORSHeaders = () => { return []; }; @@ -142,102 +107,82 @@ class Recipe extends recipeModule_1.default { return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; // helper functions below... - this.createMagicLink = (input) => - __awaiter(this, void 0, void 0, function* () { - let userInputCode = - this.config.getCustomUserInputCode !== undefined - ? yield this.config.getCustomUserInputCode(input.tenantId, input.userContext) - : undefined; - const codeInfo = yield this.recipeInterfaceImpl.createCode( - "email" in input - ? { - email: input.email, - userInputCode, - tenantId: input.tenantId, - userContext: input.userContext, - } - : { - phoneNumber: input.phoneNumber, - userInputCode, - tenantId: input.tenantId, - userContext: input.userContext, - } - ); - const appInfo = this.getAppInfo(); - let magicLink = - appInfo.websiteDomain.getAsStringDangerous() + - appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - this.getRecipeId() + - "&preAuthSessionId=" + - codeInfo.preAuthSessionId + - "&tenantId=" + - input.tenantId + - "#" + - codeInfo.linkCode; - return magicLink; - }); - this.signInUp = (input) => - __awaiter(this, void 0, void 0, function* () { - let codeInfo = yield this.recipeInterfaceImpl.createCode( - "email" in input - ? { - email: input.email, - tenantId: input.tenantId, - userContext: input.userContext, - } - : { - phoneNumber: input.phoneNumber, - tenantId: input.tenantId, - userContext: input.userContext, - } - ); - let consumeCodeResponse = yield this.recipeInterfaceImpl.consumeCode( - this.config.flowType === "MAGIC_LINK" - ? { - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - tenantId: input.tenantId, - userContext: input.userContext, - } - : { - preAuthSessionId: codeInfo.preAuthSessionId, - deviceId: codeInfo.deviceId, - userInputCode: codeInfo.userInputCode, - tenantId: input.tenantId, - userContext: input.userContext, - } - ); - if (consumeCodeResponse.status === "OK") { - return { - status: "OK", - createdNewUser: consumeCodeResponse.createdNewUser, - user: consumeCodeResponse.user, - }; - } else { - throw new Error("Failed to create user. Please retry"); - } - }); - // helper functions... - this.getEmailForUserId = (userId, userContext) => - __awaiter(this, void 0, void 0, function* () { - let userInfo = yield this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - if (userInfo.email !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "EMAIL_DOES_NOT_EXIST_ERROR", - }; - } + this.createMagicLink = async (input) => { + let userInputCode = + this.config.getCustomUserInputCode !== undefined + ? await this.config.getCustomUserInputCode(input.tenantId, input.userContext) + : undefined; + const codeInfo = await this.recipeInterfaceImpl.createCode( + "email" in input + ? { + email: input.email, + userInputCode, + tenantId: input.tenantId, + userContext: input.userContext, + } + : { + phoneNumber: input.phoneNumber, + userInputCode, + tenantId: input.tenantId, + userContext: input.userContext, + } + ); + const appInfo = this.getAppInfo(); + let magicLink = + appInfo.websiteDomain.getAsStringDangerous() + + appInfo.websiteBasePath.getAsStringDangerous() + + "/verify" + + "?rid=" + + this.getRecipeId() + + "&preAuthSessionId=" + + codeInfo.preAuthSessionId + + "&tenantId=" + + input.tenantId + + "#" + + codeInfo.linkCode; + return magicLink; + }; + this.signInUp = async (input) => { + let codeInfo = await this.recipeInterfaceImpl.createCode( + "email" in input + ? { + email: input.email, + tenantId: input.tenantId, + userContext: input.userContext, + } + : { + phoneNumber: input.phoneNumber, + tenantId: input.tenantId, + userContext: input.userContext, + } + ); + let consumeCodeResponse = await this.recipeInterfaceImpl.consumeCode( + this.config.flowType === "MAGIC_LINK" + ? { + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: codeInfo.linkCode, + tenantId: input.tenantId, + userContext: input.userContext, + } + : { + preAuthSessionId: codeInfo.preAuthSessionId, + deviceId: codeInfo.deviceId, + userInputCode: codeInfo.userInputCode, + tenantId: input.tenantId, + userContext: input.userContext, + } + ); + if (consumeCodeResponse.status === "OK") { return { - status: "UNKNOWN_USER_ID_ERROR", + status: "OK", + createdNewRecipeUser: consumeCodeResponse.createdNewRecipeUser, + recipeUserId: consumeCodeResponse.recipeUserId, + user: consumeCodeResponse.user, }; - }); + } else { + throw new Error("Failed to create user. Please retry"); + } + }; this.isInServerlessEnv = isInServerlessEnv; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); { @@ -262,12 +207,6 @@ class Recipe extends recipeModule_1.default { ingredients.smsDelivery === undefined ? new smsdelivery_1.default(this.config.getSmsDeliveryConfig()) : ingredients.smsDelivery; - postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = recipe_1.default.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); } static getInstanceOrThrowError() { if (Recipe.instance !== undefined) { diff --git a/lib/build/recipe/passwordless/recipeImplementation.js b/lib/build/recipe/passwordless/recipeImplementation.js index c6b8fd2bc..85ca9c5b3 100644 --- a/lib/build/recipe/passwordless/recipeImplementation.js +++ b/lib/build/recipe/passwordless/recipeImplementation.js @@ -1,177 +1,155 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +const recipe_1 = __importDefault(require("../accountlinking/recipe")); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const logger_1 = require("../../logger"); +const user_1 = require("../../user"); +const __1 = require("../.."); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); function getRecipeInterface(querier) { function copyAndRemoveUserContextAndTenantId(input) { let result = Object.assign({}, input); delete result.userContext; delete result.tenantId; + if (result.recipeUserId !== undefined && result.recipeUserId.getAsString !== undefined) { + result.recipeUserId = result.recipeUserId.getAsString(); + } return result; } return { - consumeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code/consume`), - copyAndRemoveUserContextAndTenantId(input) - ); - return response; - }); - }, - createCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code`), - copyAndRemoveUserContextAndTenantId(input) - ); + consumeCode: async function (input) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code/consume`), + copyAndRemoveUserContextAndTenantId(input) + ); + if (response.status !== "OK") { return response; + } + logger_1.logDebugMessage("Passwordless.consumeCode code consumed OK"); + response.user = new user_1.User(response.user); + response.recipeUserId = new recipeUserId_1.default(response.recipeUserId); + const loginMethod = response.user.loginMethods.find( + (lm) => lm.recipeUserId.getAsString() === response.recipeUserId.getAsString() + ); + if (loginMethod === undefined) { + throw new Error("This should never happen: login method not found after signin"); + } + if (!response.createdNewUser) { + // Unlike in the sign up scenario, we do not do account linking here + // cause we do not want sign in to change the potentially user ID of a user + // due to linking when this function is called by the dev in their API. + // If we did account linking + // then we would have to ask the dev to also change the session + // in such API calls. + // In the case of sign up, since we are creating a new user, it's fine + // to link there since there is no user id change really from the dev's + // point of view who is calling the sign up recipe function. + return { + status: "OK", + createdNewRecipeUser: response.createdNewUser, + user: response.user, + recipeUserId: response.recipeUserId, + }; + } + let updatedUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId: input.tenantId, + user: response.user, + userContext: input.userContext, }); + if (updatedUser === undefined) { + throw new Error("Should never come here."); + } + return { + status: "OK", + createdNewRecipeUser: response.createdNewUser, + user: updatedUser, + recipeUserId: response.recipeUserId, + }; }, - createNewCodeForDevice: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code`), - copyAndRemoveUserContextAndTenantId(input) - ); - return response; - }); + createCode: async function (input) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code`), + copyAndRemoveUserContextAndTenantId(input) + ); + return response; }, - getUserByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/user`), - copyAndRemoveUserContextAndTenantId(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }); + createNewCodeForDevice: async function (input) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code`), + copyAndRemoveUserContextAndTenantId(input) + ); + return response; }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/user"), - copyAndRemoveUserContextAndTenantId(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }); + listCodesByDeviceId: async function (input) { + let response = await querier.sendGetRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes`), + copyAndRemoveUserContextAndTenantId(input) + ); + return response.devices.length === 1 ? response.devices[0] : undefined; }, - getUserByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/user`), - copyAndRemoveUserContextAndTenantId(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }); + listCodesByEmail: async function (input) { + let response = await querier.sendGetRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes`), + copyAndRemoveUserContextAndTenantId(input) + ); + return response.devices; }, - listCodesByDeviceId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes`), - copyAndRemoveUserContextAndTenantId(input) - ); - return response.devices.length === 1 ? response.devices[0] : undefined; - }); + listCodesByPhoneNumber: async function (input) { + let response = await querier.sendGetRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes`), + copyAndRemoveUserContextAndTenantId(input) + ); + return response.devices; }, - listCodesByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes`), - copyAndRemoveUserContextAndTenantId(input) - ); - return response.devices; - }); + listCodesByPreAuthSessionId: async function (input) { + let response = await querier.sendGetRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes`), + copyAndRemoveUserContextAndTenantId(input) + ); + return response.devices.length === 1 ? response.devices[0] : undefined; }, - listCodesByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes`), - copyAndRemoveUserContextAndTenantId(input) - ); - return response.devices; - }); + revokeAllCodes: async function (input) { + await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes/remove`), + copyAndRemoveUserContextAndTenantId(input) + ); + return { + status: "OK", + }; }, - listCodesByPreAuthSessionId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes`), - copyAndRemoveUserContextAndTenantId(input) - ); - return response.devices.length === 1 ? response.devices[0] : undefined; - }); + revokeCode: async function (input) { + await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code/remove`), + copyAndRemoveUserContextAndTenantId(input) + ); + return { status: "OK" }; }, - revokeAllCodes: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/codes/remove`), - copyAndRemoveUserContextAndTenantId(input) - ); + updateUser: async function (input) { + let response = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/user`), + copyAndRemoveUserContextAndTenantId(input) + ); + if (response.status !== "OK") { + return response; + } + const user = await __1.getUser(input.recipeUserId.getAsString(), input.userContext); + if (user === undefined) { + // This means that the user was deleted between the put and get requests return { - status: "OK", + status: "UNKNOWN_USER_ID_ERROR", }; + } + await recipe_1.default.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user, + recipeUserId: input.recipeUserId, + userContext: input.userContext, }); - }, - revokeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code/remove`), - copyAndRemoveUserContextAndTenantId(input) - ); - return { status: "OK" }; - }); - }, - updateUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPutRequest( - new normalisedURLPath_1.default("/recipe/user"), - copyAndRemoveUserContextAndTenantId(input) - ); - return response; - }); + return response; }, }; } diff --git a/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.js b/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.js index ac3ebf454..b2f01e761 100644 --- a/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,90 +8,87 @@ Object.defineProperty(exports, "__esModule", { value: true }); const supertokens_1 = require("../../../../../ingredients/smsdelivery/services/supertokens"); const supertokens_2 = __importDefault(require("../../../../../supertokens")); const utils_1 = require("../../../../../utils"); -function createAndSendSmsUsingSupertokensService(input) { - return __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_2.default.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - const result = yield utils_1.postWithFetch( - supertokens_1.SUPERTOKENS_SMS_SERVICE_URL, - { - "api-version": "0", - "content-type": "application/json; charset=utf-8", - }, - { - smsInput: { - appName, - type: "PASSWORDLESS_LOGIN", - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - }, +async function createAndSendSmsUsingSupertokensService(input) { + let supertokens = supertokens_2.default.getInstanceOrThrowError(); + let appName = supertokens.appInfo.appName; + const result = await utils_1.postWithFetch( + supertokens_1.SUPERTOKENS_SMS_SERVICE_URL, + { + "api-version": "0", + "content-type": "application/json; charset=utf-8", + }, + { + smsInput: { + appName, + type: "PASSWORDLESS_LOGIN", + phoneNumber: input.phoneNumber, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + codeLifetime: input.codeLifetime, }, - { - successLog: `Passwordless login SMS sent to ${input.phoneNumber}`, - errorLogHeader: "Error sending passwordless login SMS", - } - ); - if ("error" in result) { - throw result.error; + }, + { + successLog: `Passwordless login SMS sent to ${input.phoneNumber}`, + errorLogHeader: "Error sending passwordless login SMS", } - if (result.resp.status >= 400) { - if (result.resp.status !== 429) { - if (result.resp.body.err) { - /** - * if the error is thrown from API, the response object - * will be of type `{err: string}` - */ - throw new Error(result.resp.body.err); - } else { - throw new Error(`Request failed with status code ${result.resp.status}`); - } - } else { - /** - * if we do console.log(`SMS content: ${input}`); - * Output would be: - * SMS content: [object Object] - */ + ); + if ("error" in result) { + throw result.error; + } + if (result.resp.status >= 400) { + if (result.resp.status !== 429) { + if (result.resp.body.err) { /** - * JSON.stringify takes 3 inputs - * - value: usually an object or array, to be converted - * - replacer: An array of strings and numbers that acts - * as an approved list for selecting the object - * properties that will be stringified - * - space: Adds indentation, white space, and line break characters - * to the return-value JSON text to make it easier to read - * - * console.log(JSON.stringify({"a": 1, "b": 2})) - * Output: - * {"a":1,"b":2} - * - * console.log(JSON.stringify({"a": 1, "b": 2}, null, 2)) - * Output: - * { - * "a": 1, - * "b": 2 - * } + * if the error is thrown from API, the response object + * will be of type `{err: string}` */ - console.log( - "Free daily SMS quota reached. If you want to use SuperTokens to send SMS, please sign up on supertokens.com to get your SMS API key, else you can also define your own method by overriding the service. For now, we are logging it below:" - ); - console.log(`\nSMS content: ${JSON.stringify(input, null, 2)}`); + throw new Error(result.resp.body.err); + } else { + throw new Error(`Request failed with status code ${result.resp.status}`); } + } else { + /** + * if we do console.log(`SMS content: ${input}`); + * Output would be: + * SMS content: [object Object] + */ + /** + * JSON.stringify takes 3 inputs + * - value: usually an object or array, to be converted + * - replacer: An array of strings and numbers that acts + * as an approved list for selecting the object + * properties that will be stringified + * - space: Adds indentation, white space, and line break characters + * to the return-value JSON text to make it easier to read + * + * console.log(JSON.stringify({"a": 1, "b": 2})) + * Output: + * {"a":1,"b":2} + * + * console.log(JSON.stringify({"a": 1, "b": 2}, null, 2)) + * Output: + * { + * "a": 1, + * "b": 2 + * } + */ + console.log( + "Free daily SMS quota reached. If you want to use SuperTokens to send SMS, please sign up on supertokens.com to get your SMS API key, else you can also define your own method by overriding the service. For now, we are logging it below:" + ); + console.log(`\nSMS content: ${JSON.stringify(input, null, 2)}`); } - }); + } } class BackwardCompatibilityService { constructor() { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - yield createAndSendSmsUsingSupertokensService({ - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - }); + this.sendSms = async (input) => { + await createAndSendSmsUsingSupertokensService({ + phoneNumber: input.phoneNumber, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + codeLifetime: input.codeLifetime, }); + }; } } exports.default = BackwardCompatibilityService; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.js b/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.js index 461417d66..da2224b26 100644 --- a/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.js +++ b/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -55,39 +24,38 @@ const supertokens_2 = __importDefault(require("../../../../../supertokens")); const utils_1 = require("../../../../../utils"); class SupertokensService { constructor(apiKey) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_2.default.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - const res = yield utils_1.postWithFetch( - supertokens_1.SUPERTOKENS_SMS_SERVICE_URL, - { - "api-version": "0", - "content-type": "application/json; charset=utf-8", - }, - { - apiKey: this.apiKey, - smsInput: { - type: input.type, - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - appName, - }, + this.sendSms = async (input) => { + let supertokens = supertokens_2.default.getInstanceOrThrowError(); + let appName = supertokens.appInfo.appName; + const res = await utils_1.postWithFetch( + supertokens_1.SUPERTOKENS_SMS_SERVICE_URL, + { + "api-version": "0", + "content-type": "application/json; charset=utf-8", + }, + { + apiKey: this.apiKey, + smsInput: { + type: input.type, + phoneNumber: input.phoneNumber, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + codeLifetime: input.codeLifetime, + appName, }, - { - successLog: `Passwordless login SMS sent to ${input.phoneNumber}`, - errorLogHeader: "Error sending SMS", - } - ); - if ("error" in res) { - throw res.error; - } - if (res.resp.status >= 400) { - throw new Error(res.resp.body); + }, + { + successLog: `Passwordless login SMS sent to ${input.phoneNumber}`, + errorLogHeader: "Error sending SMS", } - }); + ); + if ("error" in res) { + throw res.error; + } + if (res.resp.status >= 400) { + throw new Error(res.resp.body); + } + }; this.apiKey = apiKey; } } diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.js b/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.js index 96a763dc0..21dabe0bc 100644 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.js +++ b/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,25 +25,24 @@ const supertokens_js_override_1 = __importDefault(require("supertokens-js-overri const serviceImplementation_1 = require("./serviceImplementation"); class TwilioService { constructor(config) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - let content = yield this.serviceImpl.getContent(input); - if ("from" in this.config.twilioSettings) { - yield this.serviceImpl.sendRawSms( - Object.assign(Object.assign({}, content), { - userContext: input.userContext, - from: this.config.twilioSettings.from, - }) - ); - } else { - yield this.serviceImpl.sendRawSms( - Object.assign(Object.assign({}, content), { - userContext: input.userContext, - messagingServiceSid: this.config.twilioSettings.messagingServiceSid, - }) - ); - } - }); + this.sendSms = async (input) => { + let content = await this.serviceImpl.getContent(input); + if ("from" in this.config.twilioSettings) { + await this.serviceImpl.sendRawSms( + Object.assign(Object.assign({}, content), { + userContext: input.userContext, + from: this.config.twilioSettings.from, + }) + ); + } else { + await this.serviceImpl.sendRawSms( + Object.assign(Object.assign({}, content), { + userContext: input.userContext, + messagingServiceSid: this.config.twilioSettings.messagingServiceSid, + }) + ); + } + }; this.config = twilio_1.normaliseUserInputConfig(config); const twilioClient = twilio_2.default( config.twilioSettings.accountSid, diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.js b/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.js index f41680526..a9a078029 100644 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.js +++ b/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -54,27 +23,23 @@ exports.getServiceImplementation = void 0; const passwordlessLogin_1 = __importDefault(require("./passwordlessLogin")); function getServiceImplementation(twilioClient) { return { - sendRawSms: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if ("from" in input) { - yield twilioClient.messages.create({ - to: input.toPhoneNumber, - body: input.body, - from: input.from, - }); - } else { - yield twilioClient.messages.create({ - to: input.toPhoneNumber, - body: input.body, - messagingServiceSid: input.messagingServiceSid, - }); - } - }); + sendRawSms: async function (input) { + if ("from" in input) { + await twilioClient.messages.create({ + to: input.toPhoneNumber, + body: input.body, + from: input.from, + }); + } else { + await twilioClient.messages.create({ + to: input.toPhoneNumber, + body: input.body, + messagingServiceSid: input.messagingServiceSid, + }); + } }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return passwordlessLogin_1.default(input); - }); + getContent: async function (input) { + return passwordlessLogin_1.default(input); }, }; } diff --git a/lib/build/recipe/passwordless/types.d.ts b/lib/build/recipe/passwordless/types.d.ts index 8c0225ff3..7c6c74213 100644 --- a/lib/build/recipe/passwordless/types.d.ts +++ b/lib/build/recipe/passwordless/types.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; import { @@ -12,14 +12,8 @@ import { TypeInputWithService as SmsDeliveryTypeInputWithService, } from "../../ingredients/smsdelivery/types"; import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; -export declare type User = { - id: string; - email?: string; - phoneNumber?: string; - timeJoined: number; - tenantIds: string[]; -}; +import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export declare type TypeInput = ( | { contactMethod: "PHONE"; @@ -148,8 +142,9 @@ export declare type RecipeInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; + recipeUserId: RecipeUserId; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -160,21 +155,24 @@ export declare type RecipeInterface = { status: "RESTART_FLOW_ERROR"; } >; - getUserById: (input: { userId: string; userContext: any }) => Promise; - getUserByEmail: (input: { email: string; tenantId: string; userContext: any }) => Promise; - getUserByPhoneNumber: (input: { - phoneNumber: string; - tenantId: string; - userContext: any; - }) => Promise; updateUser: (input: { - userId: string; + recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; + }) => Promise< + | { + status: + | "OK" + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; revokeAllCodes: ( input: | { @@ -257,6 +255,10 @@ export declare type APIInterface = { preAuthSessionId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >; resendCodePOST?: ( @@ -293,7 +295,7 @@ export declare type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; session: SessionContainerInterface; } @@ -306,6 +308,10 @@ export declare type APIInterface = { | { status: "RESTART_FLOW_ERROR"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } >; emailExistsGET?: (input: { email: string; diff --git a/lib/build/recipe/session/accessToken.d.ts b/lib/build/recipe/session/accessToken.d.ts index b9412d0b0..8b0685ec8 100644 --- a/lib/build/recipe/session/accessToken.d.ts +++ b/lib/build/recipe/session/accessToken.d.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { ParsedJWTInfo } from "./jwt"; import * as jose from "jose"; +import RecipeUserId from "../../recipeUserId"; export declare function getInfoFromAccessToken( jwtInfo: ParsedJWTInfo, jwks: jose.JWTVerifyGetKey, @@ -8,6 +9,7 @@ export declare function getInfoFromAccessToken( ): Promise<{ sessionHandle: string; userId: string; + recipeUserId: RecipeUserId; refreshTokenHash1: string; parentRefreshTokenHash1: string | undefined; userData: any; diff --git a/lib/build/recipe/session/accessToken.js b/lib/build/recipe/session/accessToken.js index 90ac73257..3f0efd2cf 100644 --- a/lib/build/recipe/session/accessToken.js +++ b/lib/build/recipe/session/accessToken.js @@ -49,37 +49,6 @@ var __importStar = __setModuleDefault(result, mod); return result; }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __asyncValues = (this && this.__asyncValues) || function (o) { @@ -122,116 +91,142 @@ exports.sanitizeNumberInput = exports.validateAccessTokenStructure = exports.get const error_1 = __importDefault(require("./error")); const jose = __importStar(require("jose")); const processState_1 = require("../../processState"); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const logger_1 = require("../../logger"); const constants_1 = require("../multitenancy/constants"); -function getInfoFromAccessToken(jwtInfo, jwks, doAntiCsrfCheck) { +async function getInfoFromAccessToken(jwtInfo, jwks, doAntiCsrfCheck) { var e_1, _a; - return __awaiter(this, void 0, void 0, function* () { + var _b; + try { + // From the library examples + let payload = undefined; try { - // From the library examples - let payload = undefined; - try { - payload = (yield jose.jwtVerify(jwtInfo.rawTokenString, jwks)).payload; - } catch (error) { - // We only want to opt-into this for V2 access tokens - if ( - jwtInfo.version === 2 && - (error === null || error === void 0 ? void 0 : error.code) === "ERR_JWKS_MULTIPLE_MATCHING_KEYS" - ) { - processState_1.ProcessState.getInstance().addState( - processState_1.PROCESS_STATE.MULTI_JWKS_VALIDATION - ); - try { - // We are trying to validate the token with each key. - // Since the kid is missing from v2 tokens, this basically means we try all keys present in the cache. - for ( - var error_2 = __asyncValues(error), error_2_1; - (error_2_1 = yield error_2.next()), !error_2_1.done; + payload = (await jose.jwtVerify(jwtInfo.rawTokenString, jwks)).payload; + } catch (error) { + // We only want to opt-into this for V2 access tokens + if ( + jwtInfo.version === 2 && + (error === null || error === void 0 ? void 0 : error.code) === "ERR_JWKS_MULTIPLE_MATCHING_KEYS" + ) { + processState_1.ProcessState.getInstance().addState(processState_1.PROCESS_STATE.MULTI_JWKS_VALIDATION); + try { + // We are trying to validate the token with each key. + // Since the kid is missing from v2 tokens, this basically means we try all keys present in the cache. + for ( + var error_2 = __asyncValues(error), error_2_1; + (error_2_1 = await error_2.next()), !error_2_1.done; - ) { - const publicKey = error_2_1.value; - try { - payload = (yield jose.jwtVerify(jwtInfo.rawTokenString, publicKey)).payload; - break; - } catch (innerError) { - if ( - (innerError === null || innerError === void 0 ? void 0 : innerError.code) === - "ERR_JWS_SIGNATURE_VERIFICATION_FAILED" - ) { - continue; - } - throw innerError; - } - } - } catch (e_1_1) { - e_1 = { error: e_1_1 }; - } finally { + ) { + const publicKey = error_2_1.value; try { - if (error_2_1 && !error_2_1.done && (_a = error_2.return)) yield _a.call(error_2); - } finally { - if (e_1) throw e_1.error; + payload = (await jose.jwtVerify(jwtInfo.rawTokenString, publicKey)).payload; + break; + } catch (innerError) { + if ( + (innerError === null || innerError === void 0 ? void 0 : innerError.code) === + "ERR_JWS_SIGNATURE_VERIFICATION_FAILED" + ) { + continue; + } + throw innerError; } } - if (payload === undefined) { - throw new jose.errors.JWSSignatureVerificationFailed(); + } catch (e_1_1) { + e_1 = { error: e_1_1 }; + } finally { + try { + if (error_2_1 && !error_2_1.done && (_a = error_2.return)) await _a.call(error_2); + } finally { + if (e_1) throw e_1.error; } - } else { - throw error; } + if (payload === undefined) { + throw new jose.errors.JWSSignatureVerificationFailed(); + } + } else { + throw error; } - // This should be called before this function, but the check is very quick, so we can also do them here - validateAccessTokenStructure(payload, jwtInfo.version); - // We can mark these as defined (the ! after the calls), since validateAccessTokenPayload checks this - let userId = jwtInfo.version === 2 ? sanitizeStringInput(payload.userId) : sanitizeStringInput(payload.sub); - let expiryTime = - jwtInfo.version === 2 - ? sanitizeNumberInput(payload.expiryTime) - : sanitizeNumberInput(payload.exp) * 1000; - let timeCreated = - jwtInfo.version === 2 - ? sanitizeNumberInput(payload.timeCreated) - : sanitizeNumberInput(payload.iat) * 1000; - let userData = jwtInfo.version === 2 ? payload.userData : payload; - let sessionHandle = sanitizeStringInput(payload.sessionHandle); - let refreshTokenHash1 = sanitizeStringInput(payload.refreshTokenHash1); - let parentRefreshTokenHash1 = sanitizeStringInput(payload.parentRefreshTokenHash1); - let antiCsrfToken = sanitizeStringInput(payload.antiCsrfToken); - let tenantId = constants_1.DEFAULT_TENANT_ID; - if (jwtInfo.version >= 4) { - tenantId = sanitizeStringInput(payload.tId); - } - if (antiCsrfToken === undefined && doAntiCsrfCheck) { - throw Error("Access token does not contain the anti-csrf token."); - } - if (expiryTime < Date.now()) { - throw Error("Access token expired"); - } - return { - sessionHandle, - userId, - refreshTokenHash1, - parentRefreshTokenHash1, - userData, - antiCsrfToken, - expiryTime, - timeCreated, - tenantId, - }; - } catch (err) { - logger_1.logDebugMessage( - "getInfoFromAccessToken: Returning TRY_REFRESH_TOKEN because access token validation failed - " + - err.message - ); - throw new error_1.default({ - message: "Failed to verify access token", - type: error_1.default.TRY_REFRESH_TOKEN, - }); } - }); + // This should be called before this function, but the check is very quick, so we can also do them here + validateAccessTokenStructure(payload, jwtInfo.version); + // We can mark these as defined (the ! after the calls), since validateAccessTokenPayload checks this + let userId = jwtInfo.version === 2 ? sanitizeStringInput(payload.userId) : sanitizeStringInput(payload.sub); + let expiryTime = + jwtInfo.version === 2 ? sanitizeNumberInput(payload.expiryTime) : sanitizeNumberInput(payload.exp) * 1000; + let timeCreated = + jwtInfo.version === 2 ? sanitizeNumberInput(payload.timeCreated) : sanitizeNumberInput(payload.iat) * 1000; + let userData = jwtInfo.version === 2 ? payload.userData : payload; + let sessionHandle = sanitizeStringInput(payload.sessionHandle); + // we use ?? below cause recipeUserId may be undefined for JWTs that are of an older version. + let recipeUserId = new recipeUserId_1.default( + (_b = sanitizeStringInput(payload.rsub)) !== null && _b !== void 0 ? _b : userId + ); + let refreshTokenHash1 = sanitizeStringInput(payload.refreshTokenHash1); + let parentRefreshTokenHash1 = sanitizeStringInput(payload.parentRefreshTokenHash1); + let antiCsrfToken = sanitizeStringInput(payload.antiCsrfToken); + let tenantId = constants_1.DEFAULT_TENANT_ID; + if (jwtInfo.version >= 4) { + tenantId = sanitizeStringInput(payload.tId); + } + if (antiCsrfToken === undefined && doAntiCsrfCheck) { + throw Error("Access token does not contain the anti-csrf token."); + } + if (expiryTime < Date.now()) { + throw Error("Access token expired"); + } + return { + sessionHandle, + userId, + refreshTokenHash1, + parentRefreshTokenHash1, + userData, + antiCsrfToken, + expiryTime, + timeCreated, + recipeUserId, + tenantId, + }; + } catch (err) { + logger_1.logDebugMessage( + "getInfoFromAccessToken: Returning TRY_REFRESH_TOKEN because access token validation failed - " + + err.message + ); + throw new error_1.default({ + message: "Failed to verify access token", + type: error_1.default.TRY_REFRESH_TOKEN, + }); + } } exports.getInfoFromAccessToken = getInfoFromAccessToken; function validateAccessTokenStructure(payload, version) { - if (version >= 3) { + if (version >= 5) { + if ( + typeof payload.sub !== "string" || + typeof payload.exp !== "number" || + typeof payload.iat !== "number" || + typeof payload.sessionHandle !== "string" || + typeof payload.refreshTokenHash1 !== "string" || + typeof payload.rsub !== "string" + ) { + logger_1.logDebugMessage("validateAccessTokenStructure: Access token is using version >= 4"); + // The error message below will be logged by the error handler that translates this into a TRY_REFRESH_TOKEN_ERROR + // it would come here if we change the structure of the JWT. + throw Error("Access token does not contain all the information. Maybe the structure has changed?"); + } + } else if (version >= 4) { + if ( + typeof payload.sub !== "string" || + typeof payload.exp !== "number" || + typeof payload.iat !== "number" || + typeof payload.sessionHandle !== "string" || + typeof payload.refreshTokenHash1 !== "string" + ) { + logger_1.logDebugMessage("validateAccessTokenStructure: Access token is using version >= 4"); + // The error message below will be logged by the error handler that translates this into a TRY_REFRESH_TOKEN_ERROR + // it would come here if we change the structure of the JWT. + throw Error("Access token does not contain all the information. Maybe the structure has changed?"); + } + } else if (version >= 3) { if ( typeof payload.sub !== "string" || typeof payload.exp !== "number" || diff --git a/lib/build/recipe/session/api/implementation.js b/lib/build/recipe/session/api/implementation.js index 7c8a8b26c..76dc09204 100644 --- a/lib/build/recipe/session/api/implementation.js +++ b/lib/build/recipe/session/api/implementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -41,8 +10,23 @@ const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath" const sessionRequestFunctions_1 = require("../sessionRequestFunctions"); function getAPIInterface() { return { - refreshPOST: function ({ options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { + refreshPOST: async function ({ options, userContext }) { + return sessionRequestFunctions_1.refreshSessionInRequest({ + req: options.req, + res: options.res, + userContext, + config: options.config, + recipeInterfaceImpl: options.recipeImplementation, + }); + }, + verifySession: async function ({ verifySessionOptions, options, userContext }) { + let method = utils_1.normaliseHttpMethod(options.req.getMethod()); + if (method === "options" || method === "trace") { + return undefined; + } + let incomingPath = new normalisedURLPath_1.default(options.req.getOriginalURL()); + let refreshTokenPath = options.config.refreshTokenPath; + if (incomingPath.equals(refreshTokenPath) && method === "post") { return sessionRequestFunctions_1.refreshSessionInRequest({ req: options.req, res: options.res, @@ -50,45 +34,24 @@ function getAPIInterface() { config: options.config, recipeInterfaceImpl: options.recipeImplementation, }); - }); - }, - verifySession: function ({ verifySessionOptions, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let method = utils_1.normaliseHttpMethod(options.req.getMethod()); - if (method === "options" || method === "trace") { - return undefined; - } - let incomingPath = new normalisedURLPath_1.default(options.req.getOriginalURL()); - let refreshTokenPath = options.config.refreshTokenPath; - if (incomingPath.equals(refreshTokenPath) && method === "post") { - return sessionRequestFunctions_1.refreshSessionInRequest({ - req: options.req, - res: options.res, - userContext, - config: options.config, - recipeInterfaceImpl: options.recipeImplementation, - }); - } else { - return sessionRequestFunctions_1.getSessionFromRequest({ - req: options.req, - res: options.res, - options: verifySessionOptions, - config: options.config, - recipeInterfaceImpl: options.recipeImplementation, - userContext, - }); - } - }); + } else { + return sessionRequestFunctions_1.getSessionFromRequest({ + req: options.req, + res: options.res, + options: verifySessionOptions, + config: options.config, + recipeInterfaceImpl: options.recipeImplementation, + userContext, + }); + } }, - signOutPOST: function ({ session, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - if (session !== undefined) { - yield session.revokeSession(userContext); - } - return { - status: "OK", - }; - }); + signOutPOST: async function ({ session, userContext }) { + if (session !== undefined) { + await session.revokeSession(userContext); + } + return { + status: "OK", + }; }, }; } diff --git a/lib/build/recipe/session/api/refresh.js b/lib/build/recipe/session/api/refresh.js index 929eb18b4..7c68f0697 100644 --- a/lib/build/recipe/session/api/refresh.js +++ b/lib/build/recipe/session/api/refresh.js @@ -13,47 +13,14 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); -function handleRefreshAPI(apiImplementation, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.refreshPOST === undefined) { - return false; - } - yield apiImplementation.refreshPOST({ options, userContext }); - utils_1.send200Response(options.res, {}); - return true; - }); +async function handleRefreshAPI(apiImplementation, options, userContext) { + if (apiImplementation.refreshPOST === undefined) { + return false; + } + await apiImplementation.refreshPOST({ options, userContext }); + utils_1.send200Response(options.res, {}); + return true; } exports.default = handleRefreshAPI; diff --git a/lib/build/recipe/session/api/signout.js b/lib/build/recipe/session/api/signout.js index 47aef6898..e7763f7bf 100644 --- a/lib/build/recipe/session/api/signout.js +++ b/lib/build/recipe/session/api/signout.js @@ -13,64 +13,31 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const sessionRequestFunctions_1 = require("../sessionRequestFunctions"); -function signOutAPI(apiImplementation, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/34#issuecomment-717958537 - if (apiImplementation.signOutPOST === undefined) { - return false; - } - const session = yield sessionRequestFunctions_1.getSessionFromRequest({ - req: options.req, - res: options.res, - config: options.config, - recipeInterfaceImpl: options.recipeImplementation, - options: { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, - userContext, - }); - let result = yield apiImplementation.signOutPOST({ - options, - session, - userContext, - }); - utils_1.send200Response(options.res, result); - return true; +async function signOutAPI(apiImplementation, options, userContext) { + // Logic as per https://github.com/supertokens/supertokens-node/issues/34#issuecomment-717958537 + if (apiImplementation.signOutPOST === undefined) { + return false; + } + const session = await sessionRequestFunctions_1.getSessionFromRequest({ + req: options.req, + res: options.res, + config: options.config, + recipeInterfaceImpl: options.recipeImplementation, + options: { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }, + userContext, }); + let result = await apiImplementation.signOutPOST({ + options, + session, + userContext, + }); + utils_1.send200Response(options.res, result); + return true; } exports.default = signOutAPI; diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts b/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts index a012b36e1..1aa051606 100644 --- a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts +++ b/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts @@ -1,9 +1,11 @@ // @ts-nocheck +import RecipeUserId from "../../../recipeUserId"; import { JSONPrimitive } from "../../../types"; import { SessionClaim, SessionClaimValidator } from "../types"; export declare class PrimitiveArrayClaim extends SessionClaim { readonly fetchValue: ( userId: string, + recipeUserId: RecipeUserId, tenantId: string, userContext: any ) => Promise | T[] | undefined; diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.js b/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.js index fae40b965..d057552b5 100644 --- a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.js +++ b/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PrimitiveArrayClaim = void 0; const types_1 = require("../types"); @@ -45,38 +14,37 @@ class PrimitiveArrayClaim extends types_1.SessionClaim { this.getValueFromPayload(payload, ctx) === undefined || // We know payload[this.id] is defined since the value is not undefined in this branch (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - if (!claimVal.includes(val)) { - return { - isValid: false, - reason: { message: "wrong value", expectedToInclude: val, actualValue: claimVal }, - }; - } - return { isValid: true }; - }), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx); + if (claimVal === undefined) { + return { + isValid: false, + reason: { + message: "value does not exist", + expectedToInclude: val, + actualValue: claimVal, + }, + }; + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: "expired", + ageInSeconds, + maxAgeInSeconds, + }, + }; + } + if (!claimVal.includes(val)) { + return { + isValid: false, + reason: { message: "wrong value", expectedToInclude: val, actualValue: claimVal }, + }; + } + return { isValid: true }; + }, }; }, excludes: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { @@ -87,42 +55,37 @@ class PrimitiveArrayClaim extends types_1.SessionClaim { this.getValueFromPayload(payload, ctx) === undefined || // We know payload[this.id] is defined since the value is not undefined in this branch (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - if (claimVal.includes(val)) { - return { - isValid: false, - reason: { - message: "wrong value", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - return { isValid: true }; - }), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx); + if (claimVal === undefined) { + return { + isValid: false, + reason: { + message: "value does not exist", + expectedToNotInclude: val, + actualValue: claimVal, + }, + }; + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: "expired", + ageInSeconds, + maxAgeInSeconds, + }, + }; + } + if (claimVal.includes(val)) { + return { + isValid: false, + reason: { message: "wrong value", expectedToNotInclude: val, actualValue: claimVal }, + }; + } + return { isValid: true }; + }, }; }, includesAll: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { @@ -133,39 +96,38 @@ class PrimitiveArrayClaim extends types_1.SessionClaim { this.getValueFromPayload(payload, ctx) === undefined || // We know payload[this.id] is defined since the value is not undefined in this branch (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - const claimSet = new Set(claimVal); - const isValid = val.every((v) => claimSet.has(v)); - return isValid - ? { isValid } - : { - isValid, - reason: { message: "wrong value", expectedToInclude: val, actualValue: claimVal }, - }; - }), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx); + if (claimVal === undefined) { + return { + isValid: false, + reason: { + message: "value does not exist", + expectedToInclude: val, + actualValue: claimVal, + }, + }; + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: "expired", + ageInSeconds, + maxAgeInSeconds, + }, + }; + } + const claimSet = new Set(claimVal); + const isValid = val.every((v) => claimSet.has(v)); + return isValid + ? { isValid } + : { + isValid, + reason: { message: "wrong value", expectedToInclude: val, actualValue: claimVal }, + }; + }, }; }, includesAny: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { @@ -176,43 +138,42 @@ class PrimitiveArrayClaim extends types_1.SessionClaim { this.getValueFromPayload(payload, ctx) === undefined || // We know payload[this.id] is defined since the value is not undefined in this branch (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - const claimSet = new Set(claimVal); - const isValid = val.some((v) => claimSet.has(v)); - return isValid - ? { isValid: isValid } - : { - isValid, - reason: { - message: "wrong value", - expectedToIncludeAtLeastOneOf: val, - actualValue: claimVal, - }, - }; - }), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx); + if (claimVal === undefined) { + return { + isValid: false, + reason: { + message: "value does not exist", + expectedToNotInclude: val, + actualValue: claimVal, + }, + }; + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: "expired", + ageInSeconds, + maxAgeInSeconds, + }, + }; + } + const claimSet = new Set(claimVal); + const isValid = val.some((v) => claimSet.has(v)); + return isValid + ? { isValid: isValid } + : { + isValid, + reason: { + message: "wrong value", + expectedToIncludeAtLeastOneOf: val, + actualValue: claimVal, + }, + }; + }, }; }, excludesAll: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { @@ -223,43 +184,38 @@ class PrimitiveArrayClaim extends types_1.SessionClaim { this.getValueFromPayload(payload, ctx) === undefined || // We know payload[this.id] is defined since the value is not undefined in this branch (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - const claimSet = new Set(claimVal); - const isValid = val.every((v) => !claimSet.has(v)); - return isValid - ? { isValid: isValid } - : { - isValid, - reason: { - message: "wrong value", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - }), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx); + if (claimVal === undefined) { + return { + isValid: false, + reason: { + message: "value does not exist", + expectedToNotInclude: val, + actualValue: claimVal, + }, + }; + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: "expired", + ageInSeconds, + maxAgeInSeconds, + }, + }; + } + const claimSet = new Set(claimVal); + const isValid = val.every((v) => !claimSet.has(v)); + return isValid + ? { isValid: isValid } + : { + isValid, + reason: { message: "wrong value", expectedToNotInclude: val, actualValue: claimVal }, + }; + }, }; }, }; diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts b/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts index d1f1d7d89..8ecb30c81 100644 --- a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts +++ b/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts @@ -1,8 +1,14 @@ // @ts-nocheck +import RecipeUserId from "../../../recipeUserId"; import { JSONPrimitive } from "../../../types"; import { SessionClaim, SessionClaimValidator } from "../types"; export declare class PrimitiveClaim extends SessionClaim { - readonly fetchValue: (userId: string, tenantId: string, userContext: any) => Promise | T | undefined; + readonly fetchValue: ( + userId: string, + recipeUserId: RecipeUserId, + tenantId: string, + userContext: any + ) => Promise | T | undefined; readonly defaultMaxAgeInSeconds: number | undefined; constructor(config: { key: string; fetchValue: SessionClaim["fetchValue"]; defaultMaxAgeInSeconds?: number }); addToPayload_internal(payload: any, value: T, _userContext: any): any; diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.js b/lib/build/recipe/session/claimBaseClasses/primitiveClaim.js index ff72f2b4e..12407fabc 100644 --- a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.js +++ b/lib/build/recipe/session/claimBaseClasses/primitiveClaim.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PrimitiveClaim = void 0; const types_1 = require("../types"); @@ -45,38 +14,33 @@ class PrimitiveClaim extends types_1.SessionClaim { this.getValueFromPayload(payload, ctx) === undefined || (maxAgeInSeconds !== undefined && // We know payload[this.id] is defined since the value is not undefined in this branch payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedValue: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - if (claimVal !== val) { - return { - isValid: false, - reason: { message: "wrong value", expectedValue: val, actualValue: claimVal }, - }; - } - return { isValid: true }; - }), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx); + if (claimVal === undefined) { + return { + isValid: false, + reason: { message: "value does not exist", expectedValue: val, actualValue: claimVal }, + }; + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: "expired", + ageInSeconds, + maxAgeInSeconds, + }, + }; + } + if (claimVal !== val) { + return { + isValid: false, + reason: { message: "wrong value", expectedValue: val, actualValue: claimVal }, + }; + } + return { isValid: true }; + }, }; }, }; diff --git a/lib/build/recipe/session/constants.d.ts b/lib/build/recipe/session/constants.d.ts index 77315f542..bff82f168 100644 --- a/lib/build/recipe/session/constants.d.ts +++ b/lib/build/recipe/session/constants.d.ts @@ -4,3 +4,6 @@ export declare const REFRESH_API_PATH = "/session/refresh"; export declare const SIGNOUT_API_PATH = "/signout"; export declare const availableTokenTransferMethods: TokenTransferMethod[]; export declare const hundredYearsInMs = 3153600000000; +export declare const JWKCacheCooldownInMs = 500; +export declare const JWKCacheMaxAgeInMs = 60000; +export declare const protectedProps: string[]; diff --git a/lib/build/recipe/session/constants.js b/lib/build/recipe/session/constants.js index 06ebc6c0a..e653436ba 100644 --- a/lib/build/recipe/session/constants.js +++ b/lib/build/recipe/session/constants.js @@ -14,8 +14,21 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.hundredYearsInMs = exports.availableTokenTransferMethods = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; +exports.protectedProps = exports.JWKCacheMaxAgeInMs = exports.JWKCacheCooldownInMs = exports.hundredYearsInMs = exports.availableTokenTransferMethods = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; exports.REFRESH_API_PATH = "/session/refresh"; exports.SIGNOUT_API_PATH = "/signout"; exports.availableTokenTransferMethods = ["cookie", "header"]; exports.hundredYearsInMs = 3153600000000; +exports.JWKCacheCooldownInMs = 500; +exports.JWKCacheMaxAgeInMs = 60000; +exports.protectedProps = [ + "sub", + "iat", + "exp", + "sessionHandle", + "parentRefreshTokenHash1", + "refreshTokenHash1", + "antiCsrfToken", + "rsub", + "tId", +]; diff --git a/lib/build/recipe/session/cookieAndHeaders.d.ts b/lib/build/recipe/session/cookieAndHeaders.d.ts index 4d8feb729..f92171d16 100644 --- a/lib/build/recipe/session/cookieAndHeaders.d.ts +++ b/lib/build/recipe/session/cookieAndHeaders.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import { TokenTransferMethod, TokenType, TypeNormalisedInput } from "./types"; export declare function clearSessionFromAllTokenTransferMethods(config: TypeNormalisedInput, res: BaseResponse): void; export declare function clearSession( diff --git a/lib/build/recipe/session/error.d.ts b/lib/build/recipe/session/error.d.ts index 9ff25e067..8dcc47ef2 100644 --- a/lib/build/recipe/session/error.d.ts +++ b/lib/build/recipe/session/error.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import STError from "../../error"; +import RecipeUserId from "../../recipeUserId"; import { ClaimValidationError } from "./types"; export default class SessionError extends STError { static UNAUTHORISED: "UNAUTHORISED"; @@ -24,6 +25,7 @@ export default class SessionError extends STError { type: "TOKEN_THEFT_DETECTED"; payload: { userId: string; + recipeUserId: RecipeUserId; sessionHandle: string; }; } diff --git a/lib/build/recipe/session/framework/awsLambda.js b/lib/build/recipe/session/framework/awsLambda.js index 651c71513..13d291025 100644 --- a/lib/build/recipe/session/framework/awsLambda.js +++ b/lib/build/recipe/session/framework/awsLambda.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -42,24 +11,23 @@ const supertokens_1 = __importDefault(require("../../../supertokens")); const recipe_1 = __importDefault(require("../recipe")); const utils_1 = require("../../../utils"); function verifySession(handler, verifySessionOptions) { - return (event, context, callback) => - __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new framework_1.AWSRequest(event); - let response = new framework_1.AWSResponse(event); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); - try { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - event.session = yield sessionRecipe.verifySession(verifySessionOptions, request, response, userContext); - let handlerResult = yield handler(event, context, callback); - return response.sendResponse(handlerResult); - } catch (err) { - yield supertokens.errorHandler(err, request, response); - if (response.responseSet) { - return response.sendResponse({}); - } - throw err; + return async (event, context, callback) => { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + let request = new framework_1.AWSRequest(event); + let response = new framework_1.AWSResponse(event); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + event.session = await sessionRecipe.verifySession(verifySessionOptions, request, response, userContext); + let handlerResult = await handler(event, context, callback); + return response.sendResponse(handlerResult); + } catch (err) { + await supertokens.errorHandler(err, request, response); + if (response.responseSet) { + return response.sendResponse({}); } - }); + throw err; + } + }; } exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/express.js b/lib/build/recipe/session/framework/express.js index b2da9fbc4..19292e2c3 100644 --- a/lib/build/recipe/session/framework/express.js +++ b/lib/build/recipe/session/framework/express.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,23 +25,22 @@ const framework_1 = require("../../../framework/express/framework"); const supertokens_1 = __importDefault(require("../../../supertokens")); const utils_1 = require("../../../utils"); function verifySession(options) { - return (req, res, next) => - __awaiter(this, void 0, void 0, function* () { - const request = new framework_1.ExpressRequest(req); - const response = new framework_1.ExpressResponse(res); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); + return async (req, res, next) => { + const request = new framework_1.ExpressRequest(req); + const response = new framework_1.ExpressResponse(res); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + const sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + req.session = await sessionRecipe.verifySession(options, request, response, userContext); + next(); + } catch (err) { try { - const sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - req.session = yield sessionRecipe.verifySession(options, request, response, userContext); - next(); - } catch (err) { - try { - const supertokens = supertokens_1.default.getInstanceOrThrowError(); - yield supertokens.errorHandler(err, request, response); - } catch (_a) { - next(err); - } + const supertokens = supertokens_1.default.getInstanceOrThrowError(); + await supertokens.errorHandler(err, request, response); + } catch (_a) { + next(err); } - }); + } + }; } exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/fastify.js b/lib/build/recipe/session/framework/fastify.js index 447824e06..9007fb99c 100644 --- a/lib/build/recipe/session/framework/fastify.js +++ b/lib/build/recipe/session/framework/fastify.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,19 +25,18 @@ const framework_1 = require("../../../framework/fastify/framework"); const supertokens_1 = __importDefault(require("../../../supertokens")); const utils_1 = require("../../../utils"); function verifySession(options) { - return (req, res) => - __awaiter(this, void 0, void 0, function* () { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - let request = new framework_1.FastifyRequest(req); - let response = new framework_1.FastifyResponse(res); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); - try { - req.session = yield sessionRecipe.verifySession(options, request, response, userContext); - } catch (err) { - const supertokens = supertokens_1.default.getInstanceOrThrowError(); - yield supertokens.errorHandler(err, request, response); - throw err; - } - }); + return async (req, res) => { + let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + let request = new framework_1.FastifyRequest(req); + let response = new framework_1.FastifyResponse(res); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + req.session = await sessionRecipe.verifySession(options, request, response, userContext); + } catch (err) { + const supertokens = supertokens_1.default.getInstanceOrThrowError(); + await supertokens.errorHandler(err, request, response); + throw err; + } + }; } exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/hapi.js b/lib/build/recipe/session/framework/hapi.js index 9fe90c646..e04fa1a04 100644 --- a/lib/build/recipe/session/framework/hapi.js +++ b/lib/build/recipe/session/framework/hapi.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,31 +25,30 @@ const recipe_1 = __importDefault(require("../recipe")); const framework_1 = require("../../../framework/hapi/framework"); const utils_1 = require("../../../utils"); function verifySession(options) { - return (req, h) => - __awaiter(this, void 0, void 0, function* () { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - let request = new framework_1.HapiRequest(req); - let response = new framework_1.HapiResponse(h); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); + return async (req, h) => { + let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + let request = new framework_1.HapiRequest(req); + let response = new framework_1.HapiResponse(h); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + req.session = await sessionRecipe.verifySession(options, request, response, userContext); + } catch (err) { try { - req.session = yield sessionRecipe.verifySession(options, request, response, userContext); - } catch (err) { - try { - const supertokens = supertokens_1.default.getInstanceOrThrowError(); - yield supertokens.errorHandler(err, request, response); - if (response.responseSet) { - let resObj = response.sendResponse(true); - (req.app.lazyHeaders || []).forEach(({ key, value, allowDuplicateKey }) => { - resObj.header(key, value, { append: allowDuplicateKey }); - }); - return resObj.takeover(); - } - } catch (_a) { - // We catch and ignore since we want to re-throw the original error if handling wasn't successful - throw err; + const supertokens = supertokens_1.default.getInstanceOrThrowError(); + await supertokens.errorHandler(err, request, response); + if (response.responseSet) { + let resObj = response.sendResponse(true); + (req.app.lazyHeaders || []).forEach(({ key, value, allowDuplicateKey }) => { + resObj.header(key, value, { append: allowDuplicateKey }); + }); + return resObj.takeover(); } + } catch (_a) { + // We catch and ignore since we want to re-throw the original error if handling wasn't successful + throw err; } - return h.continue; - }); + } + return h.continue; + }; } exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/koa.js b/lib/build/recipe/session/framework/koa.js index 2172fd9c2..d60b78edf 100644 --- a/lib/build/recipe/session/framework/koa.js +++ b/lib/build/recipe/session/framework/koa.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,25 +25,24 @@ const recipe_1 = __importDefault(require("../recipe")); const framework_1 = require("../../../framework/koa/framework"); const utils_1 = require("../../../utils"); function verifySession(options) { - return (ctx, next) => - __awaiter(this, void 0, void 0, function* () { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - let request = new framework_1.KoaRequest(ctx); - let response = new framework_1.KoaResponse(ctx); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); + return async (ctx, next) => { + let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + let request = new framework_1.KoaRequest(ctx); + let response = new framework_1.KoaResponse(ctx); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + ctx.session = await sessionRecipe.verifySession(options, request, response, userContext); + } catch (err) { try { - ctx.session = yield sessionRecipe.verifySession(options, request, response, userContext); - } catch (err) { - try { - const supertokens = supertokens_1.default.getInstanceOrThrowError(); - yield supertokens.errorHandler(err, request, response); - return; - } catch (_a) { - // We catch and ignore since we want to re-throw the original error if handling wasn't successful - throw err; - } + const supertokens = supertokens_1.default.getInstanceOrThrowError(); + await supertokens.errorHandler(err, request, response); + return; + } catch (_a) { + // We catch and ignore since we want to re-throw the original error if handling wasn't successful + throw err; } - yield next(); - }); + } + await next(); + }; } exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/loopback.js b/lib/build/recipe/session/framework/loopback.js index 287902e1e..a9f76dbad 100644 --- a/lib/build/recipe/session/framework/loopback.js +++ b/lib/build/recipe/session/framework/loopback.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,26 +25,25 @@ const recipe_1 = __importDefault(require("../recipe")); const framework_1 = require("../../../framework/loopback/framework"); const utils_1 = require("../../../utils"); function verifySession(options) { - return (ctx, next) => - __awaiter(this, void 0, void 0, function* () { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - let middlewareCtx = yield ctx.get("middleware.http.context"); - let request = new framework_1.LoopbackRequest(middlewareCtx); - let response = new framework_1.LoopbackResponse(middlewareCtx); - const userContext = utils_1.makeDefaultUserContextFromAPI(request); + return async (ctx, next) => { + let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + let middlewareCtx = await ctx.get("middleware.http.context"); + let request = new framework_1.LoopbackRequest(middlewareCtx); + let response = new framework_1.LoopbackResponse(middlewareCtx); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + middlewareCtx.session = await sessionRecipe.verifySession(options, request, response, userContext); + } catch (err) { try { - middlewareCtx.session = yield sessionRecipe.verifySession(options, request, response, userContext); - } catch (err) { - try { - const supertokens = supertokens_1.default.getInstanceOrThrowError(); - yield supertokens.errorHandler(err, request, response); - return; - } catch (_a) { - // We catch and ignore since we want to re-throw the original error if handling wasn't successful - throw err; - } + const supertokens = supertokens_1.default.getInstanceOrThrowError(); + await supertokens.errorHandler(err, request, response); + return; + } catch (_a) { + // We catch and ignore since we want to re-throw the original error if handling wasn't successful + throw err; } - return yield next(); - }); + } + return await next(); + }; } exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/index.d.ts b/lib/build/recipe/session/index.d.ts index 965b4ed00..3f403f4f6 100644 --- a/lib/build/recipe/session/index.d.ts +++ b/lib/build/recipe/session/index.d.ts @@ -13,6 +13,7 @@ import { } from "./types"; import Recipe from "./recipe"; import { JSONObject } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export default class SessionWrapper { static init: typeof Recipe.init; static Error: typeof SuperTokensError; @@ -20,14 +21,14 @@ export default class SessionWrapper { req: any, res: any, tenantId: string, - userId: string, + recipeUserId: RecipeUserId, accessTokenPayload?: any, sessionDataInDatabase?: any, userContext?: any ): Promise; static createNewSessionWithoutRequestResponse( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, accessTokenPayload?: any, sessionDataInDatabase?: any, disableAntiCsrf?: boolean, @@ -50,20 +51,6 @@ export default class SessionWrapper { invalidClaims: ClaimValidationError[]; } >; - static validateClaimsInJWTPayload( - tenantId: string, - userId: string, - jwtPayload: JSONObject, - overrideGlobalClaimValidators?: ( - globalClaimValidators: SessionClaimValidator[], - userId: string, - userContext: any - ) => Promise | SessionClaimValidator[], - userContext?: any - ): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }>; static getSession(req: any, res: any): Promise; static getSession( req: any, @@ -138,8 +125,18 @@ export default class SessionWrapper { antiCsrfToken?: string, userContext?: any ): Promise; - static revokeAllSessionsForUser(userId: string, tenantId?: string, userContext?: any): Promise; - static getAllSessionHandlesForUser(userId: string, tenantId?: string, userContext?: any): Promise; + static revokeAllSessionsForUser( + userId: string, + revokeSessionsForLinkedAccounts?: boolean, + tenantId?: string, + userContext?: any + ): Promise; + static getAllSessionHandlesForUser( + userId: string, + fetchSessionsForAllLinkedAccounts?: boolean, + tenantId?: string, + userContext?: any + ): Promise; static revokeSession(sessionHandle: string, userContext?: any): Promise; static revokeMultipleSessions(sessionHandles: string[], userContext?: any): Promise; static updateSessionDataInDatabase(sessionHandle: string, newSessionData: any, userContext?: any): Promise; @@ -214,7 +211,6 @@ export declare let fetchAndSetClaim: typeof SessionWrapper.fetchAndSetClaim; export declare let setClaimValue: typeof SessionWrapper.setClaimValue; export declare let getClaimValue: typeof SessionWrapper.getClaimValue; export declare let removeClaim: typeof SessionWrapper.removeClaim; -export declare let validateClaimsInJWTPayload: typeof SessionWrapper.validateClaimsInJWTPayload; export declare let validateClaimsForSessionHandle: typeof SessionWrapper.validateClaimsForSessionHandle; export declare let Error: typeof SuperTokensError; export declare let createJWT: typeof SessionWrapper.createJWT; diff --git a/lib/build/recipe/session/index.js b/lib/build/recipe/session/index.js index ed412f989..82b519987 100644 --- a/lib/build/recipe/session/index.js +++ b/lib/build/recipe/session/index.js @@ -13,213 +13,167 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getOpenIdDiscoveryConfiguration = exports.getJWKS = exports.createJWT = exports.Error = exports.validateClaimsForSessionHandle = exports.validateClaimsInJWTPayload = exports.removeClaim = exports.getClaimValue = exports.setClaimValue = exports.fetchAndSetClaim = exports.mergeIntoAccessTokenPayload = exports.updateSessionDataInDatabase = exports.revokeMultipleSessions = exports.revokeSession = exports.getAllSessionHandlesForUser = exports.revokeAllSessionsForUser = exports.refreshSessionWithoutRequestResponse = exports.refreshSession = exports.getSessionInformation = exports.getSessionWithoutRequestResponse = exports.getSession = exports.createNewSessionWithoutRequestResponse = exports.createNewSession = exports.init = void 0; +exports.getOpenIdDiscoveryConfiguration = exports.getJWKS = exports.createJWT = exports.Error = exports.validateClaimsForSessionHandle = exports.removeClaim = exports.getClaimValue = exports.setClaimValue = exports.fetchAndSetClaim = exports.mergeIntoAccessTokenPayload = exports.updateSessionDataInDatabase = exports.revokeMultipleSessions = exports.revokeSession = exports.getAllSessionHandlesForUser = exports.revokeAllSessionsForUser = exports.refreshSessionWithoutRequestResponse = exports.refreshSession = exports.getSessionInformation = exports.getSessionWithoutRequestResponse = exports.getSession = exports.createNewSessionWithoutRequestResponse = exports.createNewSession = exports.init = void 0; const error_1 = __importDefault(require("./error")); const recipe_1 = __importDefault(require("./recipe")); const utils_1 = require("./utils"); const sessionRequestFunctions_1 = require("./sessionRequestFunctions"); +const __1 = require("../.."); const constants_1 = require("../multitenancy/constants"); +const constants_2 = require("./constants"); class SessionWrapper { - static createNewSession( + static async createNewSession( req, res, tenantId, - userId, + recipeUserId, accessTokenPayload = {}, sessionDataInDatabase = {}, userContext = {} ) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - const config = recipeInstance.config; - const appInfo = recipeInstance.getAppInfo(); - return yield sessionRequestFunctions_1.createNewSessionInRequest({ - req, - res, - userContext, - recipeInstance, - accessTokenPayload, - userId, - config, - appInfo, - sessionDataInDatabase, - tenantId, - }); + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + const config = recipeInstance.config; + const appInfo = recipeInstance.getAppInfo(); + let user = await __1.getUser(recipeUserId.getAsString(), userContext); + let userId = recipeUserId.getAsString(); + if (user !== undefined) { + userId = user.id; + } + return await sessionRequestFunctions_1.createNewSessionInRequest({ + req, + res, + userContext, + recipeInstance, + accessTokenPayload, + userId, + recipeUserId, + config, + appInfo, + sessionDataInDatabase, + tenantId, }); } - static createNewSessionWithoutRequestResponse( + static async createNewSessionWithoutRequestResponse( tenantId, - userId, + recipeUserId, accessTokenPayload = {}, sessionDataInDatabase = {}, disableAntiCsrf = false, userContext = {} ) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); - const appInfo = recipeInstance.getAppInfo(); - const issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); - let finalAccessTokenPayload = Object.assign(Object.assign({}, accessTokenPayload), { iss: issuer }); - for (const claim of claimsAddedByOtherRecipes) { - const update = yield claim.build(userId, tenantId, userContext); - finalAccessTokenPayload = Object.assign(Object.assign({}, finalAccessTokenPayload), update); - } - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewSession({ - userId, - accessTokenPayload: finalAccessTokenPayload, - sessionDataInDatabase, - disableAntiCsrf, - tenantId, - userContext, - }); + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); + const appInfo = recipeInstance.getAppInfo(); + const issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + let finalAccessTokenPayload = Object.assign(Object.assign({}, accessTokenPayload), { iss: issuer }); + for (const prop of constants_2.protectedProps) { + delete finalAccessTokenPayload[prop]; + } + let user = await __1.getUser(recipeUserId.getAsString(), userContext); + let userId = recipeUserId.getAsString(); + if (user !== undefined) { + userId = user.id; + } + for (const claim of claimsAddedByOtherRecipes) { + const update = await claim.build(userId, recipeUserId, tenantId, userContext); + finalAccessTokenPayload = Object.assign(Object.assign({}, finalAccessTokenPayload), update); + } + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewSession({ + userId, + recipeUserId, + accessTokenPayload: finalAccessTokenPayload, + sessionDataInDatabase, + disableAntiCsrf, + tenantId, + userContext, }); } - static validateClaimsForSessionHandle(sessionHandle, overrideGlobalClaimValidators, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - const recipeImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; - const sessionInfo = yield recipeImpl.getSessionInformation({ - sessionHandle, - userContext, - }); - if (sessionInfo === undefined) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } - const claimValidatorsAddedByOtherRecipes = recipe_1.default - .getInstanceOrThrowError() - .getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators = yield recipeImpl.getGlobalClaimValidators({ - userId: sessionInfo.userId, - tenantId: sessionInfo.tenantId, - claimValidatorsAddedByOtherRecipes, - userContext, - }); - const claimValidators = - overrideGlobalClaimValidators !== undefined - ? yield overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, userContext) - : globalClaimValidators; - let claimValidationResponse = yield recipeImpl.validateClaims({ - userId: sessionInfo.userId, - accessTokenPayload: sessionInfo.customClaimsInAccessTokenPayload, - claimValidators, - userContext, - }); - if (claimValidationResponse.accessTokenPayloadUpdate !== undefined) { - if ( - !(yield recipeImpl.mergeIntoAccessTokenPayload({ - sessionHandle, - accessTokenPayloadUpdate: claimValidationResponse.accessTokenPayloadUpdate, - userContext, - })) - ) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } - } + static async validateClaimsForSessionHandle(sessionHandle, overrideGlobalClaimValidators, userContext = {}) { + const recipeImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + const sessionInfo = await recipeImpl.getSessionInformation({ + sessionHandle, + userContext, + }); + if (sessionInfo === undefined) { return { - status: "OK", - invalidClaims: claimValidationResponse.invalidClaims, + status: "SESSION_DOES_NOT_EXIST_ERROR", }; + } + const claimValidatorsAddedByOtherRecipes = recipe_1.default + .getInstanceOrThrowError() + .getClaimValidatorsAddedByOtherRecipes(); + const globalClaimValidators = await recipeImpl.getGlobalClaimValidators({ + userId: sessionInfo.userId, + recipeUserId: sessionInfo.recipeUserId, + tenantId: sessionInfo.tenantId, + claimValidatorsAddedByOtherRecipes, + userContext, }); - } - static validateClaimsInJWTPayload(tenantId, userId, jwtPayload, overrideGlobalClaimValidators, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - const recipeImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; - const claimValidatorsAddedByOtherRecipes = recipe_1.default - .getInstanceOrThrowError() - .getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators = yield recipeImpl.getGlobalClaimValidators({ - tenantId, - userId, - claimValidatorsAddedByOtherRecipes, - userContext, - }); - const claimValidators = - overrideGlobalClaimValidators !== undefined - ? yield overrideGlobalClaimValidators(globalClaimValidators, userId, userContext) - : globalClaimValidators; - return recipeImpl.validateClaimsInJWTPayload({ - userId, - jwtPayload, - claimValidators, - userContext, - }); + const claimValidators = + overrideGlobalClaimValidators !== undefined + ? await overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, userContext) + : globalClaimValidators; + let claimValidationResponse = await recipeImpl.validateClaims({ + userId: sessionInfo.userId, + recipeUserId: sessionInfo.recipeUserId, + accessTokenPayload: sessionInfo.customClaimsInAccessTokenPayload, + claimValidators, + userContext, }); + if (claimValidationResponse.accessTokenPayloadUpdate !== undefined) { + if ( + !(await recipeImpl.mergeIntoAccessTokenPayload({ + sessionHandle, + accessTokenPayloadUpdate: claimValidationResponse.accessTokenPayloadUpdate, + userContext, + })) + ) { + return { + status: "SESSION_DOES_NOT_EXIST_ERROR", + }; + } + } + return { + status: "OK", + invalidClaims: claimValidationResponse.invalidClaims, + }; } - static getSession(req, res, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - const config = recipeInstance.config; - const recipeInterfaceImpl = recipeInstance.recipeInterfaceImpl; - return sessionRequestFunctions_1.getSessionFromRequest({ - req, - res, - recipeInterfaceImpl, - config, - options, - userContext, - }); + static async getSession(req, res, options, userContext) { + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + const config = recipeInstance.config; + const recipeInterfaceImpl = recipeInstance.recipeInterfaceImpl; + return sessionRequestFunctions_1.getSessionFromRequest({ + req, + res, + recipeInterfaceImpl, + config, + options, + userContext, // userContext is normalized inside the function }); } - static getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; - const session = yield recipeInterfaceImpl.getSession({ - accessToken, - antiCsrfToken, - options, - userContext, - }); - if (session !== undefined) { - const claimValidators = yield utils_1.getRequiredClaimValidators( - session, - options === null || options === void 0 ? void 0 : options.overrideGlobalClaimValidators, - userContext - ); - yield session.assertClaims(claimValidators, userContext); - } - return session; + static async getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options, userContext = {}) { + const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + const session = await recipeInterfaceImpl.getSession({ + accessToken, + antiCsrfToken, + options, + userContext, }); + if (session !== undefined) { + const claimValidators = await utils_1.getRequiredClaimValidators( + session, + options === null || options === void 0 ? void 0 : options.overrideGlobalClaimValidators, + userContext + ); + await session.assertClaims(claimValidators, userContext); + } + return session; } static getSessionInformation(sessionHandle, userContext = {}) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getSessionInformation({ @@ -252,19 +206,20 @@ class SessionWrapper { userContext, }); } - static revokeAllSessionsForUser(userId, tenantId, userContext = {}) { + static revokeAllSessionsForUser(userId, revokeSessionsForLinkedAccounts = true, tenantId, userContext = {}) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllSessionsForUser({ userId, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - revokeAcrossAllTenants: tenantId === undefined, + revokeSessionsForLinkedAccounts, userContext, }); } - static getAllSessionHandlesForUser(userId, tenantId, userContext = {}) { + static getAllSessionHandlesForUser(userId, fetchSessionsForAllLinkedAccounts = true, tenantId, userContext = {}) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getAllSessionHandlesForUser({ userId, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, fetchAcrossAllTenants: tenantId === undefined, + fetchSessionsForAllLinkedAccounts, userContext, }); } @@ -363,7 +318,6 @@ exports.fetchAndSetClaim = SessionWrapper.fetchAndSetClaim; exports.setClaimValue = SessionWrapper.setClaimValue; exports.getClaimValue = SessionWrapper.getClaimValue; exports.removeClaim = SessionWrapper.removeClaim; -exports.validateClaimsInJWTPayload = SessionWrapper.validateClaimsInJWTPayload; exports.validateClaimsForSessionHandle = SessionWrapper.validateClaimsForSessionHandle; exports.Error = SessionWrapper.Error; // JWT Functions diff --git a/lib/build/recipe/session/recipe.d.ts b/lib/build/recipe/session/recipe.d.ts index 86eb8c84e..5577843a8 100644 --- a/lib/build/recipe/session/recipe.d.ts +++ b/lib/build/recipe/session/recipe.d.ts @@ -12,7 +12,7 @@ import { import STError from "./error"; import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod } from "../../types"; import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OpenIdRecipe from "../openid/recipe"; export default class SessionRecipe extends RecipeModule { private static instance; diff --git a/lib/build/recipe/session/recipe.js b/lib/build/recipe/session/recipe.js index ad193ec14..e275b461c 100644 --- a/lib/build/recipe/session/recipe.js +++ b/lib/build/recipe/session/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -107,62 +76,59 @@ class SessionRecipe extends recipeModule_1.default { apisHandled.push(...this.openIdRecipe.getAPIsHandled()); return apisHandled; }; - this.handleAPIRequest = (id, tenantId, req, res, path, method, userContext) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - }; - if (id === constants_1.REFRESH_API_PATH) { - return yield refresh_1.default(this.apiImpl, options, userContext); - } else if (id === constants_1.SIGNOUT_API_PATH) { - return yield signout_1.default(this.apiImpl, options, userContext); - } else { - return yield this.openIdRecipe.handleAPIRequest(id, tenantId, req, res, path, method, userContext); - } - }); - this.handleError = (err, request, response) => - __awaiter(this, void 0, void 0, function* () { - if (err.fromRecipe === SessionRecipe.RECIPE_ID) { - if (err.type === error_1.default.UNAUTHORISED) { - logger_1.logDebugMessage("errorHandler: returning UNAUTHORISED"); - if ( - err.payload === undefined || - err.payload.clearTokens === undefined || - err.payload.clearTokens === true - ) { - logger_1.logDebugMessage("errorHandler: Clearing tokens because of UNAUTHORISED response"); - cookieAndHeaders_1.clearSessionFromAllTokenTransferMethods(this.config, response); - } - return yield this.config.errorHandlers.onUnauthorised(err.message, request, response); - } else if (err.type === error_1.default.TRY_REFRESH_TOKEN) { - logger_1.logDebugMessage("errorHandler: returning TRY_REFRESH_TOKEN"); - return yield this.config.errorHandlers.onTryRefreshToken(err.message, request, response); - } else if (err.type === error_1.default.TOKEN_THEFT_DETECTED) { - logger_1.logDebugMessage("errorHandler: returning TOKEN_THEFT_DETECTED"); - logger_1.logDebugMessage( - "errorHandler: Clearing tokens because of TOKEN_THEFT_DETECTED response" - ); + this.handleAPIRequest = async (id, tenantId, req, res, path, method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + }; + if (id === constants_1.REFRESH_API_PATH) { + return await refresh_1.default(this.apiImpl, options, userContext); + } else if (id === constants_1.SIGNOUT_API_PATH) { + return await signout_1.default(this.apiImpl, options, userContext); + } else { + return await this.openIdRecipe.handleAPIRequest(id, tenantId, req, res, path, method, userContext); + } + }; + this.handleError = async (err, request, response) => { + if (err.fromRecipe === SessionRecipe.RECIPE_ID) { + if (err.type === error_1.default.UNAUTHORISED) { + logger_1.logDebugMessage("errorHandler: returning UNAUTHORISED"); + if ( + err.payload === undefined || + err.payload.clearTokens === undefined || + err.payload.clearTokens === true + ) { + logger_1.logDebugMessage("errorHandler: Clearing tokens because of UNAUTHORISED response"); cookieAndHeaders_1.clearSessionFromAllTokenTransferMethods(this.config, response); - return yield this.config.errorHandlers.onTokenTheftDetected( - err.payload.sessionHandle, - err.payload.userId, - request, - response - ); - } else if (err.type === error_1.default.INVALID_CLAIMS) { - return yield this.config.errorHandlers.onInvalidClaim(err.payload, request, response); - } else { - throw err; } + return await this.config.errorHandlers.onUnauthorised(err.message, request, response); + } else if (err.type === error_1.default.TRY_REFRESH_TOKEN) { + logger_1.logDebugMessage("errorHandler: returning TRY_REFRESH_TOKEN"); + return await this.config.errorHandlers.onTryRefreshToken(err.message, request, response); + } else if (err.type === error_1.default.TOKEN_THEFT_DETECTED) { + logger_1.logDebugMessage("errorHandler: returning TOKEN_THEFT_DETECTED"); + logger_1.logDebugMessage("errorHandler: Clearing tokens because of TOKEN_THEFT_DETECTED response"); + cookieAndHeaders_1.clearSessionFromAllTokenTransferMethods(this.config, response); + return await this.config.errorHandlers.onTokenTheftDetected( + err.payload.sessionHandle, + err.payload.userId, + err.payload.recipeUserId, + request, + response + ); + } else if (err.type === error_1.default.INVALID_CLAIMS) { + return await this.config.errorHandlers.onInvalidClaim(err.payload, request, response); } else { - return yield this.openIdRecipe.handleError(err, request, response); + throw err; } - }); + } else { + return await this.openIdRecipe.handleError(err, request, response); + } + }; this.getAllCORSHeaders = () => { let corsHeaders = [...cookieAndHeaders_1.getCORSAllowedHeaders()]; corsHeaders.push(...this.openIdRecipe.getAllCORSHeaders()); @@ -174,21 +140,20 @@ class SessionRecipe extends recipeModule_1.default { (err.fromRecipe === SessionRecipe.RECIPE_ID || this.openIdRecipe.isErrorFromThisRecipe(err)) ); }; - this.verifySession = (options, request, response, userContext) => - __awaiter(this, void 0, void 0, function* () { - return yield this.apiImpl.verifySession({ - verifySessionOptions: options, - options: { - config: this.config, - req: request, - res: response, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - }, - userContext, - }); + this.verifySession = async (options, request, response, userContext) => { + return await this.apiImpl.verifySession({ + verifySessionOptions: options, + options: { + config: this.config, + req: request, + res: response, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + }, + userContext, }); + }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); logger_1.logDebugMessage("session init: antiCsrf: " + this.config.antiCsrf); logger_1.logDebugMessage("session init: cookieDomain: " + this.config.cookieDomain); @@ -220,7 +185,9 @@ class SessionRecipe extends recipeModule_1.default { if (SessionRecipe.instance !== undefined) { return SessionRecipe.instance; } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); + throw new Error( + "Initialisation not done. Did you forget to call the SuperTokens.init or Session.init function?" + ); } static init(config) { return (appInfo, isInServerlessEnv) => { diff --git a/lib/build/recipe/session/recipeImplementation.d.ts b/lib/build/recipe/session/recipeImplementation.d.ts index 5af8072a1..9c1969e84 100644 --- a/lib/build/recipe/session/recipeImplementation.d.ts +++ b/lib/build/recipe/session/recipeImplementation.d.ts @@ -10,8 +10,6 @@ export declare type Helpers = { appInfo: NormalisedAppinfo; getRecipeImpl: () => RecipeInterface; }; -export declare const JWKCacheMaxAgeInMs = 60000; -export declare const protectedProps: string[]; export default function getRecipeInterface( querier: Querier, config: TypeNormalisedInput, diff --git a/lib/build/recipe/session/recipeImplementation.js b/lib/build/recipe/session/recipeImplementation.js index 49df4f34c..99edb7219 100644 --- a/lib/build/recipe/session/recipeImplementation.js +++ b/lib/build/recipe/session/recipeImplementation.js @@ -35,44 +35,12 @@ var __importStar = __setModuleDefault(result, mod); return result; }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.protectedProps = exports.JWKCacheMaxAgeInMs = void 0; const jose_1 = require("jose"); const SessionFunctions = __importStar(require("./sessionFunctions")); const cookieAndHeaders_1 = require("./cookieAndHeaders"); @@ -83,24 +51,14 @@ const logger_1 = require("../../logger"); const jwt_1 = require("./jwt"); const accessToken_1 = require("./accessToken"); const error_1 = __importDefault(require("./error")); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); -const JWKCacheCooldownInMs = 500; -exports.JWKCacheMaxAgeInMs = 60000; -exports.protectedProps = [ - "sub", - "iat", - "exp", - "sessionHandle", - "parentRefreshTokenHash1", - "refreshTokenHash1", - "antiCsrfToken", - "tId", -]; +const constants_2 = require("./constants"); function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverrides) { const JWKS = querier.getAllCoreUrlsForPath("/.well-known/jwks.json").map((url) => jose_1.createRemoteJWKSet(new URL(url), { - cooldownDuration: JWKCacheCooldownInMs, - cacheMaxAge: exports.JWKCacheMaxAgeInMs, + cooldownDuration: constants_2.JWKCacheCooldownInMs, + cacheMaxAge: constants_2.JWKCacheMaxAgeInMs, }) ); /** @@ -110,281 +68,271 @@ function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverride Every core instance a backend is connected to is expected to connect to the same database and use the same key set for token verification. Otherwise, the result of session verification would depend on which core is currently available. */ - const combinedJWKS = (...args) => - __awaiter(this, void 0, void 0, function* () { - let lastError = undefined; - if (JWKS.length === 0) { - throw Error( - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); - } - for (const jwks of JWKS) { - try { - // We await before returning to make sure we catch the error - return yield jwks(...args); - } catch (ex) { - lastError = ex; - } + const combinedJWKS = async (...args) => { + let lastError = undefined; + if (JWKS.length === 0) { + throw Error( + "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." + ); + } + for (const jwks of JWKS) { + try { + // We await before returning to make sure we catch the error + return await jwks(...args); + } catch (ex) { + lastError = ex; } - throw lastError; - }); + } + throw lastError; + }; let obj = { - createNewSession: function ({ - userId, + createNewSession: async function ({ + recipeUserId, accessTokenPayload = {}, sessionDataInDatabase = {}, disableAntiCsrf, tenantId, }) { - return __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("createNewSession: Started"); - let response = yield SessionFunctions.createNewSession( - helpers, - tenantId, - userId, - disableAntiCsrf === true, - accessTokenPayload, - sessionDataInDatabase - ); - logger_1.logDebugMessage("createNewSession: Finished"); - const payload = jwt_1.parseJWTWithoutSignatureVerification(response.accessToken.token).payload; - return new sessionClass_1.default( - helpers, - response.accessToken.token, - cookieAndHeaders_1.buildFrontToken(response.session.userId, response.accessToken.expiry, payload), - response.refreshToken, - response.antiCsrfToken, - response.session.handle, - response.session.userId, - payload, - undefined, - true, - tenantId - ); - }); + logger_1.logDebugMessage("createNewSession: Started"); + let response = await SessionFunctions.createNewSession( + helpers, + tenantId, + recipeUserId, + disableAntiCsrf === true, + accessTokenPayload, + sessionDataInDatabase + ); + logger_1.logDebugMessage("createNewSession: Finished"); + const payload = jwt_1.parseJWTWithoutSignatureVerification(response.accessToken.token).payload; + return new sessionClass_1.default( + helpers, + response.accessToken.token, + cookieAndHeaders_1.buildFrontToken(response.session.userId, response.accessToken.expiry, payload), + response.refreshToken, + response.antiCsrfToken, + response.session.handle, + response.session.userId, + response.session.recipeUserId, + payload, + undefined, + true, + tenantId + ); }, - getGlobalClaimValidators: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return input.claimValidatorsAddedByOtherRecipes; - }); + getGlobalClaimValidators: async function (input) { + return input.claimValidatorsAddedByOtherRecipes; }, - getSession: function ({ accessToken: accessTokenString, antiCsrfToken, options }) { - return __awaiter(this, void 0, void 0, function* () { - if ( - (options === null || options === void 0 ? void 0 : options.antiCsrfCheck) !== false && - config.antiCsrf === "VIA_CUSTOM_HEADER" - ) { - throw new Error( - "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set antiCsrfCheck to false" + getSession: async function ({ accessToken: accessTokenString, antiCsrfToken, options }) { + if ( + (options === null || options === void 0 ? void 0 : options.antiCsrfCheck) !== false && + config.antiCsrf === "VIA_CUSTOM_HEADER" + ) { + throw new Error( + "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set antiCsrfCheck to false" + ); + } + logger_1.logDebugMessage("getSession: Started"); + if (accessTokenString === undefined) { + if ((options === null || options === void 0 ? void 0 : options.sessionRequired) === false) { + logger_1.logDebugMessage( + "getSession: returning undefined because accessToken is undefined and sessionRequired is false" ); + // there is no session that exists here, and the user wants session verification + // to be optional. So we return undefined. + return undefined; } - logger_1.logDebugMessage("getSession: Started"); - if (accessTokenString === undefined) { - if ((options === null || options === void 0 ? void 0 : options.sessionRequired) === false) { - logger_1.logDebugMessage( - "getSession: returning undefined because accessToken is undefined and sessionRequired is false" - ); - // there is no session that exists here, and the user wants session verification - // to be optional. So we return undefined. - return undefined; - } - logger_1.logDebugMessage("getSession: UNAUTHORISED because accessToken in request is undefined"); - throw new error_1.default({ - message: - "Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?", - type: error_1.default.UNAUTHORISED, - payload: { - // we do not clear the session here because of a - // race condition mentioned here: https://github.com/supertokens/supertokens-node/issues/17 - clearTokens: false, - }, - }); - } - let accessToken; - try { - accessToken = jwt_1.parseJWTWithoutSignatureVerification(accessTokenString); - accessToken_1.validateAccessTokenStructure(accessToken.payload, accessToken.version); - } catch (error) { - if ((options === null || options === void 0 ? void 0 : options.sessionRequired) === false) { - logger_1.logDebugMessage( - "getSession: Returning undefined because parsing failed and sessionRequired is false" - ); - return undefined; - } + logger_1.logDebugMessage("getSession: UNAUTHORISED because accessToken in request is undefined"); + throw new error_1.default({ + message: + "Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?", + type: error_1.default.UNAUTHORISED, + payload: { + // we do not clear the session here because of a + // race condition mentioned here: https://github.com/supertokens/supertokens-node/issues/17 + clearTokens: false, + }, + }); + } + let accessToken; + try { + accessToken = jwt_1.parseJWTWithoutSignatureVerification(accessTokenString); + accessToken_1.validateAccessTokenStructure(accessToken.payload, accessToken.version); + } catch (error) { + if ((options === null || options === void 0 ? void 0 : options.sessionRequired) === false) { logger_1.logDebugMessage( - "getSession: UNAUTHORISED because the accessToken couldn't be parsed or had an invalid structure" + "getSession: Returning undefined because parsing failed and sessionRequired is false" ); - throw new error_1.default({ - message: "Token parsing failed", - type: "UNAUTHORISED", - payload: { clearTokens: false }, - }); + return undefined; } - const response = yield SessionFunctions.getSession( - helpers, - accessToken, - antiCsrfToken, - (options === null || options === void 0 ? void 0 : options.antiCsrfCheck) !== false, - (options === null || options === void 0 ? void 0 : options.checkDatabase) === true + logger_1.logDebugMessage( + "getSession: UNAUTHORISED because the accessToken couldn't be parsed or had an invalid structure" ); - logger_1.logDebugMessage("getSession: Success!"); - const payload = - accessToken.version >= 3 - ? response.accessToken !== undefined - ? jwt_1.parseJWTWithoutSignatureVerification(response.accessToken.token).payload - : accessToken.payload - : response.session.userDataInJWT; - const session = new sessionClass_1.default( - helpers, - response.accessToken !== undefined ? response.accessToken.token : accessTokenString, - cookieAndHeaders_1.buildFrontToken( - response.session.userId, - response.accessToken !== undefined ? response.accessToken.expiry : response.session.expiryTime, - payload - ), - undefined, // refresh - antiCsrfToken, - response.session.handle, + throw new error_1.default({ + message: "Token parsing failed", + type: "UNAUTHORISED", + payload: { clearTokens: false }, + }); + } + const response = await SessionFunctions.getSession( + helpers, + accessToken, + antiCsrfToken, + (options === null || options === void 0 ? void 0 : options.antiCsrfCheck) !== false, + (options === null || options === void 0 ? void 0 : options.checkDatabase) === true + ); + logger_1.logDebugMessage("getSession: Success!"); + const payload = + accessToken.version >= 3 + ? response.accessToken !== undefined + ? jwt_1.parseJWTWithoutSignatureVerification(response.accessToken.token).payload + : accessToken.payload + : response.session.userDataInJWT; + const session = new sessionClass_1.default( + helpers, + response.accessToken !== undefined ? response.accessToken.token : accessTokenString, + cookieAndHeaders_1.buildFrontToken( response.session.userId, - payload, - undefined, - response.accessToken !== undefined, - response.session.tenantId - ); - return session; - }); + response.accessToken !== undefined ? response.accessToken.expiry : response.session.expiryTime, + payload + ), + undefined, // refresh + antiCsrfToken, + response.session.handle, + response.session.userId, + response.session.recipeUserId, + payload, + undefined, + response.accessToken !== undefined, + response.session.tenantId + ); + return session; }, - validateClaims: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let accessTokenPayload = input.accessTokenPayload; - let accessTokenPayloadUpdate = undefined; - const origSessionClaimPayloadJSON = JSON.stringify(accessTokenPayload); - for (const validator of input.claimValidators) { + validateClaims: async function (input) { + let accessTokenPayload = input.accessTokenPayload; + let accessTokenPayloadUpdate = undefined; + const origSessionClaimPayloadJSON = JSON.stringify(accessTokenPayload); + for (const validator of input.claimValidators) { + logger_1.logDebugMessage("updateClaimsInPayloadIfNeeded checking shouldRefetch for " + validator.id); + if ("claim" in validator && (await validator.shouldRefetch(accessTokenPayload, input.userContext))) { + logger_1.logDebugMessage("updateClaimsInPayloadIfNeeded refetching " + validator.id); + const value = await validator.claim.fetchValue( + input.userId, + input.recipeUserId, + accessTokenPayload.tId === undefined ? constants_1.DEFAULT_TENANT_ID : accessTokenPayload.tId, + input.userContext + ); logger_1.logDebugMessage( - "updateClaimsInPayloadIfNeeded checking shouldRefetch for " + validator.id + "updateClaimsInPayloadIfNeeded " + validator.id + " refetch result " + JSON.stringify(value) ); - if ( - "claim" in validator && - (yield validator.shouldRefetch(accessTokenPayload, input.userContext)) - ) { - logger_1.logDebugMessage("updateClaimsInPayloadIfNeeded refetching " + validator.id); - const value = yield validator.claim.fetchValue( - input.userId, - accessTokenPayload.tId === undefined - ? constants_1.DEFAULT_TENANT_ID - : accessTokenPayload.tId, + if (value !== undefined) { + accessTokenPayload = validator.claim.addToPayload_internal( + accessTokenPayload, + value, input.userContext ); - logger_1.logDebugMessage( - "updateClaimsInPayloadIfNeeded " + validator.id + " refetch result " + JSON.stringify(value) - ); - if (value !== undefined) { - accessTokenPayload = validator.claim.addToPayload_internal( - accessTokenPayload, - value, - input.userContext - ); - } } } - if (JSON.stringify(accessTokenPayload) !== origSessionClaimPayloadJSON) { - accessTokenPayloadUpdate = accessTokenPayload; - } - const invalidClaims = yield utils_1.validateClaimsInPayload( - input.claimValidators, - accessTokenPayload, - input.userContext - ); - return { - invalidClaims, - accessTokenPayloadUpdate, - }; - }); - }, - validateClaimsInJWTPayload: function (input) { - return __awaiter(this, void 0, void 0, function* () { - // We skip refetching here, because we have no way of updating the JWT payload here - // if we have access to the entire session other methods can be used to do validation while updating - const invalidClaims = yield utils_1.validateClaimsInPayload( - input.claimValidators, - input.jwtPayload, - input.userContext - ); - return { - status: "OK", - invalidClaims, - }; - }); + } + if (JSON.stringify(accessTokenPayload) !== origSessionClaimPayloadJSON) { + accessTokenPayloadUpdate = accessTokenPayload; + } + const invalidClaims = await utils_1.validateClaimsInPayload( + input.claimValidators, + accessTokenPayload, + input.userContext + ); + return { + invalidClaims, + accessTokenPayloadUpdate, + }; }, - getSessionInformation: function ({ sessionHandle }) { - return __awaiter(this, void 0, void 0, function* () { - return SessionFunctions.getSessionInformation(helpers, sessionHandle); - }); + getSessionInformation: async function ({ sessionHandle }) { + return SessionFunctions.getSessionInformation(helpers, sessionHandle); }, - refreshSession: function ({ refreshToken, antiCsrfToken, disableAntiCsrf }) { - return __awaiter(this, void 0, void 0, function* () { - if (disableAntiCsrf !== true && config.antiCsrf === "VIA_CUSTOM_HEADER") { - throw new Error( - "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set antiCsrfCheck to false" - ); - } - logger_1.logDebugMessage("refreshSession: Started"); - const response = yield SessionFunctions.refreshSession( - helpers, - refreshToken, - antiCsrfToken, - disableAntiCsrf + refreshSession: async function ({ refreshToken, antiCsrfToken, disableAntiCsrf }) { + if (disableAntiCsrf !== true && config.antiCsrf === "VIA_CUSTOM_HEADER") { + throw new Error( + "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set antiCsrfCheck to false" ); - logger_1.logDebugMessage("refreshSession: Success!"); - const payload = jwt_1.parseJWTWithoutSignatureVerification(response.accessToken.token).payload; - return new sessionClass_1.default( - helpers, - response.accessToken.token, - cookieAndHeaders_1.buildFrontToken(response.session.userId, response.accessToken.expiry, payload), - response.refreshToken, - response.antiCsrfToken, - response.session.handle, - response.session.userId, - payload, - undefined, - true, - payload.tId - ); - }); + } + logger_1.logDebugMessage("refreshSession: Started"); + const response = await SessionFunctions.refreshSession( + helpers, + refreshToken, + antiCsrfToken, + disableAntiCsrf + ); + logger_1.logDebugMessage("refreshSession: Success!"); + const payload = jwt_1.parseJWTWithoutSignatureVerification(response.accessToken.token).payload; + return new sessionClass_1.default( + helpers, + response.accessToken.token, + cookieAndHeaders_1.buildFrontToken(response.session.userId, response.accessToken.expiry, payload), + response.refreshToken, + response.antiCsrfToken, + response.session.handle, + response.session.userId, + response.session.recipeUserId, + payload, + undefined, + true, + payload.tId + ); }, - regenerateAccessToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let newAccessTokenPayload = - input.newAccessTokenPayload === null || input.newAccessTokenPayload === undefined - ? {} - : input.newAccessTokenPayload; - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/regenerate"), - { - accessToken: input.accessToken, - userDataInJWT: newAccessTokenPayload, - } - ); - if (response.status === "UNAUTHORISED") { - return undefined; + regenerateAccessToken: async function (input) { + let newAccessTokenPayload = + input.newAccessTokenPayload === null || input.newAccessTokenPayload === undefined + ? {} + : input.newAccessTokenPayload; + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/session/regenerate"), + { + accessToken: input.accessToken, + userDataInJWT: newAccessTokenPayload, } - return { - status: response.status, - session: { - handle: response.session.handle, - userId: response.session.userId, - userDataInJWT: response.session.userDataInJWT, - tenantId: response.session.tenantId, - }, - accessToken: response.accessToken, - }; - }); + ); + if (response.status === "UNAUTHORISED") { + return undefined; + } + return { + status: response.status, + session: { + handle: response.session.handle, + userId: response.session.userId, + recipeUserId: new recipeUserId_1.default(response.session.recipeUserId), + userDataInJWT: response.session.userDataInJWT, + tenantId: response.session.tenantId, + }, + accessToken: response.accessToken, + }; }, - revokeAllSessionsForUser: function ({ userId, tenantId, revokeAcrossAllTenants }) { - return SessionFunctions.revokeAllSessionsForUser(helpers, userId, tenantId, revokeAcrossAllTenants); + revokeAllSessionsForUser: function ({ + userId, + tenantId, + revokeAcrossAllTenants, + revokeSessionsForLinkedAccounts, + }) { + return SessionFunctions.revokeAllSessionsForUser( + helpers, + userId, + revokeSessionsForLinkedAccounts, + tenantId, + revokeAcrossAllTenants + ); }, - getAllSessionHandlesForUser: function ({ userId, tenantId, fetchAcrossAllTenants }) { - return SessionFunctions.getAllSessionHandlesForUser(helpers, userId, tenantId, fetchAcrossAllTenants); + getAllSessionHandlesForUser: function ({ + userId, + fetchSessionsForAllLinkedAccounts, + tenantId, + fetchAcrossAllTenants, + }) { + return SessionFunctions.getAllSessionHandlesForUser( + helpers, + userId, + fetchSessionsForAllLinkedAccounts, + tenantId, + fetchAcrossAllTenants + ); }, revokeSession: function ({ sessionHandle }) { return SessionFunctions.revokeSession(helpers, sessionHandle); @@ -395,47 +343,41 @@ function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverride updateSessionDataInDatabase: function ({ sessionHandle, newSessionData }) { return SessionFunctions.updateSessionDataInDatabase(helpers, sessionHandle, newSessionData); }, - mergeIntoAccessTokenPayload: function ({ sessionHandle, accessTokenPayloadUpdate, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const sessionInfo = yield this.getSessionInformation({ sessionHandle, userContext }); - if (sessionInfo === undefined) { - return false; - } - let newAccessTokenPayload = Object.assign({}, sessionInfo.customClaimsInAccessTokenPayload); - for (const key of exports.protectedProps) { + mergeIntoAccessTokenPayload: async function ({ sessionHandle, accessTokenPayloadUpdate, userContext }) { + const sessionInfo = await this.getSessionInformation({ sessionHandle, userContext }); + if (sessionInfo === undefined) { + return false; + } + let newAccessTokenPayload = Object.assign({}, sessionInfo.customClaimsInAccessTokenPayload); + for (const key of constants_2.protectedProps) { + delete newAccessTokenPayload[key]; + } + newAccessTokenPayload = Object.assign(Object.assign({}, newAccessTokenPayload), accessTokenPayloadUpdate); + for (const key of Object.keys(accessTokenPayloadUpdate)) { + if (accessTokenPayloadUpdate[key] === null) { delete newAccessTokenPayload[key]; } - newAccessTokenPayload = Object.assign( - Object.assign({}, newAccessTokenPayload), - accessTokenPayloadUpdate - ); - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete newAccessTokenPayload[key]; - } - } - return SessionFunctions.updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPayload); - }); + } + return SessionFunctions.updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPayload); }, - fetchAndSetClaim: function (input) { - return __awaiter(this, void 0, void 0, function* () { - const sessionInfo = yield this.getSessionInformation({ - sessionHandle: input.sessionHandle, - userContext: input.userContext, - }); - if (sessionInfo === undefined) { - return false; - } - const accessTokenPayloadUpdate = yield input.claim.build( - sessionInfo.userId, - sessionInfo.tenantId, - input.userContext - ); - return this.mergeIntoAccessTokenPayload({ - sessionHandle: input.sessionHandle, - accessTokenPayloadUpdate, - userContext: input.userContext, - }); + fetchAndSetClaim: async function (input) { + const sessionInfo = await this.getSessionInformation({ + sessionHandle: input.sessionHandle, + userContext: input.userContext, + }); + if (sessionInfo === undefined) { + return false; + } + const accessTokenPayloadUpdate = await input.claim.build( + sessionInfo.userId, + sessionInfo.recipeUserId, + sessionInfo.tenantId, + input.userContext + ); + return this.mergeIntoAccessTokenPayload({ + sessionHandle: input.sessionHandle, + accessTokenPayloadUpdate, + userContext: input.userContext, }); }, setClaimValue: function (input) { @@ -446,25 +388,20 @@ function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverride userContext: input.userContext, }); }, - getClaimValue: function (input) { - return __awaiter(this, void 0, void 0, function* () { - const sessionInfo = yield this.getSessionInformation({ - sessionHandle: input.sessionHandle, - userContext: input.userContext, - }); - if (sessionInfo === undefined) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } + getClaimValue: async function (input) { + const sessionInfo = await this.getSessionInformation({ + sessionHandle: input.sessionHandle, + userContext: input.userContext, + }); + if (sessionInfo === undefined) { return { - status: "OK", - value: input.claim.getValueFromPayload( - sessionInfo.customClaimsInAccessTokenPayload, - input.userContext - ), + status: "SESSION_DOES_NOT_EXIST_ERROR", }; - }); + } + return { + status: "OK", + value: input.claim.getValueFromPayload(sessionInfo.customClaimsInAccessTokenPayload, input.userContext), + }; }, removeClaim: function (input) { const accessTokenPayloadUpdate = input.claim.removeFromPayloadByMerge_internal({}, input.userContext); diff --git a/lib/build/recipe/session/sessionClass.d.ts b/lib/build/recipe/session/sessionClass.d.ts index 092cb8e83..2e071042d 100644 --- a/lib/build/recipe/session/sessionClass.d.ts +++ b/lib/build/recipe/session/sessionClass.d.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { SessionClaim, SessionClaimValidator, SessionContainerInterface, ReqResInfo, TokenInfo } from "./types"; import { Helpers } from "./recipeImplementation"; +import RecipeUserId from "../../recipeUserId"; export default class Session implements SessionContainerInterface { protected helpers: Helpers; protected accessToken: string; @@ -9,6 +10,7 @@ export default class Session implements SessionContainerInterface { protected antiCsrfToken: string | undefined; protected sessionHandle: string; protected userId: string; + protected recipeUserId: RecipeUserId; protected userDataInAccessToken: any; protected reqResInfo: ReqResInfo | undefined; protected accessTokenUpdated: boolean; @@ -21,11 +23,13 @@ export default class Session implements SessionContainerInterface { antiCsrfToken: string | undefined, sessionHandle: string, userId: string, + recipeUserId: RecipeUserId, userDataInAccessToken: any, reqResInfo: ReqResInfo | undefined, accessTokenUpdated: boolean, tenantId: string ); + getRecipeUserId(_userContext?: any): RecipeUserId; revokeSession(userContext?: any): Promise; getSessionDataFromDatabase(userContext?: any): Promise; updateSessionDataInDatabase(newSessionData: any, userContext?: any): Promise; diff --git a/lib/build/recipe/session/sessionClass.js b/lib/build/recipe/session/sessionClass.js index f38bee793..ec1a2ddd6 100644 --- a/lib/build/recipe/session/sessionClass.js +++ b/lib/build/recipe/session/sessionClass.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,10 +21,10 @@ Object.defineProperty(exports, "__esModule", { value: true }); */ const cookieAndHeaders_1 = require("./cookieAndHeaders"); const error_1 = __importDefault(require("./error")); -const recipeImplementation_1 = require("./recipeImplementation"); const utils_1 = require("./utils"); const jwt_1 = require("./jwt"); const logger_1 = require("../../logger"); +const constants_1 = require("./constants"); class Session { constructor( helpers, @@ -65,6 +34,7 @@ class Session { antiCsrfToken, sessionHandle, userId, + recipeUserId, userDataInAccessToken, reqResInfo, accessTokenUpdated, @@ -77,68 +47,62 @@ class Session { this.antiCsrfToken = antiCsrfToken; this.sessionHandle = sessionHandle; this.userId = userId; + this.recipeUserId = recipeUserId; this.userDataInAccessToken = userDataInAccessToken; this.reqResInfo = reqResInfo; this.accessTokenUpdated = accessTokenUpdated; this.tenantId = tenantId; } - revokeSession(userContext) { - return __awaiter(this, void 0, void 0, function* () { - yield this.helpers.getRecipeImpl().revokeSession({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - if (this.reqResInfo !== undefined) { - // we do not check the output of calling revokeSession - // before clearing the cookies because we are revoking the - // current API request's session. - // If we instead clear the cookies only when revokeSession - // returns true, it can cause this kind of a bug: - // https://github.com/supertokens/supertokens-node/issues/343 - cookieAndHeaders_1.clearSession( - this.helpers.config, - this.reqResInfo.res, - this.reqResInfo.transferMethod - ); - } + getRecipeUserId(_userContext) { + return this.recipeUserId; + } + async revokeSession(userContext) { + await this.helpers.getRecipeImpl().revokeSession({ + sessionHandle: this.sessionHandle, + userContext: userContext === undefined ? {} : userContext, }); + if (this.reqResInfo !== undefined) { + // we do not check the output of calling revokeSession + // before clearing the cookies because we are revoking the + // current API request's session. + // If we instead clear the cookies only when revokeSession + // returns true, it can cause this kind of a bug: + // https://github.com/supertokens/supertokens-node/issues/343 + cookieAndHeaders_1.clearSession(this.helpers.config, this.reqResInfo.res, this.reqResInfo.transferMethod); + } } - getSessionDataFromDatabase(userContext) { - return __awaiter(this, void 0, void 0, function* () { - let sessionInfo = yield this.helpers.getRecipeImpl().getSessionInformation({ + async getSessionDataFromDatabase(userContext) { + let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ + sessionHandle: this.sessionHandle, + userContext: userContext === undefined ? {} : userContext, + }); + if (sessionInfo === undefined) { + logger_1.logDebugMessage( + "getSessionDataFromDatabase: Throwing UNAUTHORISED because session does not exist anymore" + ); + throw new error_1.default({ + message: "Session does not exist anymore", + type: error_1.default.UNAUTHORISED, + }); + } + return sessionInfo.sessionDataInDatabase; + } + async updateSessionDataInDatabase(newSessionData, userContext) { + if ( + !(await this.helpers.getRecipeImpl().updateSessionDataInDatabase({ sessionHandle: this.sessionHandle, + newSessionData, userContext: userContext === undefined ? {} : userContext, + })) + ) { + logger_1.logDebugMessage( + "updateSessionDataInDatabase: Throwing UNAUTHORISED because session does not exist anymore" + ); + throw new error_1.default({ + message: "Session does not exist anymore", + type: error_1.default.UNAUTHORISED, }); - if (sessionInfo === undefined) { - logger_1.logDebugMessage( - "getSessionDataFromDatabase: Throwing UNAUTHORISED because session does not exist anymore" - ); - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - return sessionInfo.sessionDataInDatabase; - }); - } - updateSessionDataInDatabase(newSessionData, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if ( - !(yield this.helpers.getRecipeImpl().updateSessionDataInDatabase({ - sessionHandle: this.sessionHandle, - newSessionData, - userContext: userContext === undefined ? {} : userContext, - })) - ) { - logger_1.logDebugMessage( - "updateSessionDataInDatabase: Throwing UNAUTHORISED because session does not exist anymore" - ); - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - }); + } } getUserId(_userContext) { return this.userId; @@ -166,124 +130,118 @@ class Session { }; } // Any update to this function should also be reflected in the respective JWT version - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let newAccessTokenPayload = Object.assign({}, this.getAccessTokenPayload(userContext)); - for (const key of recipeImplementation_1.protectedProps) { + async mergeIntoAccessTokenPayload(accessTokenPayloadUpdate, userContext) { + let newAccessTokenPayload = Object.assign({}, this.getAccessTokenPayload(userContext)); + for (const key of constants_1.protectedProps) { + delete newAccessTokenPayload[key]; + } + newAccessTokenPayload = Object.assign(Object.assign({}, newAccessTokenPayload), accessTokenPayloadUpdate); + for (const key of Object.keys(accessTokenPayloadUpdate)) { + if (accessTokenPayloadUpdate[key] === null) { delete newAccessTokenPayload[key]; } - newAccessTokenPayload = Object.assign(Object.assign({}, newAccessTokenPayload), accessTokenPayloadUpdate); - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete newAccessTokenPayload[key]; - } - } - let response = yield this.helpers.getRecipeImpl().regenerateAccessToken({ - accessToken: this.getAccessToken(), - newAccessTokenPayload, - userContext: userContext === undefined ? {} : userContext, - }); - if (response === undefined) { - logger_1.logDebugMessage( - "mergeIntoAccessTokenPayload: Throwing UNAUTHORISED because session does not exist anymore" - ); - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - if (response.accessToken !== undefined) { - const respToken = jwt_1.parseJWTWithoutSignatureVerification(response.accessToken.token); - const payload = respToken.version < 3 ? response.session.userDataInJWT : respToken.payload; - this.userDataInAccessToken = payload; - this.accessToken = response.accessToken.token; - this.frontToken = cookieAndHeaders_1.buildFrontToken(this.userId, response.accessToken.expiry, payload); - this.accessTokenUpdated = true; - if (this.reqResInfo !== undefined) { - // We need to cast to let TS know that the accessToken in the response is defined (and we don't overwrite it with undefined) - utils_1.setAccessTokenInResponse( - this.reqResInfo.res, - this.accessToken, - this.frontToken, - this.helpers.config, - this.reqResInfo.transferMethod - ); - } - } else { - // This case means that the access token has expired between the validation and this update - // We can't update the access token on the FE, as it will need to call refresh anyway but we handle this as a successful update during this request. - // the changes will be reflected on the FE after refresh is called - this.userDataInAccessToken = Object.assign( - Object.assign({}, this.getAccessTokenPayload()), - response.session.userDataInJWT - ); - } + } + let response = await this.helpers.getRecipeImpl().regenerateAccessToken({ + accessToken: this.getAccessToken(), + newAccessTokenPayload, + userContext: userContext === undefined ? {} : userContext, }); - } - getTimeCreated(userContext) { - return __awaiter(this, void 0, void 0, function* () { - let sessionInfo = yield this.helpers.getRecipeImpl().getSessionInformation({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + if (response === undefined) { + logger_1.logDebugMessage( + "mergeIntoAccessTokenPayload: Throwing UNAUTHORISED because session does not exist anymore" + ); + throw new error_1.default({ + message: "Session does not exist anymore", + type: error_1.default.UNAUTHORISED, }); - if (sessionInfo === undefined) { - logger_1.logDebugMessage( - "getTimeCreated: Throwing UNAUTHORISED because session does not exist anymore" + } + if (response.accessToken !== undefined) { + const respToken = jwt_1.parseJWTWithoutSignatureVerification(response.accessToken.token); + const payload = respToken.version < 3 ? response.session.userDataInJWT : respToken.payload; + this.userDataInAccessToken = payload; + this.accessToken = response.accessToken.token; + this.frontToken = cookieAndHeaders_1.buildFrontToken(this.userId, response.accessToken.expiry, payload); + this.accessTokenUpdated = true; + if (this.reqResInfo !== undefined) { + // We need to cast to let TS know that the accessToken in the response is defined (and we don't overwrite it with undefined) + utils_1.setAccessTokenInResponse( + this.reqResInfo.res, + this.accessToken, + this.frontToken, + this.helpers.config, + this.reqResInfo.transferMethod ); - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); } - return sessionInfo.timeCreated; - }); + } else { + // This case means that the access token has expired between the validation and this update + // We can't update the access token on the FE, as it will need to call refresh anyway but we handle this as a successful update during this request. + // the changes will be reflected on the FE after refresh is called + this.userDataInAccessToken = Object.assign( + Object.assign({}, this.getAccessTokenPayload()), + response.session.userDataInJWT + ); + } } - getExpiry(userContext) { - return __awaiter(this, void 0, void 0, function* () { - let sessionInfo = yield this.helpers.getRecipeImpl().getSessionInformation({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + async getTimeCreated(userContext) { + let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ + sessionHandle: this.sessionHandle, + userContext: userContext === undefined ? {} : userContext, + }); + if (sessionInfo === undefined) { + logger_1.logDebugMessage("getTimeCreated: Throwing UNAUTHORISED because session does not exist anymore"); + throw new error_1.default({ + message: "Session does not exist anymore", + type: error_1.default.UNAUTHORISED, }); - if (sessionInfo === undefined) { - logger_1.logDebugMessage("getExpiry: Throwing UNAUTHORISED because session does not exist anymore"); - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - return sessionInfo.expiry; + } + return sessionInfo.timeCreated; + } + async getExpiry(userContext) { + let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ + sessionHandle: this.sessionHandle, + userContext: userContext === undefined ? {} : userContext, }); + if (sessionInfo === undefined) { + logger_1.logDebugMessage("getExpiry: Throwing UNAUTHORISED because session does not exist anymore"); + throw new error_1.default({ + message: "Session does not exist anymore", + type: error_1.default.UNAUTHORISED, + }); + } + return sessionInfo.expiry; } // Any update to this function should also be reflected in the respective JWT version - assertClaims(claimValidators, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let validateClaimResponse = yield this.helpers.getRecipeImpl().validateClaims({ - accessTokenPayload: this.getAccessTokenPayload(userContext), - userId: this.getUserId(userContext), - claimValidators, - userContext, - }); - if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) { - for (const key of recipeImplementation_1.protectedProps) { - delete validateClaimResponse.accessTokenPayloadUpdate[key]; - } - yield this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext); - } - if (validateClaimResponse.invalidClaims.length !== 0) { - throw new error_1.default({ - type: "INVALID_CLAIMS", - message: "INVALID_CLAIMS", - payload: validateClaimResponse.invalidClaims, - }); - } + async assertClaims(claimValidators, userContext) { + let validateClaimResponse = await this.helpers.getRecipeImpl().validateClaims({ + accessTokenPayload: this.getAccessTokenPayload(userContext), + userId: this.getUserId(userContext), + recipeUserId: this.getRecipeUserId(userContext), + claimValidators, + userContext, }); + if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) { + for (const key of constants_1.protectedProps) { + delete validateClaimResponse.accessTokenPayloadUpdate[key]; + } + await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext); + } + if (validateClaimResponse.invalidClaims.length !== 0) { + throw new error_1.default({ + type: "INVALID_CLAIMS", + message: "INVALID_CLAIMS", + payload: validateClaimResponse.invalidClaims, + }); + } } // Any update to this function should also be reflected in the respective JWT version - fetchAndSetClaim(claim, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const update = yield claim.build(this.getUserId(userContext), this.getTenantId(), userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - }); + async fetchAndSetClaim(claim, userContext) { + const update = await claim.build( + this.getUserId(userContext), + this.getRecipeUserId(userContext), + this.getTenantId(), + userContext + ); + return this.mergeIntoAccessTokenPayload(update, userContext); } // Any update to this function should also be reflected in the respective JWT version setClaimValue(claim, value, userContext) { @@ -291,10 +249,8 @@ class Session { return this.mergeIntoAccessTokenPayload(update, userContext); } // Any update to this function should also be reflected in the respective JWT version - getClaimValue(claim, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return claim.getValueFromPayload(yield this.getAccessTokenPayload(userContext), userContext); - }); + async getClaimValue(claim, userContext) { + return claim.getValueFromPayload(await this.getAccessTokenPayload(userContext), userContext); } // Any update to this function should also be reflected in the respective JWT version removeClaim(claim, userContext) { diff --git a/lib/build/recipe/session/sessionFunctions.d.ts b/lib/build/recipe/session/sessionFunctions.d.ts index a714ca947..89848d130 100644 --- a/lib/build/recipe/session/sessionFunctions.d.ts +++ b/lib/build/recipe/session/sessionFunctions.d.ts @@ -2,13 +2,14 @@ import { ParsedJWTInfo } from "./jwt"; import { CreateOrRefreshAPIResponse, SessionInformation } from "./types"; import { Helpers } from "./recipeImplementation"; +import RecipeUserId from "../../recipeUserId"; /** * @description call this to "login" a user. */ export declare function createNewSession( helpers: Helpers, tenantId: string, - userId: string, + recipeUserId: RecipeUserId, disableAntiCsrf: boolean, accessTokenPayload?: any, sessionDataInDatabase?: any @@ -26,6 +27,7 @@ export declare function getSession( session: { handle: string; userId: string; + recipeUserId: RecipeUserId; userDataInJWT: any; expiryTime: number; tenantId: string; @@ -61,6 +63,7 @@ export declare function refreshSession( export declare function revokeAllSessionsForUser( helpers: Helpers, userId: string, + revokeSessionsForLinkedAccounts: boolean, tenantId?: string, revokeAcrossAllTenants?: boolean ): Promise; @@ -70,6 +73,7 @@ export declare function revokeAllSessionsForUser( export declare function getAllSessionHandlesForUser( helpers: Helpers, userId: string, + fetchSessionsForAllLinkedAccounts: boolean, tenantId?: string, fetchAcrossAllTenants?: boolean ): Promise; diff --git a/lib/build/recipe/session/sessionFunctions.js b/lib/build/recipe/session/sessionFunctions.js index bcca6957c..dcb3b07b1 100644 --- a/lib/build/recipe/session/sessionFunctions.js +++ b/lib/build/recipe/session/sessionFunctions.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -55,420 +24,409 @@ const accessToken_1 = require("./accessToken"); const error_1 = __importDefault(require("./error")); const processState_1 = require("../../processState"); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const recipeImplementation_1 = require("./recipeImplementation"); const utils_1 = require("../../utils"); const logger_1 = require("../../logger"); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); +const constants_2 = require("./constants"); /** * @description call this to "login" a user. */ -function createNewSession( +async function createNewSession( helpers, tenantId, - userId, + recipeUserId, disableAntiCsrf, accessTokenPayload = {}, sessionDataInDatabase = {} ) { - return __awaiter(this, void 0, void 0, function* () { - accessTokenPayload = accessTokenPayload === null || accessTokenPayload === undefined ? {} : accessTokenPayload; - sessionDataInDatabase = - sessionDataInDatabase === null || sessionDataInDatabase === undefined ? {} : sessionDataInDatabase; - const requestBody = { - userId, - userDataInJWT: accessTokenPayload, - userDataInDatabase: sessionDataInDatabase, - useDynamicSigningKey: helpers.config.useDynamicAccessTokenSigningKey, - enableAntiCsrf: !disableAntiCsrf && helpers.config.antiCsrf === "VIA_TOKEN", - }; - const response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/session`), - requestBody - ); - return { - session: { - handle: response.session.handle, - userId: response.session.userId, - userDataInJWT: response.session.userDataInJWT, - tenantId: response.session.tenantId, - }, - accessToken: { - token: response.accessToken.token, - createdTime: response.accessToken.createdTime, - expiry: response.accessToken.expiry, - }, - refreshToken: { - token: response.refreshToken.token, - createdTime: response.refreshToken.createdTime, - expiry: response.refreshToken.expiry, - }, - antiCsrfToken: response.antiCsrfToken, - }; - }); + accessTokenPayload = accessTokenPayload === null || accessTokenPayload === undefined ? {} : accessTokenPayload; + sessionDataInDatabase = + sessionDataInDatabase === null || sessionDataInDatabase === undefined ? {} : sessionDataInDatabase; + const requestBody = { + userId: recipeUserId.getAsString(), + userDataInJWT: Object.assign({}, accessTokenPayload), + userDataInDatabase: sessionDataInDatabase, + useDynamicSigningKey: helpers.config.useDynamicAccessTokenSigningKey, + enableAntiCsrf: !disableAntiCsrf && helpers.config.antiCsrf === "VIA_TOKEN", + }; + let response = await helpers.querier.sendPostRequest( + new normalisedURLPath_1.default(`/${tenantId}/recipe/session`), + requestBody + ); + return { + session: { + handle: response.session.handle, + userId: response.session.userId, + recipeUserId: new recipeUserId_1.default(response.session.recipeUserId), + userDataInJWT: response.session.userDataInJWT, + tenantId: response.session.tenantId, + }, + accessToken: { + token: response.accessToken.token, + createdTime: response.accessToken.createdTime, + expiry: response.accessToken.expiry, + }, + refreshToken: { + token: response.refreshToken.token, + createdTime: response.refreshToken.createdTime, + expiry: response.refreshToken.expiry, + }, + antiCsrfToken: response.antiCsrfToken, + }; } exports.createNewSession = createNewSession; /** * @description authenticates a session. To be used in APIs that require authentication */ -function getSession(helpers, parsedAccessToken, antiCsrfToken, doAntiCsrfCheck, alwaysCheckCore) { +async function getSession(helpers, parsedAccessToken, antiCsrfToken, doAntiCsrfCheck, alwaysCheckCore) { var _a, _b; - return __awaiter(this, void 0, void 0, function* () { - let accessTokenInfo; - try { - /** - * get access token info using jwks - */ - accessTokenInfo = yield accessToken_1.getInfoFromAccessToken( - parsedAccessToken, - helpers.JWKS, - helpers.config.antiCsrf === "VIA_TOKEN" && doAntiCsrfCheck - ); - } catch (err) { - /** - * if error type is not TRY_REFRESH_TOKEN, we return the - * error to the user - */ - if (err.type !== error_1.default.TRY_REFRESH_TOKEN) { + let accessTokenInfo; + try { + /** + * get access token info using jwks + */ + accessTokenInfo = await accessToken_1.getInfoFromAccessToken( + parsedAccessToken, + helpers.JWKS, + helpers.config.antiCsrf === "VIA_TOKEN" && doAntiCsrfCheck + ); + } catch (err) { + /** + * if error type is not TRY_REFRESH_TOKEN, we return the + * error to the user + */ + if (err.type !== error_1.default.TRY_REFRESH_TOKEN) { + throw err; + } + /** + * if it comes here, it means token verification has failed. + * It may be due to: + * - signing key was updated and this token was signed with new key + * - access token is actually expired + * - access token was signed with the older signing key + * + * if access token is actually expired, we don't need to call core and + * just return TRY_REFRESH_TOKEN to the client + * + * if access token creation time is after this signing key was created + * we need to call core as there are chances that the token + * was signed with the updated signing key + * + * if access token creation time is before oldest signing key was created, + * so if foundASigningKeyThatIsOlderThanTheAccessToken is still false after + * the loop we just return TRY_REFRESH_TOKEN + */ + if (parsedAccessToken.version < 3) { + let payload = parsedAccessToken.payload; + const timeCreated = accessToken_1.sanitizeNumberInput(payload.timeCreated); + const expiryTime = accessToken_1.sanitizeNumberInput(payload.expiryTime); + if (expiryTime === undefined || timeCreated == undefined) { throw err; } - /** - * if it comes here, it means token verification has failed. - * It may be due to: - * - signing key was updated and this token was signed with new key - * - access token is actually expired - * - access token was signed with the older signing key - * - * if access token is actually expired, we don't need to call core and - * just return TRY_REFRESH_TOKEN to the client - * - * if access token creation time is after this signing key was created - * we need to call core as there are chances that the token - * was signed with the updated signing key - * - * if access token creation time is before oldest signing key was created, - * so if foundASigningKeyThatIsOlderThanTheAccessToken is still false after - * the loop we just return TRY_REFRESH_TOKEN - */ - if (parsedAccessToken.version < 3) { - let payload = parsedAccessToken.payload; - const timeCreated = accessToken_1.sanitizeNumberInput(payload.timeCreated); - const expiryTime = accessToken_1.sanitizeNumberInput(payload.expiryTime); - if (expiryTime === undefined || timeCreated == undefined) { - throw err; - } - if (expiryTime < Date.now()) { - throw err; - } - // We check if the token was created since the last time we refreshed the keys from the core - // Since we do not know the exact timing of the last refresh, we check against the max age - if (timeCreated <= Date.now() - recipeImplementation_1.JWKCacheMaxAgeInMs) { - throw err; - } - } else { - // Since v3 (and above) tokens contain a kid we can trust the cache-refresh mechanism of the jose library. - // This means we do not need to call the core since the signature wouldn't pass verification anyway. + if (expiryTime < Date.now()) { throw err; } - } - if (parsedAccessToken.version >= 3) { - const tokenUsesDynamicKey = parsedAccessToken.kid.startsWith("d-"); - if (tokenUsesDynamicKey !== helpers.config.useDynamicAccessTokenSigningKey) { - logger_1.logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because the access token doesn't match the useDynamicAccessTokenSigningKey in the config" - ); - throw new error_1.default({ - message: "The access token doesn't match the useDynamicAccessTokenSigningKey setting", - type: error_1.default.TRY_REFRESH_TOKEN, - }); + // We check if the token was created since the last time we refreshed the keys from the core + // Since we do not know the exact timing of the last refresh, we check against the max age + if (timeCreated <= Date.now() - constants_2.JWKCacheMaxAgeInMs) { + throw err; } + } else { + // Since v3 (and above) tokens contain a kid we can trust the cache-refresh mechanism of the jose library. + // This means we do not need to call the core since the signature wouldn't pass verification anyway. + throw err; } - // If we get here we either have a V2 token that doesn't pass verification or a valid V3> token - /** - * anti-csrf check if accesstokenInfo is not undefined, - * which means token verification was successful - */ - if (doAntiCsrfCheck) { - if (helpers.config.antiCsrf === "VIA_TOKEN") { - if (accessTokenInfo !== undefined) { - if (antiCsrfToken === undefined || antiCsrfToken !== accessTokenInfo.antiCsrfToken) { - if (antiCsrfToken === undefined) { - logger_1.logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request" - ); - throw new error_1.default({ - message: - "Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } else { - logger_1.logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token" - ); - throw new error_1.default({ - message: "anti-csrf check failed", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } + } + if (parsedAccessToken.version >= 3) { + const tokenUsesDynamicKey = parsedAccessToken.kid.startsWith("d-"); + if (tokenUsesDynamicKey !== helpers.config.useDynamicAccessTokenSigningKey) { + logger_1.logDebugMessage( + "getSession: Returning TRY_REFRESH_TOKEN because the access token doesn't match the useDynamicAccessTokenSigningKey in the config" + ); + throw new error_1.default({ + message: "The access token doesn't match the useDynamicAccessTokenSigningKey setting", + type: error_1.default.TRY_REFRESH_TOKEN, + }); + } + } + // If we get here we either have a V2 token that doesn't pass verification or a valid V3> token + /** + * anti-csrf check if accesstokenInfo is not undefined, + * which means token verification was successful + */ + if (doAntiCsrfCheck) { + if (helpers.config.antiCsrf === "VIA_TOKEN") { + if (accessTokenInfo !== undefined) { + if (antiCsrfToken === undefined || antiCsrfToken !== accessTokenInfo.antiCsrfToken) { + if (antiCsrfToken === undefined) { + logger_1.logDebugMessage( + "getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request" + ); + throw new error_1.default({ + message: + "Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API", + type: error_1.default.TRY_REFRESH_TOKEN, + }); + } else { + logger_1.logDebugMessage( + "getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token" + ); + throw new error_1.default({ + message: "anti-csrf check failed", + type: error_1.default.TRY_REFRESH_TOKEN, + }); } } - } else if (helpers.config.antiCsrf === "VIA_CUSTOM_HEADER") { - // The function should never be called by this (we check this outside the function as well) - // There we can add a bit more information to the error, so that's the primary check, this is just making sure. - throw new Error("Please either use VIA_TOKEN, NONE or call with doAntiCsrfCheck false"); } + } else if (helpers.config.antiCsrf === "VIA_CUSTOM_HEADER") { + // The function should never be called by this (we check this outside the function as well) + // There we can add a bit more information to the error, so that's the primary check, this is just making sure. + throw new Error("Please either use VIA_TOKEN, NONE or call with doAntiCsrfCheck false"); } - if ( - accessTokenInfo !== undefined && - !alwaysCheckCore && - accessTokenInfo.parentRefreshTokenHash1 === undefined - ) { - return { - session: { - handle: accessTokenInfo.sessionHandle, - userId: accessTokenInfo.userId, - userDataInJWT: accessTokenInfo.userData, - expiryTime: accessTokenInfo.expiryTime, - tenantId: accessTokenInfo.tenantId, - }, - }; - } - processState_1.ProcessState.getInstance().addState(processState_1.PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - let requestBody = { - accessToken: parsedAccessToken.rawTokenString, - antiCsrfToken, - doAntiCsrfCheck, - enableAntiCsrf: helpers.config.antiCsrf === "VIA_TOKEN", - checkDatabase: alwaysCheckCore, + } + if (accessTokenInfo !== undefined && !alwaysCheckCore && accessTokenInfo.parentRefreshTokenHash1 === undefined) { + return { + session: { + handle: accessTokenInfo.sessionHandle, + userId: accessTokenInfo.userId, + recipeUserId: accessTokenInfo.recipeUserId, + userDataInJWT: accessTokenInfo.userData, + expiryTime: accessTokenInfo.expiryTime, + tenantId: accessTokenInfo.tenantId, + }, }; - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/verify"), - requestBody - ); - if (response.status === "OK") { - delete response.status; - return Object.assign(Object.assign({}, response), { - session: { - handle: response.session.handle, - userId: response.session.userId, - expiryTime: - ((_a = response.accessToken) === null || _a === void 0 ? void 0 : _a.expiry) || // if we got a new accesstoken we take the expiry time from there - (accessTokenInfo === null || accessTokenInfo === void 0 - ? void 0 - : accessTokenInfo.expiryTime) || // if we didn't get a new access token but could validate the token take that info (alwaysCheckCore === true, or parentRefreshTokenHash1 !== null) - parsedAccessToken.payload["expiryTime"], - tenantId: - ((_b = response.session) === null || _b === void 0 ? void 0 : _b.tenantId) || - (accessTokenInfo === null || accessTokenInfo === void 0 ? void 0 : accessTokenInfo.tenantId) || - constants_1.DEFAULT_TENANT_ID, - userDataInJWT: response.session.userDataInJWT, - }, - }); - } else if (response.status === "UNAUTHORISED") { - logger_1.logDebugMessage("getSession: Returning UNAUTHORISED because of core response"); - throw new error_1.default({ - message: response.message, - type: error_1.default.UNAUTHORISED, - }); - } else { - logger_1.logDebugMessage("getSession: Returning TRY_REFRESH_TOKEN because of core response."); - throw new error_1.default({ - message: response.message, - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - }); + } + processState_1.ProcessState.getInstance().addState(processState_1.PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); + let requestBody = { + accessToken: parsedAccessToken.rawTokenString, + antiCsrfToken, + doAntiCsrfCheck, + enableAntiCsrf: helpers.config.antiCsrf === "VIA_TOKEN", + checkDatabase: alwaysCheckCore, + }; + let response = await helpers.querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/session/verify"), + requestBody + ); + if (response.status === "OK") { + delete response.status; + return Object.assign(Object.assign({}, response), { + session: { + handle: response.session.handle, + userId: response.session.userId, + recipeUserId: new recipeUserId_1.default(response.session.recipeUserId), + expiryTime: + ((_a = response.accessToken) === null || _a === void 0 ? void 0 : _a.expiry) || // if we got a new accesstoken we take the expiry time from there + (accessTokenInfo === null || accessTokenInfo === void 0 ? void 0 : accessTokenInfo.expiryTime) || // if we didn't get a new access token but could validate the token take that info (alwaysCheckCore === true, or parentRefreshTokenHash1 !== null) + parsedAccessToken.payload["expiryTime"], + tenantId: + ((_b = response.session) === null || _b === void 0 ? void 0 : _b.tenantId) || + (accessTokenInfo === null || accessTokenInfo === void 0 ? void 0 : accessTokenInfo.tenantId) || + constants_1.DEFAULT_TENANT_ID, + userDataInJWT: response.session.userDataInJWT, + }, + }); + } else if (response.status === "UNAUTHORISED") { + logger_1.logDebugMessage("getSession: Returning UNAUTHORISED because of core response"); + throw new error_1.default({ + message: response.message, + type: error_1.default.UNAUTHORISED, + }); + } else { + logger_1.logDebugMessage("getSession: Returning TRY_REFRESH_TOKEN because of core response."); + throw new error_1.default({ + message: response.message, + type: error_1.default.TRY_REFRESH_TOKEN, + }); + } } exports.getSession = getSession; /** * @description Retrieves session information from storage for a given session handle * @returns session data stored in the database, including userData and access token payload, or undefined if sessionHandle is invalid */ -function getSessionInformation(helpers, sessionHandle) { - return __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield helpers.querier.getAPIVersion(); - if (utils_1.maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error("Please use core version >= 3.5 to call this function."); - } - let response = yield helpers.querier.sendGetRequest(new normalisedURLPath_1.default(`/recipe/session`), { - sessionHandle, - }); - if (response.status === "OK") { - // Change keys to make them more readable - return { - sessionHandle: response.sessionHandle, - timeCreated: response.timeCreated, - expiry: response.expiry, - userId: response.userId, - sessionDataInDatabase: response.userDataInDatabase, - customClaimsInAccessTokenPayload: response.userDataInJWT, - tenantId: response.tenantId, - }; - } else { - return undefined; - } +async function getSessionInformation(helpers, sessionHandle) { + let apiVersion = await helpers.querier.getAPIVersion(); + if (utils_1.maxVersion(apiVersion, "2.7") === "2.7") { + throw new Error("Please use core version >= 3.5 to call this function."); + } + let response = await helpers.querier.sendGetRequest(new normalisedURLPath_1.default(`/recipe/session`), { + sessionHandle, }); + if (response.status === "OK") { + // Change keys to make them more readable + return { + sessionHandle: response.sessionHandle, + timeCreated: response.timeCreated, + expiry: response.expiry, + userId: response.userId, + recipeUserId: new recipeUserId_1.default(response.recipeUserId), + sessionDataInDatabase: response.userDataInDatabase, + customClaimsInAccessTokenPayload: response.userDataInJWT, + tenantId: response.tenantId, + }; + } else { + return undefined; + } } exports.getSessionInformation = getSessionInformation; /** * @description generates new access and refresh tokens for a given refresh token. Called when client's access token has expired. * @sideEffects calls onTokenTheftDetection if token theft is detected. */ -function refreshSession(helpers, refreshToken, antiCsrfToken, disableAntiCsrf) { - return __awaiter(this, void 0, void 0, function* () { - let requestBody = { - refreshToken, - antiCsrfToken, - enableAntiCsrf: !disableAntiCsrf && helpers.config.antiCsrf === "VIA_TOKEN", +async function refreshSession(helpers, refreshToken, antiCsrfToken, disableAntiCsrf) { + let requestBody = { + refreshToken, + antiCsrfToken, + enableAntiCsrf: !disableAntiCsrf && helpers.config.antiCsrf === "VIA_TOKEN", + }; + if (helpers.config.antiCsrf === "VIA_CUSTOM_HEADER" && !disableAntiCsrf) { + // The function should never be called by this (we check this outside the function as well) + // There we can add a bit more information to the error, so that's the primary check, this is just making sure. + throw new Error("Please either use VIA_TOKEN, NONE or call with doAntiCsrfCheck false"); + } + let response = await helpers.querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/session/refresh"), + requestBody + ); + if (response.status === "OK") { + return { + session: { + handle: response.session.handle, + userId: response.session.userId, + recipeUserId: new recipeUserId_1.default(response.session.recipeUserId), + userDataInJWT: response.session.userDataInJWT, + tenantId: response.session.tenantId, + }, + accessToken: { + token: response.accessToken.token, + createdTime: response.accessToken.createdTime, + expiry: response.accessToken.expiry, + }, + refreshToken: { + token: response.refreshToken.token, + createdTime: response.refreshToken.createdTime, + expiry: response.refreshToken.expiry, + }, + antiCsrfToken: response.antiCsrfToken, }; - if (helpers.config.antiCsrf === "VIA_CUSTOM_HEADER" && !disableAntiCsrf) { - // The function should never be called by this (we check this outside the function as well) - // There we can add a bit more information to the error, so that's the primary check, this is just making sure. - throw new Error("Please either use VIA_TOKEN, NONE or call with doAntiCsrfCheck false"); - } - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/refresh"), - requestBody - ); - if (response.status === "OK") { - return { - session: { - handle: response.session.handle, - userId: response.session.userId, - userDataInJWT: response.session.userDataInJWT, - tenantId: response.session.tenantId, - }, - accessToken: { - token: response.accessToken.token, - createdTime: response.accessToken.createdTime, - expiry: response.accessToken.expiry, - }, - refreshToken: { - token: response.refreshToken.token, - createdTime: response.refreshToken.createdTime, - expiry: response.refreshToken.expiry, - }, - antiCsrfToken: response.antiCsrfToken, - }; - } else if (response.status === "UNAUTHORISED") { - logger_1.logDebugMessage("refreshSession: Returning UNAUTHORISED because of core response"); - throw new error_1.default({ - message: response.message, - type: error_1.default.UNAUTHORISED, - }); - } else { - logger_1.logDebugMessage("refreshSession: Returning TOKEN_THEFT_DETECTED because of core response"); - throw new error_1.default({ - message: "Token theft detected", - payload: { - userId: response.session.userId, - sessionHandle: response.session.handle, - }, - type: error_1.default.TOKEN_THEFT_DETECTED, - }); - } - }); + } else if (response.status === "UNAUTHORISED") { + logger_1.logDebugMessage("refreshSession: Returning UNAUTHORISED because of core response"); + throw new error_1.default({ + message: response.message, + type: error_1.default.UNAUTHORISED, + }); + } else { + logger_1.logDebugMessage("refreshSession: Returning TOKEN_THEFT_DETECTED because of core response"); + throw new error_1.default({ + message: "Token theft detected", + payload: { + recipeUserId: new recipeUserId_1.default(response.session.recipeUserId), + userId: response.session.userId, + sessionHandle: response.session.handle, + }, + type: error_1.default.TOKEN_THEFT_DETECTED, + }); + } } exports.refreshSession = refreshSession; /** * @description deletes session info of a user from db. This only invalidates the refresh token. Not the access token. * Access tokens cannot be immediately invalidated. Unless we add a blacklisting method. Or changed the private key to sign them. */ -function revokeAllSessionsForUser(helpers, userId, tenantId, revokeAcrossAllTenants) { - return __awaiter(this, void 0, void 0, function* () { - if (tenantId === undefined) { - tenantId = constants_1.DEFAULT_TENANT_ID; +async function revokeAllSessionsForUser( + helpers, + userId, + revokeSessionsForLinkedAccounts, + tenantId, + revokeAcrossAllTenants +) { + if (tenantId === undefined) { + tenantId = constants_1.DEFAULT_TENANT_ID; + } + let response = await helpers.querier.sendPostRequest( + new normalisedURLPath_1.default(`/${tenantId}/recipe/session/remove`), + { + userId, + revokeSessionsForLinkedAccounts, + revokeAcrossAllTenants, } - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/session/remove`), - { - userId, - revokeAcrossAllTenants, - } - ); - return response.sessionHandlesRevoked; - }); + ); + return response.sessionHandlesRevoked; } exports.revokeAllSessionsForUser = revokeAllSessionsForUser; /** * @description gets all session handles for current user. Please do not call this unless this user is authenticated. */ -function getAllSessionHandlesForUser(helpers, userId, tenantId, fetchAcrossAllTenants) { - return __awaiter(this, void 0, void 0, function* () { - if (tenantId === undefined) { - tenantId = constants_1.DEFAULT_TENANT_ID; +async function getAllSessionHandlesForUser( + helpers, + userId, + fetchSessionsForAllLinkedAccounts, + tenantId, + fetchAcrossAllTenants +) { + if (tenantId === undefined) { + tenantId = constants_1.DEFAULT_TENANT_ID; + } + let response = await helpers.querier.sendGetRequest( + new normalisedURLPath_1.default(`/${tenantId}/recipe/session/user`), + { + userId, + fetchSessionsForAllLinkedAccounts, + fetchAcrossAllTenants, } - let response = yield helpers.querier.sendGetRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/session/user`), - { - userId, - fetchAcrossAllTenants, - } - ); - return response.sessionHandles; - }); + ); + return response.sessionHandles; } exports.getAllSessionHandlesForUser = getAllSessionHandlesForUser; /** * @description call to destroy one session * @returns true if session was deleted from db. Else false in case there was nothing to delete */ -function revokeSession(helpers, sessionHandle) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default(`/recipe/session/remove`), - { - sessionHandles: [sessionHandle], - } - ); - return response.sessionHandlesRevoked.length === 1; +async function revokeSession(helpers, sessionHandle) { + let response = await helpers.querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/session/remove"), { + sessionHandles: [sessionHandle], }); + return response.sessionHandlesRevoked.length === 1; } exports.revokeSession = revokeSession; /** * @description call to destroy multiple sessions * @returns list of sessions revoked */ -function revokeMultipleSessions(helpers, sessionHandles) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default(`/recipe/session/remove`), - { - sessionHandles, - } - ); - return response.sessionHandlesRevoked; +async function revokeMultipleSessions(helpers, sessionHandles) { + let response = await helpers.querier.sendPostRequest(new normalisedURLPath_1.default(`/recipe/session/remove`), { + sessionHandles, }); + return response.sessionHandlesRevoked; } exports.revokeMultipleSessions = revokeMultipleSessions; /** * @description: It provides no locking mechanism in case other processes are updating session data for this session as well. */ -function updateSessionDataInDatabase(helpers, sessionHandle, newSessionData) { - return __awaiter(this, void 0, void 0, function* () { - newSessionData = newSessionData === null || newSessionData === undefined ? {} : newSessionData; - let response = yield helpers.querier.sendPutRequest(new normalisedURLPath_1.default(`/recipe/session/data`), { - sessionHandle, - userDataInDatabase: newSessionData, - }); - if (response.status === "UNAUTHORISED") { - return false; - } - return true; +async function updateSessionDataInDatabase(helpers, sessionHandle, newSessionData) { + newSessionData = newSessionData === null || newSessionData === undefined ? {} : newSessionData; + let response = await helpers.querier.sendPutRequest(new normalisedURLPath_1.default(`/recipe/session/data`), { + sessionHandle, + userDataInDatabase: newSessionData, }); + if (response.status === "UNAUTHORISED") { + return false; + } + return true; } exports.updateSessionDataInDatabase = updateSessionDataInDatabase; -function updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPayload) { - return __awaiter(this, void 0, void 0, function* () { - newAccessTokenPayload = - newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; - let response = yield helpers.querier.sendPutRequest(new normalisedURLPath_1.default(`/recipe/jwt/data`), { - sessionHandle, - userDataInJWT: newAccessTokenPayload, - }); - if (response.status === "UNAUTHORISED") { - return false; - } - return true; +async function updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPayload) { + newAccessTokenPayload = + newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; + let response = await helpers.querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/jwt/data"), { + sessionHandle, + userDataInJWT: newAccessTokenPayload, }); + if (response.status === "UNAUTHORISED") { + return false; + } + return true; } exports.updateAccessTokenPayload = updateAccessTokenPayload; diff --git a/lib/build/recipe/session/sessionRequestFunctions.d.ts b/lib/build/recipe/session/sessionRequestFunctions.d.ts index c43bb7020..f94e2dff4 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.d.ts +++ b/lib/build/recipe/session/sessionRequestFunctions.d.ts @@ -2,6 +2,7 @@ import Recipe from "./recipe"; import { VerifySessionOptions, RecipeInterface, TypeNormalisedInput, SessionContainerInterface } from "./types"; import { NormalisedAppinfo } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export declare function getSessionFromRequest({ req, res, @@ -37,6 +38,7 @@ export declare function createNewSessionInRequest({ recipeInstance, accessTokenPayload, userId, + recipeUserId, config, appInfo, sessionDataInDatabase, @@ -48,6 +50,7 @@ export declare function createNewSessionInRequest({ recipeInstance: Recipe; accessTokenPayload: any; userId: string; + recipeUserId: RecipeUserId; config: TypeNormalisedInput; appInfo: NormalisedAppinfo; sessionDataInDatabase: any; diff --git a/lib/build/recipe/session/sessionRequestFunctions.js b/lib/build/recipe/session/sessionRequestFunctions.js index 5ab36ca75..27a22477a 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.js +++ b/lib/build/recipe/session/sessionRequestFunctions.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -49,180 +18,224 @@ const accessToken_1 = require("./accessToken"); const error_1 = __importDefault(require("./error")); // We are defining this here (and not exporting it) to reduce the scope of legacy code const LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = "sIdRefreshToken"; -function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("getSession: Started"); - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); - } - userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); - logger_1.logDebugMessage("getSession: Wrapping done"); - // This token isn't handled by getToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logger_1.logDebugMessage( - "getSession: Throwing TRY_REFRESH_TOKEN because the request is using a legacy session" - ); - // This could create a spike on refresh calls during the update of the backend SDK - throw new error_1.default({ - message: "using legacy session, please call the refresh API", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - const sessionOptional = (options === null || options === void 0 ? void 0 : options.sessionRequired) === false; - logger_1.logDebugMessage("getSession: optional validation: " + sessionOptional); - const accessTokens = {}; - // We check all token transfer methods for available access tokens - for (const transferMethod of constants_1.availableTokenTransferMethods) { - const tokenString = cookieAndHeaders_1.getToken(req, "access", transferMethod); - if (tokenString !== undefined) { - try { - const info = jwt_1.parseJWTWithoutSignatureVerification(tokenString); - accessToken_1.validateAccessTokenStructure(info.payload, info.version); - logger_1.logDebugMessage("getSession: got access token from " + transferMethod); - accessTokens[transferMethod] = info; - } catch (_a) { - logger_1.logDebugMessage( - `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` - ); - } - } - } - const allowedTransferMethod = config.getTokenTransferMethod({ - req, - forCreateNewSession: false, - userContext, +async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, options, userContext }) { + logger_1.logDebugMessage("getSession: Started"); + if (!res.wrapperUsed) { + res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); + } + if (!req.wrapperUsed) { + req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); + } + userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); + logger_1.logDebugMessage("getSession: Wrapping done"); + // This token isn't handled by getToken to limit the scope of this legacy/migration code + if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { + logger_1.logDebugMessage( + "getSession: Throwing TRY_REFRESH_TOKEN because the request is using a legacy session" + ); + // This could create a spike on refresh calls during the update of the backend SDK + throw new error_1.default({ + message: "using legacy session, please call the refresh API", + type: error_1.default.TRY_REFRESH_TOKEN, }); - let requestTransferMethod; - let accessToken; - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - accessTokens["header"] !== undefined - ) { - logger_1.logDebugMessage("getSession: using header transfer method"); - requestTransferMethod = "header"; - accessToken = accessTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - accessTokens["cookie"] !== undefined - ) { - logger_1.logDebugMessage("getSession: using cookie transfer method"); - requestTransferMethod = "cookie"; - accessToken = accessTokens["cookie"]; - } - let antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req); - let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; - if (doAntiCsrfCheck === undefined) { - doAntiCsrfCheck = utils_2.normaliseHttpMethod(req.getMethod()) !== "get"; - } - if (requestTransferMethod === "header") { - doAntiCsrfCheck = false; - } - // If the token is not present we can ignore the antiCsrf settings. - // the getSession implementation will handle checking sessionOptional - if (accessToken === undefined) { - doAntiCsrfCheck = false; - } - if (doAntiCsrfCheck && config.antiCsrf === "VIA_CUSTOM_HEADER") { - if (config.antiCsrf === "VIA_CUSTOM_HEADER") { - if (utils_2.getRidFromHeader(req) === undefined) { - logger_1.logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because custom header (rid) was not passed" - ); - throw new error_1.default({ - message: - "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request, or set doAntiCsrfCheck to false for this API", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - logger_1.logDebugMessage("getSession: VIA_CUSTOM_HEADER anti-csrf check passed"); - doAntiCsrfCheck = false; + } + const sessionOptional = (options === null || options === void 0 ? void 0 : options.sessionRequired) === false; + logger_1.logDebugMessage("getSession: optional validation: " + sessionOptional); + const accessTokens = {}; + // We check all token transfer methods for available access tokens + for (const transferMethod of constants_1.availableTokenTransferMethods) { + const tokenString = cookieAndHeaders_1.getToken(req, "access", transferMethod); + if (tokenString !== undefined) { + try { + const info = jwt_1.parseJWTWithoutSignatureVerification(tokenString); + accessToken_1.validateAccessTokenStructure(info.payload, info.version); + logger_1.logDebugMessage("getSession: got access token from " + transferMethod); + accessTokens[transferMethod] = info; + } catch (_a) { + logger_1.logDebugMessage( + `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` + ); } } - logger_1.logDebugMessage("getSession: Value of doAntiCsrfCheck is: " + doAntiCsrfCheck); - const session = yield recipeInterfaceImpl.getSession({ - accessToken: accessToken === null || accessToken === void 0 ? void 0 : accessToken.rawTokenString, - antiCsrfToken, - options: Object.assign(Object.assign({}, options), { antiCsrfCheck: doAntiCsrfCheck }), - userContext, - }); - if (session !== undefined) { - const claimValidators = yield utils_1.getRequiredClaimValidators( - session, - options === null || options === void 0 ? void 0 : options.overrideGlobalClaimValidators, - userContext - ); - yield session.assertClaims(claimValidators, userContext); - // requestTransferMethod can only be undefined here if the user overridden getSession - // to load the session by a custom method in that (very niche) case they also need to - // override how the session is attached to the response. - // In that scenario the transferMethod passed to attachToRequestResponse likely doesn't - // matter, still, we follow the general fallback logic - yield session.attachToRequestResponse({ - req, - res, - transferMethod: - requestTransferMethod !== undefined - ? requestTransferMethod - : allowedTransferMethod !== "any" - ? allowedTransferMethod - : "header", - }); + } + const allowedTransferMethod = config.getTokenTransferMethod({ + req, + forCreateNewSession: false, + userContext, + }); + let requestTransferMethod; + let accessToken; + if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "header") && + accessTokens["header"] !== undefined + ) { + logger_1.logDebugMessage("getSession: using header transfer method"); + requestTransferMethod = "header"; + accessToken = accessTokens["header"]; + } else if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && + accessTokens["cookie"] !== undefined + ) { + logger_1.logDebugMessage("getSession: using cookie transfer method"); + requestTransferMethod = "cookie"; + accessToken = accessTokens["cookie"]; + } + let antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req); + let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; + if (doAntiCsrfCheck === undefined) { + doAntiCsrfCheck = utils_2.normaliseHttpMethod(req.getMethod()) !== "get"; + } + if (requestTransferMethod === "header") { + doAntiCsrfCheck = false; + } + // If the token is not present we can ignore the antiCsrf settings. + // the getSession implementation will handle checking sessionOptional + if (accessToken === undefined) { + doAntiCsrfCheck = false; + } + if (doAntiCsrfCheck && config.antiCsrf === "VIA_CUSTOM_HEADER") { + if (config.antiCsrf === "VIA_CUSTOM_HEADER") { + if (utils_2.getRidFromHeader(req) === undefined) { + logger_1.logDebugMessage( + "getSession: Returning TRY_REFRESH_TOKEN because custom header (rid) was not passed" + ); + throw new error_1.default({ + message: + "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request, or set doAntiCsrfCheck to false for this API", + type: error_1.default.TRY_REFRESH_TOKEN, + }); + } + logger_1.logDebugMessage("getSession: VIA_CUSTOM_HEADER anti-csrf check passed"); + doAntiCsrfCheck = false; } - return session; + } + logger_1.logDebugMessage("getSession: Value of doAntiCsrfCheck is: " + doAntiCsrfCheck); + const session = await recipeInterfaceImpl.getSession({ + accessToken: accessToken === null || accessToken === void 0 ? void 0 : accessToken.rawTokenString, + antiCsrfToken, + options: Object.assign(Object.assign({}, options), { antiCsrfCheck: doAntiCsrfCheck }), + userContext, }); + if (session !== undefined) { + const claimValidators = await utils_1.getRequiredClaimValidators( + session, + options === null || options === void 0 ? void 0 : options.overrideGlobalClaimValidators, + userContext + ); + await session.assertClaims(claimValidators, userContext); + // requestTransferMethod can only be undefined here if the user overridden getSession + // to load the session by a custom method in that (very niche) case they also need to + // override how the session is attached to the response. + // In that scenario the transferMethod passed to attachToRequestResponse likely doesn't + // matter, still, we follow the general fallback logic + await session.attachToRequestResponse({ + req, + res, + transferMethod: + requestTransferMethod !== undefined + ? requestTransferMethod + : allowedTransferMethod !== "any" + ? allowedTransferMethod + : "header", + }); + } + return session; } exports.getSessionFromRequest = getSessionFromRequest; /* In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours */ -function refreshSessionInRequest({ res, req, userContext, config, recipeInterfaceImpl }) { - return __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("refreshSession: Started"); - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); +async function refreshSessionInRequest({ res, req, userContext, config, recipeInterfaceImpl }) { + logger_1.logDebugMessage("refreshSession: Started"); + if (!res.wrapperUsed) { + res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); + } + if (!req.wrapperUsed) { + req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); + } + userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); + logger_1.logDebugMessage("refreshSession: Wrapping done"); + const refreshTokens = {}; + // We check all token transfer methods for available refresh tokens + // We do this so that we can later clear all we are not overwriting + for (const transferMethod of constants_1.availableTokenTransferMethods) { + refreshTokens[transferMethod] = cookieAndHeaders_1.getToken(req, "refresh", transferMethod); + if (refreshTokens[transferMethod] !== undefined) { + logger_1.logDebugMessage("refreshSession: got refresh token from " + transferMethod); } - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); + } + const allowedTransferMethod = config.getTokenTransferMethod({ + req, + forCreateNewSession: false, + userContext, + }); + logger_1.logDebugMessage("refreshSession: getTokenTransferMethod returned " + allowedTransferMethod); + let requestTransferMethod; + let refreshToken; + if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "header") && + refreshTokens["header"] !== undefined + ) { + logger_1.logDebugMessage("refreshSession: using header transfer method"); + requestTransferMethod = "header"; + refreshToken = refreshTokens["header"]; + } else if ((allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && refreshTokens["cookie"]) { + logger_1.logDebugMessage("refreshSession: using cookie transfer method"); + requestTransferMethod = "cookie"; + refreshToken = refreshTokens["cookie"]; + } else { + // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code + if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { + logger_1.logDebugMessage( + "refreshSession: cleared legacy id refresh token because refresh token was not found" + ); + cookieAndHeaders_1.setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath"); } - userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); - logger_1.logDebugMessage("refreshSession: Wrapping done"); - const refreshTokens = {}; - // We check all token transfer methods for available refresh tokens - // We do this so that we can later clear all we are not overwriting - for (const transferMethod of constants_1.availableTokenTransferMethods) { - refreshTokens[transferMethod] = cookieAndHeaders_1.getToken(req, "refresh", transferMethod); - if (refreshTokens[transferMethod] !== undefined) { - logger_1.logDebugMessage("refreshSession: got refresh token from " + transferMethod); - } + logger_1.logDebugMessage("refreshSession: UNAUTHORISED because refresh token in request is undefined"); + throw new error_1.default({ + message: "Refresh token not found. Are you sending the refresh token in the request?", + payload: { + clearTokens: false, + }, + type: error_1.default.UNAUTHORISED, + }); + } + let disableAntiCsrf = requestTransferMethod === "header"; + const antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req); + if (config.antiCsrf === "VIA_CUSTOM_HEADER" && !disableAntiCsrf) { + if (utils_2.getRidFromHeader(req) === undefined) { + logger_1.logDebugMessage( + "refreshSession: Returning UNAUTHORISED because custom header (rid) was not passed" + ); + throw new error_1.default({ + message: "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request.", + type: error_1.default.UNAUTHORISED, + payload: { + clearTokens: false, // see https://github.com/supertokens/supertokens-node/issues/141 + }, + }); } - const allowedTransferMethod = config.getTokenTransferMethod({ - req, - forCreateNewSession: false, + disableAntiCsrf = true; + } + let session; + try { + session = await recipeInterfaceImpl.refreshSession({ + refreshToken: refreshToken, + antiCsrfToken, + disableAntiCsrf, userContext, }); - logger_1.logDebugMessage("refreshSession: getTokenTransferMethod returned " + allowedTransferMethod); - let requestTransferMethod; - let refreshToken; + } catch (ex) { if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - refreshTokens["header"] !== undefined + error_1.default.isErrorFromSuperTokens(ex) && + (ex.type === error_1.default.TOKEN_THEFT_DETECTED || ex.payload.clearTokens === true) ) { - logger_1.logDebugMessage("refreshSession: using header transfer method"); - requestTransferMethod = "header"; - refreshToken = refreshTokens["header"]; - } else if ((allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && refreshTokens["cookie"]) { - logger_1.logDebugMessage("refreshSession: using cookie transfer method"); - requestTransferMethod = "cookie"; - refreshToken = refreshTokens["cookie"]; - } else { - // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code + // We clear the LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME here because we want to limit the scope of this legacy/migration code + // so the token clearing functions in the error handlers do not if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { logger_1.logDebugMessage( - "refreshSession: cleared legacy id refresh token because refresh token was not found" + "refreshSession: cleared legacy id refresh token because refresh is clearing other tokens" ); cookieAndHeaders_1.setCookie( config, @@ -233,161 +246,109 @@ function refreshSessionInRequest({ res, req, userContext, config, recipeInterfac "accessTokenPath" ); } - logger_1.logDebugMessage("refreshSession: UNAUTHORISED because refresh token in request is undefined"); - throw new error_1.default({ - message: "Refresh token not found. Are you sending the refresh token in the request?", - payload: { - clearTokens: false, - }, - type: error_1.default.UNAUTHORISED, - }); - } - let disableAntiCsrf = requestTransferMethod === "header"; - const antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req); - if (config.antiCsrf === "VIA_CUSTOM_HEADER" && !disableAntiCsrf) { - if (utils_2.getRidFromHeader(req) === undefined) { - logger_1.logDebugMessage( - "refreshSession: Returning UNAUTHORISED because custom header (rid) was not passed" - ); - throw new error_1.default({ - message: "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request.", - type: error_1.default.UNAUTHORISED, - payload: { - clearTokens: false, // see https://github.com/supertokens/supertokens-node/issues/141 - }, - }); - } - disableAntiCsrf = true; } - let session; - try { - session = yield recipeInterfaceImpl.refreshSession({ - refreshToken: refreshToken, - antiCsrfToken, - disableAntiCsrf, - userContext, - }); - } catch (ex) { - if ( - error_1.default.isErrorFromSuperTokens(ex) && - (ex.type === error_1.default.TOKEN_THEFT_DETECTED || ex.payload.clearTokens === true) - ) { - // We clear the LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME here because we want to limit the scope of this legacy/migration code - // so the token clearing functions in the error handlers do not - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logger_1.logDebugMessage( - "refreshSession: cleared legacy id refresh token because refresh is clearing other tokens" - ); - cookieAndHeaders_1.setCookie( - config, - res, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "accessTokenPath" - ); - } - } - throw ex; - } - logger_1.logDebugMessage("refreshSession: Attaching refreshed session info as " + requestTransferMethod); - // We clear the tokens in all token transfer methods we are not going to overwrite - for (const transferMethod of constants_1.availableTokenTransferMethods) { - if (transferMethod !== requestTransferMethod && refreshTokens[transferMethod] !== undefined) { - cookieAndHeaders_1.clearSession(config, res, transferMethod); - } + throw ex; + } + logger_1.logDebugMessage("refreshSession: Attaching refreshed session info as " + requestTransferMethod); + // We clear the tokens in all token transfer methods we are not going to overwrite + for (const transferMethod of constants_1.availableTokenTransferMethods) { + if (transferMethod !== requestTransferMethod && refreshTokens[transferMethod] !== undefined) { + cookieAndHeaders_1.clearSession(config, res, transferMethod); } - yield session.attachToRequestResponse({ - req, - res, - transferMethod: requestTransferMethod, - }); - logger_1.logDebugMessage("refreshSession: Success!"); - // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logger_1.logDebugMessage("refreshSession: cleared legacy id refresh token after successful refresh"); - cookieAndHeaders_1.setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath"); - } - return session; + } + await session.attachToRequestResponse({ + req, + res, + transferMethod: requestTransferMethod, }); + logger_1.logDebugMessage("refreshSession: Success!"); + // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code + if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { + logger_1.logDebugMessage("refreshSession: cleared legacy id refresh token after successful refresh"); + cookieAndHeaders_1.setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath"); + } + return session; } exports.refreshSessionInRequest = refreshSessionInRequest; -function createNewSessionInRequest({ +async function createNewSessionInRequest({ req, res, userContext, recipeInstance, accessTokenPayload, userId, + recipeUserId, config, appInfo, sessionDataInDatabase, tenantId, }) { - return __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("createNewSession: Started"); - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); - } - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); - } - logger_1.logDebugMessage("createNewSession: Wrapping done"); - userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); - const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); - const issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); - let finalAccessTokenPayload = Object.assign(Object.assign({}, accessTokenPayload), { iss: issuer }); - for (const claim of claimsAddedByOtherRecipes) { - const update = yield claim.build(userId, tenantId, userContext); - finalAccessTokenPayload = Object.assign(Object.assign({}, finalAccessTokenPayload), update); - } - logger_1.logDebugMessage("createNewSession: Access token payload built"); - let outputTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: true, userContext }); - if (outputTransferMethod === "any") { - outputTransferMethod = "header"; - } - logger_1.logDebugMessage("createNewSession: using transfer method " + outputTransferMethod); + logger_1.logDebugMessage("createNewSession: Started"); + if (!req.wrapperUsed) { + req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); + } + if (!res.wrapperUsed) { + res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); + } + logger_1.logDebugMessage("createNewSession: Wrapping done"); + userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); + const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); + const issuer = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + let finalAccessTokenPayload = Object.assign(Object.assign({}, accessTokenPayload), { iss: issuer }); + for (const prop of constants_1.protectedProps) { + delete finalAccessTokenPayload[prop]; + } + for (const claim of claimsAddedByOtherRecipes) { + const update = await claim.build(userId, recipeUserId, tenantId, userContext); + finalAccessTokenPayload = Object.assign(Object.assign({}, finalAccessTokenPayload), update); + } + logger_1.logDebugMessage("createNewSession: Access token payload built"); + let outputTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: true, userContext }); + if (outputTransferMethod === "any") { + outputTransferMethod = "header"; + } + logger_1.logDebugMessage("createNewSession: using transfer method " + outputTransferMethod); + if ( + outputTransferMethod === "cookie" && + config.cookieSameSite === "none" && + !config.cookieSecure && + !( + (appInfo.topLevelAPIDomain === "localhost" || utils_2.isAnIpAddress(appInfo.topLevelAPIDomain)) && + (appInfo.topLevelWebsiteDomain === "localhost" || utils_2.isAnIpAddress(appInfo.topLevelWebsiteDomain)) + ) + ) { + // We can allow insecure cookie when both website & API domain are localhost or an IP + // When either of them is a different domain, API domain needs to have https and a secure cookie to work + throw new Error( + "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." + ); + } + const disableAntiCsrf = outputTransferMethod === "header"; + const session = await recipeInstance.recipeInterfaceImpl.createNewSession({ + userId, + recipeUserId, + accessTokenPayload: finalAccessTokenPayload, + sessionDataInDatabase, + disableAntiCsrf, + tenantId, + userContext, + }); + logger_1.logDebugMessage("createNewSession: Session created in core built"); + for (const transferMethod of constants_1.availableTokenTransferMethods) { if ( - outputTransferMethod === "cookie" && - config.cookieSameSite === "none" && - !config.cookieSecure && - !( - (appInfo.topLevelAPIDomain === "localhost" || utils_2.isAnIpAddress(appInfo.topLevelAPIDomain)) && - (appInfo.topLevelWebsiteDomain === "localhost" || utils_2.isAnIpAddress(appInfo.topLevelWebsiteDomain)) - ) + transferMethod !== outputTransferMethod && + cookieAndHeaders_1.getToken(req, "access", transferMethod) !== undefined ) { - // We can allow insecure cookie when both website & API domain are localhost or an IP - // When either of them is a different domain, API domain needs to have https and a secure cookie to work - throw new Error( - "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." - ); - } - const disableAntiCsrf = outputTransferMethod === "header"; - const session = yield recipeInstance.recipeInterfaceImpl.createNewSession({ - userId, - accessTokenPayload: finalAccessTokenPayload, - sessionDataInDatabase, - disableAntiCsrf, - tenantId, - userContext, - }); - logger_1.logDebugMessage("createNewSession: Session created in core built"); - for (const transferMethod of constants_1.availableTokenTransferMethods) { - if ( - transferMethod !== outputTransferMethod && - cookieAndHeaders_1.getToken(req, "access", transferMethod) !== undefined - ) { - cookieAndHeaders_1.clearSession(config, res, transferMethod); - } + cookieAndHeaders_1.clearSession(config, res, transferMethod); } - logger_1.logDebugMessage("createNewSession: Cleared old tokens"); - yield session.attachToRequestResponse({ - req, - res, - transferMethod: outputTransferMethod, - }); - logger_1.logDebugMessage("createNewSession: Attached new tokens to res"); - return session; + } + logger_1.logDebugMessage("createNewSession: Cleared old tokens"); + await session.attachToRequestResponse({ + req, + res, + transferMethod: outputTransferMethod, }); + logger_1.logDebugMessage("createNewSession: Attached new tokens to res"); + return session; } exports.createNewSessionInRequest = createNewSessionInRequest; diff --git a/lib/build/recipe/session/types.d.ts b/lib/build/recipe/session/types.d.ts index 52b49066b..2303c715d 100644 --- a/lib/build/recipe/session/types.d.ts +++ b/lib/build/recipe/session/types.d.ts @@ -1,11 +1,12 @@ // @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface } from "../jwt/types"; import OverrideableBuilder from "supertokens-js-override"; import { RecipeInterface as OpenIdRecipeInterface, APIInterface as OpenIdAPIInterface } from "../openid/types"; import { JSONObject, JSONValue } from "../../types"; import { GeneralErrorResponse } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export declare type KeyInfo = { publicKey: string; expiryTime: number; @@ -21,6 +22,7 @@ export declare type CreateOrRefreshAPIResponse = { session: { handle: string; userId: string; + recipeUserId: RecipeUserId; userDataInJWT: any; tenantId: string; }; @@ -131,7 +133,13 @@ export interface ErrorHandlerMiddleware { (message: string, request: BaseRequest, response: BaseResponse): Promise; } export interface TokenTheftErrorHandlerMiddleware { - (sessionHandle: string, userId: string, request: BaseRequest, response: BaseResponse): Promise; + ( + sessionHandle: string, + userId: string, + recipeUserId: RecipeUserId, + request: BaseRequest, + response: BaseResponse + ): Promise; } export interface InvalidClaimErrorHandlerMiddleware { (validatorErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse): Promise; @@ -155,6 +163,7 @@ export interface VerifySessionOptions { export declare type RecipeInterface = { createNewSession(input: { userId: string; + recipeUserId: RecipeUserId; accessTokenPayload?: any; sessionDataInDatabase?: any; disableAntiCsrf?: boolean; @@ -164,6 +173,7 @@ export declare type RecipeInterface = { getGlobalClaimValidators(input: { tenantId: string; userId: string; + recipeUserId: RecipeUserId; claimValidatorsAddedByOtherRecipes: SessionClaimValidator[]; userContext: any; }): Promise | SessionClaimValidator[]; @@ -189,12 +199,14 @@ export declare type RecipeInterface = { getSessionInformation(input: { sessionHandle: string; userContext: any }): Promise; revokeAllSessionsForUser(input: { userId: string; + revokeSessionsForLinkedAccounts: boolean; tenantId: string; revokeAcrossAllTenants?: boolean; userContext: any; }): Promise; getAllSessionHandlesForUser(input: { userId: string; + fetchSessionsForAllLinkedAccounts: boolean; tenantId: string; fetchAcrossAllTenants?: boolean; userContext: any; @@ -224,6 +236,7 @@ export declare type RecipeInterface = { session: { handle: string; userId: string; + recipeUserId: RecipeUserId; userDataInJWT: any; tenantId: string; }; @@ -237,6 +250,7 @@ export declare type RecipeInterface = { >; validateClaims(input: { userId: string; + recipeUserId: RecipeUserId; accessTokenPayload: any; claimValidators: SessionClaimValidator[]; userContext: any; @@ -244,15 +258,6 @@ export declare type RecipeInterface = { invalidClaims: ClaimValidationError[]; accessTokenPayloadUpdate?: any; }>; - validateClaimsInJWTPayload(input: { - userId: string; - jwtPayload: JSONObject; - claimValidators: SessionClaimValidator[]; - userContext: any; - }): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }>; fetchAndSetClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; setClaimValue(input: { sessionHandle: string; @@ -280,6 +285,7 @@ export interface SessionContainerInterface { getSessionDataFromDatabase(userContext?: any): Promise; updateSessionDataInDatabase(newSessionData: any, userContext?: any): Promise; getUserId(userContext?: any): string; + getRecipeUserId(userContext?: any): RecipeUserId; getTenantId(userContext?: any): string; getAccessTokenPayload(userContext?: any): any; getHandle(userContext?: any): string; @@ -337,6 +343,7 @@ export declare type APIInterface = { export declare type SessionInformation = { sessionHandle: string; userId: string; + recipeUserId: RecipeUserId; sessionDataInDatabase: any; expiry: number; customClaimsInAccessTokenPayload: any; @@ -381,7 +388,12 @@ export declare abstract class SessionClaim { * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. */ - abstract fetchValue(userId: string, tenantId: string, userContext: any): Promise | T | undefined; + abstract fetchValue( + userId: string, + recipeUserId: RecipeUserId, + tenantId: string, + userContext: any + ): Promise | T | undefined; /** * Saves the provided value into the payload, by cloning and updating the entire object. * @@ -406,7 +418,7 @@ export declare abstract class SessionClaim { * @returns Claim value */ abstract getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; - build(userId: string, tenantId: string, userContext?: any): Promise; + build(userId: string, recipeUserId: RecipeUserId, tenantId: string, userContext?: any): Promise; } export declare type ReqResInfo = { res: BaseResponse; diff --git a/lib/build/recipe/session/types.js b/lib/build/recipe/session/types.js index b92752356..28aa0c726 100644 --- a/lib/build/recipe/session/types.js +++ b/lib/build/recipe/session/types.js @@ -1,49 +1,16 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SessionClaim = void 0; class SessionClaim { constructor(key) { this.key = key; } - build(userId, tenantId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const value = yield this.fetchValue(userId, tenantId, userContext); - if (value === undefined) { - return {}; - } - return this.addToPayload_internal({}, value, userContext); - }); + async build(userId, recipeUserId, tenantId, userContext) { + const value = await this.fetchValue(userId, recipeUserId, tenantId, userContext); + if (value === undefined) { + return {}; + } + return this.addToPayload_internal({}, value, userContext); } } exports.SessionClaim = SessionClaim; diff --git a/lib/build/recipe/session/utils.d.ts b/lib/build/recipe/session/utils.d.ts index 753592dd4..dbeaf47c7 100644 --- a/lib/build/recipe/session/utils.d.ts +++ b/lib/build/recipe/session/utils.d.ts @@ -10,7 +10,8 @@ import { } from "./types"; import SessionRecipe from "./recipe"; import { NormalisedAppinfo } from "../../types"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import RecipeUserId from "../../recipeUserId"; export declare function sendTryRefreshTokenResponse( recipeInstance: SessionRecipe, _: string, @@ -33,7 +34,8 @@ export declare function sendTokenTheftDetectedResponse( recipeInstance: SessionRecipe, sessionHandle: string, _: string, - __: BaseRequest, + __: RecipeUserId, + ___: BaseRequest, response: BaseResponse ): Promise; export declare function normaliseSessionScopeOrThrowError(sessionScope: string): string; diff --git a/lib/build/recipe/session/utils.js b/lib/build/recipe/session/utils.js index 8f90fdb9e..3cc35050f 100644 --- a/lib/build/recipe/session/utils.js +++ b/lib/build/recipe/session/utils.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -58,40 +27,32 @@ const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const utils_1 = require("../../utils"); const utils_2 = require("../../utils"); const logger_1 = require("../../logger"); -function sendTryRefreshTokenResponse(recipeInstance, _, __, response) { - return __awaiter(this, void 0, void 0, function* () { - utils_2.sendNon200ResponseWithMessage( - response, - "try refresh token", - recipeInstance.config.sessionExpiredStatusCode - ); - }); +async function sendTryRefreshTokenResponse(recipeInstance, _, __, response) { + utils_2.sendNon200ResponseWithMessage( + response, + "try refresh token", + recipeInstance.config.sessionExpiredStatusCode + ); } exports.sendTryRefreshTokenResponse = sendTryRefreshTokenResponse; -function sendUnauthorisedResponse(recipeInstance, _, __, response) { - return __awaiter(this, void 0, void 0, function* () { - utils_2.sendNon200ResponseWithMessage(response, "unauthorised", recipeInstance.config.sessionExpiredStatusCode); - }); +async function sendUnauthorisedResponse(recipeInstance, _, __, response) { + utils_2.sendNon200ResponseWithMessage(response, "unauthorised", recipeInstance.config.sessionExpiredStatusCode); } exports.sendUnauthorisedResponse = sendUnauthorisedResponse; -function sendInvalidClaimResponse(recipeInstance, claimValidationErrors, __, response) { - return __awaiter(this, void 0, void 0, function* () { - utils_2.sendNon200Response(response, recipeInstance.config.invalidClaimStatusCode, { - message: "invalid claim", - claimValidationErrors, - }); +async function sendInvalidClaimResponse(recipeInstance, claimValidationErrors, __, response) { + utils_2.sendNon200Response(response, recipeInstance.config.invalidClaimStatusCode, { + message: "invalid claim", + claimValidationErrors, }); } exports.sendInvalidClaimResponse = sendInvalidClaimResponse; -function sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, _, __, response) { - return __awaiter(this, void 0, void 0, function* () { - yield recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext: {} }); - utils_2.sendNon200ResponseWithMessage( - response, - "token theft detected", - recipeInstance.config.sessionExpiredStatusCode - ); - }); +async function sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, _, __, ___, response) { + await recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext: {} }); + utils_2.sendNon200ResponseWithMessage( + response, + "token theft detected", + recipeInstance.config.sessionExpiredStatusCode + ); } exports.sendTokenTheftDetectedResponse = sendTokenTheftDetectedResponse; function normaliseSessionScopeOrThrowError(sessionScope) { @@ -176,18 +137,22 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { : "NONE" : config.antiCsrf; let errorHandlers = { - onTokenTheftDetected: (sessionHandle, userId, request, response) => - __awaiter(this, void 0, void 0, function* () { - return yield sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, userId, request, response); - }), - onTryRefreshToken: (message, request, response) => - __awaiter(this, void 0, void 0, function* () { - return yield sendTryRefreshTokenResponse(recipeInstance, message, request, response); - }), - onUnauthorised: (message, request, response) => - __awaiter(this, void 0, void 0, function* () { - return yield sendUnauthorisedResponse(recipeInstance, message, request, response); - }), + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { + return await sendTokenTheftDetectedResponse( + recipeInstance, + sessionHandle, + userId, + recipeUserId, + request, + response + ); + }, + onTryRefreshToken: async (message, request, response) => { + return await sendTryRefreshTokenResponse(recipeInstance, message, request, response); + }, + onUnauthorised: async (message, request, response) => { + return await sendUnauthorisedResponse(recipeInstance, message, request, response); + }, onInvalidClaim: (validationErrors, request, response) => { return sendInvalidClaimResponse(recipeInstance, validationErrors, request, response); }, @@ -278,42 +243,39 @@ function setAccessTokenInResponse(res, accessToken, frontToken, config, transfer } } exports.setAccessTokenInResponse = setAccessTokenInResponse; -function getRequiredClaimValidators(session, overrideGlobalClaimValidators, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const claimValidatorsAddedByOtherRecipes = recipe_1.default - .getInstanceOrThrowError() - .getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators = yield recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getGlobalClaimValidators({ - userId: session.getUserId(), - tenantId: session.getTenantId(), - claimValidatorsAddedByOtherRecipes, - userContext, - }); - return overrideGlobalClaimValidators !== undefined - ? yield overrideGlobalClaimValidators(globalClaimValidators, session, userContext) - : globalClaimValidators; - }); +async function getRequiredClaimValidators(session, overrideGlobalClaimValidators, userContext) { + const claimValidatorsAddedByOtherRecipes = recipe_1.default + .getInstanceOrThrowError() + .getClaimValidatorsAddedByOtherRecipes(); + const globalClaimValidators = await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.getGlobalClaimValidators({ + userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), + tenantId: session.getTenantId(), + claimValidatorsAddedByOtherRecipes, + userContext, + }); + return overrideGlobalClaimValidators !== undefined + ? await overrideGlobalClaimValidators(globalClaimValidators, session, userContext) + : globalClaimValidators; } exports.getRequiredClaimValidators = getRequiredClaimValidators; -function validateClaimsInPayload(claimValidators, newAccessTokenPayload, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const validationErrors = []; - for (const validator of claimValidators) { - const claimValidationResult = yield validator.validate(newAccessTokenPayload, userContext); - logger_1.logDebugMessage( - "validateClaimsInPayload " + validator.id + " validation res " + JSON.stringify(claimValidationResult) - ); - if (!claimValidationResult.isValid) { - validationErrors.push({ - id: validator.id, - reason: claimValidationResult.reason, - }); - } +async function validateClaimsInPayload(claimValidators, newAccessTokenPayload, userContext) { + const validationErrors = []; + for (const validator of claimValidators) { + const claimValidationResult = await validator.validate(newAccessTokenPayload, userContext); + logger_1.logDebugMessage( + "validateClaimsInPayload " + validator.id + " validation res " + JSON.stringify(claimValidationResult) + ); + if (!claimValidationResult.isValid) { + validationErrors.push({ + id: validator.id, + reason: claimValidationResult.reason, + }); } - return validationErrors; - }); + } + return validationErrors; } exports.validateClaimsInPayload = validateClaimsInPayload; function defaultGetTokenTransferMethod({ req, forCreateNewSession }) { diff --git a/lib/build/recipe/thirdparty/api/appleRedirect.js b/lib/build/recipe/thirdparty/api/appleRedirect.js index be2e7d8af..8179554a3 100644 --- a/lib/build/recipe/thirdparty/api/appleRedirect.js +++ b/lib/build/recipe/thirdparty/api/appleRedirect.js @@ -13,51 +13,18 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); -function appleRedirectHandler(apiImplementation, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.appleRedirectHandlerPOST === undefined) { - return false; - } - let body = yield options.req.getFormData(); - // this will redirect the user... - yield apiImplementation.appleRedirectHandlerPOST({ - formPostInfoFromProvider: body, - options, - userContext, - }); - return true; +async function appleRedirectHandler(apiImplementation, options, userContext) { + if (apiImplementation.appleRedirectHandlerPOST === undefined) { + return false; + } + let body = await options.req.getFormData(); + // this will redirect the user... + await apiImplementation.appleRedirectHandlerPOST({ + formPostInfoFromProvider: body, + options, + userContext, }); + return true; } exports.default = appleRedirectHandler; diff --git a/lib/build/recipe/thirdparty/api/authorisationUrl.js b/lib/build/recipe/thirdparty/api/authorisationUrl.js index a74b7a6d7..b5f572f4f 100644 --- a/lib/build/recipe/thirdparty/api/authorisationUrl.js +++ b/lib/build/recipe/thirdparty/api/authorisationUrl.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,48 +21,46 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -function authorisationUrlAPI(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.authorisationUrlGET === undefined) { - return false; - } - const thirdPartyId = options.req.getKeyValueFromQuery("thirdPartyId"); - const redirectURIOnProviderDashboard = options.req.getKeyValueFromQuery("redirectURIOnProviderDashboard"); - const clientType = options.req.getKeyValueFromQuery("clientType"); - if (thirdPartyId === undefined || typeof thirdPartyId !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the thirdPartyId as a GET param", - }); - } - if (redirectURIOnProviderDashboard === undefined || typeof redirectURIOnProviderDashboard !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the redirectURIOnProviderDashboard as a GET param", - }); - } - const providerResponse = yield options.recipeImplementation.getProvider({ - thirdPartyId, - clientType, - tenantId, - userContext, +async function authorisationUrlAPI(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.authorisationUrlGET === undefined) { + return false; + } + const thirdPartyId = options.req.getKeyValueFromQuery("thirdPartyId"); + const redirectURIOnProviderDashboard = options.req.getKeyValueFromQuery("redirectURIOnProviderDashboard"); + const clientType = options.req.getKeyValueFromQuery("clientType"); + if (thirdPartyId === undefined || typeof thirdPartyId !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the thirdPartyId as a GET param", }); - if (providerResponse === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: `the provider ${thirdPartyId} could not be found in the configuration`, - }); - } - const provider = providerResponse; - let result = yield apiImplementation.authorisationUrlGET({ - provider, - redirectURIOnProviderDashboard, - tenantId, - options, - userContext, + } + if (redirectURIOnProviderDashboard === undefined || typeof redirectURIOnProviderDashboard !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the redirectURIOnProviderDashboard as a GET param", + }); + } + const providerResponse = await options.recipeImplementation.getProvider({ + thirdPartyId, + clientType, + tenantId, + userContext, + }); + if (providerResponse === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: `the provider ${thirdPartyId} could not be found in the configuration`, }); - utils_1.send200Response(options.res, result); - return true; + } + const provider = providerResponse; + let result = await apiImplementation.authorisationUrlGET({ + provider, + redirectURIOnProviderDashboard, + tenantId, + options, + userContext, }); + utils_1.send200Response(options.res, result); + return true; } exports.default = authorisationUrlAPI; diff --git a/lib/build/recipe/thirdparty/api/implementation.js b/lib/build/recipe/thirdparty/api/implementation.js index 1b53ba141..ade405ca3 100644 --- a/lib/build/recipe/thirdparty/api/implementation.js +++ b/lib/build/recipe/thirdparty/api/implementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -37,113 +6,269 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); const session_1 = __importDefault(require("../../session")); -const recipe_1 = __importDefault(require("../../emailverification/recipe")); +const recipe_1 = __importDefault(require("../../accountlinking/recipe")); +const __1 = require("../../.."); +const emailverification_1 = __importDefault(require("../../emailverification")); +const recipe_2 = __importDefault(require("../../emailverification/recipe")); function getAPIInterface() { return { - authorisationUrlGET: function ({ provider, redirectURIOnProviderDashboard, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const authUrl = yield provider.getAuthorisationRedirectURL({ - redirectURIOnProviderDashboard, - userContext, - }); - return Object.assign({ status: "OK" }, authUrl); + authorisationUrlGET: async function ({ provider, redirectURIOnProviderDashboard, userContext }) { + const authUrl = await provider.getAuthorisationRedirectURL({ + redirectURIOnProviderDashboard, + userContext, }); + return Object.assign({ status: "OK" }, authUrl); }, - signInUpPOST: function (input) { - return __awaiter(this, void 0, void 0, function* () { - const { provider, tenantId, options, userContext } = input; - let oAuthTokensToUse = {}; - if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) { - oAuthTokensToUse = yield provider.exchangeAuthCodeForOAuthTokens({ - redirectURIInfo: input.redirectURIInfo, + signInUpPOST: async function (input) { + const { provider, tenantId, options, userContext } = input; + let oAuthTokensToUse = {}; + if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) { + oAuthTokensToUse = await provider.exchangeAuthCodeForOAuthTokens({ + redirectURIInfo: input.redirectURIInfo, + userContext, + }); + } else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) { + oAuthTokensToUse = input.oAuthTokens; + } else { + throw Error("should never come here"); + } + const userInfo = await provider.getUserInfo({ oAuthTokens: oAuthTokensToUse, userContext }); + if (userInfo.email === undefined && provider.config.requireEmail === false) { + userInfo.email = { + id: await provider.config.generateFakeEmail({ + thirdPartyUserId: userInfo.thirdPartyUserId, + tenantId, userContext, - }); - } else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) { - oAuthTokensToUse = input.oAuthTokens; - } else { - throw Error("should never come here"); - } - const userInfo = yield provider.getUserInfo({ oAuthTokens: oAuthTokensToUse, userContext }); - if (userInfo.email === undefined && provider.config.requireEmail === false) { - userInfo.email = { - id: yield provider.config.generateFakeEmail({ - thirdPartyUserId: userInfo.thirdPartyUserId, - tenantId, - userContext, - }), - isVerified: true, + }), + isVerified: true, + }; + } + let emailInfo = userInfo.email; + if (emailInfo === undefined) { + return { + status: "NO_EMAIL_GIVEN_BY_PROVIDER", + }; + } + let existingUsers = await __1.listUsersByAccountInfo( + tenantId, + { + thirdParty: { + id: provider.id, + userId: userInfo.thirdPartyUserId, + }, + }, + false, + userContext + ); + if (existingUsers.length === 0) { + let isSignUpAllowed = await recipe_1.default.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "thirdparty", + email: emailInfo.id, + thirdParty: { + id: provider.id, + userId: userInfo.thirdPartyUserId, + }, + }, + isVerified: emailInfo.isVerified, + tenantId, + userContext, + }); + if (!isSignUpAllowed) { + // On the frontend, this should show a UI of asking the user + // to login using a different method. + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_006)", }; } - let emailInfo = userInfo.email; - if (emailInfo === undefined) { + } else { + // this is a sign in. So before we proceed, we need to check if an email change + // is allowed since the email could have changed from the social provider's side. + // We do this check here and not in the recipe function cause we want to keep the + // recipe function checks to a minimum so that the dev has complete control of + // what they can do. + // The isEmailChangeAllowed function takes in a isVerified boolean. Now, even though + // we already have that from the input, that's just what the provider says. If the + // provider says that the email is NOT verified, it could have been that the email + // is verified on the user's account via supertokens on a previous sign in / up. + // So we just check that as well before calling isEmailChangeAllowed + if (existingUsers.length > 1) { + throw new Error( + "You have found a bug. Please report it on https://github.com/supertokens/supertokens-node/issues" + ); + } + let recipeUserId = undefined; + existingUsers[0].loginMethods.forEach((lM) => { + if ( + lM.hasSameThirdPartyInfoAs({ + id: provider.id, + userId: userInfo.thirdPartyUserId, + }) + ) { + recipeUserId = lM.recipeUserId; + } + }); + if (!emailInfo.isVerified && recipe_2.default.getInstance() !== undefined) { + emailInfo.isVerified = await emailverification_1.default.isEmailVerified( + recipeUserId, + emailInfo.id, + userContext + ); + } + /** + * In this API, during only a sign in, we check for isEmailChangeAllowed first, then + * change the email by calling the recipe function, then check if is sign in allowed. + * This may result in a few states where email change is allowed, but still, sign in + * is not allowed: + * + * Various outcomes of isSignInAllowed vs isEmailChangeAllowed + * isSignInAllowed result: + * - is primary user -> TRUE + * - is recipe user + * - other recipe user exists + * - no -> TRUE + * - yes + * - email verified -> TRUE + * - email unverified -> FALSE + * - other primary user exists + * - no -> TRUE + * - yes + * - email verification status + * - this && primary -> TRUE + * - !this && !primary -> FALSE + * - this && !primary -> FALSE + * - !this && primary -> FALSE + * + * isEmailChangeAllowed result: + * - is primary user -> TRUE + * - is recipe user + * - other recipe user exists + * - no -> TRUE + * - yes + * - email verified -> TRUE + * - email unverified -> TRUE + * - other primary user exists + * - no -> TRUE + * - yes + * - email verification status + * - this && primary -> TRUE + * - !this && !primary -> FALSE + * - this && !primary -> TRUE + * - !this && primary -> FALSE + * + * Based on the above, isEmailChangeAllowed can return true, but isSignInAllowed will return false + * in the following situations: + * - If a recipe user is signing in with a new email, other recipe users with the same email exist, + * and one of them is unverfied. In this case, the email change will happen in the social login + * recipe, but the user will not be able to login anyway. + * + * - If the recipe user is signing in with a new email, there exists a primary user with the same + * email, but this new email is verified for the recipe user already, but the primary user's email + * is not verified. + */ + let isEmailChangeAllowed = await recipe_1.default.getInstance().isEmailChangeAllowed({ + user: existingUsers[0], + isVerified: emailInfo.isVerified, + newEmail: emailInfo.id, + userContext, + }); + if (!isEmailChangeAllowed) { return { - status: "NO_EMAIL_GIVEN_BY_PROVIDER", + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)", }; } - let response = yield options.recipeImplementation.signInUp({ - thirdPartyId: provider.id, - thirdPartyUserId: userInfo.thirdPartyUserId, - email: emailInfo.id, - oAuthTokens: oAuthTokensToUse, - rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, + } + let response = await options.recipeImplementation.signInUp({ + thirdPartyId: provider.id, + thirdPartyUserId: userInfo.thirdPartyUserId, + email: emailInfo.id, + isVerified: emailInfo.isVerified, + oAuthTokens: oAuthTokensToUse, + rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, + tenantId, + userContext, + }); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + return response; + } + let loginMethod = undefined; + for (let i = 0; i < response.user.loginMethods.length; i++) { + if ( + response.user.loginMethods[i].hasSameThirdPartyInfoAs({ + id: provider.id, + userId: userInfo.thirdPartyUserId, + }) + ) { + loginMethod = response.user.loginMethods[i]; + } + } + if (loginMethod === undefined) { + throw new Error("Should never come here"); + } + if (existingUsers.length > 0) { + // Here we do this check after sign in is done cause: + // - We first want to check if the credentials are correct first or not + // - The above recipe function marks the email as verified if other linked users + // with the same email are verified. The function below checks for the email verification + // so we want to call it only once this is up to date, + // - Even though the above call to signInUp is state changing (it changes the email + // of the user), it's OK to do this check here cause the isSignInAllowed checks + // conditions related to account linking and not related to email change. Email change + // condition checking happens before calling the recipe function anyway. + let isSignInAllowed = await recipe_1.default.getInstance().isSignInAllowed({ + user: response.user, tenantId, userContext, }); - // we set the email as verified if already verified by the OAuth provider. - // This block was added because of https://github.com/supertokens/supertokens-core/issues/295 - if (emailInfo.isVerified) { - const emailVerificationInstance = recipe_1.default.getInstance(); - if (emailVerificationInstance) { - const tokenResponse = yield emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( - { - userId: response.user.id, - email: response.user.email, - tenantId, - userContext, - } - ); - if (tokenResponse.status === "OK") { - yield emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ - token: tokenResponse.token, - tenantId, - userContext, - }); - } - } + if (!isSignInAllowed) { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)", + }; } - let session = yield session_1.default.createNewSession( - options.req, - options.res, + // we do account linking only during sign in here cause during sign up, + // the recipe function above does account linking for us. + response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ tenantId, - response.user.id, - {}, - {}, - userContext - ); - return { - status: "OK", - createdNewUser: response.createdNewUser, user: response.user, - session, - oAuthTokens: oAuthTokensToUse, - rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, - }; - }); + userContext, + }); + } + let session = await session_1.default.createNewSession( + options.req, + options.res, + tenantId, + loginMethod.recipeUserId, + {}, + {}, + userContext + ); + return { + status: "OK", + createdNewRecipeUser: response.createdNewRecipeUser, + user: response.user, + session, + oAuthTokens: oAuthTokensToUse, + rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, + }; }, - appleRedirectHandlerPOST: function ({ formPostInfoFromProvider, options }) { - return __awaiter(this, void 0, void 0, function* () { - const stateInBase64 = formPostInfoFromProvider.state; - const state = Buffer.from(stateInBase64, "base64").toString(); - const stateObj = JSON.parse(state); - const redirectURI = stateObj.frontendRedirectURI; - const urlObj = new URL(redirectURI); - for (const [key, value] of Object.entries(formPostInfoFromProvider)) { - urlObj.searchParams.set(key, `${value}`); - } - options.res.setHeader("Location", urlObj.toString(), false); - options.res.setStatusCode(303); - options.res.sendHTMLResponse(""); - }); + appleRedirectHandlerPOST: async function ({ formPostInfoFromProvider, options }) { + const stateInBase64 = formPostInfoFromProvider.state; + const state = Buffer.from(stateInBase64, "base64").toString(); + const stateObj = JSON.parse(state); + const redirectURI = stateObj.frontendRedirectURI; + const urlObj = new URL(redirectURI); + for (const [key, value] of Object.entries(formPostInfoFromProvider)) { + urlObj.searchParams.set(key, `${value}`); + } + options.res.setHeader("Location", urlObj.toString(), false); + options.res.setStatusCode(303); + options.res.sendHTMLResponse(""); }, }; } diff --git a/lib/build/recipe/thirdparty/api/signinup.js b/lib/build/recipe/thirdparty/api/signinup.js index 1e7a657ed..243d82579 100644 --- a/lib/build/recipe/thirdparty/api/signinup.js +++ b/lib/build/recipe/thirdparty/api/signinup.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,73 +21,66 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const error_1 = __importDefault(require("../error")); const utils_1 = require("../../../utils"); -function signInUpAPI(apiImplementation, tenantId, options, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.signInUpPOST === undefined) { - return false; - } - const bodyParams = yield options.req.getJSONBody(); - const thirdPartyId = bodyParams.thirdPartyId; - const clientType = bodyParams.clientType; - if (thirdPartyId === undefined || typeof thirdPartyId !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the thirdPartyId in request body", - }); - } - let redirectURIInfo; - let oAuthTokens; - if (bodyParams.redirectURIInfo !== undefined) { - if (bodyParams.redirectURIInfo.redirectURIOnProviderDashboard === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the redirectURIOnProviderDashboard in request body", - }); - } - redirectURIInfo = bodyParams.redirectURIInfo; - } else if (bodyParams.oAuthTokens !== undefined) { - oAuthTokens = bodyParams.oAuthTokens; - } else { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide one of redirectURIInfo or oAuthTokens in the request body", - }); - } - const providerResponse = yield options.recipeImplementation.getProvider({ - thirdPartyId, - tenantId, - clientType, - userContext, +async function signInUpAPI(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.signInUpPOST === undefined) { + return false; + } + const bodyParams = await options.req.getJSONBody(); + const thirdPartyId = bodyParams.thirdPartyId; + const clientType = bodyParams.clientType; + if (thirdPartyId === undefined || typeof thirdPartyId !== "string") { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the thirdPartyId in request body", }); - if (providerResponse === undefined) { + } + let redirectURIInfo; + let oAuthTokens; + if (bodyParams.redirectURIInfo !== undefined) { + if (bodyParams.redirectURIInfo.redirectURIOnProviderDashboard === undefined) { throw new error_1.default({ type: error_1.default.BAD_INPUT_ERROR, - message: `the provider ${thirdPartyId} could not be found in the configuration`, + message: "Please provide the redirectURIOnProviderDashboard in request body", }); } - const provider = providerResponse; - let result = yield apiImplementation.signInUpPOST({ - provider, - redirectURIInfo, - oAuthTokens, - tenantId, - options, - userContext, + redirectURIInfo = bodyParams.redirectURIInfo; + } else if (bodyParams.oAuthTokens !== undefined) { + oAuthTokens = bodyParams.oAuthTokens; + } else { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide one of redirectURIInfo or oAuthTokens in the request body", }); - if (result.status === "OK") { - utils_1.send200Response(options.res, { - status: result.status, - user: result.user, - createdNewUser: result.createdNewUser, - }); - } else if (result.status === "NO_EMAIL_GIVEN_BY_PROVIDER") { - utils_1.send200Response(options.res, { - status: "NO_EMAIL_GIVEN_BY_PROVIDER", - }); - } else { - utils_1.send200Response(options.res, result); - } - return true; + } + const providerResponse = await options.recipeImplementation.getProvider({ + thirdPartyId, + tenantId, + clientType, + userContext, + }); + if (providerResponse === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: `the provider ${thirdPartyId} could not be found in the configuration`, + }); + } + const provider = providerResponse; + let result = await apiImplementation.signInUpPOST({ + provider, + redirectURIInfo, + oAuthTokens, + tenantId, + options, + userContext, }); + if (result.status === "OK") { + utils_1.send200Response( + options.res, + Object.assign({ status: result.status }, utils_1.getBackwardsCompatibleUserInfo(options.req, result)) + ); + } else { + utils_1.send200Response(options.res, result); + } + return true; } exports.default = signInUpAPI; diff --git a/lib/build/recipe/thirdparty/index.d.ts b/lib/build/recipe/thirdparty/index.d.ts index 8d5d1718a..bf669dca0 100644 --- a/lib/build/recipe/thirdparty/index.d.ts +++ b/lib/build/recipe/thirdparty/index.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, User, APIInterface, APIOptions, TypeProvider } from "./types"; +import { RecipeInterface, APIInterface, APIOptions, TypeProvider } from "./types"; export default class Wrapper { static init: typeof Recipe.init; static Error: typeof SuperTokensError; @@ -16,26 +16,27 @@ export default class Wrapper { thirdPartyId: string, thirdPartyUserId: string, email: string, + isVerified: boolean, userContext?: any - ): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; - static getUserById(userId: string, userContext?: any): Promise; - static getUsersByEmail(tenantId: string, email: string, userContext?: any): Promise; - static getUserByThirdPartyInfo( - tenantId: string, - thirdPartyId: string, - thirdPartyUserId: string, - userContext?: any - ): Promise; + ): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: import("../../types").User; + recipeUserId: import("../..").RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; } export declare let init: typeof Recipe.init; export declare let Error: typeof SuperTokensError; export declare let getProvider: typeof Wrapper.getProvider; export declare let manuallyCreateOrUpdateUser: typeof Wrapper.manuallyCreateOrUpdateUser; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUsersByEmail: typeof Wrapper.getUsersByEmail; -export declare let getUserByThirdPartyInfo: typeof Wrapper.getUserByThirdPartyInfo; -export type { RecipeInterface, User, APIInterface, APIOptions, TypeProvider }; +export type { RecipeInterface, APIInterface, APIOptions, TypeProvider }; diff --git a/lib/build/recipe/thirdparty/index.js b/lib/build/recipe/thirdparty/index.js index 7b0da009e..941228742 100644 --- a/lib/build/recipe/thirdparty/index.js +++ b/lib/build/recipe/thirdparty/index.js @@ -13,84 +13,39 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getUserByThirdPartyInfo = exports.getUsersByEmail = exports.getUserById = exports.manuallyCreateOrUpdateUser = exports.getProvider = exports.Error = exports.init = void 0; +exports.manuallyCreateOrUpdateUser = exports.getProvider = exports.Error = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); const constants_1 = require("../multitenancy/constants"); class Wrapper { - static getProvider(tenantId, thirdPartyId, clientType, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getProvider({ - thirdPartyId, - clientType, - tenantId, - userContext, - }); - }); - } - static manuallyCreateOrUpdateUser(tenantId, thirdPartyId, thirdPartyUserId, email, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.manuallyCreateOrUpdateUser({ - thirdPartyId, - thirdPartyUserId, - email, - tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - userContext, - }); - }); - } - static getUserById(userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - static getUsersByEmail(tenantId, email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ - email, + static async getProvider(tenantId, thirdPartyId, clientType, userContext = {}) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getProvider({ + thirdPartyId, + clientType, tenantId, userContext, }); } - static getUserByThirdPartyInfo(tenantId, thirdPartyId, thirdPartyUserId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ + static async manuallyCreateOrUpdateUser( + tenantId, + thirdPartyId, + thirdPartyUserId, + email, + isVerified, + userContext = {} + ) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.manuallyCreateOrUpdateUser({ thirdPartyId, thirdPartyUserId, - tenantId, + email, + tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + isVerified, userContext, }); } @@ -102,6 +57,3 @@ exports.init = Wrapper.init; exports.Error = Wrapper.Error; exports.getProvider = Wrapper.getProvider; exports.manuallyCreateOrUpdateUser = Wrapper.manuallyCreateOrUpdateUser; -exports.getUserById = Wrapper.getUserById; -exports.getUsersByEmail = Wrapper.getUsersByEmail; -exports.getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; diff --git a/lib/build/recipe/thirdparty/providers/activeDirectory.js b/lib/build/recipe/thirdparty/providers/activeDirectory.js index dc17bc5b0..ac9da9a13 100644 --- a/lib/build/recipe/thirdparty/providers/activeDirectory.js +++ b/lib/build/recipe/thirdparty/providers/activeDirectory.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -58,23 +27,21 @@ function ActiveDirectory(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function ({ clientType, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig({ clientType, userContext }); - if (config.oidcDiscoveryEndpoint === undefined) { - if (config.additionalConfig == undefined || config.additionalConfig.directoryId == undefined) { - throw new Error( - "Please provide the directoryId in the additionalConfig of the Active Directory provider." - ); - } - config.oidcDiscoveryEndpoint = `https://login.microsoftonline.com/${config.additionalConfig.directoryId}/v2.0/`; - } - if (config.scope === undefined) { - config.scope = ["openid", "email"]; + originalImplementation.getConfigForClientType = async function ({ clientType, userContext }) { + const config = await oGetConfig({ clientType, userContext }); + if (config.oidcDiscoveryEndpoint === undefined) { + if (config.additionalConfig == undefined || config.additionalConfig.directoryId == undefined) { + throw new Error( + "Please provide the directoryId in the additionalConfig of the Active Directory provider." + ); } - // TODO later if required, client assertion impl - return config; - }); + config.oidcDiscoveryEndpoint = `https://login.microsoftonline.com/${config.additionalConfig.directoryId}/v2.0/`; + } + if (config.scope === undefined) { + config.scope = ["openid", "email"]; + } + // TODO later if required, client assertion impl + return config; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/apple.js b/lib/build/recipe/thirdparty/providers/apple.js index e9589623f..e97d722f2 100644 --- a/lib/build/recipe/thirdparty/providers/apple.js +++ b/lib/build/recipe/thirdparty/providers/apple.js @@ -49,37 +49,6 @@ var __importStar = __setModuleDefault(result, mod); return result; }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -89,18 +58,16 @@ Object.defineProperty(exports, "__esModule", { value: true }); const custom_1 = __importStar(require("./custom")); const jose = __importStar(require("jose")); const crypto_1 = __importDefault(require("crypto")); -function getClientSecret(clientId, keyId, teamId, privateKey) { - return __awaiter(this, void 0, void 0, function* () { - const alg = "ES256"; - return new jose.SignJWT({}) - .setProtectedHeader({ alg, kid: keyId, typ: "JWT" }) - .setIssuer(teamId) - .setIssuedAt() - .setExpirationTime("180days") - .setAudience("https://appleid.apple.com") - .setSubject(custom_1.getActualClientIdFromDevelopmentClientId(clientId)) - .sign(crypto_1.default.createPrivateKey(privateKey.replace(/\\n/g, "\n"))); - }); +async function getClientSecret(clientId, keyId, teamId, privateKey) { + const alg = "ES256"; + return new jose.SignJWT({}) + .setProtectedHeader({ alg, kid: keyId, typ: "JWT" }) + .setIssuer(teamId) + .setIssuedAt() + .setExpirationTime("180days") + .setAudience("https://appleid.apple.com") + .setSubject(custom_1.getActualClientIdFromDevelopmentClientId(clientId)) + .sign(crypto_1.default.createPrivateKey(privateKey.replace(/\\n/g, "\n"))); } function Apple(input) { if (input.config.name === undefined) { @@ -116,89 +83,83 @@ function Apple(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function ({ clientType, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig({ clientType, userContext }); - if (config.scope === undefined) { - config.scope = ["openid", "email"]; - } - if (config.clientSecret === undefined) { - if ( - config.additionalConfig === undefined || - config.additionalConfig.keyId === undefined || - config.additionalConfig.teamId === undefined || - config.additionalConfig.privateKey === undefined - ) { - throw new Error( - "Please ensure that keyId, teamId and privateKey are provided in the additionalConfig" - ); - } - config.clientSecret = yield getClientSecret( - config.clientId, - config.additionalConfig.keyId, - config.additionalConfig.teamId, - config.additionalConfig.privateKey + originalImplementation.getConfigForClientType = async function ({ clientType, userContext }) { + const config = await oGetConfig({ clientType, userContext }); + if (config.scope === undefined) { + config.scope = ["openid", "email"]; + } + if (config.clientSecret === undefined) { + if ( + config.additionalConfig === undefined || + config.additionalConfig.keyId === undefined || + config.additionalConfig.teamId === undefined || + config.additionalConfig.privateKey === undefined + ) { + throw new Error( + "Please ensure that keyId, teamId and privateKey are provided in the additionalConfig" ); } - return config; - }); + config.clientSecret = await getClientSecret( + config.clientId, + config.additionalConfig.keyId, + config.additionalConfig.teamId, + config.additionalConfig.privateKey + ); + } + return config; }; const oExchangeAuthCodeForOAuthTokens = originalImplementation.exchangeAuthCodeForOAuthTokens; - originalImplementation.exchangeAuthCodeForOAuthTokens = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const response = yield oExchangeAuthCodeForOAuthTokens(input); - const user = input.redirectURIInfo.redirectURIQueryParams.user; - if (user !== undefined) { - if (typeof user === "string") { - response.user = JSON.parse(user); - } else if (typeof user === "object") { - response.user = user; - } + originalImplementation.exchangeAuthCodeForOAuthTokens = async function (input) { + const response = await oExchangeAuthCodeForOAuthTokens(input); + const user = input.redirectURIInfo.redirectURIQueryParams.user; + if (user !== undefined) { + if (typeof user === "string") { + response.user = JSON.parse(user); + } else if (typeof user === "object") { + response.user = user; } - return response; - }); + } + return response; }; const oGetUserInfo = originalImplementation.getUserInfo; - originalImplementation.getUserInfo = function (input) { + originalImplementation.getUserInfo = async function (input) { var _a, _b; - return __awaiter(this, void 0, void 0, function* () { - const response = yield oGetUserInfo(input); - const user = input.oAuthTokens.user; - if (user !== undefined) { - if (typeof user === "string") { - response.rawUserInfoFromProvider = Object.assign( - Object.assign({}, response.rawUserInfoFromProvider), - { - fromIdTokenPayload: Object.assign( - Object.assign( - {}, - (_a = response.rawUserInfoFromProvider) === null || _a === void 0 - ? void 0 - : _a.fromIdTokenPayload - ), - { user: JSON.parse(user) } + const response = await oGetUserInfo(input); + const user = input.oAuthTokens.user; + if (user !== undefined) { + if (typeof user === "string") { + response.rawUserInfoFromProvider = Object.assign( + Object.assign({}, response.rawUserInfoFromProvider), + { + fromIdTokenPayload: Object.assign( + Object.assign( + {}, + (_a = response.rawUserInfoFromProvider) === null || _a === void 0 + ? void 0 + : _a.fromIdTokenPayload ), - } - ); - } else if (typeof user === "object") { - response.rawUserInfoFromProvider = Object.assign( - Object.assign({}, response.rawUserInfoFromProvider), - { - fromIdTokenPayload: Object.assign( - Object.assign( - {}, - (_b = response.rawUserInfoFromProvider) === null || _b === void 0 - ? void 0 - : _b.fromIdTokenPayload - ), - { user } + { user: JSON.parse(user) } + ), + } + ); + } else if (typeof user === "object") { + response.rawUserInfoFromProvider = Object.assign( + Object.assign({}, response.rawUserInfoFromProvider), + { + fromIdTokenPayload: Object.assign( + Object.assign( + {}, + (_b = response.rawUserInfoFromProvider) === null || _b === void 0 + ? void 0 + : _b.fromIdTokenPayload ), - } - ); - } + { user } + ), + } + ); } - return response; - }); + } + return response; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/bitbucket.js b/lib/build/recipe/thirdparty/providers/bitbucket.js index 53adad929..500aae8a4 100644 --- a/lib/build/recipe/thirdparty/providers/bitbucket.js +++ b/lib/build/recipe/thirdparty/providers/bitbucket.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -70,60 +39,56 @@ function Bitbucket(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig(input); - if (config.scope === undefined) { - config.scope = ["account", "email"]; - } - return config; - }); + originalImplementation.getConfigForClientType = async function (input) { + const config = await oGetConfig(input); + if (config.scope === undefined) { + config.scope = ["account", "email"]; + } + return config; }; - originalImplementation.getUserInfo = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const accessToken = input.oAuthTokens.access_token; - if (accessToken === undefined) { - throw new Error("Access token not found"); - } - const headers = { - Authorization: `Bearer ${accessToken}`, - }; - let rawUserInfoFromProvider = { - fromUserInfoAPI: {}, - fromIdTokenPayload: {}, - }; - const userInfoFromAccessToken = yield utils_1.doGetRequest( - "https://api.bitbucket.org/2.0/user", - undefined, - headers - ); - rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken; - const userInfoFromEmail = yield utils_1.doGetRequest( - "https://api.bitbucket.org/2.0/user/emails", - undefined, - headers - ); - rawUserInfoFromProvider.fromUserInfoAPI.email = userInfoFromEmail; - let email = undefined; - let isVerified = false; - for (const emailInfo of userInfoFromEmail.values) { - if (emailInfo.is_primary) { - email = emailInfo.email; - isVerified = emailInfo.is_confirmed; - } + originalImplementation.getUserInfo = async function (input) { + const accessToken = input.oAuthTokens.access_token; + if (accessToken === undefined) { + throw new Error("Access token not found"); + } + const headers = { + Authorization: `Bearer ${accessToken}`, + }; + let rawUserInfoFromProvider = { + fromUserInfoAPI: {}, + fromIdTokenPayload: {}, + }; + const userInfoFromAccessToken = await utils_1.doGetRequest( + "https://api.bitbucket.org/2.0/user", + undefined, + headers + ); + rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken; + const userInfoFromEmail = await utils_1.doGetRequest( + "https://api.bitbucket.org/2.0/user/emails", + undefined, + headers + ); + rawUserInfoFromProvider.fromUserInfoAPI.email = userInfoFromEmail; + let email = undefined; + let isVerified = false; + for (const emailInfo of userInfoFromEmail.values) { + if (emailInfo.is_primary) { + email = emailInfo.email; + isVerified = emailInfo.is_confirmed; } - return { - thirdPartyUserId: rawUserInfoFromProvider.fromUserInfoAPI.uuid, - email: - email === undefined - ? undefined - : { - id: email, - isVerified, - }, - rawUserInfoFromProvider, - }; - }); + } + return { + thirdPartyUserId: rawUserInfoFromProvider.fromUserInfoAPI.uuid, + email: + email === undefined + ? undefined + : { + id: email, + isVerified, + }, + rawUserInfoFromProvider, + }; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/boxySaml.js b/lib/build/recipe/thirdparty/providers/boxySaml.js index f3d06697d..78c09af93 100644 --- a/lib/build/recipe/thirdparty/providers/boxySaml.js +++ b/lib/build/recipe/thirdparty/providers/boxySaml.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -65,24 +34,22 @@ function BoxySAML(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function ({ clientType, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig({ clientType, userContext }); - if (config.additionalConfig === undefined || config.additionalConfig.boxyURL === undefined) { - throw new Error("Please provide the boxyURL in the additionalConfig"); - } - const boxyURL = config.additionalConfig.boxyURL; - if (config.authorizationEndpoint === undefined) { - config.authorizationEndpoint = `${boxyURL}/api/oauth/authorize`; - } - if (config.tokenEndpoint === undefined) { - config.tokenEndpoint = `${boxyURL}/api/oauth/token`; - } - if (config.userInfoEndpoint === undefined) { - config.userInfoEndpoint = `${boxyURL}/api/oauth/userinfo`; - } - return config; - }); + originalImplementation.getConfigForClientType = async function ({ clientType, userContext }) { + const config = await oGetConfig({ clientType, userContext }); + if (config.additionalConfig === undefined || config.additionalConfig.boxyURL === undefined) { + throw new Error("Please provide the boxyURL in the additionalConfig"); + } + const boxyURL = config.additionalConfig.boxyURL; + if (config.authorizationEndpoint === undefined) { + config.authorizationEndpoint = `${boxyURL}/api/oauth/authorize`; + } + if (config.tokenEndpoint === undefined) { + config.tokenEndpoint = `${boxyURL}/api/oauth/token`; + } + if (config.userInfoEndpoint === undefined) { + config.userInfoEndpoint = `${boxyURL}/api/oauth/userinfo`; + } + return config; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/configUtils.js b/lib/build/recipe/thirdparty/providers/configUtils.js index 22ce0016f..4a51c3820 100644 --- a/lib/build/recipe/thirdparty/providers/configUtils.js +++ b/lib/build/recipe/thirdparty/providers/configUtils.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -44,12 +13,10 @@ function getProviderConfigForClient(providerConfig, clientConfig) { return Object.assign(Object.assign({}, providerConfig), clientConfig); } exports.getProviderConfigForClient = getProviderConfigForClient; -function fetchAndSetConfig(provider, clientType, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let config = yield provider.getConfigForClientType({ clientType, userContext }); - config = yield utils_1.discoverOIDCEndpoints(config); - provider.config = config; - }); +async function fetchAndSetConfig(provider, clientType, userContext) { + let config = await provider.getConfigForClientType({ clientType, userContext }); + config = await utils_1.discoverOIDCEndpoints(config); + provider.config = config; } function createProvider(input) { if (input.config.thirdPartyId.startsWith("active-directory")) { @@ -79,17 +46,15 @@ function createProvider(input) { } return custom_1.default(input); } -function findAndCreateProviderInstance(providers, thirdPartyId, clientType, userContext) { - return __awaiter(this, void 0, void 0, function* () { - for (const providerInput of providers) { - if (providerInput.config.thirdPartyId === thirdPartyId) { - let providerInstance = createProvider(providerInput); - yield fetchAndSetConfig(providerInstance, clientType, userContext); - return providerInstance; - } +async function findAndCreateProviderInstance(providers, thirdPartyId, clientType, userContext) { + for (const providerInput of providers) { + if (providerInput.config.thirdPartyId === thirdPartyId) { + let providerInstance = createProvider(providerInput); + await fetchAndSetConfig(providerInstance, clientType, userContext); + return providerInstance; } - return undefined; - }); + } + return undefined; } exports.findAndCreateProviderInstance = findAndCreateProviderInstance; function mergeConfig(staticConfig, coreConfig) { diff --git a/lib/build/recipe/thirdparty/providers/custom.js b/lib/build/recipe/thirdparty/providers/custom.js index 5637f2aec..5dcd21542 100644 --- a/lib/build/recipe/thirdparty/providers/custom.js +++ b/lib/build/recipe/thirdparty/providers/custom.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -181,182 +150,169 @@ function NewProvider(input) { ), }; if (input.config.generateFakeEmail === undefined) { - input.config.generateFakeEmail = function ({ thirdPartyUserId }) { - return __awaiter(this, void 0, void 0, function* () { - return `${thirdPartyUserId}@${input.config.thirdPartyId}.fakeemail.com`; - }); + input.config.generateFakeEmail = async function ({ thirdPartyUserId }) { + return `${thirdPartyUserId}@${input.config.thirdPartyId}.fakeemail.com`; }; } let jwks; let impl = { id: input.config.thirdPartyId, config: Object.assign(Object.assign({}, input.config), { clientId: "temp" }), - getConfigForClientType: function ({ clientType }) { - return __awaiter(this, void 0, void 0, function* () { - if (clientType === undefined) { - if (input.config.clients === undefined || input.config.clients.length !== 1) { - throw new Error("please provide exactly one client config or pass clientType or tenantId"); - } - return configUtils_1.getProviderConfigForClient(input.config, input.config.clients[0]); + getConfigForClientType: async function ({ clientType }) { + if (clientType === undefined) { + if (input.config.clients === undefined || input.config.clients.length !== 1) { + throw new Error("please provide exactly one client config or pass clientType or tenantId"); } - if (input.config.clients !== undefined) { - for (const client of input.config.clients) { - if (client.clientType === clientType) { - return configUtils_1.getProviderConfigForClient(input.config, client); - } + return configUtils_1.getProviderConfigForClient(input.config, input.config.clients[0]); + } + if (input.config.clients !== undefined) { + for (const client of input.config.clients) { + if (client.clientType === clientType) { + return configUtils_1.getProviderConfigForClient(input.config, client); } } - throw new Error(`Could not find client config for clientType: ${clientType}`); - }); + } + throw new Error(`Could not find client config for clientType: ${clientType}`); }, - getAuthorisationRedirectURL: function ({ redirectURIOnProviderDashboard }) { - return __awaiter(this, void 0, void 0, function* () { - const queryParams = { - client_id: impl.config.clientId, - redirect_uri: redirectURIOnProviderDashboard, - response_type: "code", - }; - if (impl.config.scope !== undefined) { - queryParams.scope = impl.config.scope.join(" "); - } - let pkceCodeVerifier = undefined; - if (impl.config.clientSecret === undefined || impl.config.forcePKCE) { - const { code_challenge, code_verifier } = pkce_challenge_1.default(64); // According to https://www.rfc-editor.org/rfc/rfc7636, length must be between 43 and 128 - queryParams["code_challenge"] = code_challenge; - queryParams["code_challenge_method"] = "S256"; - pkceCodeVerifier = code_verifier; - } - if (impl.config.authorizationEndpointQueryParams !== undefined) { - for (const [key, value] of Object.entries(impl.config.authorizationEndpointQueryParams)) { - if (value === null) { - delete queryParams[key]; - } else { - queryParams[key] = value; - } + getAuthorisationRedirectURL: async function ({ redirectURIOnProviderDashboard }) { + const queryParams = { + client_id: impl.config.clientId, + redirect_uri: redirectURIOnProviderDashboard, + response_type: "code", + }; + if (impl.config.scope !== undefined) { + queryParams.scope = impl.config.scope.join(" "); + } + let pkceCodeVerifier = undefined; + if (impl.config.clientSecret === undefined || impl.config.forcePKCE) { + const { code_challenge, code_verifier } = pkce_challenge_1.default(64); // According to https://www.rfc-editor.org/rfc/rfc7636, length must be between 43 and 128 + queryParams["code_challenge"] = code_challenge; + queryParams["code_challenge_method"] = "S256"; + pkceCodeVerifier = code_verifier; + } + if (impl.config.authorizationEndpointQueryParams !== undefined) { + for (const [key, value] of Object.entries(impl.config.authorizationEndpointQueryParams)) { + if (value === null) { + delete queryParams[key]; + } else { + queryParams[key] = value; } } - if (impl.config.authorizationEndpoint === undefined) { - throw new Error("ThirdParty provider's authorizationEndpoint is not configured."); - } - let url = impl.config.authorizationEndpoint; - /* Transformation needed for dev keys BEGIN */ - if (isUsingDevelopmentClientId(impl.config.clientId)) { - queryParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); - queryParams["actual_redirect_uri"] = url; - url = DEV_OAUTH_AUTHORIZATION_URL; - } - /* Transformation needed for dev keys END */ - const urlObj = new URL(url); - for (const [key, value] of Object.entries(queryParams)) { - urlObj.searchParams.set(key, value); - } - return { - urlWithQueryParams: urlObj.toString(), - pkceCodeVerifier: pkceCodeVerifier, - }; - }); + } + if (impl.config.authorizationEndpoint === undefined) { + throw new Error("ThirdParty provider's authorizationEndpoint is not configured."); + } + let url = impl.config.authorizationEndpoint; + /* Transformation needed for dev keys BEGIN */ + if (isUsingDevelopmentClientId(impl.config.clientId)) { + queryParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); + queryParams["actual_redirect_uri"] = url; + url = DEV_OAUTH_AUTHORIZATION_URL; + } + /* Transformation needed for dev keys END */ + const urlObj = new URL(url); + for (const [key, value] of Object.entries(queryParams)) { + urlObj.searchParams.set(key, value); + } + return { + urlWithQueryParams: urlObj.toString(), + pkceCodeVerifier: pkceCodeVerifier, + }; }, - exchangeAuthCodeForOAuthTokens: function ({ redirectURIInfo }) { - return __awaiter(this, void 0, void 0, function* () { - if (impl.config.tokenEndpoint === undefined) { - throw new Error("ThirdParty provider's tokenEndpoint is not configured."); - } - const tokenAPIURL = impl.config.tokenEndpoint; - const accessTokenAPIParams = { - client_id: impl.config.clientId, - redirect_uri: redirectURIInfo.redirectURIOnProviderDashboard, - code: redirectURIInfo.redirectURIQueryParams["code"], - grant_type: "authorization_code", - }; - if (impl.config.clientSecret !== undefined) { - accessTokenAPIParams["client_secret"] = impl.config.clientSecret; + exchangeAuthCodeForOAuthTokens: async function ({ redirectURIInfo }) { + if (impl.config.tokenEndpoint === undefined) { + throw new Error("ThirdParty provider's tokenEndpoint is not configured."); + } + const tokenAPIURL = impl.config.tokenEndpoint; + const accessTokenAPIParams = { + client_id: impl.config.clientId, + redirect_uri: redirectURIInfo.redirectURIOnProviderDashboard, + code: redirectURIInfo.redirectURIQueryParams["code"], + grant_type: "authorization_code", + }; + if (impl.config.clientSecret !== undefined) { + accessTokenAPIParams["client_secret"] = impl.config.clientSecret; + } + if (redirectURIInfo.pkceCodeVerifier !== undefined) { + accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; + } + for (const key in impl.config.tokenEndpointBodyParams) { + if (impl.config.tokenEndpointBodyParams[key] === null) { + delete accessTokenAPIParams[key]; + } else { + accessTokenAPIParams[key] = impl.config.tokenEndpointBodyParams[key]; } - if (redirectURIInfo.pkceCodeVerifier !== undefined) { - accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; + } + /* Transformation needed for dev keys BEGIN */ + if (isUsingDevelopmentClientId(impl.config.clientId)) { + accessTokenAPIParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); + accessTokenAPIParams["redirect_uri"] = DEV_OAUTH_REDIRECT_URL; + } + /* Transformation needed for dev keys END */ + return await utils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); + }, + getUserInfo: async function ({ oAuthTokens, userContext }) { + const accessToken = oAuthTokens["access_token"]; + const idToken = oAuthTokens["id_token"]; + let rawUserInfoFromProvider = { + fromUserInfoAPI: {}, + fromIdTokenPayload: {}, + }; + if (idToken && impl.config.jwksURI !== undefined) { + if (jwks === undefined) { + jwks = jose_1.createRemoteJWKSet(new URL(impl.config.jwksURI)); } - for (const key in impl.config.tokenEndpointBodyParams) { - if (impl.config.tokenEndpointBodyParams[key] === null) { - delete accessTokenAPIParams[key]; - } else { - accessTokenAPIParams[key] = impl.config.tokenEndpointBodyParams[key]; + rawUserInfoFromProvider.fromIdTokenPayload = await utils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( + idToken, + jwks, + { + audience: getActualClientIdFromDevelopmentClientId(impl.config.clientId), } + ); + if (impl.config.validateIdTokenPayload !== undefined) { + await impl.config.validateIdTokenPayload({ + idTokenPayload: rawUserInfoFromProvider.fromIdTokenPayload, + clientConfig: impl.config, + userContext, + }); } - /* Transformation needed for dev keys BEGIN */ - if (isUsingDevelopmentClientId(impl.config.clientId)) { - accessTokenAPIParams["client_id"] = getActualClientIdFromDevelopmentClientId(impl.config.clientId); - accessTokenAPIParams["redirect_uri"] = DEV_OAUTH_REDIRECT_URL; - } - /* Transformation needed for dev keys END */ - return yield utils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); - }); - }, - getUserInfo: function ({ oAuthTokens, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const accessToken = oAuthTokens["access_token"]; - const idToken = oAuthTokens["id_token"]; - let rawUserInfoFromProvider = { - fromUserInfoAPI: {}, - fromIdTokenPayload: {}, + } + if (accessToken && impl.config.userInfoEndpoint !== undefined) { + const headers = { + Authorization: "Bearer " + accessToken, }; - if (idToken && impl.config.jwksURI !== undefined) { - if (jwks === undefined) { - jwks = jose_1.createRemoteJWKSet(new URL(impl.config.jwksURI)); - } - rawUserInfoFromProvider.fromIdTokenPayload = yield utils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( - idToken, - jwks, - { - audience: getActualClientIdFromDevelopmentClientId(impl.config.clientId), + const queryParams = {}; + if (impl.config.userInfoEndpointHeaders !== undefined) { + for (const [key, value] of Object.entries(impl.config.userInfoEndpointHeaders)) { + if (value === null) { + delete headers[key]; + } else { + headers[key] = value; } - ); - if (impl.config.validateIdTokenPayload !== undefined) { - yield impl.config.validateIdTokenPayload({ - idTokenPayload: rawUserInfoFromProvider.fromIdTokenPayload, - clientConfig: impl.config, - userContext, - }); } } - if (accessToken && impl.config.userInfoEndpoint !== undefined) { - const headers = { - Authorization: "Bearer " + accessToken, - }; - const queryParams = {}; - if (impl.config.userInfoEndpointHeaders !== undefined) { - for (const [key, value] of Object.entries(impl.config.userInfoEndpointHeaders)) { - if (value === null) { - delete headers[key]; - } else { - headers[key] = value; - } - } - } - if (impl.config.userInfoEndpointQueryParams !== undefined) { - for (const [key, value] of Object.entries(impl.config.userInfoEndpointQueryParams)) { - if (value === null) { - delete queryParams[key]; - } else { - queryParams[key] = value; - } + if (impl.config.userInfoEndpointQueryParams !== undefined) { + for (const [key, value] of Object.entries(impl.config.userInfoEndpointQueryParams)) { + if (value === null) { + delete queryParams[key]; + } else { + queryParams[key] = value; } } - const userInfoFromAccessToken = yield utils_1.doGetRequest( - impl.config.userInfoEndpoint, - queryParams, - headers - ); - rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken; } - const userInfoResult = getSupertokensUserInfoResultFromRawUserInfo( - impl.config, - rawUserInfoFromProvider + const userInfoFromAccessToken = await utils_1.doGetRequest( + impl.config.userInfoEndpoint, + queryParams, + headers ); - return { - thirdPartyUserId: userInfoResult.thirdPartyUserId, - email: userInfoResult.email, - rawUserInfoFromProvider: rawUserInfoFromProvider, - }; - }); + rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken; + } + const userInfoResult = getSupertokensUserInfoResultFromRawUserInfo(impl.config, rawUserInfoFromProvider); + return { + thirdPartyUserId: userInfoResult.thirdPartyUserId, + email: userInfoResult.email, + rawUserInfoFromProvider: rawUserInfoFromProvider, + }; }, }; // No need to use an overrideable builder here because the functions in the `TypeProvider` diff --git a/lib/build/recipe/thirdparty/providers/discord.js b/lib/build/recipe/thirdparty/providers/discord.js index 4809f6627..cf161c55c 100644 --- a/lib/build/recipe/thirdparty/providers/discord.js +++ b/lib/build/recipe/thirdparty/providers/discord.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -60,14 +29,12 @@ function Discord(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function ({ clientType, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig({ clientType, userContext }); - if (config.scope === undefined) { - config.scope = ["identify", "email"]; - } - return config; - }); + originalImplementation.getConfigForClientType = async function ({ clientType, userContext }) { + const config = await oGetConfig({ clientType, userContext }); + if (config.scope === undefined) { + config.scope = ["identify", "email"]; + } + return config; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/facebook.js b/lib/build/recipe/thirdparty/providers/facebook.js index e3b3b6a61..2c049e086 100644 --- a/lib/build/recipe/thirdparty/providers/facebook.js +++ b/lib/build/recipe/thirdparty/providers/facebook.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -60,28 +29,24 @@ function Facebook(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig(input); - if (config.scope === undefined) { - config.scope = ["email"]; - } - return config; - }); + originalImplementation.getConfigForClientType = async function (input) { + const config = await oGetConfig(input); + if (config.scope === undefined) { + config.scope = ["email"]; + } + return config; }; const oGetUserInfo = originalImplementation.getUserInfo; - originalImplementation.getUserInfo = function (input) { - return __awaiter(this, void 0, void 0, function* () { - originalImplementation.config.userInfoEndpointQueryParams = Object.assign( - { access_token: input.oAuthTokens.access_token, fields: "id,email", format: "json" }, - originalImplementation.config.userInfoEndpointQueryParams - ); - originalImplementation.config.userInfoEndpointHeaders = Object.assign( - Object.assign({}, originalImplementation.config.userInfoEndpointHeaders), - { Authorization: null } - ); - return yield oGetUserInfo(input); - }); + originalImplementation.getUserInfo = async function (input) { + originalImplementation.config.userInfoEndpointQueryParams = Object.assign( + { access_token: input.oAuthTokens.access_token, fields: "id,email", format: "json" }, + originalImplementation.config.userInfoEndpointQueryParams + ); + originalImplementation.config.userInfoEndpointHeaders = Object.assign( + Object.assign({}, originalImplementation.config.userInfoEndpointHeaders), + { Authorization: null } + ); + return await oGetUserInfo(input); }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/github.js b/lib/build/recipe/thirdparty/providers/github.js index 67aa54a08..e7fd91dbd 100644 --- a/lib/build/recipe/thirdparty/providers/github.js +++ b/lib/build/recipe/thirdparty/providers/github.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -87,45 +56,37 @@ function Github(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig(input); - if (config.scope === undefined) { - config.scope = ["read:user", "user:email"]; - } - return config; - }); + originalImplementation.getConfigForClientType = async function (input) { + const config = await oGetConfig(input); + if (config.scope === undefined) { + config.scope = ["read:user", "user:email"]; + } + return config; }; - originalImplementation.getUserInfo = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const headers = { - Authorization: `Bearer ${input.oAuthTokens.access_token}`, - Accept: "application/vnd.github.v3+json", - }; - const rawResponse = {}; - const emailInfoResp = yield cross_fetch_1.default("https://api.github.com/user/emails", { headers }); - if (emailInfoResp.status >= 400) { - throw new Error( - `Getting userInfo failed with ${emailInfoResp.status}: ${yield emailInfoResp.text()}` - ); - } - const emailInfo = yield emailInfoResp.json(); - rawResponse.emails = emailInfo; - const userInfoResp = yield cross_fetch_1.default("https://api.github.com/user", { headers }); - if (userInfoResp.status >= 400) { - throw new Error( - `Getting userInfo failed with ${userInfoResp.status}: ${yield userInfoResp.text()}` - ); - } - const userInfo = yield userInfoResp.json(); - rawResponse.user = userInfo; - const rawUserInfoFromProvider = { - fromUserInfoAPI: rawResponse, - fromIdTokenPayload: {}, - }; - const userInfoResult = getSupertokensUserInfoFromRawUserInfoResponseForGithub(rawUserInfoFromProvider); - return Object.assign(Object.assign({}, userInfoResult), { rawUserInfoFromProvider }); - }); + originalImplementation.getUserInfo = async function (input) { + const headers = { + Authorization: `Bearer ${input.oAuthTokens.access_token}`, + Accept: "application/vnd.github.v3+json", + }; + const rawResponse = {}; + const emailInfoResp = await cross_fetch_1.default("https://api.github.com/user/emails", { headers }); + if (emailInfoResp.status >= 400) { + throw new Error(`Getting userInfo failed with ${emailInfoResp.status}: ${await emailInfoResp.text()}`); + } + const emailInfo = await emailInfoResp.json(); + rawResponse.emails = emailInfo; + const userInfoResp = await cross_fetch_1.default("https://api.github.com/user", { headers }); + if (userInfoResp.status >= 400) { + throw new Error(`Getting userInfo failed with ${userInfoResp.status}: ${await userInfoResp.text()}`); + } + const userInfo = await userInfoResp.json(); + rawResponse.user = userInfo; + const rawUserInfoFromProvider = { + fromUserInfoAPI: rawResponse, + fromIdTokenPayload: {}, + }; + const userInfoResult = getSupertokensUserInfoFromRawUserInfoResponseForGithub(rawUserInfoFromProvider); + return Object.assign(Object.assign({}, userInfoResult), { rawUserInfoFromProvider }); }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/gitlab.js b/lib/build/recipe/thirdparty/providers/gitlab.js index 874c4e954..a7f121c5d 100644 --- a/lib/build/recipe/thirdparty/providers/gitlab.js +++ b/lib/build/recipe/thirdparty/providers/gitlab.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -63,21 +32,19 @@ function Gitlab(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig(input); - if (config.scope === undefined) { - config.scope = ["openid", "email"]; - } - if (config.oidcDiscoveryEndpoint === undefined) { - if (config.additionalConfig !== undefined && config.additionalConfig.gitlabBaseUrl !== undefined) { - config.oidcDiscoveryEndpoint = config.additionalConfig.gitlabBaseUrl; - } else { - config.oidcDiscoveryEndpoint = "https://gitlab.com"; - } + originalImplementation.getConfigForClientType = async function (input) { + const config = await oGetConfig(input); + if (config.scope === undefined) { + config.scope = ["openid", "email"]; + } + if (config.oidcDiscoveryEndpoint === undefined) { + if (config.additionalConfig !== undefined && config.additionalConfig.gitlabBaseUrl !== undefined) { + config.oidcDiscoveryEndpoint = config.additionalConfig.gitlabBaseUrl; + } else { + config.oidcDiscoveryEndpoint = "https://gitlab.com"; } - return config; - }); + } + return config; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/google.js b/lib/build/recipe/thirdparty/providers/google.js index 4e44af828..db1139473 100644 --- a/lib/build/recipe/thirdparty/providers/google.js +++ b/lib/build/recipe/thirdparty/providers/google.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -51,14 +20,12 @@ function Google(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig(input); - if (config.scope === undefined) { - config.scope = ["openid", "email"]; - } - return config; - }); + originalImplementation.getConfigForClientType = async function (input) { + const config = await oGetConfig(input); + if (config.scope === undefined) { + config.scope = ["openid", "email"]; + } + return config; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/googleWorkspaces.js b/lib/build/recipe/thirdparty/providers/googleWorkspaces.js index 0391075f8..b7c9cc6fc 100644 --- a/lib/build/recipe/thirdparty/providers/googleWorkspaces.js +++ b/lib/build/recipe/thirdparty/providers/googleWorkspaces.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -42,24 +11,21 @@ function GoogleWorkspaces(input) { input.config.name = "Google Workspaces"; } if (input.config.validateIdTokenPayload === undefined) { - input.config.validateIdTokenPayload = function (input) { + input.config.validateIdTokenPayload = async function (input) { var _a, _b, _c; - return __awaiter(this, void 0, void 0, function* () { + if ( + ((_a = input.clientConfig.additionalConfig) === null || _a === void 0 ? void 0 : _a.hd) !== undefined && + ((_b = input.clientConfig.additionalConfig) === null || _b === void 0 ? void 0 : _b.hd) !== "*" + ) { if ( - ((_a = input.clientConfig.additionalConfig) === null || _a === void 0 ? void 0 : _a.hd) !== - undefined && - ((_b = input.clientConfig.additionalConfig) === null || _b === void 0 ? void 0 : _b.hd) !== "*" + ((_c = input.clientConfig.additionalConfig) === null || _c === void 0 ? void 0 : _c.hd) !== + input.idTokenPayload.hd ) { - if ( - ((_c = input.clientConfig.additionalConfig) === null || _c === void 0 ? void 0 : _c.hd) !== - input.idTokenPayload.hd - ) { - throw new Error( - "the value for hd claim in the id token does not match the value provided in the config" - ); - } + throw new Error( + "the value for hd claim in the id token does not match the value provided in the config" + ); } - }); + } }; } input.config.authorizationEndpointQueryParams = Object.assign( @@ -69,16 +35,14 @@ function GoogleWorkspaces(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig(input); - config.additionalConfig = Object.assign({ hd: "*" }, config.additionalConfig); - config.authorizationEndpointQueryParams = Object.assign( - Object.assign({}, config.authorizationEndpointQueryParams), - { hd: config.additionalConfig.hd } - ); - return config; - }); + originalImplementation.getConfigForClientType = async function (input) { + const config = await oGetConfig(input); + config.additionalConfig = Object.assign({ hd: "*" }, config.additionalConfig); + config.authorizationEndpointQueryParams = Object.assign( + Object.assign({}, config.authorizationEndpointQueryParams), + { hd: config.additionalConfig.hd } + ); + return config; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/linkedin.js b/lib/build/recipe/thirdparty/providers/linkedin.js index 425dbe9ea..9e1e482a8 100644 --- a/lib/build/recipe/thirdparty/providers/linkedin.js +++ b/lib/build/recipe/thirdparty/providers/linkedin.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -51,57 +20,52 @@ function Linkedin(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig(input); - if (config.scope === undefined) { - config.scope = ["r_emailaddress", "r_liteprofile"]; - } - return config; - }); + originalImplementation.getConfigForClientType = async function (input) { + const config = await oGetConfig(input); + if (config.scope === undefined) { + config.scope = ["r_emailaddress", "r_liteprofile"]; + } + return config; }; - originalImplementation.getUserInfo = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const accessToken = input.oAuthTokens.access_token; - if (accessToken === undefined) { - throw new Error("Access token not found"); - } - const headers = { - Authorization: `Bearer ${accessToken}`, - }; - let rawUserInfoFromProvider = { - fromUserInfoAPI: {}, - fromIdTokenPayload: {}, - }; - const userInfoFromAccessToken = yield utils_1.doGetRequest( - "https://api.linkedin.com/v2/me", - undefined, - headers - ); - rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken; - const emailAPIURL = "https://api.linkedin.com/v2/emailAddress"; - const userInfoFromEmail = yield utils_1.doGetRequest( - emailAPIURL, - { q: "members", projection: "(elements*(handle~))" }, - headers - ); - if (userInfoFromEmail.elements && userInfoFromEmail.elements.length > 0) { - rawUserInfoFromProvider.fromUserInfoAPI.email = - userInfoFromEmail.elements[0]["handle~"].emailAddress; - } - rawUserInfoFromProvider.fromUserInfoAPI = Object.assign( - Object.assign({}, rawUserInfoFromProvider.fromUserInfoAPI), - userInfoFromEmail - ); - return { - thirdPartyUserId: rawUserInfoFromProvider.fromUserInfoAPI.id, - email: { - id: rawUserInfoFromProvider.fromUserInfoAPI.email, - isVerified: false, - }, - rawUserInfoFromProvider, - }; - }); + originalImplementation.getUserInfo = async function (input) { + const accessToken = input.oAuthTokens.access_token; + if (accessToken === undefined) { + throw new Error("Access token not found"); + } + const headers = { + Authorization: `Bearer ${accessToken}`, + }; + let rawUserInfoFromProvider = { + fromUserInfoAPI: {}, + fromIdTokenPayload: {}, + }; + const userInfoFromAccessToken = await utils_1.doGetRequest( + "https://api.linkedin.com/v2/me", + undefined, + headers + ); + rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken; + const emailAPIURL = "https://api.linkedin.com/v2/emailAddress"; + const userInfoFromEmail = await utils_1.doGetRequest( + emailAPIURL, + { q: "members", projection: "(elements*(handle~))" }, + headers + ); + if (userInfoFromEmail.elements && userInfoFromEmail.elements.length > 0) { + rawUserInfoFromProvider.fromUserInfoAPI.email = userInfoFromEmail.elements[0]["handle~"].emailAddress; + } + rawUserInfoFromProvider.fromUserInfoAPI = Object.assign( + Object.assign({}, rawUserInfoFromProvider.fromUserInfoAPI), + userInfoFromEmail + ); + return { + thirdPartyUserId: rawUserInfoFromProvider.fromUserInfoAPI.id, + email: { + id: rawUserInfoFromProvider.fromUserInfoAPI.email, + isVerified: false, + }, + rawUserInfoFromProvider, + }; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/okta.js b/lib/build/recipe/thirdparty/providers/okta.js index 3f866b3ab..659472ef9 100644 --- a/lib/build/recipe/thirdparty/providers/okta.js +++ b/lib/build/recipe/thirdparty/providers/okta.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -44,21 +13,19 @@ function Okta(input) { const oOverride = input.override; input.override = function (originalImplementation) { const oGetConfig = originalImplementation.getConfigForClientType; - originalImplementation.getConfigForClientType = function (input) { - return __awaiter(this, void 0, void 0, function* () { - const config = yield oGetConfig(input); - if (config.oidcDiscoveryEndpoint === undefined) { - if (config.additionalConfig == undefined || config.additionalConfig.oktaDomain == undefined) { - throw new Error("Please provide the oktaDomain in the additionalConfig of the Okta provider."); - } - config.oidcDiscoveryEndpoint = `${config.additionalConfig.oktaDomain}`; - } - if (config.scope === undefined) { - config.scope = ["openid", "email"]; + originalImplementation.getConfigForClientType = async function (input) { + const config = await oGetConfig(input); + if (config.oidcDiscoveryEndpoint === undefined) { + if (config.additionalConfig == undefined || config.additionalConfig.oktaDomain == undefined) { + throw new Error("Please provide the oktaDomain in the additionalConfig of the Okta provider."); } - // TODO later if required, client assertion impl - return config; - }); + config.oidcDiscoveryEndpoint = `${config.additionalConfig.oktaDomain}`; + } + if (config.scope === undefined) { + config.scope = ["openid", "email"]; + } + // TODO later if required, client assertion impl + return config; }; if (oOverride !== undefined) { originalImplementation = oOverride(originalImplementation); diff --git a/lib/build/recipe/thirdparty/providers/utils.js b/lib/build/recipe/thirdparty/providers/utils.js index 942351b86..7e5896f2c 100644 --- a/lib/build/recipe/thirdparty/providers/utils.js +++ b/lib/build/recipe/thirdparty/providers/utils.js @@ -35,37 +35,6 @@ var __importStar = __setModuleDefault(result, mod); return result; }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -78,112 +47,92 @@ const jose = __importStar(require("jose")); const normalisedURLDomain_1 = __importDefault(require("../../../normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); const logger_1 = require("../../../logger"); -function doGetRequest(url, queryParams, headers) { - return __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage( - `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify( - headers - )}` - ); - if ((headers === null || headers === void 0 ? void 0 : headers["Accept"]) === undefined) { - headers = Object.assign(Object.assign({}, headers), { Accept: "application/json" }); - } - const finalURL = new URL(url); - finalURL.search = new URLSearchParams(queryParams).toString(); - let response = yield cross_fetch_1.default(finalURL.toString(), { - headers: headers, - }); - if (response.status >= 400) { - logger_1.logDebugMessage( - `Received response with status ${response.status} and body ${yield response.clone().text()}` - ); - throw new Error( - `Received response with status ${response.status} and body ${yield response.clone().text()}` - ); - } - const respData = yield response.clone().json(); +async function doGetRequest(url, queryParams, headers) { + logger_1.logDebugMessage( + `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify(headers)}` + ); + if ((headers === null || headers === void 0 ? void 0 : headers["Accept"]) === undefined) { + headers = Object.assign(Object.assign({}, headers), { Accept: "application/json" }); + } + const finalURL = new URL(url); + finalURL.search = new URLSearchParams(queryParams).toString(); + let response = await cross_fetch_1.default(finalURL.toString(), { + headers: headers, + }); + if (response.status >= 400) { logger_1.logDebugMessage( - `Received response with status ${response.status} and body ${JSON.stringify(respData)}` + `Received response with status ${response.status} and body ${await response.clone().text()}` ); - return respData; - }); + throw new Error(`Received response with status ${response.status} and body ${await response.clone().text()}`); + } + const respData = await response.clone().json(); + logger_1.logDebugMessage(`Received response with status ${response.status} and body ${JSON.stringify(respData)}`); + return respData; } exports.doGetRequest = doGetRequest; -function doPostRequest(url, params, headers) { - return __awaiter(this, void 0, void 0, function* () { - if (headers === undefined) { - headers = {}; - } - headers["Content-Type"] = "application/x-www-form-urlencoded"; - headers["Accept"] = "application/json"; // few providers like github don't send back json response by default - logger_1.logDebugMessage( - `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` - ); - const body = new URLSearchParams(params).toString(); - let response = yield cross_fetch_1.default(url, { - method: "POST", - body, - headers, - }); - if (response.status >= 400) { - logger_1.logDebugMessage( - `Received response with status ${response.status} and body ${yield response.clone().text()}` - ); - throw new Error( - `Received response with status ${response.status} and body ${yield response.clone().text()}` - ); - } - const respData = yield response.clone().json(); +async function doPostRequest(url, params, headers) { + if (headers === undefined) { + headers = {}; + } + headers["Content-Type"] = "application/x-www-form-urlencoded"; + headers["Accept"] = "application/json"; // few providers like github don't send back json response by default + logger_1.logDebugMessage( + `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` + ); + const body = new URLSearchParams(params).toString(); + let response = await cross_fetch_1.default(url, { + method: "POST", + body, + headers, + }); + if (response.status >= 400) { logger_1.logDebugMessage( - `Received response with status ${response.status} and body ${JSON.stringify(respData)}` + `Received response with status ${response.status} and body ${await response.clone().text()}` ); - return respData; - }); + throw new Error(`Received response with status ${response.status} and body ${await response.clone().text()}`); + } + const respData = await response.clone().json(); + logger_1.logDebugMessage(`Received response with status ${response.status} and body ${JSON.stringify(respData)}`); + return respData; } exports.doPostRequest = doPostRequest; -function verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, otherOptions) { - return __awaiter(this, void 0, void 0, function* () { - const { payload } = yield jose.jwtVerify(idToken, jwks, otherOptions); - return payload; - }); +async function verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, otherOptions) { + const { payload } = await jose.jwtVerify(idToken, jwks, otherOptions); + return payload; } exports.verifyIdTokenFromJWKSEndpointAndGetPayload = verifyIdTokenFromJWKSEndpointAndGetPayload; // OIDC utils var oidcInfoMap = {}; -function getOIDCDiscoveryInfo(issuer) { - return __awaiter(this, void 0, void 0, function* () { - const normalizedDomain = new normalisedURLDomain_1.default(issuer); - let normalizedPath = new normalisedURLPath_1.default(issuer); - const openIdConfigPath = new normalisedURLPath_1.default("/.well-known/openid-configuration"); - normalizedPath = normalizedPath.appendPath(openIdConfigPath); - if (oidcInfoMap[issuer] !== undefined) { - return oidcInfoMap[issuer]; - } - const oidcInfo = yield doGetRequest( - normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous() - ); - oidcInfoMap[issuer] = oidcInfo; - return oidcInfo; - }); +async function getOIDCDiscoveryInfo(issuer) { + const normalizedDomain = new normalisedURLDomain_1.default(issuer); + let normalizedPath = new normalisedURLPath_1.default(issuer); + const openIdConfigPath = new normalisedURLPath_1.default("/.well-known/openid-configuration"); + normalizedPath = normalizedPath.appendPath(openIdConfigPath); + if (oidcInfoMap[issuer] !== undefined) { + return oidcInfoMap[issuer]; + } + const oidcInfo = await doGetRequest( + normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous() + ); + oidcInfoMap[issuer] = oidcInfo; + return oidcInfo; } -function discoverOIDCEndpoints(config) { - return __awaiter(this, void 0, void 0, function* () { - if (config.oidcDiscoveryEndpoint !== undefined) { - const oidcInfo = yield getOIDCDiscoveryInfo(config.oidcDiscoveryEndpoint); - if (oidcInfo.authorization_endpoint !== undefined && config.authorizationEndpoint === undefined) { - config.authorizationEndpoint = oidcInfo.authorization_endpoint; - } - if (oidcInfo.token_endpoint !== undefined && config.tokenEndpoint === undefined) { - config.tokenEndpoint = oidcInfo.token_endpoint; - } - if (oidcInfo.userinfo_endpoint !== undefined && config.userInfoEndpoint === undefined) { - config.userInfoEndpoint = oidcInfo.userinfo_endpoint; - } - if (oidcInfo.jwks_uri !== undefined && config.jwksURI === undefined) { - config.jwksURI = oidcInfo.jwks_uri; - } +async function discoverOIDCEndpoints(config) { + if (config.oidcDiscoveryEndpoint !== undefined) { + const oidcInfo = await getOIDCDiscoveryInfo(config.oidcDiscoveryEndpoint); + if (oidcInfo.authorization_endpoint !== undefined && config.authorizationEndpoint === undefined) { + config.authorizationEndpoint = oidcInfo.authorization_endpoint; } - return config; - }); + if (oidcInfo.token_endpoint !== undefined && config.tokenEndpoint === undefined) { + config.tokenEndpoint = oidcInfo.token_endpoint; + } + if (oidcInfo.userinfo_endpoint !== undefined && config.userInfoEndpoint === undefined) { + config.userInfoEndpoint = oidcInfo.userinfo_endpoint; + } + if (oidcInfo.jwks_uri !== undefined && config.jwksURI === undefined) { + config.jwksURI = oidcInfo.jwks_uri; + } + } + return config; } exports.discoverOIDCEndpoints = discoverOIDCEndpoints; diff --git a/lib/build/recipe/thirdparty/recipe.d.ts b/lib/build/recipe/thirdparty/recipe.d.ts index 49727655d..688c3b2a4 100644 --- a/lib/build/recipe/thirdparty/recipe.d.ts +++ b/lib/build/recipe/thirdparty/recipe.d.ts @@ -4,8 +4,7 @@ import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from ". import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, ProviderInput } from "./types"; import STError from "./error"; import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; +import type { BaseRequest, BaseResponse } from "../../framework"; export default class Recipe extends RecipeModule { private static instance; static RECIPE_ID: string; @@ -38,5 +37,4 @@ export default class Recipe extends RecipeModule { handleError: (err: STError, _request: BaseRequest, _response: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; isErrorFromThisRecipe: (err: any) => err is STError; - getEmailForUserId: GetEmailForUserIdFunc; } diff --git a/lib/build/recipe/thirdparty/recipe.js b/lib/build/recipe/thirdparty/recipe.js index b4ea6e5c0..2c4870278 100644 --- a/lib/build/recipe/thirdparty/recipe.js +++ b/lib/build/recipe/thirdparty/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -52,8 +21,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const recipeModule_1 = __importDefault(require("../../recipeModule")); const utils_1 = require("./utils"); -const recipe_1 = __importDefault(require("../emailverification/recipe")); -const recipe_2 = __importDefault(require("../multitenancy/recipe")); +const recipe_1 = __importDefault(require("../multitenancy/recipe")); const error_1 = __importDefault(require("./error")); const constants_1 = require("./constants"); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); @@ -90,51 +58,35 @@ class Recipe extends recipeModule_1.default { }, ]; }; - this.handleAPIRequest = (id, tenantId, req, res, _path, _method, userContext) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - providers: this.providers, - req, - res, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.SIGN_IN_UP_API) { - return yield signinup_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.AUTHORISATION_API) { - return yield authorisationUrl_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.APPLE_REDIRECT_HANDLER) { - return yield appleRedirect_1.default(this.apiImpl, options, userContext); - } - return false; - }); - this.handleError = (err, _request, _response) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); + this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + providers: this.providers, + req, + res, + appInfo: this.getAppInfo(), + }; + if (id === constants_1.SIGN_IN_UP_API) { + return await signinup_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.AUTHORISATION_API) { + return await authorisationUrl_1.default(this.apiImpl, tenantId, options, userContext); + } else if (id === constants_1.APPLE_REDIRECT_HANDLER) { + return await appleRedirect_1.default(this.apiImpl, options, userContext); + } + return false; + }; + this.handleError = async (err, _request, _response) => { + throw err; + }; this.getAllCORSHeaders = () => { return []; }; this.isErrorFromThisRecipe = (err) => { return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; - // helper functions... - this.getEmailForUserId = (userId, userContext) => - __awaiter(this, void 0, void 0, function* () { - let userInfo = yield this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }); this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); this.isInServerlessEnv = isInServerlessEnv; this.providers = this.config.signInAndUpFeature.providers; @@ -149,11 +101,7 @@ class Recipe extends recipeModule_1.default { this.apiImpl = builder.override(this.config.override.apis).build(); } postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = recipe_1.default.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - const mtRecipe = recipe_2.default.getInstance(); + const mtRecipe = recipe_1.default.getInstance(); if (mtRecipe !== undefined) { mtRecipe.staticThirdPartyProviders = this.config.signInAndUpFeature.providers; } diff --git a/lib/build/recipe/thirdparty/recipeImplementation.js b/lib/build/recipe/thirdparty/recipeImplementation.js index a80a7d159..36b701e3f 100644 --- a/lib/build/recipe/thirdparty/recipeImplementation.js +++ b/lib/build/recipe/thirdparty/recipeImplementation.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -38,104 +7,118 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const configUtils_1 = require("./providers/configUtils"); -const recipe_1 = __importDefault(require("../multitenancy/recipe")); +const recipe_1 = __importDefault(require("../accountlinking/recipe")); +const recipe_2 = __importDefault(require("../multitenancy/recipe")); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); +const __1 = require("../.."); +const user_1 = require("../../user"); function getRecipeImplementation(querier, providers) { return { - signInUp: function ({ thirdPartyId, thirdPartyUserId, email, oAuthTokens, rawUserInfoFromProvider, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/signinup`), - { - thirdPartyId, - thirdPartyUserId, - email: { id: email }, - } - ); - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - oAuthTokens, - rawUserInfoFromProvider, - }; + manuallyCreateOrUpdateUser: async function ({ + thirdPartyId, + thirdPartyUserId, + email, + isVerified, + tenantId, + userContext, + }) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/${tenantId}/recipe/signinup`), + { + thirdPartyId, + thirdPartyUserId, + email: { id: email, isVerified }, + } + ); + if (response.status !== "OK") { + return response; + } + response.user = new user_1.User(response.user); + response.recipeUserId = new recipeUserId_1.default(response.recipeUserId); + await recipe_1.default.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: response.user, + recipeUserId: response.recipeUserId, + userContext, }); - }, - manuallyCreateOrUpdateUser: function ({ thirdPartyId, thirdPartyUserId, email, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/signinup`), - { - thirdPartyId, - thirdPartyUserId, - email: { id: email }, - } - ); + // we do this so that we get the updated user (in case the above + // function updated the verification status) and can return that + response.user = await __1.getUser(response.recipeUserId.getAsString(), userContext); + if (!response.createdNewUser) { + // Unlike in the sign up scenario, we do not do account linking here + // cause we do not want sign in to change the potentially user ID of a user + // due to linking when this function is called by the dev in their API. + // If we did account linking + // then we would have to ask the dev to also change the session + // in such API calls. + // In the case of sign up, since we are creating a new user, it's fine + // to link there since there is no user id change really from the dev's + // point of view who is calling the sign up recipe function. return { status: "OK", - createdNewUser: response.createdNewUser, + createdNewRecipeUser: response.createdNewUser, user: response.user, + recipeUserId: response.recipeUserId, }; + } + let updatedUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, }); + return { + status: "OK", + createdNewRecipeUser: response.createdNewUser, + user: updatedUser, + recipeUserId: response.recipeUserId, + }; }, - getUserById: function ({ userId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user"), { - userId, - }); - if (response.status === "OK") { - return response.user; - } else { - return undefined; - } - }); - }, - getUsersByEmail: function ({ email, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let users = []; - users = (yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/users/by-email`), - { - email, - } - )).users; - return users; - }); - }, - getUserByThirdPartyInfo: function ({ thirdPartyId, thirdPartyUserId, tenantId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${tenantId}/recipe/user`), - { - thirdPartyId, - thirdPartyUserId, - } - ); - if (response.status === "OK") { - return response.user; - } else { - return undefined; - } + signInUp: async function ({ + thirdPartyId, + thirdPartyUserId, + email, + isVerified, + tenantId, + userContext, + oAuthTokens, + rawUserInfoFromProvider, + }) { + let response = await this.manuallyCreateOrUpdateUser({ + thirdPartyId, + thirdPartyUserId, + email, + tenantId, + isVerified, + userContext, }); + if (response.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)", + }; + } + if (response.status === "OK") { + return Object.assign(Object.assign({}, response), { oAuthTokens, rawUserInfoFromProvider }); + } + return response; }, - getProvider: function ({ thirdPartyId, tenantId, clientType, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const mtRecipe = recipe_1.default.getInstanceOrThrowError(); - const tenantConfig = yield mtRecipe.recipeInterfaceImpl.getTenant({ tenantId, userContext }); - if (tenantConfig === undefined) { - throw new Error("Tenant not found"); - } - const mergedProviders = configUtils_1.mergeProvidersFromCoreAndStatic( - tenantConfig.thirdParty.providers, - providers - ); - const provider = yield configUtils_1.findAndCreateProviderInstance( - mergedProviders, - thirdPartyId, - clientType, - userContext - ); - return provider; - }); + getProvider: async function ({ thirdPartyId, tenantId, clientType, userContext }) { + const mtRecipe = recipe_2.default.getInstanceOrThrowError(); + const tenantConfig = await mtRecipe.recipeInterfaceImpl.getTenant({ tenantId, userContext }); + if (tenantConfig === undefined) { + throw new Error("Tenant not found"); + } + const mergedProviders = configUtils_1.mergeProvidersFromCoreAndStatic( + tenantConfig.thirdParty.providers, + providers + ); + const provider = await configUtils_1.findAndCreateProviderInstance( + mergedProviders, + thirdPartyId, + clientType, + userContext + ); + return provider; }, }; } diff --git a/lib/build/recipe/thirdparty/types.d.ts b/lib/build/recipe/thirdparty/types.d.ts index 09f242e90..7473fc040 100644 --- a/lib/build/recipe/thirdparty/types.d.ts +++ b/lib/build/recipe/thirdparty/types.d.ts @@ -1,9 +1,10 @@ // @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import { NormalisedAppinfo } from "../../types"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export declare type UserInfo = { thirdPartyUserId: string; email?: { @@ -94,16 +95,6 @@ export declare type TypeProvider = { }) => Promise; getUserInfo: (input: { oAuthTokens: any; userContext: any }) => Promise; }; -export declare type User = { - id: string; - timeJoined: number; - email: string; - thirdParty: { - id: string; - userId: string; - }; - tenantIds: string[]; -}; export declare type ProviderConfig = CommonProviderConfig & { clients?: ProviderClientConfig[]; }; @@ -138,14 +129,6 @@ export declare type TypeNormalisedInput = { }; }; export declare type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - getUsersByEmail(input: { email: string; tenantId: string; userContext: any }): Promise; - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise; getProvider(input: { thirdPartyId: string; tenantId: string; @@ -156,6 +139,7 @@ export declare type RecipeInterface = { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any; }; @@ -169,33 +153,52 @@ export declare type RecipeInterface = { }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { - [key: string]: any; - }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { - [key: string]: any; - }; - fromUserInfoAPI?: { - [key: string]: any; - }; - }; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + recipeUserId: RecipeUserId; + user: User; + oAuthTokens: { + [key: string]: any; + }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; manuallyCreateOrUpdateUser(input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; }; export declare type APIOptions = { recipeImplementation: RecipeInterface; @@ -249,7 +252,7 @@ export declare type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; session: SessionContainerInterface; oAuthTokens: { @@ -267,6 +270,10 @@ export declare type APIInterface = { | { status: "NO_EMAIL_GIVEN_BY_PROVIDER"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); appleRedirectHandlerPOST: diff --git a/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.js b/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.js index 0023e4411..8426efee6 100644 --- a/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.js +++ b/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.js @@ -1,66 +1,18 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getIterfaceImpl(apiImplmentation) { var _a, _b, _c; - const signInUpPOSTFromThirdPartyEmailPassword = - (_a = apiImplmentation.thirdPartySignInUpPOST) === null || _a === void 0 ? void 0 : _a.bind(apiImplmentation); return { authorisationUrlGET: - (_b = apiImplmentation.authorisationUrlGET) === null || _b === void 0 ? void 0 : _b.bind(apiImplmentation), + (_a = apiImplmentation.authorisationUrlGET) === null || _a === void 0 ? void 0 : _a.bind(apiImplmentation), appleRedirectHandlerPOST: - (_c = apiImplmentation.appleRedirectHandlerPOST) === null || _c === void 0 + (_b = apiImplmentation.appleRedirectHandlerPOST) === null || _b === void 0 ? void 0 - : _c.bind(apiImplmentation), + : _b.bind(apiImplmentation), signInUpPOST: - signInUpPOSTFromThirdPartyEmailPassword === undefined - ? undefined - : function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield signInUpPOSTFromThirdPartyEmailPassword(input); - if (result.status === "OK") { - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return Object.assign(Object.assign({}, result), { - user: Object.assign(Object.assign({}, result.user), { - thirdParty: Object.assign({}, result.user.thirdParty), - }), - }); - } - return result; - }); - }, + (_c = apiImplmentation.thirdPartySignInUpPOST) === null || _c === void 0 + ? void 0 + : _c.bind(apiImplmentation), }; } exports.default = getIterfaceImpl; diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts index e3b6e87a8..8ebd5dc12 100644 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,16 +1,11 @@ // @ts-nocheck import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; -import { RecipeInterface as EmailPasswordRecipeInterface } from "../../../../emailpassword"; import { NormalisedAppinfo } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { private emailPasswordBackwardCompatibilityService; - constructor( - emailPasswordRecipeInterfaceImpl: EmailPasswordRecipeInterface, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean - ); + constructor(appInfo: NormalisedAppinfo, isInServerlessEnv: boolean); sendEmail: ( input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { userContext: any; diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js index ee09e5c75..c8e5994e1 100644 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -40,14 +9,12 @@ const backwardCompatibility_1 = __importDefault( require("../../../../emailpassword/emaildelivery/services/backwardCompatibility") ); class BackwardCompatibilityService { - constructor(emailPasswordRecipeInterfaceImpl, appInfo, isInServerlessEnv) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.emailPasswordBackwardCompatibilityService.sendEmail(input); - }); + constructor(appInfo, isInServerlessEnv) { + this.sendEmail = async (input) => { + await this.emailPasswordBackwardCompatibilityService.sendEmail(input); + }; { this.emailPasswordBackwardCompatibilityService = new backwardCompatibility_1.default( - emailPasswordRecipeInterfaceImpl, appInfo, isInServerlessEnv ); diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.js b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.js index ab6aa1bf8..9b8f1b7c7 100644 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.js +++ b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,10 +8,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); const smtp_1 = __importDefault(require("../../../../emailpassword/emaildelivery/services/smtp")); class SMTPService { constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.emailPasswordSMTPService.sendEmail(input); - }); + this.sendEmail = async (input) => { + await this.emailPasswordSMTPService.sendEmail(input); + }; this.emailPasswordSMTPService = new smtp_1.default(config); } } diff --git a/lib/build/recipe/thirdpartyemailpassword/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/index.d.ts index 934838e1c..ae5b2d5fa 100644 --- a/lib/build/recipe/thirdpartyemailpassword/index.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/index.d.ts @@ -1,9 +1,10 @@ // @ts-nocheck import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions } from "./types"; +import { RecipeInterface, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions } from "./types"; import { TypeProvider } from "../thirdparty/types"; import { TypeEmailPasswordEmailDeliveryInput } from "../emailpassword/types"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init: typeof Recipe.init; static Error: typeof SuperTokensError; @@ -18,18 +19,24 @@ export default class Wrapper { thirdPartyId: string, thirdPartyUserId: string, email: string, + isVerified: boolean, userContext?: any - ): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; - static getUserByThirdPartyInfo( - tenantId: string, - thirdPartyId: string, - thirdPartyUserId: string, - userContext?: any - ): Promise; + ): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: import("../../types").User; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; static emailPasswordSignUp( tenantId: string, email: string, @@ -38,7 +45,8 @@ export default class Wrapper { ): Promise< | { status: "OK"; - user: User; + user: import("../../types").User; + recipeUserId: RecipeUserId; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -52,17 +60,17 @@ export default class Wrapper { ): Promise< | { status: "OK"; - user: User; + user: import("../../types").User; + recipeUserId: RecipeUserId; } | { status: "WRONG_CREDENTIALS_ERROR"; } >; - static getUserById(userId: string, userContext?: any): Promise; - static getUsersByEmail(tenantId: string, email: string, userContext?: any): Promise; static createResetPasswordToken( tenantId: string, userId: string, + email: string, userContext?: any ): Promise< | { @@ -78,17 +86,38 @@ export default class Wrapper { token: string, newPassword: string, userContext?: any + ): Promise< + | { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; + } + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "PASSWORD_POLICY_VIOLATED_ERROR"; + failureReason: string; + } + >; + static consumePasswordResetToken( + tenantId: string, + token: string, + userContext?: any ): Promise< | { status: "OK"; - userId?: string | undefined; + email: string; + userId: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; } >; static updateEmailOrPassword(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext?: any; @@ -96,7 +125,11 @@ export default class Wrapper { tenantIdForPasswordPolicy?: string; }): Promise< | { - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR"; + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; @@ -106,6 +139,7 @@ export default class Wrapper { static createResetPasswordLink( tenantId: string, userId: string, + email: string, userContext?: any ): Promise< | { @@ -119,6 +153,7 @@ export default class Wrapper { static sendResetPasswordEmail( tenantId: string, userId: string, + email: string, userContext?: any ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR"; @@ -135,13 +170,11 @@ export declare let emailPasswordSignUp: typeof Wrapper.emailPasswordSignUp; export declare let emailPasswordSignIn: typeof Wrapper.emailPasswordSignIn; export declare let thirdPartyGetProvider: typeof Wrapper.thirdPartyGetProvider; export declare let thirdPartyManuallyCreateOrUpdateUser: typeof Wrapper.thirdPartyManuallyCreateOrUpdateUser; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByThirdPartyInfo: typeof Wrapper.getUserByThirdPartyInfo; -export declare let getUsersByEmail: typeof Wrapper.getUsersByEmail; export declare let createResetPasswordToken: typeof Wrapper.createResetPasswordToken; export declare let resetPasswordUsingToken: typeof Wrapper.resetPasswordUsingToken; +export declare let consumePasswordResetToken: typeof Wrapper.consumePasswordResetToken; export declare let updateEmailOrPassword: typeof Wrapper.updateEmailOrPassword; -export type { RecipeInterface, TypeProvider, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions }; +export type { RecipeInterface, TypeProvider, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions }; export declare let createResetPasswordLink: typeof Wrapper.createResetPasswordLink; export declare let sendResetPasswordEmail: typeof Wrapper.sendResetPasswordEmail; export declare let sendEmail: typeof Wrapper.sendEmail; diff --git a/lib/build/recipe/thirdpartyemailpassword/index.js b/lib/build/recipe/thirdpartyemailpassword/index.js index 1e5c12374..d52b53fa7 100644 --- a/lib/build/recipe/thirdpartyemailpassword/index.js +++ b/lib/build/recipe/thirdpartyemailpassword/index.js @@ -13,72 +13,41 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendEmail = exports.sendResetPasswordEmail = exports.createResetPasswordLink = exports.updateEmailOrPassword = exports.resetPasswordUsingToken = exports.createResetPasswordToken = exports.getUsersByEmail = exports.getUserByThirdPartyInfo = exports.getUserById = exports.thirdPartyManuallyCreateOrUpdateUser = exports.thirdPartyGetProvider = exports.emailPasswordSignIn = exports.emailPasswordSignUp = exports.Error = exports.init = void 0; +exports.sendEmail = exports.sendResetPasswordEmail = exports.createResetPasswordLink = exports.updateEmailOrPassword = exports.consumePasswordResetToken = exports.resetPasswordUsingToken = exports.createResetPasswordToken = exports.thirdPartyManuallyCreateOrUpdateUser = exports.thirdPartyGetProvider = exports.emailPasswordSignIn = exports.emailPasswordSignUp = exports.Error = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); const utils_1 = require("../emailpassword/utils"); +const __1 = require("../.."); class Wrapper { - static thirdPartyGetProvider(tenantId, thirdPartyId, clientType, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyGetProvider({ - thirdPartyId, - clientType, - tenantId, - userContext, - }); - }); - } - static thirdPartyManuallyCreateOrUpdateUser(tenantId, thirdPartyId, thirdPartyUserId, email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ + static async thirdPartyGetProvider(tenantId, thirdPartyId, clientType, userContext = {}) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyGetProvider({ thirdPartyId, - thirdPartyUserId, - email, + clientType, tenantId, userContext, }); } - static getUserByThirdPartyInfo(tenantId, thirdPartyId, thirdPartyUserId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ + static thirdPartyManuallyCreateOrUpdateUser( + tenantId, + thirdPartyId, + thirdPartyUserId, + email, + isVerified, + userContext = {} + ) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ thirdPartyId, thirdPartyUserId, + email, + isVerified, tenantId, userContext, }); @@ -99,34 +68,39 @@ class Wrapper { userContext, }); } - static getUserById(userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - static getUsersByEmail(tenantId, email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ + static createResetPasswordToken(tenantId, userId, email, userContext = {}) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ + userId, email, tenantId, userContext, }); } - static createResetPasswordToken(tenantId, userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ - userId, - tenantId, + static async resetPasswordUsingToken(tenantId, token, newPassword, userContext) { + const consumeResp = await Wrapper.consumePasswordResetToken(tenantId, token, userContext); + if (consumeResp.status !== "OK") { + return consumeResp; + } + return await Wrapper.updateEmailOrPassword({ + recipeUserId: new recipeUserId_1.default(consumeResp.userId), + email: consumeResp.email, + password: newPassword, + tenantIdForPasswordPolicy: tenantId, userContext, }); } - static resetPasswordUsingToken(tenantId, token, newPassword, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + static consumePasswordResetToken(tenantId, token, userContext = {}) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, - newPassword, tenantId, userContext, }); } static updateEmailOrPassword(input) { + var _a; return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword( - Object.assign(Object.assign({ userContext: {} }, input), { + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, tenantIdForPasswordPolicy: input.tenantIdForPasswordPolicy === undefined ? constants_1.DEFAULT_TENANT_ID @@ -134,50 +108,58 @@ class Wrapper { }) ); } - static createResetPasswordLink(tenantId, userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let token = yield exports.createResetPasswordToken(userId, tenantId, userContext); - if (token.status === "UNKNOWN_USER_ID_ERROR") { - return token; - } - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return { - status: "OK", - link: utils_1.getPasswordResetLink({ - appInfo: recipeInstance.getAppInfo(), - recipeId: recipeInstance.getRecipeId(), - token: token.token, - tenantId, - }), - }; - }); - } - static sendResetPasswordEmail(tenantId, userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let link = yield exports.createResetPasswordLink(userId, tenantId, userContext); - if (link.status === "UNKNOWN_USER_ID_ERROR") { - return link; - } - yield exports.sendEmail({ - passwordResetLink: link.link, - type: "PASSWORD_RESET", - user: yield exports.getUserById(userId, userContext), + static async createResetPasswordLink(tenantId, userId, email, userContext = {}) { + let token = await exports.createResetPasswordToken(userId, tenantId, email, userContext); + if (token.status === "UNKNOWN_USER_ID_ERROR") { + return token; + } + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + return { + status: "OK", + link: utils_1.getPasswordResetLink({ + appInfo: recipeInstance.getAppInfo(), + recipeId: recipeInstance.getRecipeId(), + token: token.token, tenantId, - userContext, - }); - return { - status: "OK", - }; - }); + }), + }; } - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign(Object.assign({ userContext: {} }, input), { - tenantId: input.tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : input.tenantId, - }) - ); + static async sendResetPasswordEmail(tenantId, userId, email, userContext = {}) { + const user = await __1.getUser(userId, userContext); + if (!user) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + const loginMethod = user.loginMethods.find((m) => m.recipeId === "emailpassword" && m.hasSameEmailAs(email)); + if (!loginMethod) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + let link = await exports.createResetPasswordLink(userId, tenantId, email, userContext); + if (link.status === "UNKNOWN_USER_ID_ERROR") { + return link; + } + await exports.sendEmail({ + passwordResetLink: link.link, + type: "PASSWORD_RESET", + user: { + email: loginMethod.email, + id: user.id, + recipeUserId: loginMethod.recipeUserId, + }, + tenantId, + userContext, }); + return { + status: "OK", + }; + } + static async sendEmail(input) { + var _a; + return await recipe_1.default.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + tenantId: input.tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : input.tenantId, + }) + ); } } exports.default = Wrapper; @@ -189,11 +171,9 @@ exports.emailPasswordSignUp = Wrapper.emailPasswordSignUp; exports.emailPasswordSignIn = Wrapper.emailPasswordSignIn; exports.thirdPartyGetProvider = Wrapper.thirdPartyGetProvider; exports.thirdPartyManuallyCreateOrUpdateUser = Wrapper.thirdPartyManuallyCreateOrUpdateUser; -exports.getUserById = Wrapper.getUserById; -exports.getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; -exports.getUsersByEmail = Wrapper.getUsersByEmail; exports.createResetPasswordToken = Wrapper.createResetPasswordToken; exports.resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; +exports.consumePasswordResetToken = Wrapper.consumePasswordResetToken; exports.updateEmailOrPassword = Wrapper.updateEmailOrPassword; exports.createResetPasswordLink = Wrapper.createResetPasswordLink; exports.sendResetPasswordEmail = Wrapper.sendResetPasswordEmail; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts b/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts index c49bf3123..3103cfebb 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts @@ -3,7 +3,7 @@ import RecipeModule from "../../recipeModule"; import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; import EmailPasswordRecipe from "../emailpassword/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import STError from "./error"; import { TypeInput, diff --git a/lib/build/recipe/thirdpartyemailpassword/recipe.js b/lib/build/recipe/thirdpartyemailpassword/recipe.js index e308d548d..f5664d520 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipe.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipe.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -76,50 +45,37 @@ class Recipe extends recipeModule_1.default { apisHandled.push(...this.thirdPartyRecipe.getAPIsHandled()); return apisHandled; }; - this.handleAPIRequest = (id, tenantId, req, res, path, method, userContext) => - __awaiter(this, void 0, void 0, function* () { - if ( - (yield this.emailPasswordRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== - undefined - ) { - return yield this.emailPasswordRecipe.handleAPIRequest( - id, - tenantId, - req, - res, - path, - method, - userContext - ); - } - if ( - (yield this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== undefined - ) { - return yield this.thirdPartyRecipe.handleAPIRequest( - id, - tenantId, - req, - res, - path, - method, - userContext - ); - } - return false; - }); - this.handleError = (err, request, response) => - __awaiter(this, void 0, void 0, function* () { - if (err.fromRecipe === Recipe.RECIPE_ID) { - throw err; - } else { - if (this.emailPasswordRecipe.isErrorFromThisRecipe(err)) { - return yield this.emailPasswordRecipe.handleError(err, request, response); - } else if (this.thirdPartyRecipe.isErrorFromThisRecipe(err)) { - return yield this.thirdPartyRecipe.handleError(err, request, response); - } - throw err; + this.handleAPIRequest = async (id, tenantId, req, res, path, method, userContext) => { + if ( + (await this.emailPasswordRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== undefined + ) { + return await this.emailPasswordRecipe.handleAPIRequest( + id, + tenantId, + req, + res, + path, + method, + userContext + ); + } + if ((await this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== undefined) { + return await this.thirdPartyRecipe.handleAPIRequest(id, tenantId, req, res, path, method, userContext); + } + return false; + }; + this.handleError = async (err, request, response) => { + if (err.fromRecipe === Recipe.RECIPE_ID) { + throw err; + } else { + if (this.emailPasswordRecipe.isErrorFromThisRecipe(err)) { + return await this.emailPasswordRecipe.handleError(err, request, response); + } else if (this.thirdPartyRecipe.isErrorFromThisRecipe(err)) { + return await this.thirdPartyRecipe.handleError(err, request, response); } - }); + throw err; + } + }; this.getAllCORSHeaders = () => { let corsHeaders = [...this.emailPasswordRecipe.getAllCORSHeaders()]; corsHeaders.push(...this.thirdPartyRecipe.getAllCORSHeaders()); @@ -158,9 +114,7 @@ class Recipe extends recipeModule_1.default { */ this.emailDelivery = ingredients.emailDelivery === undefined - ? new emaildelivery_1.default( - this.config.getEmailDeliveryConfig(emailPasswordRecipeImplementation, this.isInServerlessEnv) - ) + ? new emaildelivery_1.default(this.config.getEmailDeliveryConfig(this.isInServerlessEnv)) : ingredients.emailDelivery; this.emailPasswordRecipe = recipes.emailPasswordInstance !== undefined diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js index 8d8772bdd..8dc91787e 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js @@ -1,83 +1,24 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getRecipeInterface(recipeInterface) { return { - signUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.emailPasswordSignUp(input); - }); - }, - signIn: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.emailPasswordSignIn(input); - }); + signUp: async function (input) { + return await recipeInterface.emailPasswordSignUp(input); }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty !== undefined) { - // either user is undefined or it's a thirdparty user. - return undefined; - } - return user; - }); + signIn: async function (input) { + return recipeInterface.emailPasswordSignIn(input); }, - getUserByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.getUsersByEmail(input); - for (let i = 0; i < result.length; i++) { - if (result[i].thirdParty === undefined) { - return result[i]; - } - } - return undefined; - }); + createResetPasswordToken: async function (input) { + return recipeInterface.createResetPasswordToken(input); }, - createResetPasswordToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.createResetPasswordToken(input); - }); + consumePasswordResetToken: async function (input) { + return recipeInterface.consumePasswordResetToken(input); }, - resetPasswordUsingToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.resetPasswordUsingToken(input); - }); + createNewRecipeUser: async function (input) { + return recipeInterface.createNewEmailPasswordRecipeUser(input); }, - updateEmailOrPassword: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.updateEmailOrPassword(input); - }); + updateEmailOrPassword: async function (input) { + return recipeInterface.updateEmailOrPassword(input); }, }; } diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js index a29e2a379..2619678d3 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -40,6 +9,7 @@ const recipeImplementation_1 = __importDefault(require("../../emailpassword/reci const recipeImplementation_2 = __importDefault(require("../../thirdparty/recipeImplementation")); const emailPasswordRecipeImplementation_1 = __importDefault(require("./emailPasswordRecipeImplementation")); const thirdPartyRecipeImplementation_1 = __importDefault(require("./thirdPartyRecipeImplementation")); +const __1 = require("../../../"); function getRecipeInterface(emailPasswordQuerier, getEmailPasswordConfig, thirdPartyQuerier, providers = []) { let originalEmailPasswordImplementation = recipeImplementation_1.default( emailPasswordQuerier, @@ -47,103 +17,66 @@ function getRecipeInterface(emailPasswordQuerier, getEmailPasswordConfig, thirdP ); let originalThirdPartyImplementation = recipeImplementation_2.default(thirdPartyQuerier, providers); return { - emailPasswordSignUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield originalEmailPasswordImplementation.signUp.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); + createNewEmailPasswordRecipeUser: async function (input) { + return await originalEmailPasswordImplementation.createNewRecipeUser.bind( + emailPasswordRecipeImplementation_1.default(this) + )(input); }, - emailPasswordSignIn: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalEmailPasswordImplementation.signIn.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); + emailPasswordSignUp: async function (input) { + return await originalEmailPasswordImplementation.signUp.bind( + emailPasswordRecipeImplementation_1.default(this) + )(input); }, - thirdPartySignInUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalThirdPartyImplementation.signInUp.bind(thirdPartyRecipeImplementation_1.default(this))( - input - ); - }); + emailPasswordSignIn: async function (input) { + return originalEmailPasswordImplementation.signIn.bind(emailPasswordRecipeImplementation_1.default(this))( + input + ); }, - thirdPartyManuallyCreateOrUpdateUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalThirdPartyImplementation.manuallyCreateOrUpdateUser.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); + thirdPartySignInUp: async function (input) { + return originalThirdPartyImplementation.signInUp.bind(thirdPartyRecipeImplementation_1.default(this))( + input + ); }, - thirdPartyGetProvider: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalThirdPartyImplementation.getProvider.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); + thirdPartyManuallyCreateOrUpdateUser: async function (input) { + return originalThirdPartyImplementation.manuallyCreateOrUpdateUser.bind( + thirdPartyRecipeImplementation_1.default(this) + )(input); }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield originalEmailPasswordImplementation.getUserById.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - if (user !== undefined) { - return user; - } - return yield originalThirdPartyImplementation.getUserById.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); + thirdPartyGetProvider: async function (input) { + return originalThirdPartyImplementation.getProvider.bind(thirdPartyRecipeImplementation_1.default(this))( + input + ); }, - getUsersByEmail: function ({ email, tenantId, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let userFromEmailPass = yield originalEmailPasswordImplementation.getUserByEmail.bind( - emailPasswordRecipeImplementation_1.default(this) - )({ email, tenantId, userContext }); - let usersFromThirdParty = yield originalThirdPartyImplementation.getUsersByEmail.bind( - thirdPartyRecipeImplementation_1.default(this) - )({ email, tenantId, userContext }); - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }); + createResetPasswordToken: async function (input) { + return originalEmailPasswordImplementation.createResetPasswordToken.bind( + emailPasswordRecipeImplementation_1.default(this) + )(input); }, - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); + consumePasswordResetToken: async function (input) { + return originalEmailPasswordImplementation.consumePasswordResetToken.bind( + emailPasswordRecipeImplementation_1.default(this) + )(input); }, - createResetPasswordToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalEmailPasswordImplementation.createResetPasswordToken.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); - }, - resetPasswordUsingToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalEmailPasswordImplementation.resetPasswordUsingToken.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); - }, - updateEmailOrPassword: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield this.getUserById({ userId: input.userId, userContext: input.userContext }); - if (user === undefined) { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } else if (user.thirdParty !== undefined) { - throw new Error("Cannot update email or password of a user who signed up using third party login."); - } - return originalEmailPasswordImplementation.updateEmailOrPassword.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); + updateEmailOrPassword: async function (input) { + let user = await __1.getUser(input.recipeUserId.getAsString(), input.userContext); + if (user === undefined) { + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + } + let inputUserIdIsPointingToEmailPasswordUser = + user.loginMethods.find((lM) => { + return ( + lM.recipeId === "emailpassword" && + lM.recipeUserId.getAsString() === input.recipeUserId.getAsString() + ); + }) !== undefined; + if (!inputUserIdIsPointingToEmailPasswordUser) { + throw new Error("Cannot update email or password of a user who signed up using third party login."); + } + return originalEmailPasswordImplementation.updateEmailOrPassword.bind( + emailPasswordRecipeImplementation_1.default(this) + )(input); }, }; } diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js index 66f4b4680..0202e58b6 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js @@ -1,122 +1,27 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getRecipeInterface(recipeInterface) { return { - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserByThirdPartyInfo(input); - if (user === undefined || user.thirdParty === undefined) { - return undefined; - } - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - tenantIds: user.tenantIds, - }; - }); - }, - signInUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.thirdPartySignInUp(input); - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: { - email: result.user.email, - id: result.user.id, - timeJoined: result.user.timeJoined, - thirdParty: result.user.thirdParty, - tenantIds: result.user.tenantIds, - }, - oAuthTokens: result.oAuthTokens, - rawUserInfoFromProvider: result.rawUserInfoFromProvider, - }; - }); + signInUp: async function (input) { + return await recipeInterface.thirdPartySignInUp(input); }, - manuallyCreateOrUpdateUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.thirdPartyManuallyCreateOrUpdateUser(input); - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: { - email: result.user.email, - id: result.user.id, - timeJoined: result.user.timeJoined, - thirdParty: result.user.thirdParty, - tenantIds: result.user.tenantIds, - }, - }; - }); - }, - getProvider: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.thirdPartyGetProvider(input); - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty === undefined) { - // either user is undefined or it's an email password user. - return undefined; - } - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - tenantIds: user.tenantIds, - }; - }); + manuallyCreateOrUpdateUser: async function (input) { + let result = await recipeInterface.thirdPartyManuallyCreateOrUpdateUser(input); + if (result.status !== "OK") { + return result; + } + if (result.user.thirdParty === undefined) { + throw new Error("Should never come here"); + } + return { + status: "OK", + createdNewRecipeUser: result.createdNewRecipeUser, + user: result.user, + recipeUserId: result.recipeUserId, + }; }, - getUsersByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let users = yield recipeInterface.getUsersByEmail(input); - // we filter out all non thirdparty users. - return users.filter((u) => { - return u.thirdParty !== undefined; - }); - }); + getProvider: async function (input) { + return await recipeInterface.thirdPartyGetProvider(input); }, }; } diff --git a/lib/build/recipe/thirdpartyemailpassword/types.d.ts b/lib/build/recipe/thirdpartyemailpassword/types.d.ts index 1b968cce0..b7f119254 100644 --- a/lib/build/recipe/thirdpartyemailpassword/types.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/types.d.ts @@ -9,11 +9,9 @@ import { } from "../thirdparty/types"; import { NormalisedFormField, - TypeFormField, TypeInputFormField, APIOptions as EmailPasswordAPIOptionsOriginal, TypeEmailPasswordEmailDeliveryInput, - RecipeInterface as EPRecipeInterface, } from "../emailpassword/types"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; @@ -21,28 +19,8 @@ import { TypeInput as EmailDeliveryTypeInput, TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; -import { GeneralErrorResponse } from "../../types"; -export declare type User = { - id: string; - timeJoined: number; - email: string; - thirdParty?: { - id: string; - userId: string; - }; - tenantIds: string[]; -}; -export declare type TypeContextEmailPasswordSignUp = { - loginType: "emailpassword"; - formFields: TypeFormField[]; -}; -export declare type TypeContextEmailPasswordSignIn = { - loginType: "emailpassword"; -}; -export declare type TypeContextThirdParty = { - loginType: "thirdparty"; - thirdPartyAuthCodeResponse: any; -}; +import { GeneralErrorResponse, User as GlobalUser, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export declare type TypeInputSignUp = { formFields?: TypeInputFormField[]; }; @@ -65,7 +43,6 @@ export declare type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; providers: ProviderInput[]; getEmailDeliveryConfig: ( - emailPasswordRecipeImpl: EPRecipeInterface, isInServerlessEnv: boolean ) => EmailDeliveryTypeInputWithService; override: { @@ -77,14 +54,6 @@ export declare type TypeNormalisedInput = { }; }; export declare type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - getUsersByEmail(input: { email: string; tenantId: string; userContext: any }): Promise; - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise; thirdPartyGetProvider(input: { thirdPartyId: string; clientType?: string; @@ -95,6 +64,7 @@ export declare type RecipeInterface = { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any; }; @@ -108,33 +78,52 @@ export declare type RecipeInterface = { }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { - [key: string]: any; - }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { - [key: string]: any; - }; - fromUserInfoAPI?: { - [key: string]: any; - }; - }; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + oAuthTokens: { + [key: string]: any; + }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; thirdPartyManuallyCreateOrUpdateUser(input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: GlobalUser; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; emailPasswordSignUp(input: { email: string; password: string; @@ -143,7 +132,22 @@ export declare type RecipeInterface = { }): Promise< | { status: "OK"; - user: User; + user: GlobalUser; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } + >; + createNewEmailPasswordRecipeUser(input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: GlobalUser; + recipeUserId: RecipeUserId; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -157,7 +161,8 @@ export declare type RecipeInterface = { }): Promise< | { status: "OK"; - user: User; + user: GlobalUser; + recipeUserId: RecipeUserId; } | { status: "WRONG_CREDENTIALS_ERROR"; @@ -165,6 +170,7 @@ export declare type RecipeInterface = { >; createResetPasswordToken(input: { userId: string; + email: string; tenantId: string; userContext: any; }): Promise< @@ -176,26 +182,22 @@ export declare type RecipeInterface = { status: "UNKNOWN_USER_ID_ERROR"; } >; - resetPasswordUsingToken(input: { + consumePasswordResetToken(input: { token: string; - newPassword: string; tenantId: string; userContext: any; }): Promise< | { status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; + email: string; + userId: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; } >; updateEmailOrPassword(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext: any; @@ -205,6 +207,10 @@ export declare type RecipeInterface = { | { status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string; @@ -258,6 +264,10 @@ export declare type APIInterface = { | { status: "OK"; } + | { + status: "PASSWORD_RESET_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); passwordResetPOST: @@ -274,11 +284,16 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - userId?: string; + email: string; + user: GlobalUser; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; } + | { + status: "PASSWORD_POLICY_VIOLATED_ERROR"; + failureReason: string; + } | GeneralErrorResponse >); thirdPartySignInUpPOST: @@ -306,8 +321,8 @@ export declare type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; - user: User; + createdNewRecipeUser: boolean; + user: GlobalUser; session: SessionContainerInterface; oAuthTokens: { [key: string]: any; @@ -324,6 +339,10 @@ export declare type APIInterface = { | { status: "NO_EMAIL_GIVEN_BY_PROVIDER"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); emailPasswordSignInPOST: @@ -339,9 +358,13 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - user: User; + user: GlobalUser; session: SessionContainerInterface; } + | { + status: "SIGN_IN_NOT_ALLOWED"; + reason: string; + } | { status: "WRONG_CREDENTIALS_ERROR"; } @@ -360,9 +383,13 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - user: User; + user: GlobalUser; session: SessionContainerInterface; } + | { + status: "SIGN_UP_NOT_ALLOWED"; + reason: string; + } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } diff --git a/lib/build/recipe/thirdpartyemailpassword/utils.js b/lib/build/recipe/thirdpartyemailpassword/utils.js index 5c70f616d..f9b76a060 100644 --- a/lib/build/recipe/thirdpartyemailpassword/utils.js +++ b/lib/build/recipe/thirdpartyemailpassword/utils.js @@ -36,7 +36,7 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { }, config === null || config === void 0 ? void 0 : config.override ); - function getEmailDeliveryConfig(emailPasswordRecipeImpl, isInServerlessEnv) { + function getEmailDeliveryConfig(isInServerlessEnv) { var _a; let emailService = (_a = config === null || config === void 0 ? void 0 : config.emailDelivery) === null || _a === void 0 @@ -48,7 +48,7 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { * createAndSendEmailUsingSupertokensService implementation */ if (emailService === undefined) { - emailService = new backwardCompatibility_1.default(emailPasswordRecipeImpl, appInfo, isInServerlessEnv); + emailService = new backwardCompatibility_1.default(appInfo, isInServerlessEnv); } return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.emailDelivery), { /** diff --git a/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.js b/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.js index e3d01c889..8426efee6 100644 --- a/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.js +++ b/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.js @@ -1,66 +1,18 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getIterfaceImpl(apiImplmentation) { var _a, _b, _c; - const signInUpPOSTFromThirdPartyPasswordless = - (_a = apiImplmentation.thirdPartySignInUpPOST) === null || _a === void 0 ? void 0 : _a.bind(apiImplmentation); return { authorisationUrlGET: - (_b = apiImplmentation.authorisationUrlGET) === null || _b === void 0 ? void 0 : _b.bind(apiImplmentation), + (_a = apiImplmentation.authorisationUrlGET) === null || _a === void 0 ? void 0 : _a.bind(apiImplmentation), appleRedirectHandlerPOST: - (_c = apiImplmentation.appleRedirectHandlerPOST) === null || _c === void 0 + (_b = apiImplmentation.appleRedirectHandlerPOST) === null || _b === void 0 ? void 0 - : _c.bind(apiImplmentation), + : _b.bind(apiImplmentation), signInUpPOST: - signInUpPOSTFromThirdPartyPasswordless === undefined - ? undefined - : function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield signInUpPOSTFromThirdPartyPasswordless(input); - if (result.status === "OK") { - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return Object.assign(Object.assign({}, result), { - user: Object.assign(Object.assign({}, result.user), { - thirdParty: Object.assign({}, result.user.thirdParty), - }), - }); - } - return result; - }); - }, + (_c = apiImplmentation.thirdPartySignInUpPOST) === null || _c === void 0 + ? void 0 + : _c.bind(apiImplmentation), }; } exports.default = getIterfaceImpl; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.js index 8bf949cdb..173cc7faf 100644 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -41,10 +10,9 @@ const backwardCompatibility_1 = __importDefault( ); class BackwardCompatibilityService { constructor(appInfo) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.passwordlessBackwardCompatibilityService.sendEmail(input); - }); + this.sendEmail = async (input) => { + await this.passwordlessBackwardCompatibilityService.sendEmail(input); + }; { this.passwordlessBackwardCompatibilityService = new backwardCompatibility_1.default(appInfo); } diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.js index 88e970f1c..785f10a01 100644 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.js +++ b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -45,10 +14,9 @@ const passwordlessServiceImplementation_1 = __importDefault( ); class SMTPService { constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - return yield this.passwordlessSMTPService.sendEmail(input); - }); + this.sendEmail = async (input) => { + return await this.passwordlessSMTPService.sendEmail(input); + }; const transporter = nodemailer_1.createTransport({ host: config.smtpSettings.host, port: config.smtpSettings.port, diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.js index 61a874879..11616a61e 100644 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.js +++ b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,22 +25,18 @@ const passwordlessServiceImplementation_1 = __importDefault(require("./passwordl function getServiceImplementation(transporter, from) { let passwordlessServiceImpl = serviceImplementation_1.getServiceImplementation(transporter, from); return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); + sendRawEmail: async function (input) { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + html: input.body, }); }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield passwordlessServiceImpl.getContent.bind(passwordlessServiceImplementation_1.default(this))( - input - ); - }); + getContent: async function (input) { + return await passwordlessServiceImpl.getContent.bind(passwordlessServiceImplementation_1.default(this))( + input + ); }, }; } diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.js index 9da544281..e93143347 100644 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.js +++ b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.js @@ -13,49 +13,14 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getServiceInterface(thirdpartyPasswordlessServiceImplementation) { return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return thirdpartyPasswordlessServiceImplementation.sendRawEmail(input); - }); + sendRawEmail: async function (input) { + return thirdpartyPasswordlessServiceImplementation.sendRawEmail(input); }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield thirdpartyPasswordlessServiceImplementation.getContent(input); - }); + getContent: async function (input) { + return await thirdpartyPasswordlessServiceImplementation.getContent(input); }, }; } diff --git a/lib/build/recipe/thirdpartypasswordless/index.d.ts b/lib/build/recipe/thirdpartypasswordless/index.d.ts index 4dba23a31..505061fa5 100644 --- a/lib/build/recipe/thirdpartypasswordless/index.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/index.d.ts @@ -3,7 +3,6 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; import { RecipeInterface, - User, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions, @@ -11,6 +10,7 @@ import { } from "./types"; import { TypeProvider } from "../thirdparty/types"; import { TypePasswordlessSmsDeliveryInput } from "../passwordless/types"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init: typeof Recipe.init; static Error: typeof SuperTokensError; @@ -25,20 +25,24 @@ export default class Wrapper { thirdPartyId: string, thirdPartyUserId: string, email: string, + isVerified: boolean, userContext?: any - ): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; - static getUserByThirdPartyInfo( - tenantId: string, - thirdPartyId: string, - thirdPartyUserId: string, - userContext?: any - ): Promise; - static getUserById(userId: string, userContext?: any): Promise; - static getUsersByEmail(tenantId: string, email: string, userContext?: any): Promise; + ): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: import("../../types").User; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; static createCode( input: ( | { @@ -100,8 +104,9 @@ export default class Wrapper { ): Promise< | { status: "OK"; - createdNewUser: boolean; - user: User; + createdNewRecipeUser: boolean; + user: import("../../types").User; + recipeUserId: RecipeUserId; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -112,19 +117,24 @@ export default class Wrapper { status: "RESTART_FLOW_ERROR"; } >; - static getUserByPhoneNumber(input: { - phoneNumber: string; - tenantId: string; - userContext?: any; - }): Promise; static updatePasswordlessUser(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; userContext?: any; - }): Promise<{ - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; + }): Promise< + | { + status: + | "OK" + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; static revokeAllCodes( input: | { @@ -194,8 +204,9 @@ export default class Wrapper { } ): Promise<{ status: string; - createdNewUser: boolean; - user: import("../passwordless/types").User; + createdNewRecipeUser: boolean; + recipeUserId: RecipeUserId; + user: import("../../types").User; }>; static sendEmail( input: TypeThirdPartyPasswordlessEmailDeliveryInput & { @@ -213,12 +224,8 @@ export declare let Error: typeof SuperTokensError; export declare let thirdPartyGetProvider: typeof Wrapper.thirdPartyGetProvider; export declare let thirdPartyManuallyCreateOrUpdateUser: typeof Wrapper.thirdPartyManuallyCreateOrUpdateUser; export declare let passwordlessSignInUp: typeof Wrapper.passwordlessSignInUp; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByThirdPartyInfo: typeof Wrapper.getUserByThirdPartyInfo; -export declare let getUsersByEmail: typeof Wrapper.getUsersByEmail; export declare let createCode: typeof Wrapper.createCode; export declare let consumeCode: typeof Wrapper.consumeCode; -export declare let getUserByPhoneNumber: typeof Wrapper.getUserByPhoneNumber; export declare let listCodesByDeviceId: typeof Wrapper.listCodesByDeviceId; export declare let listCodesByEmail: typeof Wrapper.listCodesByEmail; export declare let listCodesByPhoneNumber: typeof Wrapper.listCodesByPhoneNumber; @@ -228,6 +235,6 @@ export declare let updatePasswordlessUser: typeof Wrapper.updatePasswordlessUser export declare let revokeAllCodes: typeof Wrapper.revokeAllCodes; export declare let revokeCode: typeof Wrapper.revokeCode; export declare let createMagicLink: typeof Wrapper.createMagicLink; -export type { RecipeInterface, TypeProvider, User, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions }; +export type { RecipeInterface, TypeProvider, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions }; export declare let sendEmail: typeof Wrapper.sendEmail; export declare let sendSms: typeof Wrapper.sendSms; diff --git a/lib/build/recipe/thirdpartypasswordless/index.js b/lib/build/recipe/thirdpartypasswordless/index.js index 90cdaef9b..74818d841 100644 --- a/lib/build/recipe/thirdpartypasswordless/index.js +++ b/lib/build/recipe/thirdpartypasswordless/index.js @@ -13,162 +13,152 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendSms = exports.sendEmail = exports.createMagicLink = exports.revokeCode = exports.revokeAllCodes = exports.updatePasswordlessUser = exports.createNewCodeForDevice = exports.listCodesByPreAuthSessionId = exports.listCodesByPhoneNumber = exports.listCodesByEmail = exports.listCodesByDeviceId = exports.getUserByPhoneNumber = exports.consumeCode = exports.createCode = exports.getUsersByEmail = exports.getUserByThirdPartyInfo = exports.getUserById = exports.passwordlessSignInUp = exports.thirdPartyManuallyCreateOrUpdateUser = exports.thirdPartyGetProvider = exports.Error = exports.init = void 0; +exports.sendSms = exports.sendEmail = exports.createMagicLink = exports.revokeCode = exports.revokeAllCodes = exports.updatePasswordlessUser = exports.createNewCodeForDevice = exports.listCodesByPreAuthSessionId = exports.listCodesByPhoneNumber = exports.listCodesByEmail = exports.listCodesByDeviceId = exports.consumeCode = exports.createCode = exports.passwordlessSignInUp = exports.thirdPartyManuallyCreateOrUpdateUser = exports.thirdPartyGetProvider = exports.Error = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); class Wrapper { - static thirdPartyGetProvider(tenantId, thirdPartyId, clientType, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyGetProvider({ - thirdPartyId, - tenantId, - clientType, - userContext, - }); - }); - } - static thirdPartyManuallyCreateOrUpdateUser(tenantId, thirdPartyId, thirdPartyUserId, email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ + static async thirdPartyGetProvider(tenantId, thirdPartyId, clientType, userContext = {}) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyGetProvider({ thirdPartyId, - thirdPartyUserId, - email, tenantId, + clientType, userContext, }); } - static getUserByThirdPartyInfo(tenantId, thirdPartyId, thirdPartyUserId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ + static thirdPartyManuallyCreateOrUpdateUser( + tenantId, + thirdPartyId, + thirdPartyUserId, + email, + isVerified, + userContext = {} + ) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ thirdPartyId, thirdPartyUserId, - tenantId, - userContext, - }); - } - static getUserById(userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - static getUsersByEmail(tenantId, email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, + isVerified, tenantId, userContext, }); } static createCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createCode(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createCode( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static createNewCodeForDevice(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createNewCodeForDevice(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static consumeCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.consumeCode(Object.assign({ userContext: {} }, input)); - } - static getUserByPhoneNumber(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getUserByPhoneNumber(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static updatePasswordlessUser(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.updatePasswordlessUser(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updatePasswordlessUser( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static revokeAllCodes(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeAllCodes(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static revokeCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeCode(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static listCodesByEmail(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByEmail(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static listCodesByPhoneNumber(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByPhoneNumber(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static listCodesByDeviceId(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByDeviceId(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static listCodesByPreAuthSessionId(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByPreAuthSessionId(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static createMagicLink(input) { - return recipe_1.default - .getInstanceOrThrowError() - .passwordlessRecipe.createMagicLink(Object.assign({ userContext: {} }, input)); + var _a; + return recipe_1.default.getInstanceOrThrowError().passwordlessRecipe.createMagicLink( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } static passwordlessSignInUp(input) { - return recipe_1.default - .getInstanceOrThrowError() - .passwordlessRecipe.signInUp(Object.assign({ userContext: {} }, input)); - } - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .emailDelivery.ingredientInterfaceImpl.sendEmail(Object.assign({ userContext: {} }, input)); - }); - } - static sendSms(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .smsDelivery.ingredientInterfaceImpl.sendSms(Object.assign({ userContext: {} }, input)); - }); + var _a; + return recipe_1.default.getInstanceOrThrowError().passwordlessRecipe.signInUp( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); + } + static async sendEmail(input) { + var _a; + return await recipe_1.default.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); + } + static async sendSms(input) { + var _a; + return await recipe_1.default.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms( + Object.assign(Object.assign({}, input), { + userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + }) + ); } } exports.default = Wrapper; @@ -179,12 +169,8 @@ exports.Error = Wrapper.Error; exports.thirdPartyGetProvider = Wrapper.thirdPartyGetProvider; exports.thirdPartyManuallyCreateOrUpdateUser = Wrapper.thirdPartyManuallyCreateOrUpdateUser; exports.passwordlessSignInUp = Wrapper.passwordlessSignInUp; -exports.getUserById = Wrapper.getUserById; -exports.getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; -exports.getUsersByEmail = Wrapper.getUsersByEmail; exports.createCode = Wrapper.createCode; exports.consumeCode = Wrapper.consumeCode; -exports.getUserByPhoneNumber = Wrapper.getUserByPhoneNumber; exports.listCodesByDeviceId = Wrapper.listCodesByDeviceId; exports.listCodesByEmail = Wrapper.listCodesByEmail; exports.listCodesByPhoneNumber = Wrapper.listCodesByPhoneNumber; diff --git a/lib/build/recipe/thirdpartypasswordless/recipe.d.ts b/lib/build/recipe/thirdpartypasswordless/recipe.d.ts index e84d62a8c..1e88d13b5 100644 --- a/lib/build/recipe/thirdpartypasswordless/recipe.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/recipe.d.ts @@ -3,7 +3,7 @@ import RecipeModule from "../../recipeModule"; import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; import PasswordlessRecipe from "../passwordless/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import STError from "./error"; import { TypeInput, diff --git a/lib/build/recipe/thirdpartypasswordless/recipe.js b/lib/build/recipe/thirdpartypasswordless/recipe.js index 944469e16..86925e016 100644 --- a/lib/build/recipe/thirdpartypasswordless/recipe.js +++ b/lib/build/recipe/thirdpartypasswordless/recipe.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -77,50 +46,37 @@ class Recipe extends recipeModule_1.default { apisHandled.push(...this.thirdPartyRecipe.getAPIsHandled()); return apisHandled; }; - this.handleAPIRequest = (id, tenantId, req, res, path, method, userContext) => - __awaiter(this, void 0, void 0, function* () { - if ( - (yield this.passwordlessRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== - undefined - ) { - return yield this.passwordlessRecipe.handleAPIRequest( - id, - tenantId, - req, - res, - path, - method, - userContext - ); - } - if ( - (yield this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== undefined - ) { - return yield this.thirdPartyRecipe.handleAPIRequest( - id, - tenantId, - req, - res, - path, - method, - userContext - ); - } - return false; - }); - this.handleError = (err, request, response) => - __awaiter(this, void 0, void 0, function* () { - if (err.fromRecipe === Recipe.RECIPE_ID) { - throw err; - } else { - if (this.passwordlessRecipe.isErrorFromThisRecipe(err)) { - return yield this.passwordlessRecipe.handleError(err, request, response); - } else if (this.thirdPartyRecipe.isErrorFromThisRecipe(err)) { - return yield this.thirdPartyRecipe.handleError(err, request, response); - } - throw err; + this.handleAPIRequest = async (id, tenantId, req, res, path, method, userContext) => { + if ( + (await this.passwordlessRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== undefined + ) { + return await this.passwordlessRecipe.handleAPIRequest( + id, + tenantId, + req, + res, + path, + method, + userContext + ); + } + if ((await this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== undefined) { + return await this.thirdPartyRecipe.handleAPIRequest(id, tenantId, req, res, path, method, userContext); + } + return false; + }; + this.handleError = async (err, request, response) => { + if (err.fromRecipe === Recipe.RECIPE_ID) { + throw err; + } else { + if (this.passwordlessRecipe.isErrorFromThisRecipe(err)) { + return await this.passwordlessRecipe.handleError(err, request, response); + } else if (this.thirdPartyRecipe.isErrorFromThisRecipe(err)) { + return await this.thirdPartyRecipe.handleError(err, request, response); } - }); + throw err; + } + }; this.getAllCORSHeaders = () => { let corsHeaders = [...this.passwordlessRecipe.getAllCORSHeaders()]; corsHeaders.push(...this.thirdPartyRecipe.getAllCORSHeaders()); diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.js b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.js index 3c5c8ad5b..cf0b019b0 100644 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.js +++ b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -40,151 +9,93 @@ const recipeImplementation_1 = __importDefault(require("../../passwordless/recip const recipeImplementation_2 = __importDefault(require("../../thirdparty/recipeImplementation")); const passwordlessRecipeImplementation_1 = __importDefault(require("./passwordlessRecipeImplementation")); const thirdPartyRecipeImplementation_1 = __importDefault(require("./thirdPartyRecipeImplementation")); +const __1 = require("../../../"); function getRecipeInterface(passwordlessQuerier, thirdPartyQuerier, providers = []) { let originalPasswordlessImplementation = recipeImplementation_1.default(passwordlessQuerier); let originalThirdPartyImplementation = recipeImplementation_2.default(thirdPartyQuerier, providers); return { - consumeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.consumeCode.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - createCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.createCode.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - createNewCodeForDevice: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.createNewCodeForDevice.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - getUserByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.getUserByPhoneNumber.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - listCodesByDeviceId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.listCodesByDeviceId.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - listCodesByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.listCodesByEmail.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - listCodesByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.listCodesByPhoneNumber.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - listCodesByPreAuthSessionId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.listCodesByPreAuthSessionId.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - revokeAllCodes: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.revokeAllCodes.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - revokeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.revokeCode.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - updatePasswordlessUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield this.getUserById({ userId: input.userId, userContext: input.userContext }); - if (user === undefined) { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } else if ("thirdParty" in user) { - throw new Error( - "Cannot update passwordless user info for those who signed up using third party login." + consumeCode: async function (input) { + return originalPasswordlessImplementation.consumeCode.bind( + passwordlessRecipeImplementation_1.default(this) + )(input); + }, + createCode: async function (input) { + return originalPasswordlessImplementation.createCode.bind(passwordlessRecipeImplementation_1.default(this))( + input + ); + }, + createNewCodeForDevice: async function (input) { + return originalPasswordlessImplementation.createNewCodeForDevice.bind( + passwordlessRecipeImplementation_1.default(this) + )(input); + }, + listCodesByDeviceId: async function (input) { + return originalPasswordlessImplementation.listCodesByDeviceId.bind( + passwordlessRecipeImplementation_1.default(this) + )(input); + }, + listCodesByEmail: async function (input) { + return originalPasswordlessImplementation.listCodesByEmail.bind( + passwordlessRecipeImplementation_1.default(this) + )(input); + }, + listCodesByPhoneNumber: async function (input) { + return originalPasswordlessImplementation.listCodesByPhoneNumber.bind( + passwordlessRecipeImplementation_1.default(this) + )(input); + }, + listCodesByPreAuthSessionId: async function (input) { + return originalPasswordlessImplementation.listCodesByPreAuthSessionId.bind( + passwordlessRecipeImplementation_1.default(this) + )(input); + }, + revokeAllCodes: async function (input) { + return originalPasswordlessImplementation.revokeAllCodes.bind( + passwordlessRecipeImplementation_1.default(this) + )(input); + }, + revokeCode: async function (input) { + return originalPasswordlessImplementation.revokeCode.bind(passwordlessRecipeImplementation_1.default(this))( + input + ); + }, + updatePasswordlessUser: async function (input) { + let user = await __1.getUser(input.recipeUserId.getAsString(), input.userContext); + if (user === undefined) { + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + } + let inputUserIdIsPointingToPasswordlessUser = + user.loginMethods.find((lM) => { + return ( + lM.recipeId === "passwordless" && + lM.recipeUserId.getAsString() === input.recipeUserId.getAsString() ); - } - return originalPasswordlessImplementation.updateUser.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - thirdPartySignInUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalThirdPartyImplementation.signInUp.bind(thirdPartyRecipeImplementation_1.default(this))( - input + }) !== undefined; + if (!inputUserIdIsPointingToPasswordlessUser) { + throw new Error( + "Cannot update a user who signed up using third party login using updatePasswordlessUser." ); - }); - }, - thirdPartyManuallyCreateOrUpdateUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalThirdPartyImplementation.manuallyCreateOrUpdateUser.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); - }, - thirdPartyGetProvider: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalThirdPartyImplementation.getProvider.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield originalPasswordlessImplementation.getUserById.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - if (user !== undefined) { - return user; - } - return yield originalThirdPartyImplementation.getUserById.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); - }, - getUsersByEmail: function ({ email, tenantId, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let userFromEmailPass = yield originalPasswordlessImplementation.getUserByEmail.bind( - passwordlessRecipeImplementation_1.default(this) - )({ email, tenantId, userContext }); - let usersFromThirdParty = yield originalThirdPartyImplementation.getUsersByEmail.bind( - thirdPartyRecipeImplementation_1.default(this) - )({ email, tenantId, userContext }); - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }); - }, - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); + } + return originalPasswordlessImplementation.updateUser.bind(passwordlessRecipeImplementation_1.default(this))( + input + ); + }, + thirdPartySignInUp: async function (input) { + return originalThirdPartyImplementation.signInUp.bind(thirdPartyRecipeImplementation_1.default(this))( + input + ); + }, + thirdPartyManuallyCreateOrUpdateUser: async function (input) { + return originalThirdPartyImplementation.manuallyCreateOrUpdateUser.bind( + thirdPartyRecipeImplementation_1.default(this) + )(input); + }, + thirdPartyGetProvider: async function (input) { + return originalThirdPartyImplementation.getProvider.bind(thirdPartyRecipeImplementation_1.default(this))( + input + ); }, }; } diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.js b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.js index 34cc8cf85..87e0a4ff5 100644 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.js +++ b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.js @@ -1,119 +1,36 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getRecipeInterface(recipeInterface) { return { - consumeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.consumeCode(input); - }); - }, - createCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.createCode(input); - }); - }, - createNewCodeForDevice: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.createNewCodeForDevice(input); - }); - }, - getUserByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let users = yield recipeInterface.getUsersByEmail(input); - for (let i = 0; i < users.length; i++) { - let u = users[i]; - if (!("thirdParty" in u)) { - return u; - } - } - return undefined; - }); + consumeCode: async function (input) { + return await recipeInterface.consumeCode(input); }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user !== undefined && "thirdParty" in user) { - // this is a thirdparty user. - return undefined; - } - return user; - }); + createCode: async function (input) { + return await recipeInterface.createCode(input); }, - getUserByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserByPhoneNumber(input); - if (user !== undefined && "thirdParty" in user) { - // this is a thirdparty user. - return undefined; - } - return user; - }); + createNewCodeForDevice: async function (input) { + return await recipeInterface.createNewCodeForDevice(input); }, - listCodesByDeviceId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.listCodesByDeviceId(input); - }); + listCodesByDeviceId: async function (input) { + return await recipeInterface.listCodesByDeviceId(input); }, - listCodesByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.listCodesByEmail(input); - }); + listCodesByEmail: async function (input) { + return await recipeInterface.listCodesByEmail(input); }, - listCodesByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.listCodesByPhoneNumber(input); - }); + listCodesByPhoneNumber: async function (input) { + return await recipeInterface.listCodesByPhoneNumber(input); }, - listCodesByPreAuthSessionId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.listCodesByPreAuthSessionId(input); - }); + listCodesByPreAuthSessionId: async function (input) { + return await recipeInterface.listCodesByPreAuthSessionId(input); }, - revokeAllCodes: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.revokeAllCodes(input); - }); + revokeAllCodes: async function (input) { + return await recipeInterface.revokeAllCodes(input); }, - revokeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.revokeCode(input); - }); + revokeCode: async function (input) { + return await recipeInterface.revokeCode(input); }, - updateUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.updatePasswordlessUser(input); - }); + updateUser: async function (input) { + return await recipeInterface.updatePasswordlessUser(input); }, }; } diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js index 94032cb98..1b2b14eeb 100644 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js +++ b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js @@ -1,98 +1,27 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; Object.defineProperty(exports, "__esModule", { value: true }); function getRecipeInterface(recipeInterface) { return { - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserByThirdPartyInfo(input); - if (user === undefined || !("thirdParty" in user)) { - return undefined; - } - return user; - }); - }, - signInUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.thirdPartySignInUp(input); - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: result.user, - oAuthTokens: result.oAuthTokens, - rawUserInfoFromProvider: result.rawUserInfoFromProvider, - }; - }); + signInUp: async function (input) { + return await recipeInterface.thirdPartySignInUp(input); }, - manuallyCreateOrUpdateUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.thirdPartyManuallyCreateOrUpdateUser(input); - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: result.user, - }; - }); - }, - getProvider: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.thirdPartyGetProvider(input); - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user === undefined || !("thirdParty" in user)) { - // either user is undefined or it's an email password user. - return undefined; - } - return user; - }); + manuallyCreateOrUpdateUser: async function (input) { + let result = await recipeInterface.thirdPartyManuallyCreateOrUpdateUser(input); + if (result.status !== "OK") { + return result; + } + if (!("thirdParty" in result.user)) { + throw new Error("Should never come here"); + } + return { + status: "OK", + createdNewRecipeUser: result.createdNewRecipeUser, + recipeUserId: result.recipeUserId, + user: result.user, + }; }, - getUsersByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let users = yield recipeInterface.getUsersByEmail(input); - // we filter out all non thirdparty users. - return users.filter((u) => { - return "thirdParty" in u; - }); - }); + getProvider: async function (input) { + return await recipeInterface.thirdPartyGetProvider(input); }, }; } diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.js b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.js index 617e5be76..7b2354aef 100644 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -41,10 +10,9 @@ const backwardCompatibility_1 = __importDefault( ); class BackwardCompatibilityService { constructor() { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.passwordlessBackwardCompatibilityService.sendSms(input); - }); + this.sendSms = async (input) => { + await this.passwordlessBackwardCompatibilityService.sendSms(input); + }; this.passwordlessBackwardCompatibilityService = new backwardCompatibility_1.default(); } } diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.js b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.js index 4c764c79d..f30667e34 100644 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.js +++ b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,10 +8,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); const supertokens_1 = __importDefault(require("../../../../passwordless/smsdelivery/services/supertokens")); class SupertokensService { constructor(apiKey) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.passwordlessSupertokensService.sendSms(input); - }); + this.sendSms = async (input) => { + await this.passwordlessSupertokensService.sendSms(input); + }; this.passwordlessSupertokensService = new supertokens_1.default(apiKey); } } diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.js b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.js index 4dcafed68..467b2dd00 100644 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.js +++ b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -39,10 +8,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); const index_1 = __importDefault(require("../../../../passwordless/smsdelivery/services/twilio/index")); class TwilioService { constructor(config) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.passwordlessTwilioService.sendSms(input); - }); + this.sendSms = async (input) => { + await this.passwordlessTwilioService.sendSms(input); + }; this.passwordlessTwilioService = new index_1.default(config); } } diff --git a/lib/build/recipe/thirdpartypasswordless/types.d.ts b/lib/build/recipe/thirdpartypasswordless/types.d.ts index 866dc8a72..c1829077f 100644 --- a/lib/build/recipe/thirdpartypasswordless/types.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/types.d.ts @@ -23,25 +23,9 @@ import { TypeInput as SmsDeliveryTypeInput, TypeInputWithService as SmsDeliveryTypeInputWithService, } from "../../ingredients/smsdelivery/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export declare type DeviceType = DeviceTypeOriginal; -export declare type User = ( - | { - email?: string; - phoneNumber?: string; - } - | { - email: string; - thirdParty: { - id: string; - userId: string; - }; - } -) & { - id: string; - timeJoined: number; - tenantIds: string[]; -}; export declare type TypeInput = ( | { contactMethod: "PHONE"; @@ -114,23 +98,11 @@ export declare type TypeNormalisedInput = ( }; }; export declare type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - getUsersByEmail(input: { email: string; tenantId: string; userContext: any }): Promise; - getUserByPhoneNumber: (input: { - phoneNumber: string; - tenantId: string; - userContext: any; - }) => Promise; - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise; thirdPartySignInUp(input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any; }; @@ -144,33 +116,52 @@ export declare type RecipeInterface = { }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { - [key: string]: any; - }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { - [key: string]: any; - }; - fromUserInfoAPI?: { - [key: string]: any; - }; - }; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + oAuthTokens: { + [key: string]: any; + }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; thirdPartyManuallyCreateOrUpdateUser(input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; thirdPartyGetProvider(input: { thirdPartyId: string; clientType?: string; @@ -238,8 +229,9 @@ export declare type RecipeInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; + recipeUserId: RecipeUserId; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -251,13 +243,23 @@ export declare type RecipeInterface = { } >; updatePasswordlessUser: (input: { - userId: string; + recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; + }) => Promise< + | { + status: + | "OK" + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; revokeAllCodes: ( input: | { @@ -341,7 +343,7 @@ export declare type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; session: SessionContainerInterface; oAuthTokens: { @@ -359,6 +361,10 @@ export declare type APIInterface = { | { status: "NO_EMAIL_GIVEN_BY_PROVIDER"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); appleRedirectHandlerPOST: @@ -390,6 +396,10 @@ export declare type APIInterface = { preAuthSessionId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); resendCodePOST: @@ -430,7 +440,7 @@ export declare type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; session: SessionContainerInterface; } @@ -443,6 +453,10 @@ export declare type APIInterface = { | { status: "RESTART_FLOW_ERROR"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } >); passwordlessUserEmailExistsGET: | undefined diff --git a/lib/build/recipe/usermetadata/index.js b/lib/build/recipe/usermetadata/index.js index c5d267836..e6be0a417 100644 --- a/lib/build/recipe/usermetadata/index.js +++ b/lib/build/recipe/usermetadata/index.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -53,29 +22,23 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.clearUserMetadata = exports.updateUserMetadata = exports.getUserMetadata = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); class Wrapper { - static getUserMetadata(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserMetadata({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); + static async getUserMetadata(userId, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserMetadata({ + userId, + userContext: userContext === undefined ? {} : userContext, }); } - static updateUserMetadata(userId, metadataUpdate, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateUserMetadata({ - userId, - metadataUpdate, - userContext: userContext === undefined ? {} : userContext, - }); + static async updateUserMetadata(userId, metadataUpdate, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateUserMetadata({ + userId, + metadataUpdate, + userContext: userContext === undefined ? {} : userContext, }); } - static clearUserMetadata(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.clearUserMetadata({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); + static async clearUserMetadata(userId, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.clearUserMetadata({ + userId, + userContext: userContext === undefined ? {} : userContext, }); } } diff --git a/lib/build/recipe/usermetadata/recipe.d.ts b/lib/build/recipe/usermetadata/recipe.d.ts index e48e751ae..53d2c80ad 100644 --- a/lib/build/recipe/usermetadata/recipe.d.ts +++ b/lib/build/recipe/usermetadata/recipe.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; @@ -18,7 +18,7 @@ export default class Recipe extends RecipeModule { getAPIsHandled(): APIHandled[]; handleAPIRequest: ( _: string, - ______: string | undefined, + _tenantId: string | undefined, __: BaseRequest, ___: BaseResponse, ____: normalisedURLPath, diff --git a/lib/build/recipe/usermetadata/recipe.js b/lib/build/recipe/usermetadata/recipe.js index c2d65f279..58265e3f9 100644 --- a/lib/build/recipe/usermetadata/recipe.js +++ b/lib/build/recipe/usermetadata/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -60,17 +29,9 @@ class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); // This stub is required to implement RecipeModule - this.handleAPIRequest = ( - _, - ______, // TODO tenantId - __, - ___, - ____, - _____ - ) => - __awaiter(this, void 0, void 0, function* () { - throw new Error("Should never come here"); - }); + this.handleAPIRequest = async (_, _tenantId, __, ___, ____, _____) => { + throw new Error("Should never come here"); + }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); this.isInServerlessEnv = isInServerlessEnv; { diff --git a/lib/build/recipe/userroles/index.js b/lib/build/recipe/userroles/index.js index 69fe39039..b19da7fe8 100644 --- a/lib/build/recipe/userroles/index.js +++ b/lib/build/recipe/userroles/index.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -55,91 +24,71 @@ const permissionClaim_1 = require("./permissionClaim"); const recipe_1 = __importDefault(require("./recipe")); const userRoleClaim_1 = require("./userRoleClaim"); class Wrapper { - static addRoleToUser(tenantId, userId, role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.addRoleToUser({ - userId, - role, - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); + static async addRoleToUser(tenantId, userId, role, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.addRoleToUser({ + userId, + role, + tenantId, + userContext: userContext === undefined ? {} : userContext, }); } - static removeUserRole(tenantId, userId, role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removeUserRole({ - userId, - role, - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); + static async removeUserRole(tenantId, userId, role, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removeUserRole({ + userId, + role, + tenantId, + userContext: userContext === undefined ? {} : userContext, }); } - static getRolesForUser(tenantId, userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getRolesForUser({ - userId, - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); + static async getRolesForUser(tenantId, userId, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getRolesForUser({ + userId, + tenantId, + userContext: userContext === undefined ? {} : userContext, }); } - static getUsersThatHaveRole(tenantId, role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersThatHaveRole({ - role, - tenantId, - userContext: userContext === undefined ? {} : userContext, - }); + static async getUsersThatHaveRole(tenantId, role, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersThatHaveRole({ + role, + tenantId, + userContext: userContext === undefined ? {} : userContext, }); } - static createNewRoleOrAddPermissions(role, permissions, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewRoleOrAddPermissions({ - role, - permissions, - userContext: userContext === undefined ? {} : userContext, - }); + static async createNewRoleOrAddPermissions(role, permissions, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewRoleOrAddPermissions({ + role, + permissions, + userContext: userContext === undefined ? {} : userContext, }); } - static getPermissionsForRole(role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getPermissionsForRole({ - role, - userContext: userContext === undefined ? {} : userContext, - }); + static async getPermissionsForRole(role, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext: userContext === undefined ? {} : userContext, }); } - static removePermissionsFromRole(role, permissions, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removePermissionsFromRole({ - role, - permissions, - userContext: userContext === undefined ? {} : userContext, - }); + static async removePermissionsFromRole(role, permissions, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removePermissionsFromRole({ + role, + permissions, + userContext: userContext === undefined ? {} : userContext, }); } - static getRolesThatHavePermission(permission, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getRolesThatHavePermission({ - permission, - userContext: userContext === undefined ? {} : userContext, - }); + static async getRolesThatHavePermission(permission, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getRolesThatHavePermission({ + permission, + userContext: userContext === undefined ? {} : userContext, }); } - static deleteRole(role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.deleteRole({ - role, - userContext: userContext === undefined ? {} : userContext, - }); + static async deleteRole(role, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.deleteRole({ + role, + userContext: userContext === undefined ? {} : userContext, }); } - static getAllRoles(userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getAllRoles({ - userContext: userContext === undefined ? {} : userContext, - }); + static async getAllRoles(userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getAllRoles({ + userContext: userContext === undefined ? {} : userContext, }); } } diff --git a/lib/build/recipe/userroles/permissionClaim.js b/lib/build/recipe/userroles/permissionClaim.js index fc11c59dc..6ee522b54 100644 --- a/lib/build/recipe/userroles/permissionClaim.js +++ b/lib/build/recipe/userroles/permissionClaim.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -46,30 +15,28 @@ class PermissionClaimClass extends primitiveArrayClaim_1.PrimitiveArrayClaim { constructor() { super({ key: "st-perm", - fetchValue(userId, tenantId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipe = recipe_1.default.getInstanceOrThrowError(); - // We fetch the roles because the rolesClaim may not be present in the payload - const userRoles = yield recipe.recipeInterfaceImpl.getRolesForUser({ - userId, - tenantId, + async fetchValue(userId, _recipeUserId, tenantId, userContext) { + const recipe = recipe_1.default.getInstanceOrThrowError(); + // We fetch the roles because the rolesClaim may not be present in the payload + const userRoles = await recipe.recipeInterfaceImpl.getRolesForUser({ + userId, + tenantId, + userContext, + }); + // We use a set to filter out duplicates + const userPermissions = new Set(); + for (const role of userRoles.roles) { + const rolePermissions = await recipe.recipeInterfaceImpl.getPermissionsForRole({ + role, userContext, }); - // We use a set to filter out duplicates - const userPermissions = new Set(); - for (const role of userRoles.roles) { - const rolePermissions = yield recipe.recipeInterfaceImpl.getPermissionsForRole({ - role, - userContext, - }); - if (rolePermissions.status === "OK") { - for (const perm of rolePermissions.permissions) { - userPermissions.add(perm); - } + if (rolePermissions.status === "OK") { + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); } } - return Array.from(userPermissions); - }); + } + return Array.from(userPermissions); }, defaultMaxAgeInSeconds: 300, }); diff --git a/lib/build/recipe/userroles/recipe.d.ts b/lib/build/recipe/userroles/recipe.d.ts index e48e751ae..53d2c80ad 100644 --- a/lib/build/recipe/userroles/recipe.d.ts +++ b/lib/build/recipe/userroles/recipe.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; @@ -18,7 +18,7 @@ export default class Recipe extends RecipeModule { getAPIsHandled(): APIHandled[]; handleAPIRequest: ( _: string, - ______: string | undefined, + _tenantId: string | undefined, __: BaseRequest, ___: BaseResponse, ____: normalisedURLPath, diff --git a/lib/build/recipe/userroles/recipe.js b/lib/build/recipe/userroles/recipe.js index 17b86676c..53f5d56b6 100644 --- a/lib/build/recipe/userroles/recipe.js +++ b/lib/build/recipe/userroles/recipe.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -64,17 +33,9 @@ class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); // This stub is required to implement RecipeModule - this.handleAPIRequest = ( - _, - ______, // TODO tenantId - __, - ___, - ____, - _____ - ) => - __awaiter(this, void 0, void 0, function* () { - throw new Error("Should never come here"); - }); + this.handleAPIRequest = async (_, _tenantId, __, ___, ____, _____) => { + throw new Error("Should never come here"); + }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); this.isInServerlessEnv = isInServerlessEnv; { diff --git a/lib/build/recipe/userroles/userRoleClaim.js b/lib/build/recipe/userroles/userRoleClaim.js index 29eb89c97..6415692a1 100644 --- a/lib/build/recipe/userroles/userRoleClaim.js +++ b/lib/build/recipe/userroles/userRoleClaim.js @@ -1,35 +1,4 @@ "use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -46,16 +15,14 @@ class UserRoleClaimClass extends primitiveArrayClaim_1.PrimitiveArrayClaim { constructor() { super({ key: "st-role", - fetchValue(userId, tenantId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipe = recipe_1.default.getInstanceOrThrowError(); - const res = yield recipe.recipeInterfaceImpl.getRolesForUser({ - userId, - tenantId, - userContext, - }); - return res.roles; + async fetchValue(userId, _recipeUserId, tenantId, userContext) { + const recipe = recipe_1.default.getInstanceOrThrowError(); + const res = await recipe.recipeInterfaceImpl.getRolesForUser({ + userId, + tenantId, + userContext, }); + return res.roles; }, defaultMaxAgeInSeconds: 300, }); diff --git a/lib/build/recipeModule.js b/lib/build/recipeModule.js index 708f3ea09..7b3eef889 100644 --- a/lib/build/recipeModule.js +++ b/lib/build/recipeModule.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -60,49 +29,48 @@ class RecipeModule { this.getAppInfo = () => { return this.appInfo; }; - this.returnAPIIdIfCanHandleRequest = (path, method, userContext) => - __awaiter(this, void 0, void 0, function* () { - let apisHandled = this.getAPIsHandled(); - const basePathStr = this.appInfo.apiBasePath.getAsStringDangerous(); - const pathStr = path.getAsStringDangerous(); - const regex = new RegExp(`^${basePathStr}(?:/([a-zA-Z0-9-]+))?(/.*)$`); - const match = pathStr.match(regex); - let tenantId = constants_1.DEFAULT_TENANT_ID; - let remainingPath = undefined; - if (match) { - tenantId = match[1]; - remainingPath = new normalisedURLPath_1.default(match[2]); - } - // Multitenancy recipe is an always initialized recipe and needs to be imported this way - // so that there is no circular dependency. Otherwise there would be cyclic dependency - // between `supertokens.ts` -> `recipeModule.ts` -> `multitenancy/recipe.ts` - let MultitenancyRecipe = require("./recipe/multitenancy/recipe").default; - const mtRecipe = MultitenancyRecipe.getInstanceOrThrowError(); - for (let i = 0; i < apisHandled.length; i++) { - let currAPI = apisHandled[i]; - if (!currAPI.disabled && currAPI.method === method) { - if (this.appInfo.apiBasePath.appendPath(currAPI.pathWithoutApiBasePath).equals(path)) { - const finalTenantId = yield mtRecipe.recipeInterfaceImpl.getTenantId({ - tenantIdFromFrontend: constants_1.DEFAULT_TENANT_ID, - userContext, - }); - return { id: currAPI.id, tenantId: finalTenantId }; - } else if ( - remainingPath !== undefined && - this.appInfo.apiBasePath - .appendPath(currAPI.pathWithoutApiBasePath) - .equals(this.appInfo.apiBasePath.appendPath(remainingPath)) - ) { - const finalTenantId = yield mtRecipe.recipeInterfaceImpl.getTenantId({ - tenantIdFromFrontend: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - userContext, - }); - return { id: currAPI.id, tenantId: finalTenantId }; - } + this.returnAPIIdIfCanHandleRequest = async (path, method, userContext) => { + let apisHandled = this.getAPIsHandled(); + const basePathStr = this.appInfo.apiBasePath.getAsStringDangerous(); + const pathStr = path.getAsStringDangerous(); + const regex = new RegExp(`^${basePathStr}(?:/([a-zA-Z0-9-]+))?(/.*)$`); + const match = pathStr.match(regex); + let tenantId = constants_1.DEFAULT_TENANT_ID; + let remainingPath = undefined; + if (match) { + tenantId = match[1]; + remainingPath = new normalisedURLPath_1.default(match[2]); + } + // Multitenancy recipe is an always initialized recipe and needs to be imported this way + // so that there is no circular dependency. Otherwise there would be cyclic dependency + // between `supertokens.ts` -> `recipeModule.ts` -> `multitenancy/recipe.ts` + let MultitenancyRecipe = require("./recipe/multitenancy/recipe").default; + const mtRecipe = MultitenancyRecipe.getInstanceOrThrowError(); + for (let i = 0; i < apisHandled.length; i++) { + let currAPI = apisHandled[i]; + if (!currAPI.disabled && currAPI.method === method) { + if (this.appInfo.apiBasePath.appendPath(currAPI.pathWithoutApiBasePath).equals(path)) { + const finalTenantId = await mtRecipe.recipeInterfaceImpl.getTenantId({ + tenantIdFromFrontend: constants_1.DEFAULT_TENANT_ID, + userContext, + }); + return { id: currAPI.id, tenantId: finalTenantId }; + } else if ( + remainingPath !== undefined && + this.appInfo.apiBasePath + .appendPath(currAPI.pathWithoutApiBasePath) + .equals(this.appInfo.apiBasePath.appendPath(remainingPath)) + ) { + const finalTenantId = await mtRecipe.recipeInterfaceImpl.getTenantId({ + tenantIdFromFrontend: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, + userContext, + }); + return { id: currAPI.id, tenantId: finalTenantId }; } } - return undefined; - }); + } + return undefined; + }; this.recipeId = recipeId; this.appInfo = appInfo; } diff --git a/lib/build/recipeUserId.d.ts b/lib/build/recipeUserId.d.ts new file mode 100644 index 000000000..b72ed9e07 --- /dev/null +++ b/lib/build/recipeUserId.d.ts @@ -0,0 +1,6 @@ +// @ts-nocheck +export default class RecipeUserId { + private recipeUserId; + constructor(recipeUserId: string); + getAsString: () => string; +} diff --git a/lib/build/recipeUserId.js b/lib/build/recipeUserId.js new file mode 100644 index 000000000..6da404dc1 --- /dev/null +++ b/lib/build/recipeUserId.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class RecipeUserId { + constructor(recipeUserId) { + this.getAsString = () => { + return this.recipeUserId; + }; + if (recipeUserId === undefined) { + throw new Error("recipeUserId cannot be undefined. Please check for bugs in code"); + } + this.recipeUserId = recipeUserId; + } +} +exports.default = RecipeUserId; diff --git a/lib/build/supertokens.d.ts b/lib/build/supertokens.d.ts index 2c5e99484..1e7ab3210 100644 --- a/lib/build/supertokens.d.ts +++ b/lib/build/supertokens.d.ts @@ -2,8 +2,8 @@ import { TypeInput, NormalisedAppinfo, HTTPMethod, SuperTokensInfo } from "./types"; import RecipeModule from "./recipeModule"; import NormalisedURLPath from "./normalisedURLPath"; -import { BaseRequest, BaseResponse } from "./framework"; -import { TypeFramework } from "./framework/types"; +import type { BaseRequest, BaseResponse } from "./framework"; +import type { TypeFramework } from "./framework/types"; export default class SuperTokens { private static instance; framework: TypeFramework; @@ -28,25 +28,6 @@ export default class SuperTokens { ) => Promise; getAllCORSHeaders: () => string[]; getUserCount: (includeRecipeIds?: string[] | undefined, tenantId?: string | undefined) => Promise; - getUsers: (input: { - timeJoinedOrder: "ASC" | "DESC"; - limit?: number; - paginationToken?: string; - includeRecipeIds?: string[]; - query?: object; - tenantId: string; - }) => Promise<{ - users: { - recipeId: string; - user: any; - }[]; - nextPaginationToken?: string | undefined; - }>; - deleteUser: (input: { - userId: string; - }) => Promise<{ - status: "OK"; - }>; createUserIdMapping: (input: { superTokensUserId: string; externalUserId: string; diff --git a/lib/build/supertokens.js b/lib/build/supertokens.js index 0b0d8d487..5aaf75405 100644 --- a/lib/build/supertokens.js +++ b/lib/build/supertokens.js @@ -13,37 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -62,10 +31,9 @@ const constants_2 = require("./recipe/multitenancy/constants"); class SuperTokens { constructor(config) { var _a, _b; - this.handleAPI = (matchedRecipe, id, tenantId, request, response, path, method, userContext) => - __awaiter(this, void 0, void 0, function* () { - return yield matchedRecipe.handleAPIRequest(id, tenantId, request, response, path, method, userContext); - }); + this.handleAPI = async (matchedRecipe, id, tenantId, request, response, path, method, userContext) => { + return await matchedRecipe.handleAPIRequest(id, tenantId, request, response, path, method, userContext); + }; this.getAllCORSHeaders = () => { let headerSet = new Set(); headerSet.add(constants_1.HEADER_RID); @@ -78,270 +46,207 @@ class SuperTokens { }); return Array.from(headerSet); }; - this.getUserCount = (includeRecipeIds, tenantId) => - __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error( - "Please use core version >= 3.5 to call this function. Otherwise, you can call .getUserCount() instead (for example, EmailPassword.getUserCount())" - ); - } - let includeRecipeIdsStr = undefined; - if (includeRecipeIds !== undefined) { - includeRecipeIdsStr = includeRecipeIds.join(","); - } - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default( - `/${tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId}/users/count` - ), - { - includeRecipeIds: includeRecipeIdsStr, - includeAllTenants: tenantId === undefined, - } + this.getUserCount = async (includeRecipeIds, tenantId) => { + let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (utils_1.maxVersion(apiVersion, "2.7") === "2.7") { + throw new Error( + "Please use core version >= 3.5 to call this function. Otherwise, you can call .getUserCount() instead (for example, EmailPassword.getUserCount())" ); - return Number(response.count); - }); - this.getUsers = (input) => - __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error( - "Please use core version >= 3.5 to call this function. Otherwise, you can call .getUsersOldestFirst() or .getUsersNewestFirst() instead (for example, EmailPassword.getUsersOldestFirst())" - ); - } - let includeRecipeIdsStr = undefined; - if (input.includeRecipeIds !== undefined) { - includeRecipeIdsStr = input.includeRecipeIds.join(","); - } - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default(`/${input.tenantId}/users`), - Object.assign(Object.assign({}, input.query), { - includeRecipeIds: includeRecipeIdsStr, - timeJoinedOrder: input.timeJoinedOrder, - limit: input.limit, - paginationToken: input.paginationToken, - }) - ); - const users = response.users; - return { - users, - nextPaginationToken: response.nextPaginationToken, - }; - }); - this.deleteUser = (input) => - __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.10", cdiVersion) === cdiVersion) { - // delete user is only available >= CDI 2.10 - yield querier.sendPostRequest(new normalisedURLPath_1.default("/user/remove"), { - userId: input.userId, - }); - return { - status: "OK", - }; - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.7.0"); - } - }); - this.createUserIdMapping = function (input) { - return __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { - // create userId mapping is only available >= CDI 2.15 - return yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/userid/map"), { - superTokensUserId: input.superTokensUserId, - externalUserId: input.externalUserId, - externalUserIdInfo: input.externalUserIdInfo, - force: input.force, - }); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); + } + let includeRecipeIdsStr = undefined; + if (includeRecipeIds !== undefined) { + includeRecipeIdsStr = includeRecipeIds.join(","); + } + let response = await querier.sendGetRequest( + new normalisedURLPath_1.default( + `/${tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId}/users/count` + ), + { + includeRecipeIds: includeRecipeIdsStr, + includeAllTenants: tenantId === undefined, } - }); + ); + return Number(response.count); }; - this.getUserIdMapping = function (input) { - return __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { - // create userId mapping is only available >= CDI 2.15 - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/userid/map"), { - userId: input.userId, - userIdType: input.userIdType, - }); - return response; - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }); + this.createUserIdMapping = async function (input) { + let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); + let cdiVersion = await querier.getAPIVersion(); + if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { + // create userId mapping is only available >= CDI 2.15 + return await querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/userid/map"), { + superTokensUserId: input.superTokensUserId, + externalUserId: input.externalUserId, + externalUserIdInfo: input.externalUserIdInfo, + force: input.force, + }); + } else { + throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); + } }; - this.deleteUserIdMapping = function (input) { - return __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { - return yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/userid/map/remove"), { + this.getUserIdMapping = async function (input) { + let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); + let cdiVersion = await querier.getAPIVersion(); + if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { + // create userId mapping is only available >= CDI 2.15 + let response = await querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/userid/map"), { + userId: input.userId, + userIdType: input.userIdType, + }); + return response; + } else { + throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); + } + }; + this.deleteUserIdMapping = async function (input) { + let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); + let cdiVersion = await querier.getAPIVersion(); + if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { + return await querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/userid/map/remove"), { + userId: input.userId, + userIdType: input.userIdType, + force: input.force, + }); + } else { + throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); + } + }; + this.updateOrDeleteUserIdMappingInfo = async function (input) { + let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); + let cdiVersion = await querier.getAPIVersion(); + if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { + return await querier.sendPutRequest( + new normalisedURLPath_1.default("/recipe/userid/external-user-id-info"), + { userId: input.userId, userIdType: input.userIdType, - force: input.force, - }); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }); + externalUserIdInfo: input.externalUserIdInfo, + } + ); + } else { + throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); + } }; - this.updateOrDeleteUserIdMappingInfo = function (input) { - return __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { - return yield querier.sendPutRequest( - new normalisedURLPath_1.default("/recipe/userid/external-user-id-info"), - { - userId: input.userId, - userIdType: input.userIdType, - externalUserIdInfo: input.externalUserIdInfo, - } + this.middleware = async (request, response, userContext) => { + logger_1.logDebugMessage("middleware: Started"); + let path = this.appInfo.apiGatewayPath.appendPath( + new normalisedURLPath_1.default(request.getOriginalURL()) + ); + let method = utils_1.normaliseHttpMethod(request.getMethod()); + // if the prefix of the URL doesn't match the base path, we skip + if (!path.startsWith(this.appInfo.apiBasePath)) { + logger_1.logDebugMessage( + "middleware: Not handling because request path did not start with config path. Request path: " + + path.getAsStringDangerous() + ); + return false; + } + let requestRID = utils_1.getRidFromHeader(request); + logger_1.logDebugMessage("middleware: requestRID is: " + requestRID); + if (requestRID === "anti-csrf") { + // see https://github.com/supertokens/supertokens-node/issues/202 + requestRID = undefined; + } + if (requestRID !== undefined) { + let matchedRecipe = undefined; + // we loop through all recipe modules to find the one with the matching rId + for (let i = 0; i < this.recipeModules.length; i++) { + logger_1.logDebugMessage( + "middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId() ); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); + if (this.recipeModules[i].getRecipeId() === requestRID) { + matchedRecipe = this.recipeModules[i]; + break; + } } - }); - }; - this.middleware = (request, response, userContext) => - __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("middleware: Started"); - let path = this.appInfo.apiGatewayPath.appendPath( - new normalisedURLPath_1.default(request.getOriginalURL()) - ); - let method = utils_1.normaliseHttpMethod(request.getMethod()); - // if the prefix of the URL doesn't match the base path, we skip - if (!path.startsWith(this.appInfo.apiBasePath)) { + if (matchedRecipe === undefined) { + logger_1.logDebugMessage("middleware: Not handling because no recipe matched"); + // we could not find one, so we skip + return false; + } + logger_1.logDebugMessage("middleware: Matched with recipe ID: " + matchedRecipe.getRecipeId()); + let idResult = await matchedRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext); + if (idResult === undefined) { logger_1.logDebugMessage( - "middleware: Not handling because request path did not start with config path. Request path: " + - path.getAsStringDangerous() + "middleware: Not handling because recipe doesn't handle request path or method. Request path: " + + path.getAsStringDangerous() + + ", request method: " + + method ); + // the matched recipe doesn't handle this path and http method return false; } - let requestRID = utils_1.getRidFromHeader(request); - logger_1.logDebugMessage("middleware: requestRID is: " + requestRID); - if (requestRID === "anti-csrf") { - // see https://github.com/supertokens/supertokens-node/issues/202 - requestRID = undefined; + logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); + // give task to the matched recipe + let requestHandled = await matchedRecipe.handleAPIRequest( + idResult.id, + idResult.tenantId, + request, + response, + path, + method, + userContext + ); + if (!requestHandled) { + logger_1.logDebugMessage("middleware: Not handled because API returned requestHandled as false"); + return false; } - if (requestRID !== undefined) { - let matchedRecipe = undefined; - // we loop through all recipe modules to find the one with the matching rId - for (let i = 0; i < this.recipeModules.length; i++) { - logger_1.logDebugMessage( - "middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId() - ); - if (this.recipeModules[i].getRecipeId() === requestRID) { - matchedRecipe = this.recipeModules[i]; - break; - } - } - if (matchedRecipe === undefined) { - logger_1.logDebugMessage("middleware: Not handling because no recipe matched"); - // we could not find one, so we skip - return false; - } - logger_1.logDebugMessage("middleware: Matched with recipe ID: " + matchedRecipe.getRecipeId()); - let idResult = yield matchedRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext); - if (idResult === undefined) { - logger_1.logDebugMessage( - "middleware: Not handling because recipe doesn't handle request path or method. Request path: " + - path.getAsStringDangerous() + - ", request method: " + - method - ); - // the matched recipe doesn't handle this path and http method - return false; - } - logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); - // give task to the matched recipe - let requestHandled = yield matchedRecipe.handleAPIRequest( - idResult.id, - idResult.tenantId, - request, - response, - path, - method, - userContext + logger_1.logDebugMessage("middleware: Ended"); + return true; + } else { + // we loop through all recipe modules to find the one with the matching path and method + for (let i = 0; i < this.recipeModules.length; i++) { + logger_1.logDebugMessage( + "middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId() ); - if (!requestHandled) { - logger_1.logDebugMessage( - "middleware: Not handled because API returned requestHandled as false" - ); - return false; - } - logger_1.logDebugMessage("middleware: Ended"); - return true; - } else { - // we loop through all recipe modules to find the one with the matching path and method - for (let i = 0; i < this.recipeModules.length; i++) { - logger_1.logDebugMessage( - "middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId() - ); - let idResult = yield this.recipeModules[i].returnAPIIdIfCanHandleRequest( + let idResult = await this.recipeModules[i].returnAPIIdIfCanHandleRequest(path, method, userContext); + if (idResult !== undefined) { + logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); + let requestHandled = await this.recipeModules[i].handleAPIRequest( + idResult.id, + idResult.tenantId, + request, + response, path, method, userContext ); - if (idResult !== undefined) { + if (!requestHandled) { logger_1.logDebugMessage( - "middleware: Request being handled by recipe. ID is: " + idResult.id + "middleware: Not handled because API returned requestHandled as false" ); - let requestHandled = yield this.recipeModules[i].handleAPIRequest( - idResult.id, - idResult.tenantId, - request, - response, - path, - method, - userContext - ); - if (!requestHandled) { - logger_1.logDebugMessage( - "middleware: Not handled because API returned requestHandled as false" - ); - return false; - } - logger_1.logDebugMessage("middleware: Ended"); - return true; + return false; } + logger_1.logDebugMessage("middleware: Ended"); + return true; } - logger_1.logDebugMessage("middleware: Not handling because no recipe matched"); - return false; } - }); - this.errorHandler = (err, request, response) => - __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("errorHandler: Started"); - if (error_1.default.isErrorFromSuperTokens(err)) { - logger_1.logDebugMessage("errorHandler: Error is from SuperTokens recipe. Message: " + err.message); - if (err.type === error_1.default.BAD_INPUT_ERROR) { - logger_1.logDebugMessage("errorHandler: Sending 400 status code response"); - return utils_1.sendNon200ResponseWithMessage(response, err.message, 400); - } - for (let i = 0; i < this.recipeModules.length; i++) { + logger_1.logDebugMessage("middleware: Not handling because no recipe matched"); + return false; + } + }; + this.errorHandler = async (err, request, response) => { + logger_1.logDebugMessage("errorHandler: Started"); + if (error_1.default.isErrorFromSuperTokens(err)) { + logger_1.logDebugMessage("errorHandler: Error is from SuperTokens recipe. Message: " + err.message); + if (err.type === error_1.default.BAD_INPUT_ERROR) { + logger_1.logDebugMessage("errorHandler: Sending 400 status code response"); + return utils_1.sendNon200ResponseWithMessage(response, err.message, 400); + } + for (let i = 0; i < this.recipeModules.length; i++) { + logger_1.logDebugMessage( + "errorHandler: Checking recipe for match: " + this.recipeModules[i].getRecipeId() + ); + if (this.recipeModules[i].isErrorFromThisRecipe(err)) { logger_1.logDebugMessage( - "errorHandler: Checking recipe for match: " + this.recipeModules[i].getRecipeId() + "errorHandler: Matched with recipeID: " + this.recipeModules[i].getRecipeId() ); - if (this.recipeModules[i].isErrorFromThisRecipe(err)) { - logger_1.logDebugMessage( - "errorHandler: Matched with recipeID: " + this.recipeModules[i].getRecipeId() - ); - return yield this.recipeModules[i].handleError(err, request, response); - } + return await this.recipeModules[i].handleError(err, request, response); } } - throw err; - }); + } + throw err; + }; this.getRequestFromUserContext = (userContext) => { if (userContext === undefined) { return undefined; diff --git a/lib/build/types.d.ts b/lib/build/types.d.ts index 424dbdb3c..0c650218f 100644 --- a/lib/build/types.d.ts +++ b/lib/build/types.d.ts @@ -3,6 +3,7 @@ import RecipeModule from "./recipeModule"; import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; import { TypeFramework } from "./framework/types"; +import { RecipeLevelUser } from "./recipe/accountlinking/types"; export declare type AppInfo = { appName: string; websiteDomain: string; @@ -51,3 +52,23 @@ export declare type GeneralErrorResponse = { status: "GENERAL_ERROR"; message: string; }; +export declare type User = { + id: string; + timeJoined: number; + isPrimaryUser: boolean; + tenantIds: string[]; + emails: string[]; + phoneNumbers: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: (RecipeLevelUser & { + verified: boolean; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + toJson: () => any; + })[]; + toJson: () => any; +}; diff --git a/lib/build/user.d.ts b/lib/build/user.d.ts new file mode 100644 index 000000000..ccdddb396 --- /dev/null +++ b/lib/build/user.d.ts @@ -0,0 +1,59 @@ +// @ts-nocheck +import { RecipeLevelUser } from "./recipe/accountlinking/types"; +import RecipeUserId from "./recipeUserId"; +import { JSONObject, User as UserType } from "./types"; +export declare class LoginMethod implements RecipeLevelUser { + readonly recipeId: RecipeLevelUser["recipeId"]; + readonly recipeUserId: RecipeUserId; + readonly tenantIds: string[]; + readonly email?: string; + readonly phoneNumber?: string; + readonly thirdParty?: RecipeLevelUser["thirdParty"]; + readonly verified: boolean; + readonly timeJoined: number; + constructor(loginMethod: UserWithoutHelperFunctions["loginMethods"][number]); + hasSameEmailAs(email: string | undefined): boolean; + hasSamePhoneNumberAs(phoneNumber: string | undefined): boolean; + hasSameThirdPartyInfoAs(thirdParty?: { id: string; userId: string }): boolean; + toJson(): JSONObject; +} +export declare class User implements UserType { + readonly id: string; + readonly isPrimaryUser: boolean; + readonly tenantIds: string[]; + readonly emails: string[]; + readonly phoneNumbers: string[]; + readonly thirdParty: { + id: string; + userId: string; + }[]; + readonly loginMethods: LoginMethod[]; + readonly timeJoined: number; + constructor(user: UserWithoutHelperFunctions); + toJson(): JSONObject; +} +export declare type UserWithoutHelperFunctions = { + id: string; + timeJoined: number; + isPrimaryUser: boolean; + emails: string[]; + phoneNumbers: string[]; + tenantIds: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: { + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + recipeUserId: string; + tenantIds: string[]; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; + verified: boolean; + timeJoined: number; + }[]; +}; diff --git a/lib/build/user.js b/lib/build/user.js new file mode 100644 index 000000000..5179ebc6f --- /dev/null +++ b/lib/build/user.js @@ -0,0 +1,94 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.User = exports.LoginMethod = void 0; +const recipeUserId_1 = __importDefault(require("./recipeUserId")); +const max_1 = __importDefault(require("libphonenumber-js/max")); +class LoginMethod { + constructor(loginMethod) { + this.recipeId = loginMethod.recipeId; + this.recipeUserId = new recipeUserId_1.default(loginMethod.recipeUserId); + this.tenantIds = loginMethod.tenantIds; + this.email = loginMethod.email; + this.phoneNumber = loginMethod.phoneNumber; + this.thirdParty = loginMethod.thirdParty; + this.timeJoined = loginMethod.timeJoined; + this.verified = loginMethod.verified; + } + hasSameEmailAs(email) { + if (email === undefined) { + return false; + } + // this needs to be the same as what's done in the core. + email = email.toLowerCase().trim(); + return this.email !== undefined && this.email === email; + } + hasSamePhoneNumberAs(phoneNumber) { + if (phoneNumber === undefined) { + return false; + } + const parsedPhoneNumber = max_1.default(phoneNumber.trim(), { extract: false }); + if (parsedPhoneNumber === undefined) { + // this means that the phone number is not valid according to the E.164 standard. + // but we still just trim it. + phoneNumber = phoneNumber.trim(); + } else { + phoneNumber = parsedPhoneNumber.format("E.164"); + } + return this.phoneNumber !== undefined && this.phoneNumber === phoneNumber; + } + hasSameThirdPartyInfoAs(thirdParty) { + if (thirdParty === undefined) { + return false; + } + thirdParty.id = thirdParty.id.trim(); + thirdParty.userId = thirdParty.userId.trim(); + return ( + this.thirdParty !== undefined && + this.thirdParty.id === thirdParty.id && + this.thirdParty.userId === thirdParty.userId + ); + } + toJson() { + return { + recipeId: this.recipeId, + recipeUserId: this.recipeUserId.getAsString(), + tenantIds: this.tenantIds, + email: this.email, + phoneNumber: this.phoneNumber, + thirdParty: this.thirdParty, + timeJoined: this.timeJoined, + verified: this.verified, + }; + } +} +exports.LoginMethod = LoginMethod; +class User { + constructor(user) { + this.id = user.id; + this.isPrimaryUser = user.isPrimaryUser; + this.tenantIds = user.tenantIds; + this.emails = user.emails; + this.phoneNumbers = user.phoneNumbers; + this.thirdParty = user.thirdParty; + this.timeJoined = user.timeJoined; + this.loginMethods = user.loginMethods.map((m) => new LoginMethod(m)); + } + toJson() { + return { + id: this.id, + isPrimaryUser: this.isPrimaryUser, + tenantIds: this.tenantIds, + emails: this.emails, + phoneNumbers: this.phoneNumbers, + thirdParty: this.thirdParty, + loginMethods: this.loginMethods.map((m) => m.toJson()), + timeJoined: this.timeJoined, + }; + } +} +exports.User = User; diff --git a/lib/build/utils.d.ts b/lib/build/utils.d.ts index 45d7e2f2d..6d796ecce 100644 --- a/lib/build/utils.d.ts +++ b/lib/build/utils.d.ts @@ -1,6 +1,8 @@ // @ts-nocheck import type { AppInfo, NormalisedAppinfo, HTTPMethod, JSONObject } from "./types"; import type { BaseRequest, BaseResponse } from "./framework"; +import { User } from "./user"; +import { SessionContainer } from "./recipe/session"; export declare function getLargestVersionFromIntersection(v1: string[], v2: string[]): string | undefined; export declare function maxVersion(version1: string, version2: string): string; export declare function normaliseInputAppInfoOrThrowError(appInfo: AppInfo): NormalisedAppinfo; @@ -9,6 +11,15 @@ export declare function sendNon200ResponseWithMessage(res: BaseResponse, message export declare function sendNon200Response(res: BaseResponse, statusCode: number, body: JSONObject): void; export declare function send200Response(res: BaseResponse, responseJson: any): void; export declare function isAnIpAddress(ipaddress: string): boolean; +export declare function getBackwardsCompatibleUserInfo( + req: BaseRequest, + result: { + user: User; + session: SessionContainer; + createdNewRecipeUser?: boolean; + } +): JSONObject; +export declare function doesRequestSupportFDI(req: BaseRequest, version: string): boolean; export declare function getRidFromHeader(req: BaseRequest): string | undefined; export declare function frontendHasInterceptor(req: BaseRequest): boolean; export declare function humaniseMilliseconds(ms: number): string; diff --git a/lib/build/utils.js b/lib/build/utils.js index aa5c24f5f..b210e2623 100644 --- a/lib/build/utils.js +++ b/lib/build/utils.js @@ -35,44 +35,13 @@ var __importStar = __setModuleDefault(result, mod); return result; }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = void 0; +exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.doesRequestSupportFDI = exports.getBackwardsCompatibleUserInfo = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = void 0; const psl = __importStar(require("psl")); const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); @@ -169,16 +138,86 @@ function sendNon200Response(res, statusCode, body) { exports.sendNon200Response = sendNon200Response; function send200Response(res, responseJson) { logger_1.logDebugMessage("Sending response to client with status code: 200"); + responseJson = deepTransform(responseJson); res.setStatusCode(200); res.sendJSONResponse(responseJson); } exports.send200Response = send200Response; +// this function tries to convert the json response based on the toJson function +// defined in the objects in the input. This is primarily used to convert the RecipeUserId +// type to a string type before sending it to the client. +function deepTransform(obj) { + let out = Array.isArray(obj) ? [] : {}; + for (let key in obj) { + let val = obj[key]; + if (val && typeof val === "object" && val["toJson"] !== undefined && typeof val["toJson"] === "function") { + out[key] = val.toJson(); + } else if (val && typeof val === "object") { + out[key] = deepTransform(val); + } else { + out[key] = val; + } + } + return out; +} function isAnIpAddress(ipaddress) { return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( ipaddress ); } exports.isAnIpAddress = isAnIpAddress; +function getBackwardsCompatibleUserInfo(req, result) { + let resp; + if (doesRequestSupportFDI(req, "1.18")) { + resp = { + user: result.user.toJson(), + }; + if (result.createdNewRecipeUser !== undefined) { + resp.createdNewRecipeUser = result.createdNewRecipeUser; + } + return resp; + } else { + const loginMethod = result.user.loginMethods.find( + (lm) => lm.recipeUserId.getAsString() === result.session.getRecipeUserId().getAsString() + ); + if (loginMethod === undefined) { + throw new Error("This should never happen: session and user mismatch"); + } + const userObj = { + id: result.session.getUserId(), + timeJoined: result.user.timeJoined, + }; + if (loginMethod.thirdParty) { + userObj.thirdParty = loginMethod.thirdParty; + } + if (loginMethod.email) { + userObj.email = loginMethod.email; + } + if (loginMethod.phoneNumber) { + userObj.phoneNumber = loginMethod.phoneNumber; + } + resp = { + user: userObj, + }; + if (result.createdNewRecipeUser !== undefined) { + resp.createdNewUser = result.createdNewRecipeUser; + } + } + return resp; +} +exports.getBackwardsCompatibleUserInfo = getBackwardsCompatibleUserInfo; +function doesRequestSupportFDI(req, version) { + let requestFDI = req.getHeaderValue(constants_1.HEADER_FDI); + if (requestFDI === undefined) { + // By default we assume they want to use the latest FDI, this also helps with tests + return true; + } + if (requestFDI === version || maxVersion(version, requestFDI) !== version) { + return true; + } + return false; +} +exports.doesRequestSupportFDI = doesRequestSupportFDI; function getRidFromHeader(req) { return req.getHeaderValue(constants_1.HEADER_RID); } @@ -243,48 +282,46 @@ function getFromObjectCaseInsensitive(key, object) { return object[matchedKeys[0]]; } exports.getFromObjectCaseInsensitive = getFromObjectCaseInsensitive; -function postWithFetch(url, headers, body, { successLog, errorLogHeader }) { - return __awaiter(this, void 0, void 0, function* () { - let error; - let resp; - try { - const fetchResp = yield cross_fetch_1.default(url, { - method: "POST", - body: JSON.stringify(body), - headers, - }); - const respText = yield fetchResp.text(); - resp = { - status: fetchResp.status, - body: JSON.parse(respText), - }; - if (fetchResp.status < 300) { - logger_1.logDebugMessage(successLog); - return { resp }; - } - logger_1.logDebugMessage(errorLogHeader); - logger_1.logDebugMessage(`Error status: ${fetchResp.status}`); - logger_1.logDebugMessage(`Error response: ${respText}`); - } catch (caught) { - error = caught; - logger_1.logDebugMessage(errorLogHeader); - if (error instanceof Error) { - logger_1.logDebugMessage(`Error: ${error.message}`); - } else { - logger_1.logDebugMessage(`Error: ${JSON.stringify(error)}`); - } +async function postWithFetch(url, headers, body, { successLog, errorLogHeader }) { + let error; + let resp; + try { + const fetchResp = await cross_fetch_1.default(url, { + method: "POST", + body: JSON.stringify(body), + headers, + }); + const respText = await fetchResp.text(); + resp = { + status: fetchResp.status, + body: JSON.parse(respText), + }; + if (fetchResp.status < 300) { + logger_1.logDebugMessage(successLog); + return { resp }; } - logger_1.logDebugMessage("Logging the input below:"); - logger_1.logDebugMessage(JSON.stringify(body, null, 2)); - if (error !== undefined) { - return { - error, - }; + logger_1.logDebugMessage(errorLogHeader); + logger_1.logDebugMessage(`Error status: ${fetchResp.status}`); + logger_1.logDebugMessage(`Error response: ${respText}`); + } catch (caught) { + error = caught; + logger_1.logDebugMessage(errorLogHeader); + if (error instanceof Error) { + logger_1.logDebugMessage(`Error: ${error.message}`); + } else { + logger_1.logDebugMessage(`Error: ${JSON.stringify(error)}`); } + } + logger_1.logDebugMessage("Logging the input below:"); + logger_1.logDebugMessage(JSON.stringify(body, null, 2)); + if (error !== undefined) { return { - resp: resp, + error, }; - }); + } + return { + resp: resp, + }; } exports.postWithFetch = postWithFetch; function normaliseEmail(email) { diff --git a/lib/build/version.d.ts b/lib/build/version.d.ts index afae2d125..bceeca454 100644 --- a/lib/build/version.d.ts +++ b/lib/build/version.d.ts @@ -1,4 +1,4 @@ // @ts-nocheck -export declare const version = "15.2.0"; +export declare const version = "16.0.0"; export declare const cdiSupported: string[]; -export declare const dashboardVersion = "0.7"; +export declare const dashboardVersion = "0.8"; diff --git a/lib/build/version.js b/lib/build/version.js index f3de85970..a77ef7cf0 100644 --- a/lib/build/version.js +++ b/lib/build/version.js @@ -15,7 +15,7 @@ exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; * License for the specific language governing permissions and limitations * under the License. */ -exports.version = "15.2.0"; -exports.cdiSupported = ["3.0"]; +exports.version = "16.0.0"; +exports.cdiSupported = ["4.0"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} -exports.dashboardVersion = "0.7"; +exports.dashboardVersion = "0.8"; diff --git a/lib/ts/index.ts b/lib/ts/index.ts index f83ee2c27..b93303ebc 100644 --- a/lib/ts/index.ts +++ b/lib/ts/index.ts @@ -15,12 +15,19 @@ import SuperTokens from "./supertokens"; import SuperTokensError from "./error"; +import { User as UserType } from "./types"; +import AccountLinking from "./recipe/accountlinking/recipe"; +import { AccountInfo } from "./recipe/accountlinking/types"; +import RecipeUserId from "./recipeUserId"; +import { User } from "./user"; // For Express export default class SuperTokensWrapper { static init = SuperTokens.init; static Error = SuperTokensError; + static RecipeUserId = RecipeUserId; + static User = User; static getAllCORSHeaders() { return SuperTokens.getInstanceOrThrowError().getAllCORSHeaders(); @@ -35,14 +42,15 @@ export default class SuperTokensWrapper { limit?: number; paginationToken?: string; includeRecipeIds?: string[]; - query?: object; + query?: { [key: string]: string }; }): Promise<{ - users: { recipeId: string; user: any }[]; + users: UserType[]; nextPaginationToken?: string; }> { - return SuperTokens.getInstanceOrThrowError().getUsers({ + return AccountLinking.getInstance().recipeInterfaceImpl.getUsers({ timeJoinedOrder: "ASC", ...input, + userContext: undefined, }); } @@ -51,20 +59,15 @@ export default class SuperTokensWrapper { limit?: number; paginationToken?: string; includeRecipeIds?: string[]; - query?: object; + query?: { [key: string]: string }; }): Promise<{ - users: { recipeId: string; user: any }[]; + users: UserType[]; nextPaginationToken?: string; }> { - return SuperTokens.getInstanceOrThrowError().getUsers({ + return AccountLinking.getInstance().recipeInterfaceImpl.getUsers({ timeJoinedOrder: "DESC", ...input, - }); - } - - static deleteUser(userId: string) { - return SuperTokens.getInstanceOrThrowError().deleteUser({ - userId, + userContext: undefined, }); } @@ -97,6 +100,37 @@ export default class SuperTokensWrapper { return SuperTokens.getInstanceOrThrowError().updateOrDeleteUserIdMappingInfo(input); } + static async getUser(userId: string, userContext?: any) { + return await AccountLinking.getInstance().recipeInterfaceImpl.getUser({ + userId, + userContext: userContext === undefined ? {} : userContext, + }); + } + + static async listUsersByAccountInfo( + tenantId: string, + accountInfo: AccountInfo, + doUnionOfAccountInfo: boolean = false, + userContext?: any + ) { + return await AccountLinking.getInstance().recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo, + doUnionOfAccountInfo, + userContext: userContext === undefined ? {} : userContext, + }); + } + static async deleteUser(userId: string, removeAllLinkedAccounts: boolean = true, userContext?: any) { + return await AccountLinking.getInstance().recipeInterfaceImpl.deleteUser({ + userId, + removeAllLinkedAccounts, + userContext: userContext === undefined ? {} : userContext, + }); + } + static convertToRecipeUserId(recipeUserId: string): RecipeUserId { + return new RecipeUserId(recipeUserId); + } + static getRequestFromUserContext(userContext: any | undefined) { return SuperTokens.getInstanceOrThrowError().getRequestFromUserContext(userContext); } @@ -122,6 +156,15 @@ export let deleteUserIdMapping = SuperTokensWrapper.deleteUserIdMapping; export let updateOrDeleteUserIdMappingInfo = SuperTokensWrapper.updateOrDeleteUserIdMappingInfo; +export let getUser = SuperTokensWrapper.getUser; + +export let listUsersByAccountInfo = SuperTokensWrapper.listUsersByAccountInfo; + +export let convertToRecipeUserId = SuperTokensWrapper.convertToRecipeUserId; + export let getRequestFromUserContext = SuperTokensWrapper.getRequestFromUserContext; export let Error = SuperTokensWrapper.Error; + +export { default as RecipeUserId } from "./recipeUserId"; +export { User } from "./user"; diff --git a/lib/ts/processState.ts b/lib/ts/processState.ts index fd8c1446b..794dba645 100644 --- a/lib/ts/processState.ts +++ b/lib/ts/processState.ts @@ -18,6 +18,11 @@ export enum PROCESS_STATE { CALLING_SERVICE_IN_GET_API_VERSION, CALLING_SERVICE_IN_REQUEST_HELPER, MULTI_JWKS_VALIDATION, + + IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS, + IS_SIGN_UP_ALLOWED_CALLED, + IS_SIGN_IN_ALLOWED_CALLED, + IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED, } export class ProcessState { diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index cb4a36917..a4ba3b334 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -346,7 +346,9 @@ export class Querier { } catch (err) { if ( err.message !== undefined && - (err.message.includes("Failed to fetch") || err.message.includes("ECONNREFUSED")) + (err.message.includes("Failed to fetch") || + err.message.includes("ECONNREFUSED") || + err.code === "ECONNREFUSED") ) { return await this.sendRequestHelper(path, method, requestFunc, numberOfTries - 1, retryInfoMap); } diff --git a/lib/ts/recipe/accountlinking/index.ts b/lib/ts/recipe/accountlinking/index.ts new file mode 100644 index 000000000..30da994c5 --- /dev/null +++ b/lib/ts/recipe/accountlinking/index.ts @@ -0,0 +1,171 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import Recipe from "./recipe"; +import type { RecipeInterface, AccountInfoWithRecipeId } from "./types"; +import RecipeUserId from "../../recipeUserId"; +import { getUser } from "../.."; + +export default class Wrapper { + static init = Recipe.init; + + /** + * This is a function which is a combination of createPrimaryUser and + * linkAccounts where the input recipeUserId is either linked to a user that it can be + * linked to, or is made into a primary user. + * + * The output will be the user ID of the user that it was linked to, or it will be the + * same as the input recipeUserId if it was made into a primary user, or if there was + * no linking that happened. + */ + static async createPrimaryUserIdOrLinkAccounts( + tenantId: string, + recipeUserId: RecipeUserId, + userContext: any = {} + ) { + const user = await getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + // Should never really come here unless a programming error happened in the app + throw new Error("Unknown recipeUserId"); + } + + return await Recipe.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user, + userContext, + }); + } + + /** + * This function returns the primary user that the input recipe ID can be + * linked to. It can be used to determine which primary account the linking + * will happen to if the input recipe user ID was to be linked. + * + * If the function returns undefined, it means that there is no primary user + * that the input recipe ID can be linked to, and therefore it can be made + * into a primary user itself. + */ + static async getPrimaryUserThatCanBeLinkedToRecipeUserId( + tenantId: string, + recipeUserId: RecipeUserId, + userContext: any = {} + ) { + const user = await getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + // Should never really come here unless a programming error happened in the app + throw new Error("Unknown recipeUserId"); + } + return await Recipe.getInstance().getPrimaryUserThatCanBeLinkedToRecipeUserId({ + tenantId, + user, + userContext, + }); + } + + static async canCreatePrimaryUser(recipeUserId: RecipeUserId, userContext: any = {}) { + return await Recipe.getInstance().recipeInterfaceImpl.canCreatePrimaryUser({ + recipeUserId, + userContext, + }); + } + + static async createPrimaryUser(recipeUserId: RecipeUserId, userContext: any = {}) { + return await Recipe.getInstance().recipeInterfaceImpl.createPrimaryUser({ + recipeUserId, + userContext, + }); + } + + static async canLinkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext: any = {}) { + return await Recipe.getInstance().recipeInterfaceImpl.canLinkAccounts({ + recipeUserId, + primaryUserId, + userContext, + }); + } + + static async linkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext: any = {}) { + return await Recipe.getInstance().recipeInterfaceImpl.linkAccounts({ + recipeUserId, + primaryUserId, + userContext, + }); + } + + static async unlinkAccount(recipeUserId: RecipeUserId, userContext: any = {}) { + return await Recipe.getInstance().recipeInterfaceImpl.unlinkAccount({ + recipeUserId, + userContext, + }); + } + + static async isSignUpAllowed( + tenantId: string, + newUser: AccountInfoWithRecipeId, + isVerified: boolean, + userContext?: any + ) { + return await Recipe.getInstance().isSignUpAllowed({ + newUser, + isVerified, + tenantId, + userContext, + }); + } + + static async isSignInAllowed(tenantId: string, recipeUserId: RecipeUserId, userContext: any = {}) { + const user = await getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + // Should never really come here unless a programming error happened in the app + throw new Error("Unknown recipeUserId"); + } + + return await Recipe.getInstance().isSignInAllowed({ + user, + tenantId, + userContext, + }); + } + + static async isEmailChangeAllowed( + recipeUserId: RecipeUserId, + newEmail: string, + isVerified: boolean, + userContext?: any + ) { + const user = await getUser(recipeUserId.getAsString(), userContext); + + return await Recipe.getInstance().isEmailChangeAllowed({ + user, + newEmail, + isVerified, + userContext, + }); + } +} + +export const init = Wrapper.init; +export const canCreatePrimaryUser = Wrapper.canCreatePrimaryUser; +export const createPrimaryUser = Wrapper.createPrimaryUser; +export const canLinkAccounts = Wrapper.canLinkAccounts; +export const linkAccounts = Wrapper.linkAccounts; +export const unlinkAccount = Wrapper.unlinkAccount; +export const createPrimaryUserIdOrLinkAccounts = Wrapper.createPrimaryUserIdOrLinkAccounts; +export const getPrimaryUserThatCanBeLinkedToRecipeUserId = Wrapper.getPrimaryUserThatCanBeLinkedToRecipeUserId; +export const isSignUpAllowed = Wrapper.isSignUpAllowed; +export const isSignInAllowed = Wrapper.isSignInAllowed; +export const isEmailChangeAllowed = Wrapper.isEmailChangeAllowed; + +export type { RecipeInterface }; diff --git a/lib/ts/recipe/accountlinking/recipe.ts b/lib/ts/recipe/accountlinking/recipe.ts new file mode 100644 index 000000000..52a7d9372 --- /dev/null +++ b/lib/ts/recipe/accountlinking/recipe.ts @@ -0,0 +1,802 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import error from "../../error"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import normalisedURLPath from "../../normalisedURLPath"; +import RecipeModule from "../../recipeModule"; +import type { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, User } from "../../types"; +import type { TypeNormalisedInput, RecipeInterface, TypeInput, AccountInfoWithRecipeId } from "./types"; +import { validateAndNormaliseUserInput } from "./utils"; +import OverrideableBuilder from "supertokens-js-override"; +import RecipeImplementation from "./recipeImplementation"; +import { Querier } from "../../querier"; +import SuperTokensError from "../../error"; +import supertokens from "../../supertokens"; +import RecipeUserId from "../../recipeUserId"; +import { ProcessState, PROCESS_STATE } from "../../processState"; +import { logDebugMessage } from "../../logger"; +import EmailVerificationRecipe from "../emailverification/recipe"; +import { LoginMethod } from "../../user"; + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined; + + static RECIPE_ID = "accountlinking"; + + config: TypeNormalisedInput; + + recipeInterfaceImpl: RecipeInterface; + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + config: TypeInput | undefined, + _recipes: {}, + _ingredients: {} + ) { + super(recipeId, appInfo); + this.config = validateAndNormaliseUserInput(appInfo, config); + + { + let builder = new OverrideableBuilder( + RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId), this.config, this) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe( + Recipe.RECIPE_ID, + appInfo, + config, + {}, + { + emailDelivery: undefined, + } + ); + return Recipe.instance; + } else { + throw new Error("AccountLinking recipe has already been initialised. Please check your code for bugs."); + } + }; + } + + // we auto init the account linking recipe here cause we always require this + // to be initialized even if the user has not initialized it. + // The side effect of this is that if there are any APIs or errors specific to this recipe, + // those won't be handled by the supertokens middleware and error handler (cause this recipe + // is not in the recipeList). + static getInstance(): Recipe { + if (Recipe.instance === undefined) { + Recipe.init()( + supertokens.getInstanceOrThrowError().appInfo, + supertokens.getInstanceOrThrowError().isInServerlessEnv + ); + } + return Recipe.instance!; + } + + getAPIsHandled(): APIHandled[] { + // APIs won't be added to the supertokens middleware cause we are auto initializing + // it in getInstance function + return []; + } + + handleAPIRequest( + _id: string, + _tenantId: string, + _req: BaseRequest, + _response: BaseResponse, + _path: normalisedURLPath, + _method: HTTPMethod + ): Promise { + throw new Error("Should never come here"); + } + + handleError(error: error, _request: BaseRequest, _response: BaseResponse): Promise { + // Errors won't come here cause we are auto initializing + // it in getInstance function + throw error; + } + + getAllCORSHeaders(): string[] { + return []; + } + + isErrorFromThisRecipe(err: any): err is error { + return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + } + + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + Recipe.instance = undefined; + } + + // this function returns the user ID for which the session will be created. + createPrimaryUserIdOrLinkAccounts = async ({ + tenantId, + user, + userContext, + }: { + tenantId: string; + user: User; + userContext: any; + }): Promise => { + logDebugMessage("createPrimaryUserIdOrLinkAccounts called"); + // TODO: fix this + if (user === undefined) { + // This can come here if the user is using session + email verification + // recipe with a user ID that is not known to supertokens. In this case, + // we do not allow linking for such users. + return user; + } + + if (user.isPrimaryUser) { + return user; + } + + // now we try and find a linking candidate. + let primaryUser = await this.getPrimaryUserThatCanBeLinkedToRecipeUserId({ + tenantId, + user: user, + userContext, + }); + + if (primaryUser === undefined) { + logDebugMessage("createPrimaryUserIdOrLinkAccounts user can become a primary user"); + // this means that this can become a primary user. + + // we can use the 0 index cause this user is + // not a primary user. + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + user.loginMethods[0], + undefined, + tenantId, + userContext + ); + + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logDebugMessage( + "createPrimaryUserIdOrLinkAccounts not creating primary user because shouldAutomaticallyLink is false" + ); + return user; + } + + if (shouldDoAccountLinking.shouldRequireVerification && !user.loginMethods[0].verified) { + logDebugMessage( + "createPrimaryUserIdOrLinkAccounts not creating primary user because shouldRequireVerification is true but the login method is not verified" + ); + return user; + } + logDebugMessage("createPrimaryUserIdOrLinkAccounts creating primary user"); + + let createPrimaryUserResult = await this.recipeInterfaceImpl.createPrimaryUser({ + recipeUserId: user.loginMethods[0].recipeUserId, + userContext, + }); + + if (createPrimaryUserResult.status === "OK") { + logDebugMessage("createPrimaryUserIdOrLinkAccounts created primary user"); + return createPrimaryUserResult.user; + } + + // status is "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" or "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" + + // if it comes here, it means that the recipe + // user ID is already linked to another primary + // user id (race condition), or that some other + // primary user ID exists with the same email / phone number (again, race condition). + // So we do recursion here to try again. + return await this.createPrimaryUserIdOrLinkAccounts({ + tenantId, + user, + userContext, + }); + } else { + logDebugMessage("createPrimaryUserIdOrLinkAccounts got linking candidate"); + if (primaryUser.id === user.id) { + logDebugMessage("createPrimaryUserIdOrLinkAccounts user already linked"); + // This can only happen cause of a race condition cause we already check + // if the input recipeUserId is a primary user early on in the function. + return user; + } + // this means that we found a primary user ID which can be linked to this recipe user ID. So we try and link them. + + // we can use the 0 index cause this user is + // not a primary user. + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + user.loginMethods[0], + primaryUser, + tenantId, + userContext + ); + + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logDebugMessage( + "createPrimaryUserIdOrLinkAccounts not linking because shouldAutomaticallyLink is false" + ); + return user; + } + + if (shouldDoAccountLinking.shouldRequireVerification && !user.loginMethods[0].verified) { + logDebugMessage( + "createPrimaryUserIdOrLinkAccounts not linking because shouldRequireVerification is true but the login method is not verified" + ); + return user; + } + + logDebugMessage("createPrimaryUserIdOrLinkAccounts linking"); + let linkAccountsResult = await this.recipeInterfaceImpl.linkAccounts({ + recipeUserId: user.loginMethods[0].recipeUserId, + primaryUserId: primaryUser.id, + userContext, + }); + + if (linkAccountsResult.status === "OK") { + logDebugMessage("createPrimaryUserIdOrLinkAccounts successfully linked"); + return linkAccountsResult.user; + } else if ( + linkAccountsResult.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + // this can happen cause of a race condition + // wherein the recipe user ID get's linked to + // some other primary user whilst this function is running. + // But this is OK, we can just return the primary user it is linked to + logDebugMessage("createPrimaryUserIdOrLinkAccounts already linked to another user"); + return linkAccountsResult.user; + } else if (linkAccountsResult.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + logDebugMessage("createPrimaryUserIdOrLinkAccounts linking failed because of a race condition"); + // this can be possible during a race condition wherein the primary user + // that we fetched somehow is no more a primary user. This can happen if + // the unlink function was called in parallel on that user. So we can just retry + return await this.createPrimaryUserIdOrLinkAccounts({ + tenantId, + user, + userContext, + }); + } else { + logDebugMessage("createPrimaryUserIdOrLinkAccounts linking failed because of a race condition"); + // status is "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + // it can come here if the recipe user ID + // can't be linked to the primary user ID cause + // the email / phone number is associated with some other primary user ID. + // This can happen due to a race condition in which + // the email has changed from one primary user to another during this function call, + // or it can happen if the accounts to link table + // contains a user which this is supposed to + // be linked to, but we can't link it cause during the time the user was added to that table and now, some other primary user has + // got this email. + + // So we try again, but without caring about + // the accounts to link table (cause they we will end up in an infinite recursion). + return await this.createPrimaryUserIdOrLinkAccounts({ + tenantId, + user, + userContext, + }); + } + } + }; + + getPrimaryUserThatCanBeLinkedToRecipeUserId = async ({ + tenantId, + user, + userContext, + }: { + tenantId: string; + user: User; + userContext: any; + }): Promise => { + // first we check if this user itself is a + // primary user or not. If it is, we return that. + if (user.isPrimaryUser) { + return user; + } + + // finally, we try and find a primary user based on + // the email / phone number / third party ID. + let users = await this.recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo: user.loginMethods[0], + doUnionOfAccountInfo: true, + userContext, + }); + logDebugMessage(`getPrimaryUserThatCanBeLinkedToRecipeUserId found ${users.length} matching users`); + let pUsers = users.filter((u) => u.isPrimaryUser); + logDebugMessage(`getPrimaryUserThatCanBeLinkedToRecipeUserId found ${pUsers.length} matching primary users`); + if (pUsers.length > 1) { + // this means that the new user has account info such that it's + // spread across multiple primary user IDs. In this case, even + // if we return one of them, it won't be able to be linked anyway + // cause if we did, it would mean 2 primary users would have the + // same account info. So we return undefined + + /** + * this being said, with the current set of auth recipes, it should + * never come here - cause: + * ----> If the recipeuserid is a passwordless user, then it can have either a phone + * email or both. If it has just one of them, then anyway 2 primary users can't + * exist with the same phone number / email. If it has both, then the only way + * that it can have multiple primary users returned is if there is another passwordless + * primary user with the same phone number - which is not possible, cause phone + * numbers are unique across passwordless users. + * + * ----> If the input is a third party user, then it has third party info and an email. Now there can be able to primary user with the same email, but + * there can't be another thirdparty user with the same third party info (since that is unique). + * Nor can there an email password primary user with the same email along with another + * thirdparty primary user with the same email (since emails can't be the same across primary users). + * + * ----> If the input is an email password user, then it has an email. There can't be multiple primary users with the same email anyway. + */ + throw new Error("You found a bug. Please report it on github.com/supertokens/supertokens-node"); + } + return pUsers.length === 0 ? undefined : pUsers[0]; + }; + + isSignInAllowed = async ({ + user, + tenantId, + userContext, + }: { + user: User; + tenantId: string; + userContext: any; + }): Promise => { + ProcessState.getInstance().addState(PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED); + + if (user.isPrimaryUser || user.loginMethods[0].verified) { + return true; + } + + return this.isSignInUpAllowedHelper({ + accountInfo: user.loginMethods[0], + isVerified: user.loginMethods[0].verified, + tenantId, + isSignIn: true, + userContext, + }); + }; + + isSignUpAllowed = async ({ + newUser, + isVerified, + tenantId, + userContext, + }: { + newUser: AccountInfoWithRecipeId; + isVerified: boolean; + tenantId: string; + userContext: any; + }): Promise => { + ProcessState.getInstance().addState(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED); + if (newUser.email !== undefined && newUser.phoneNumber !== undefined) { + // we do this check cause below when we call listUsersByAccountInfo, + // we only pass in one of email or phone number + throw new Error("Please pass one of email or phone number, not both"); + } + + return this.isSignInUpAllowedHelper({ + accountInfo: newUser, + isVerified, + tenantId, + userContext, + isSignIn: false, + }); + }; + + isSignInUpAllowedHelper = async ({ + accountInfo, + isVerified, + tenantId, + isSignIn, + userContext, + }: { + accountInfo: AccountInfoWithRecipeId | LoginMethod; + isVerified: boolean; + tenantId: string; + isSignIn: boolean; + userContext: any; + }): Promise => { + ProcessState.getInstance().addState(PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED); + // since this is a recipe level user, we have to do the following checks + // before allowing sign in. We do these checks cause sign in also attempts + // account linking: + // - If there is no primary user for this user's account info, then + // we check if any recipe user exist with the same info and it's not verified. If we + // find one, we disallow signing in cause when this user becomes a primary user, + // it may cause that other recipe user to be linked to this and if that recipe user + // is owned by an attacker, it will lead to an account take over case + // - If there exists another primary user, and if this user is not verified, we will + // disallow cause if after sign in, this user sends an email verification email + // to the email, then the primary user may click on it by mistake and get their account + // taken over. + // - If there exists another primary user, and that user's email is not verified, + // then we disallow sign in cause that primary user may be owned by an attacker + // and after this email is verified, it will link to that account causing account + // takeover. + + // we find other accounts based on the email / phone number. + // we do not pass in third party info, or both email or phone + // cause we want to guarantee that the output array contains just one + // primary user. + let users = await this.recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo, + doUnionOfAccountInfo: true, + userContext, + }); + if (users.length === 0) { + logDebugMessage("isSignInUpAllowedHelper returning true because no user with given account info"); + // this is a brand new email / phone number, so we allow sign up. + return true; + } + + if (users.length === 1 && isSignIn) { + logDebugMessage( + "isSignInUpAllowedHelper returning true because this is sign in and there is only a single user with the given account info" + ); + return true; + } + // now we check if there exists some primary user with the same email / phone number + // such that that info is not verified for that account. In this case, we do not allow + // sign up cause we cannot link this new account to that primary account yet (since + // the email / phone is unverified - this is to prevent an attack where an attacker + // might have access to the unverified account's primary user and we do not want to + // link this account to that one), and we can't make this a primary user either (since + // then there would be two primary users with the same email / phone number - which is + // not allowed..) + const primaryUsers = users.filter((u) => u.isPrimaryUser); + if (primaryUsers.length === 0) { + logDebugMessage("isSignInUpAllowedHelper no primary user exists"); + // since there is no primary user, it means that this user, if signed up, will end up + // being the primary user. In this case, we check if any of the non primary user's + // are in an unverified state having the same account info, and if they are, then we + // disallow this sign up, cause if the user becomes the primary user, and then the other + // account which is unverified sends an email verification email, the legit user might + // click on the link and that account (which was unverified and could have been controlled + // by an attacker), will end up getting linked to this account. + + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + accountInfo, + undefined, + tenantId, + userContext + ); + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logDebugMessage("isSignInUpAllowedHelper returning true because account linking is disabled"); + return true; + } + if (!shouldDoAccountLinking.shouldRequireVerification) { + logDebugMessage( + "isSignInUpAllowedHelper returning true because dec does not require email verification" + ); + // the dev says they do not care about verification before account linking + // so we are OK with the risk mentioned above. + return true; + } + + let shouldAllow = true; + for (let i = 0; i < users.length; i++) { + let currUser = users[i]; // all these are not primary users, so we can use + // loginMethods[0] to get the account info. + let thisIterationIsVerified = false; + if (accountInfo.email !== undefined) { + if ( + currUser.loginMethods[0].hasSameEmailAs(accountInfo.email) && + currUser.loginMethods[0].verified + ) { + logDebugMessage("isSignInUpAllowedHelper found same email for another user and verified"); + thisIterationIsVerified = true; + } + } + + if (accountInfo.phoneNumber !== undefined) { + if ( + currUser.loginMethods[0].hasSamePhoneNumberAs(accountInfo.phoneNumber) && + currUser.loginMethods[0].verified + ) { + logDebugMessage( + "isSignInUpAllowedHelper found same phone number for another user and verified" + ); + thisIterationIsVerified = true; + } + } + if (!thisIterationIsVerified) { + // even if one of the users is not verified, we do not allow sign up (see why above). + // Sure, this allows attackers to create email password accounts with an email + // to block actual users from signing up, but that's ok, since those + // users will just see an email already exists error and then will try another + // login method. They can also still just go through the password reset flow + // and then gain access to their email password account (which can then be verified). + logDebugMessage( + "isSignInUpAllowedHelper returning false cause one of the other recipe level users is not verified" + ); + shouldAllow = false; + break; + } + } + ProcessState.getInstance().addState(PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS); + logDebugMessage("isSignInUpAllowedHelper returning " + shouldAllow); + return shouldAllow; + } else { + if (primaryUsers.length > 1) { + throw new Error( + "You have found a bug. Please report to https://github.com/supertokens/supertokens-node/issues" + ); + } + let primaryUser = primaryUsers[0]; + logDebugMessage("isSignInUpAllowedHelper primary user found"); + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + accountInfo, + primaryUser, + tenantId, + userContext + ); + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logDebugMessage("isSignInUpAllowedHelper returning true because account linking is disabled"); + return true; + } + if (!shouldDoAccountLinking.shouldRequireVerification) { + logDebugMessage( + "isSignInUpAllowedHelper returning true because dec does not require email verification" + ); + // the dev says they do not care about verification before account linking + // so we can link this new user to the primary user post recipe user creation + // even if that user's email / phone number is not verified. + return true; + } + + if (!isVerified) { + logDebugMessage( + "isSignInUpAllowedHelper returning false because new user's email is not verified, and primary user with the same email was found." + ); + // this will exist early with a false here cause it means that + // if we come here, the newUser will be linked to the primary user post email + // verification. Whilst this seems OK, there is a risk that the actual user might + // click on the email verification link thinking that they it's for their existing + // (legit) account, and then the attacker (who signed up with email password maybe) + // will have access to the account - cause email verification will cause account linking. + + // We do this AFTER calling shouldDoAutomaticAccountLinking cause + // in case email verification is not required, then linking should not be + // an issue anyway. + return false; + } + + // we check for even if one is verified as opposed to all being unverified cause + // even if one is verified, we know that the email / phone number is owned by the + // primary account holder. + + // we only check this for primary user and not other users in the users array cause + // they are all recipe users. The reason why we ignore them is cause, in normal + // situations, they should not exist cause: + // - if primary user was created first, then the recipe user creation would not + // be allowed via unverified means of login method (like email password). + // - if recipe user was created first, and is unverified, then the primary user + // sign up should not be possible either. + for (let i = 0; i < primaryUser.loginMethods.length; i++) { + let lM = primaryUser.loginMethods[i]; + if (lM.email !== undefined) { + if (lM.hasSameEmailAs(accountInfo.email) && lM.verified) { + logDebugMessage( + "isSignInUpAllowedHelper returning true cause found same email for primary user and verified" + ); + return true; + } + } + + if (lM.phoneNumber !== undefined) { + if (lM.hasSamePhoneNumberAs(accountInfo.phoneNumber) && lM.verified) { + logDebugMessage( + "isSignInUpAllowedHelper returning true cause found same phone number for primary user and verified" + ); + return true; + } + } + } + logDebugMessage( + "isSignInUpAllowedHelper returning false cause primary user does not have the same email or phone number that is verified" + //"isSignInUpAllowedHelper returning false because new user's email is not verified, and primary user with the same email was found." + ); + return false; + } + }; + + isEmailChangeAllowed = async (input: { + user?: User; + newEmail: string; + isVerified: boolean; + userContext: any; + }): Promise => { + /** + * The purpose of this function is to check that if a recipe user ID's email + * can be changed or not. There are two conditions for when it can't be changed: + * - If the recipe user is a primary user, then we need to check that the new email + * doesn't belong to any other primary user. If it does, we disallow the change + * since multiple primary user's can't have the same account info. + * + * - If the recipe user is NOT a primary user, and if isVerified is false, then + * we check if there exists a primary user with the same email, and if it does + * we disallow the email change cause if this email is changed, and an email + * verification email is sent, then the primary user may end up clicking + * on the link by mistake, causing account linking to happen which can result + * in account take over if this recipe user is malicious. + */ + + let user = input.user; + + if (user === undefined) { + throw new Error("Passed in recipe user id does not exist"); + } + + for (const tenantId of user.tenantIds) { + let existingUsersWithNewEmail = await this.recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId: user.tenantIds[0], + accountInfo: { + email: input.newEmail, + }, + doUnionOfAccountInfo: false, + userContext: input.userContext, + }); + + let primaryUserForNewEmail = existingUsersWithNewEmail.filter((u) => u.isPrimaryUser); + if (primaryUserForNewEmail.length > 1) { + throw new Error("You found a bug. Please report it on github.com/supertokens/supertokens-node"); + } + + if (user.isPrimaryUser) { + // this is condition one from the above comment. + if (primaryUserForNewEmail.length === 1 && primaryUserForNewEmail[0].id !== user.id) { + logDebugMessage( + "isEmailChangeAllowed: returning false cause email change will lead to two primary users having same email" + ); + return false; + } + logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is primary and new email doesn't belong to any other primary user` + ); + continue; + } else { + if (input.isVerified) { + logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary and new email is verified` + ); + continue; + } + + if (user.loginMethods[0].email === input.newEmail) { + logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary and new email is same as the older one` + ); + continue; + } + + if (primaryUserForNewEmail.length === 1) { + let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + user.loginMethods[0], + primaryUserForNewEmail[0], + tenantId, + input.userContext + ); + + if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary there exists a primary user exists with the new email, but the dev does not have account linking enabled.` + ); + continue; + } + + if (!shouldDoAccountLinking.shouldRequireVerification) { + logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary there exists a primary user exists with the new email, but the dev does not require email verification.` + ); + continue; + } + + logDebugMessage( + "isEmailChangeAllowed: returning false cause input user is not a primary there exists a primary user exists with the new email." + ); + return false; + } + + logDebugMessage( + `isEmailChangeAllowed: can change on ${tenantId} cause input user is not a primary no primary user exists with the new email` + ); + continue; + } + } + logDebugMessage( + "isEmailChangeAllowed: returning true cause email change can happen on all tenants the user is part of" + ); + return true; + }; + + verifyEmailForRecipeUserIfLinkedAccountsAreVerified = async (input: { + user: User; + recipeUserId: RecipeUserId; + userContext: any; + }) => { + try { + EmailVerificationRecipe.getInstanceOrThrowError(); + } catch (ignored) { + // if email verification recipe is not initialized, we do a no-op + return; + } + // This is just a helper function cause it's called in many places + // like during sign up, sign in and post linking accounts. + // This is not exposed to the developer as it's called in the relevant + // recipe functions. + // We do not do this in the core cause email verification is a different + // recipe. + // Finally, we only mark the email of this recipe user as verified and not + // the other recipe users in the primary user (if this user's email is verified), + // cause when those other users sign in, this function will be called for them anyway + if (input.user.isPrimaryUser) { + let recipeUserEmail: string | undefined = undefined; + let isAlreadyVerified = false; + input.user.loginMethods.forEach((lm) => { + if (lm.recipeUserId.getAsString() === input.recipeUserId.getAsString()) { + recipeUserEmail = lm.email; + isAlreadyVerified = lm.verified; + } + }); + + if (recipeUserEmail !== undefined) { + if (isAlreadyVerified) { + return; + } + let shouldVerifyEmail = false; + input.user.loginMethods.forEach((lm) => { + if (lm.hasSameEmailAs(recipeUserEmail) && lm.verified) { + shouldVerifyEmail = true; + } + }); + + if (shouldVerifyEmail) { + let resp = await EmailVerificationRecipe.getInstanceOrThrowError().recipeInterfaceImpl.createEmailVerificationToken( + { + // While the token we create here is tenant specific, the verification status is not + // So we can use any tenantId the user is associated with here as long as we use the + // same in the verifyEmailUsingToken call + tenantId: input.user.tenantIds[0], + recipeUserId: input.recipeUserId, + email: recipeUserEmail, + userContext: input.userContext, + } + ); + if (resp.status === "OK") { + // we purposely pass in false below cause we don't want account + // linking to happen + await EmailVerificationRecipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken( + { + // See comment about tenantId in the createEmailVerificationToken params + tenantId: input.user.tenantIds[0], + token: resp.token, + attemptAccountLinking: false, + userContext: input.userContext, + } + ); + } + } + } + } + }; +} diff --git a/lib/ts/recipe/accountlinking/recipeImplementation.ts b/lib/ts/recipe/accountlinking/recipeImplementation.ts new file mode 100644 index 000000000..75cdefb25 --- /dev/null +++ b/lib/ts/recipe/accountlinking/recipeImplementation.ts @@ -0,0 +1,308 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { AccountInfo, RecipeInterface, TypeNormalisedInput } from "./types"; +import { Querier } from "../../querier"; +import NormalisedURLPath from "../../normalisedURLPath"; +import RecipeUserId from "../../recipeUserId"; +import type AccountLinkingRecipe from "./recipe"; +import { User } from "../../user"; +import type { User as UserType } from "../../types"; + +export default function getRecipeImplementation( + querier: Querier, + config: TypeNormalisedInput, + recipeInstance: AccountLinkingRecipe +): RecipeInterface { + return { + getUsers: async function ( + this: RecipeInterface, + { + timeJoinedOrder, + limit, + paginationToken, + includeRecipeIds, + query, + }: { + timeJoinedOrder: "ASC" | "DESC"; + limit?: number; + paginationToken?: string; + includeRecipeIds?: string[]; + query?: { [key: string]: string }; + } + ): Promise<{ + users: UserType[]; + nextPaginationToken?: string; + }> { + let includeRecipeIdsStr = undefined; + if (includeRecipeIds !== undefined) { + includeRecipeIdsStr = includeRecipeIds.join(","); + } + let response = await querier.sendGetRequest(new NormalisedURLPath("/users"), { + includeRecipeIds: includeRecipeIdsStr, + timeJoinedOrder: timeJoinedOrder, + limit: limit, + paginationToken: paginationToken, + ...query, + }); + return { + users: response.users.map((u: any) => new User(u)), + nextPaginationToken: response.nextPaginationToken, + }; + }, + canCreatePrimaryUser: async function ( + this: RecipeInterface, + { + recipeUserId, + }: { + recipeUserId: RecipeUserId; + } + ): Promise< + | { + status: "OK"; + wasAlreadyAPrimaryUser: boolean; + } + | { + status: + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + > { + return await querier.sendGetRequest(new NormalisedURLPath("/recipe/accountlinking/user/primary/check"), { + recipeUserId: recipeUserId.getAsString(), + }); + }, + + createPrimaryUser: async function ( + this: RecipeInterface, + { + recipeUserId, + }: { + recipeUserId: RecipeUserId; + userContext: any; + } + ): Promise< + | { + status: "OK"; + user: User; + wasAlreadyAPrimaryUser: boolean; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + > { + let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/accountlinking/user/primary"), { + recipeUserId: recipeUserId.getAsString(), + }); + if (response.status === "OK") { + response.user = new User(response.user); + } + return response; + }, + + canLinkAccounts: async function ( + this: RecipeInterface, + { + recipeUserId, + primaryUserId, + }: { + recipeUserId: RecipeUserId; + primaryUserId: string; + } + ): Promise< + | { + status: "OK"; + accountsAlreadyLinked: boolean; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + description: string; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + | { + status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; + } + > { + let result = await querier.sendGetRequest(new NormalisedURLPath("/recipe/accountlinking/user/link/check"), { + recipeUserId: recipeUserId.getAsString(), + primaryUserId, + }); + + return result; + }, + + linkAccounts: async function ( + this: RecipeInterface, + { + recipeUserId, + primaryUserId, + userContext, + }: { + recipeUserId: RecipeUserId; + primaryUserId: string; + userContext: any; + } + ): Promise< + | { + status: "OK"; + accountsAlreadyLinked: boolean; + user: User; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + user: User; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + | { + status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; + } + > { + const accountsLinkingResult = await querier.sendPostRequest( + new NormalisedURLPath("/recipe/accountlinking/user/link"), + { + recipeUserId: recipeUserId.getAsString(), + primaryUserId, + } + ); + + if ( + ["OK", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"].includes( + accountsLinkingResult.status + ) + ) { + accountsLinkingResult.user = new User(accountsLinkingResult.user); + } + + if (accountsLinkingResult.status === "OK") { + let user: UserType = accountsLinkingResult.user; + if (!accountsLinkingResult.accountsAlreadyLinked) { + await recipeInstance.verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: user, + recipeUserId, + userContext, + }); + + const updatedUser = await this.getUser({ + userId: primaryUserId, + userContext, + }); + if (updatedUser === undefined) { + throw Error("this error should never be thrown"); + } + user = updatedUser; + let loginMethodInfo = user.loginMethods.find( + (u) => u.recipeUserId.getAsString() === recipeUserId.getAsString() + ); + if (loginMethodInfo === undefined) { + throw Error("this error should never be thrown"); + } + + await config.onAccountLinked(user, loginMethodInfo, userContext); + } + accountsLinkingResult.user = user; + } + + return accountsLinkingResult; + }, + + unlinkAccount: async function ( + this: RecipeInterface, + { + recipeUserId, + }: { + recipeUserId: RecipeUserId; + } + ): Promise<{ + status: "OK"; + wasRecipeUserDeleted: boolean; + wasLinked: boolean; + }> { + let accountsUnlinkingResult = await querier.sendPostRequest( + new NormalisedURLPath("/recipe/accountlinking/user/unlink"), + { + recipeUserId: recipeUserId.getAsString(), + } + ); + return accountsUnlinkingResult; + }, + + getUser: async function (this: RecipeInterface, { userId }: { userId: string }): Promise { + let result = await querier.sendGetRequest(new NormalisedURLPath("/user/id"), { + userId, + }); + if (result.status === "OK") { + return new User(result.user); + } + return undefined; + }, + + listUsersByAccountInfo: async function ( + this: RecipeInterface, + { + tenantId, + accountInfo, + doUnionOfAccountInfo, + }: { tenantId: string; accountInfo: AccountInfo; doUnionOfAccountInfo: boolean } + ): Promise { + let result = await querier.sendGetRequest( + new NormalisedURLPath(`${tenantId ?? "public"}/users/by-accountinfo`), + { + email: accountInfo.email, + phoneNumber: accountInfo.phoneNumber, + thirdPartyId: accountInfo.thirdParty?.id, + thirdPartyUserId: accountInfo.thirdParty?.userId, + doUnionOfAccountInfo, + } + ); + return result.users.map((u: any) => new User(u)); + }, + + deleteUser: async function ( + this: RecipeInterface, + { + userId, + removeAllLinkedAccounts, + }: { + userId: string; + removeAllLinkedAccounts: boolean; + } + ): Promise<{ + status: "OK"; + }> { + return await querier.sendPostRequest(new NormalisedURLPath("/user/remove"), { + userId, + removeAllLinkedAccounts, + }); + }, + }; +} diff --git a/lib/ts/recipe/accountlinking/types.ts b/lib/ts/recipe/accountlinking/types.ts new file mode 100644 index 000000000..e52d83cb7 --- /dev/null +++ b/lib/ts/recipe/accountlinking/types.ts @@ -0,0 +1,201 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from "supertokens-js-override"; +import type { User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; + +export type TypeInput = { + onAccountLinked?: (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => Promise; + shouldDoAutomaticAccountLinking?: ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + tenantId: string, + userContext: any + ) => Promise< + | { + shouldAutomaticallyLink: false; + } + | { + shouldAutomaticallyLink: true; + shouldRequireVerification: boolean; + } + >; + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + }; +}; + +export type TypeNormalisedInput = { + onAccountLinked: (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => Promise; + shouldDoAutomaticAccountLinking: ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + tenantId: string, + userContext: any + ) => Promise< + | { + shouldAutomaticallyLink: false; + } + | { + shouldAutomaticallyLink: true; + shouldRequireVerification: boolean; + } + >; + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + }; +}; + +export type RecipeInterface = { + getUsers: (input: { + timeJoinedOrder: "ASC" | "DESC"; + limit?: number; + paginationToken?: string; + includeRecipeIds?: string[]; + query?: { [key: string]: string }; + userContext: any; + }) => Promise<{ + users: User[]; + nextPaginationToken?: string; + }>; + canCreatePrimaryUser: (input: { + recipeUserId: RecipeUserId; + userContext: any; + }) => Promise< + | { + status: "OK"; + wasAlreadyAPrimaryUser: boolean; + } + | { + status: + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + >; + createPrimaryUser: (input: { + recipeUserId: RecipeUserId; + userContext: any; + }) => Promise< + | { + status: "OK"; + user: User; + wasAlreadyAPrimaryUser: boolean; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + >; + canLinkAccounts: (input: { + recipeUserId: RecipeUserId; + primaryUserId: string; + userContext: any; + }) => Promise< + | { + status: "OK"; + accountsAlreadyLinked: boolean; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + description: string; + primaryUserId: string; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + | { + status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; + } + >; + linkAccounts: (input: { + recipeUserId: RecipeUserId; + primaryUserId: string; + userContext: any; + }) => Promise< + | { + status: "OK"; + accountsAlreadyLinked: boolean; + user: User; + } + | { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + user: User; + } + | { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + primaryUserId: string; + description: string; + } + | { + status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; + } + >; + unlinkAccount: (input: { + recipeUserId: RecipeUserId; + userContext: any; + }) => Promise<{ + status: "OK"; + wasRecipeUserDeleted: boolean; + wasLinked: boolean; + }>; + getUser: (input: { userId: string; userContext: any }) => Promise; + listUsersByAccountInfo: (input: { + tenantId: string; + accountInfo: AccountInfo; + doUnionOfAccountInfo: boolean; + userContext: any; + }) => Promise; + deleteUser: (input: { + userId: string; + removeAllLinkedAccounts: boolean; + userContext: any; + }) => Promise<{ status: "OK" }>; +}; + +export type AccountInfo = { + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; +}; + +export type AccountInfoWithRecipeId = { + recipeId: "emailpassword" | "thirdparty" | "passwordless"; +} & AccountInfo; + +export type RecipeLevelUser = { + tenantIds: string[]; + timeJoined: number; + recipeUserId: RecipeUserId; +} & AccountInfoWithRecipeId; diff --git a/lib/ts/recipe/accountlinking/utils.ts b/lib/ts/recipe/accountlinking/utils.ts new file mode 100644 index 000000000..c4c819563 --- /dev/null +++ b/lib/ts/recipe/accountlinking/utils.ts @@ -0,0 +1,44 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { NormalisedAppinfo } from "../../types"; +import type { TypeInput, RecipeInterface, TypeNormalisedInput } from "./types"; + +async function defaultOnAccountLinked() {} + +async function defaultShouldDoAutomaticAccountLinking(): Promise<{ + shouldAutomaticallyLink: false; +}> { + return { + shouldAutomaticallyLink: false, + }; +} + +export function validateAndNormaliseUserInput(_: NormalisedAppinfo, config?: TypeInput): TypeNormalisedInput { + let onAccountLinked = config?.onAccountLinked || defaultOnAccountLinked; + let shouldDoAutomaticAccountLinking = + config?.shouldDoAutomaticAccountLinking || defaultShouldDoAutomaticAccountLinking; + + let override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + ...config?.override, + }; + + return { + override, + onAccountLinked, + shouldDoAutomaticAccountLinking, + }; +} diff --git a/lib/ts/recipe/dashboard/api/userdetails/userDelete.ts b/lib/ts/recipe/dashboard/api/userdetails/userDelete.ts index 65de7236c..10788198a 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userDelete.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userDelete.ts @@ -1,6 +1,6 @@ -import SuperTokens from "../../../../supertokens"; import { APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; +import { deleteUser } from "../../../.."; type Response = { status: "OK"; @@ -8,17 +8,21 @@ type Response = { export const userDelete = async (_: APIInterface, ___: string, options: APIOptions, __: any): Promise => { const userId = options.req.getKeyValueFromQuery("userId"); + let removeAllLinkedAccountsQueryValue = options.req.getKeyValueFromQuery("removeAllLinkedAccounts"); + if (removeAllLinkedAccountsQueryValue !== undefined) { + removeAllLinkedAccountsQueryValue = removeAllLinkedAccountsQueryValue.trim().toLowerCase(); + } + const removeAllLinkedAccounts = + removeAllLinkedAccountsQueryValue === undefined ? undefined : removeAllLinkedAccountsQueryValue === "true"; - if (userId === undefined) { + if (userId === undefined || userId === "") { throw new STError({ message: "Missing required parameter 'userId'", type: STError.BAD_INPUT_ERROR, }); } - await SuperTokens.getInstanceOrThrowError().deleteUser({ - userId, - }); + await deleteUser(userId, removeAllLinkedAccounts); return { status: "OK", diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts index dc81a24be..3148ee0df 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts @@ -1,7 +1,8 @@ import { APIFunction, APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; -import EmailVerificationRecipe from "../../../emailverification/recipe"; import EmailVerification from "../../../emailverification"; +import EmailVerificationRecipe from "../../../emailverification/recipe"; +import RecipeUserId from "../../../../recipeUserId"; type Response = | { @@ -12,18 +13,18 @@ type Response = status: "FEATURE_NOT_ENABLED_ERROR"; }; -export const userEmailverifyGet: APIFunction = async ( +export const userEmailVerifyGet: APIFunction = async ( _: APIInterface, ___: string, options: APIOptions, userContext: any ): Promise => { const req = options.req; - const userId = req.getKeyValueFromQuery("userId"); + const recipeUserId = req.getKeyValueFromQuery("recipeUserId"); - if (userId === undefined) { + if (recipeUserId === undefined) { throw new STError({ - message: "Missing required parameter 'userId'", + message: "Missing required parameter 'recipeUserId'", type: STError.BAD_INPUT_ERROR, }); } @@ -36,7 +37,7 @@ export const userEmailverifyGet: APIFunction = async ( }; } - const response = await EmailVerification.isEmailVerified(userId, undefined, userContext); + const response = await EmailVerification.isEmailVerified(new RecipeUserId(recipeUserId), undefined, userContext); return { status: "OK", isVerified: response, diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts index 324e9050a..d2e26ee4a 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts @@ -1,6 +1,7 @@ import { APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; import EmailVerification from "../../../emailverification"; +import RecipeUserId from "../../../../recipeUserId"; type Response = { status: "OK"; @@ -13,12 +14,12 @@ export const userEmailVerifyPut = async ( userContext: any ): Promise => { const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; + const recipeUserId = requestBody.recipeUserId; const verified = requestBody.verified; - if (userId === undefined || typeof userId !== "string") { + if (recipeUserId === undefined || typeof recipeUserId !== "string") { throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", + message: "Required parameter 'recipeUserId' is missing or has an invalid type", type: STError.BAD_INPUT_ERROR, }); } @@ -33,7 +34,7 @@ export const userEmailVerifyPut = async ( if (verified) { const tokenResponse = await EmailVerification.createEmailVerificationToken( tenantId, - userId, + new RecipeUserId(recipeUserId), undefined, userContext ); @@ -55,7 +56,7 @@ export const userEmailVerifyPut = async ( throw new Error("Should not come here"); } } else { - await EmailVerification.unverifyEmail(userId, undefined, userContext); + await EmailVerification.unverifyEmail(new RecipeUserId(recipeUserId), undefined, userContext); } return { diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts index d4bebf41f..e7197d854 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts @@ -1,6 +1,7 @@ import { APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; import EmailVerification from "../../../emailverification"; +import { convertToRecipeUserId, getUser } from "../../../.."; type Response = { status: "OK" | "EMAIL_ALREADY_VERIFIED_ERROR"; @@ -13,14 +14,28 @@ export const userEmailVerifyTokenPost = async ( userContext: any ): Promise => { const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; + const recipeUserId = requestBody.recipeUserId; - if (userId === undefined || typeof userId !== "string") { + if (recipeUserId === undefined || typeof recipeUserId !== "string") { throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", + message: "Required parameter 'recipeUserId' is missing or has an invalid type", type: STError.BAD_INPUT_ERROR, }); } + const user = await getUser(recipeUserId, userContext); - return await EmailVerification.sendEmailVerificationEmail(tenantId, userId, undefined, userContext); + if (!user) { + throw new STError({ + message: "Unknown 'recipeUserId'", + type: STError.BAD_INPUT_ERROR, + }); + } + + return await EmailVerification.sendEmailVerificationEmail( + tenantId, + user.id, + convertToRecipeUserId(recipeUserId), + undefined, + userContext + ); }; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userGet.ts index 98065350c..60ef9fbe3 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userGet.ts @@ -1,37 +1,17 @@ -import { - APIFunction, - APIInterface, - APIOptions, - EmailPasswordUser, - PasswordlessUser, - ThirdPartyUser, -} from "../../types"; +import { APIFunction, APIInterface, APIOptions, UserWithFirstAndLastName } from "../../types"; import STError from "../../../../error"; -import { getUserForRecipeId, isRecipeInitialised, isValidRecipeId } from "../../utils"; import UserMetaDataRecipe from "../../../usermetadata/recipe"; import UserMetaData from "../../../usermetadata"; +import { getUser } from "../../../.."; +import { User } from "../../../../types"; type Response = | { status: "NO_USER_FOUND_ERROR"; } - | { - status: "RECIPE_NOT_INITIALISED"; - } | { status: "OK"; - recipeId: "emailpassword"; - user: EmailPasswordUser; - } - | { - status: "OK"; - recipeId: "thirdparty"; - user: ThirdPartyUser; - } - | { - status: "OK"; - recipeId: "passwordless"; - user: PasswordlessUser; + user: UserWithFirstAndLastName; }; export const userGet: APIFunction = async ( @@ -41,38 +21,15 @@ export const userGet: APIFunction = async ( userContext: any ): Promise => { const userId = options.req.getKeyValueFromQuery("userId"); - const recipeId = options.req.getKeyValueFromQuery("recipeId"); - if (userId === undefined) { + if (userId === undefined || userId === "") { throw new STError({ message: "Missing required parameter 'userId'", type: STError.BAD_INPUT_ERROR, }); } - if (recipeId === undefined) { - throw new STError({ - message: "Missing required parameter 'recipeId'", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (!isValidRecipeId(recipeId)) { - throw new STError({ - message: "Invalid recipe id", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (!isRecipeInitialised(recipeId)) { - return { - status: "RECIPE_NOT_INITIALISED", - }; - } - - let user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined = ( - await getUserForRecipeId(userId, recipeId) - ).user; + let user: User | undefined = await getUser(userId, userContext); if (user === undefined) { return { @@ -83,31 +40,25 @@ export const userGet: APIFunction = async ( try { UserMetaDataRecipe.getInstanceOrThrowError(); } catch (_) { - user = { - ...user, - firstName: "FEATURE_NOT_ENABLED", - lastName: "FEATURE_NOT_ENABLED", - }; - return { status: "OK", - recipeId: recipeId as any, - user, + user: { + ...user.toJson(), + firstName: "FEATURE_NOT_ENABLED", + lastName: "FEATURE_NOT_ENABLED", + }, }; } const userMetaData = await UserMetaData.getUserMetadata(userId, userContext); const { first_name, last_name } = userMetaData.metadata; - user = { - ...user, - firstName: first_name === undefined ? "" : first_name, - lastName: last_name === undefined ? "" : last_name, - }; - return { status: "OK", - recipeId: recipeId as any, - user, + user: { + ...user.toJson(), + firstName: first_name === undefined ? "" : first_name, + lastName: last_name === undefined ? "" : last_name, + }, }; }; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts index 4dd1016c7..3ffa684bc 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts @@ -20,7 +20,7 @@ export const userMetaDataGet: APIFunction = async ( ): Promise => { const userId = options.req.getKeyValueFromQuery("userId"); - if (userId === undefined) { + if (userId === undefined || userId === "") { throw new STError({ message: "Missing required parameter 'userId'", type: STError.BAD_INPUT_ERROR, diff --git a/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts index 9f2d2cfed..fb2dae948 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts @@ -4,7 +4,7 @@ import EmailPasswordRecipe from "../../../emailpassword/recipe"; import EmailPassword from "../../../emailpassword"; import ThirdPartyEmailPasswordRecipe from "../../../thirdpartyemailpassword/recipe"; import ThirdPartyEmailPassword from "../../../thirdpartyemailpassword"; -import { FORM_FIELD_PASSWORD_ID } from "../../../emailpassword/constants"; +import RecipeUserId from "../../../../recipeUserId"; type Response = | { @@ -22,12 +22,12 @@ export const userPasswordPut = async ( userContext: any ): Promise => { const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; + const recipeUserId = requestBody.recipeUserId; const newPassword = requestBody.newPassword; - if (userId === undefined || typeof userId !== "string") { + if (recipeUserId === undefined || typeof recipeUserId !== "string") { throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", + message: "Required parameter 'recipeUserId' is missing or has an invalid type", type: STError.BAD_INPUT_ERROR, }); } @@ -59,73 +59,51 @@ export const userPasswordPut = async ( } if (recipeToUse === "emailpassword") { - let passwordFormFields = EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( - (field) => field.id === FORM_FIELD_PASSWORD_ID - ); - - let passwordValidationError = await passwordFormFields[0].validate(newPassword, tenantId); + const updateResponse = await EmailPassword.updateEmailOrPassword({ + recipeUserId: new RecipeUserId(recipeUserId), + password: newPassword, + tenantIdForPasswordPolicy: tenantId, + userContext, + }); - if (passwordValidationError !== undefined) { + if ( + updateResponse.status === "UNKNOWN_USER_ID_ERROR" || + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + // Techincally it can but its an edge case so we assume that it wont + throw new Error("Should never come here"); + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { return { status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, + error: updateResponse.failureReason, }; } - - const passwordResetToken = await EmailPassword.createResetPasswordToken(tenantId, userId, userContext); - - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { - // Techincally it can but its an edge case so we assume that it wont - throw new Error("Should never come here"); - } - - const passwordResetResponse = await EmailPassword.resetPasswordUsingToken( - tenantId, - passwordResetToken.token, - newPassword, - userContext - ); - - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } - return { status: "OK", }; } - let passwordFormFields = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( - (field) => field.id === FORM_FIELD_PASSWORD_ID - ); - - let passwordValidationError = await passwordFormFields[0].validate(newPassword, tenantId); - - if (passwordValidationError !== undefined) { + const updateResponse = await ThirdPartyEmailPassword.updateEmailOrPassword({ + recipeUserId: new RecipeUserId(recipeUserId), + password: newPassword, + tenantIdForPasswordPolicy: tenantId, + userContext, + }); + + if ( + updateResponse.status === "UNKNOWN_USER_ID_ERROR" || + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + // Techincally it can but its an edge case so we assume that it wont + throw new Error("Should never come here"); + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { return { status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, + error: updateResponse.failureReason, }; } - - const passwordResetToken = await ThirdPartyEmailPassword.createResetPasswordToken(tenantId, userId, userContext); - - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { - // Techincally it can but its an edge case so we assume that it wont - throw new Error("Should never come here"); - } - - const passwordResetResponse = await ThirdPartyEmailPassword.resetPasswordUsingToken( - tenantId, - passwordResetToken.token, - newPassword, - userContext - ); - - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } - return { status: "OK", }; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userPut.ts index a67f97322..804035251 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userPut.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userPut.ts @@ -13,6 +13,7 @@ import UserMetadataRecipe from "../../../usermetadata/recipe"; import UserMetadata from "../../../usermetadata"; import { FORM_FIELD_EMAIL_ID } from "../../../emailpassword/constants"; import { defaultValidateEmail, defaultValidatePhoneNumber } from "../../../passwordless/utils"; +import RecipeUserId from "../../../../recipeUserId"; type Response = | { @@ -31,11 +32,19 @@ type Response = | { status: "INVALID_PHONE_ERROR"; error: string; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + error: string; + } + | { + status: "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + error: string; }; const updateEmailForRecipeId = async ( recipeId: "emailpassword" | "thirdparty" | "passwordless" | "thirdpartyemailpassword" | "thirdpartypasswordless", - userId: string, + recipeUserId: RecipeUserId, email: string, tenantId: string, userContext: any @@ -50,6 +59,10 @@ const updateEmailForRecipeId = async ( | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } > => { if (recipeId === "emailpassword") { let emailFormFields = EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( @@ -66,7 +79,7 @@ const updateEmailForRecipeId = async ( } const emailUpdateResponse = await EmailPassword.updateEmailOrPassword({ - userId, + recipeUserId, email, userContext, }); @@ -75,6 +88,13 @@ const updateEmailForRecipeId = async ( return { status: "EMAIL_ALREADY_EXISTS_ERROR", }; + } else if (emailUpdateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + return { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR", + reason: emailUpdateResponse.reason, + }; + } else if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") { + throw new Error("Should never come here"); } return { @@ -97,7 +117,7 @@ const updateEmailForRecipeId = async ( } const emailUpdateResponse = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId, + recipeUserId, email, userContext, }); @@ -106,9 +126,12 @@ const updateEmailForRecipeId = async ( return { status: "EMAIL_ALREADY_EXISTS_ERROR", }; - } - - if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") { + } else if (emailUpdateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + return { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR", + reason: emailUpdateResponse.reason, + }; + } else if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") { throw new Error("Should never come here"); } @@ -147,7 +170,7 @@ const updateEmailForRecipeId = async ( } const updateResult = await Passwordless.updateUser({ - userId, + recipeUserId, email, userContext, }); @@ -162,6 +185,16 @@ const updateEmailForRecipeId = async ( }; } + if ( + updateResult.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" || + updateResult.status === "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" + ) { + return { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR", + reason: updateResult.reason, + }; + } + return { status: "OK", }; @@ -197,7 +230,7 @@ const updateEmailForRecipeId = async ( } const updateResult = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId, + recipeUserId, email, userContext, }); @@ -225,7 +258,7 @@ const updateEmailForRecipeId = async ( const updatePhoneForRecipeId = async ( recipeId: "emailpassword" | "thirdparty" | "passwordless" | "thirdpartyemailpassword" | "thirdpartypasswordless", - userId: string, + recipeUserId: RecipeUserId, phone: string, tenantId: string, userContext: any @@ -240,6 +273,10 @@ const updatePhoneForRecipeId = async ( | { status: "PHONE_ALREADY_EXISTS_ERROR"; } + | { + status: "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } > => { if (recipeId === "passwordless") { let isValidPhone = true; @@ -271,7 +308,7 @@ const updatePhoneForRecipeId = async ( } const updateResult = await Passwordless.updateUser({ - userId, + recipeUserId, phoneNumber: phone, userContext, }); @@ -285,6 +322,12 @@ const updatePhoneForRecipeId = async ( status: "PHONE_ALREADY_EXISTS_ERROR", }; } + if (updateResult.status === "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR") { + return { + status: updateResult.status, + reason: updateResult.reason, + }; + } return { status: "OK", @@ -321,7 +364,7 @@ const updatePhoneForRecipeId = async ( } const updateResult = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId, + recipeUserId, phoneNumber: phone, userContext, }); @@ -354,16 +397,16 @@ export const userPut = async ( userContext: any ): Promise => { const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; + const recipeUserId = requestBody.recipeUserId; const recipeId = requestBody.recipeId; const firstName = requestBody.firstName; const lastName = requestBody.lastName; const email = requestBody.email; const phone = requestBody.phone; - if (userId === undefined || typeof userId !== "string") { + if (recipeUserId === undefined || typeof recipeUserId !== "string") { throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", + message: "Required parameter 'recipeUserId' is missing or has an invalid type", type: STError.BAD_INPUT_ERROR, }); } @@ -410,7 +453,7 @@ export const userPut = async ( }); } - let userResponse = await getUserForRecipeId(userId, recipeId); + let userResponse = await getUserForRecipeId(new RecipeUserId(recipeUserId), recipeId); if (userResponse.user === undefined || userResponse.recipe === undefined) { throw new Error("Should never come here"); @@ -436,19 +479,26 @@ export const userPut = async ( metaDataUpdate["last_name"] = lastName.trim(); } - await UserMetadata.updateUserMetadata(userId, metaDataUpdate, userContext); + await UserMetadata.updateUserMetadata(userResponse.user.id, metaDataUpdate, userContext); } } if (email.trim() !== "") { const emailUpdateResponse = await updateEmailForRecipeId( userResponse.recipe, - userId, + new RecipeUserId(recipeUserId), email.trim(), tenantId, userContext ); + if (emailUpdateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + return { + error: emailUpdateResponse.reason, + status: emailUpdateResponse.status, + }; + } + if (emailUpdateResponse.status !== "OK") { return emailUpdateResponse; } @@ -457,12 +507,19 @@ export const userPut = async ( if (phone.trim() !== "") { const phoneUpdateResponse = await updatePhoneForRecipeId( userResponse.recipe, - userId, + new RecipeUserId(recipeUserId), phone.trim(), tenantId, userContext ); + if (phoneUpdateResponse.status === "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR") { + return { + error: phoneUpdateResponse.reason, + status: phoneUpdateResponse.status, + }; + } + if (phoneUpdateResponse.status !== "OK") { return phoneUpdateResponse; } diff --git a/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts index fa1fcff9f..44b9f88db 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts @@ -24,14 +24,14 @@ export const userSessionsGet: APIFunction = async ( ): Promise => { const userId = options.req.getKeyValueFromQuery("userId"); - if (userId === undefined) { + if (userId === undefined || userId === "") { throw new STError({ message: "Missing required parameter 'userId'", type: STError.BAD_INPUT_ERROR, }); } - const response = await Session.getAllSessionHandlesForUser(userId, undefined, userContext); + const response = await Session.getAllSessionHandlesForUser(userId, undefined, undefined, userContext); let sessions: SessionType[] = []; let sessionInfoPromises: Promise[] = []; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userUnlinkGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userUnlinkGet.ts new file mode 100644 index 000000000..f9632756e --- /dev/null +++ b/lib/ts/recipe/dashboard/api/userdetails/userUnlinkGet.ts @@ -0,0 +1,30 @@ +import { APIInterface, APIOptions } from "../../types"; +import STError from "../../../../error"; +import AccountLinking from "../../../accountlinking"; +import RecipeUserId from "../../../../recipeUserId"; + +type Response = { + status: "OK"; +}; + +export const userUnlink = async ( + _: APIInterface, + ___: string, + options: APIOptions, + userContext: any +): Promise => { + const recipeUserId = options.req.getKeyValueFromQuery("recipeUserId"); + + if (recipeUserId === undefined) { + throw new STError({ + message: "Required field recipeUserId is missing", + type: STError.BAD_INPUT_ERROR, + }); + } + + await AccountLinking.unlinkAccount(new RecipeUserId(recipeUserId), userContext); + + return { + status: "OK", + }; +}; diff --git a/lib/ts/recipe/dashboard/api/usersGet.ts b/lib/ts/recipe/dashboard/api/usersGet.ts index 3142c2327..f8e91d5c3 100644 --- a/lib/ts/recipe/dashboard/api/usersGet.ts +++ b/lib/ts/recipe/dashboard/api/usersGet.ts @@ -12,40 +12,16 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { APIInterface, APIOptions } from "../types"; +import { APIInterface, APIOptions, UserWithFirstAndLastName } from "../types"; import STError from "../../../error"; -import SuperTokens from "../../../supertokens"; +import { getUsersNewestFirst, getUsersOldestFirst } from "../../.."; import UserMetaDataRecipe from "../../usermetadata/recipe"; import UserMetaData from "../../usermetadata"; export type Response = { status: "OK"; nextPaginationToken?: string; - users: { - recipeId: string; - user: { - id: string; - timeJoined: number; - firstName?: string; - lastName?: string; - tenantIds: string[]; - } & ( - | { - email: string; - } - | { - email: string; - thirdParty: { - id: string; - userId: string; - }; - } - | { - email?: string; - phoneNumber?: string; - } - ); - }[]; + users: UserWithFirstAndLastName[]; }; export default async function usersGet( @@ -79,13 +55,21 @@ export default async function usersGet( let paginationToken = options.req.getKeyValueFromQuery("paginationToken"); const query = getSearchParamsFromURL(options.req.getOriginalURL()); - let usersResponse = await SuperTokens.getInstanceOrThrowError().getUsers({ - query, - timeJoinedOrder: timeJoinedOrder, - limit: parseInt(limit), - paginationToken, - tenantId, - }); + + let usersResponse = + timeJoinedOrder === "DESC" + ? await getUsersNewestFirst({ + tenantId, + query, + limit: parseInt(limit), + paginationToken, + }) + : await getUsersOldestFirst({ + tenantId, + query, + limit: parseInt(limit), + paginationToken, + }); // If the UserMetaData recipe has been initialised, fetch first and last name try { @@ -94,33 +78,27 @@ export default async function usersGet( // Recipe has not been initialised, return without first name and last name return { status: "OK", - users: usersResponse.users, + users: usersResponse.users.map((u) => u.toJson()), nextPaginationToken: usersResponse.nextPaginationToken, }; } - let updatedUsersArray: { - recipeId: string; - user: any; - }[] = []; + let updatedUsersArray: UserWithFirstAndLastName[] = []; let metaDataFetchPromises: (() => Promise)[] = []; for (let i = 0; i < usersResponse.users.length; i++) { - const userObj = usersResponse.users[i]; + const userObj = usersResponse.users[i].toJson(); metaDataFetchPromises.push( (): Promise => new Promise(async (resolve, reject) => { try { - const userMetaDataResponse = await UserMetaData.getUserMetadata(userObj.user.id, userContext); + const userMetaDataResponse = await UserMetaData.getUserMetadata(userObj.id, userContext); const { first_name, last_name } = userMetaDataResponse.metadata; updatedUsersArray[i] = { - recipeId: userObj.recipeId, - user: { - ...userObj.user, - firstName: first_name, - lastName: last_name, - }, + ...userObj, + firstName: first_name, + lastName: last_name, }; resolve(true); } catch (e) { diff --git a/lib/ts/recipe/dashboard/constants.ts b/lib/ts/recipe/dashboard/constants.ts index 60df0c66b..49bbabf68 100644 --- a/lib/ts/recipe/dashboard/constants.ts +++ b/lib/ts/recipe/dashboard/constants.ts @@ -28,3 +28,5 @@ export const USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token"; export const SEARCH_TAGS_API = "/api/search/tags"; export const DASHBOARD_ANALYTICS_API = "/api/analytics"; export const TENANTS_LIST_API = "/api/tenants/list"; + +export const UNLINK_USER = "/api/user/unlink"; diff --git a/lib/ts/recipe/dashboard/recipe.ts b/lib/ts/recipe/dashboard/recipe.ts index dcc3fbf51..39323c06c 100644 --- a/lib/ts/recipe/dashboard/recipe.ts +++ b/lib/ts/recipe/dashboard/recipe.ts @@ -36,9 +36,10 @@ import { USER_PASSWORD_API, USER_SESSIONS_API, VALIDATE_KEY_API, + UNLINK_USER, } from "./constants"; import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import dashboard from "./api/dashboard"; import error from "../../error"; import validateKey from "./api/validateKey"; @@ -46,7 +47,7 @@ import apiKeyProtector from "./api/apiKeyProtector"; import usersGet from "./api/usersGet"; import usersCountGet from "./api/usersCountGet"; import { userGet } from "./api/userdetails/userGet"; -import { userEmailverifyGet } from "./api/userdetails/userEmailVerifyGet"; +import { userEmailVerifyGet } from "./api/userdetails/userEmailVerifyGet"; import { userMetaDataGet } from "./api/userdetails/userMetadataGet"; import { userSessionsGet } from "./api/userdetails/userSessionsGet"; import { userDelete } from "./api/userdetails/userDelete"; @@ -61,6 +62,7 @@ import signOut from "./api/signOut"; import { getSearchTags } from "./api/search/tagsGet"; import analyticsPost from "./api/analytics"; import listTenants from "./api/listTenants"; +import { userUnlink } from "./api/userdetails/userUnlinkGet"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -245,6 +247,12 @@ export default class Recipe extends RecipeModule { disabled: false, method: "get", }, + { + id: UNLINK_USER, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(UNLINK_USER)), + disabled: false, + method: "get", + }, ]; }; @@ -301,7 +309,7 @@ export default class Recipe extends RecipeModule { } } else if (id === USER_EMAIL_VERIFY_API) { if (req.getMethod() === "get") { - apiFunction = userEmailverifyGet; + apiFunction = userEmailVerifyGet; } if (req.getMethod() === "put") { @@ -335,6 +343,8 @@ export default class Recipe extends RecipeModule { apiFunction = analyticsPost; } else if (id === TENANTS_LIST_API) { apiFunction = listTenants; + } else if (id === UNLINK_USER) { + apiFunction = userUnlink; } // If the id doesnt match any APIs return false diff --git a/lib/ts/recipe/dashboard/types.ts b/lib/ts/recipe/dashboard/types.ts index 8e0ebf490..2def9f258 100644 --- a/lib/ts/recipe/dashboard/types.ts +++ b/lib/ts/recipe/dashboard/types.ts @@ -14,8 +14,8 @@ */ import OverrideableBuilder from "supertokens-js-override"; -import { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo } from "../../types"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import { NormalisedAppinfo, User } from "../../types"; export type TypeInput = { apiKey?: string; @@ -72,27 +72,7 @@ export type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless"; export type AuthMode = "api-key" | "email-password"; -type CommonUserInformation = { - id: string; - timeJoined: number; - firstName: string; - lastName: string; - tenantIds: string[]; -}; - -export type EmailPasswordUser = CommonUserInformation & { - email: string; -}; - -export type ThirdPartyUser = CommonUserInformation & { - email: string; - thirdParty: { - id: string; - userId: string; - }; -}; - -export type PasswordlessUser = CommonUserInformation & { - email?: string; - phone?: string; +export type UserWithFirstAndLastName = User & { + firstName?: string; + lastName?: string; }; diff --git a/lib/ts/recipe/dashboard/utils.ts b/lib/ts/recipe/dashboard/utils.ts index 70af2d1cd..048294993 100644 --- a/lib/ts/recipe/dashboard/utils.ts +++ b/lib/ts/recipe/dashboard/utils.ts @@ -13,29 +13,25 @@ * under the License. */ -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import { normaliseEmail, sendNon200ResponseWithMessage } from "../../utils"; import { DASHBOARD_API } from "./constants"; import { APIInterface, - EmailPasswordUser, - PasswordlessUser, RecipeIdForUser, RecipeInterface, - ThirdPartyUser, TypeInput, TypeNormalisedInput, + UserWithFirstAndLastName, } from "./types"; +import AccountLinking from "../accountlinking/recipe"; import EmailPasswordRecipe from "../emailpassword/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; import PasswordlessRecipe from "../passwordless/recipe"; -import EmailPassword from "../emailpassword"; -import ThirdParty from "../thirdparty"; -import Passwordless from "../passwordless"; -import ThirdPartyEmailPassword from "../thirdpartyemailpassword"; import ThirdPartyEmailPasswordRecipe from "../thirdpartyemailpassword/recipe"; -import ThirdPartyPasswordless from "../thirdpartypasswordless"; import ThirdPartyPasswordlessRecipe from "../thirdpartypasswordless/recipe"; +import RecipeUserId from "../../recipeUserId"; +import { User } from "../../types"; import { logDebugMessage } from "../../logger"; export function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput { @@ -72,10 +68,38 @@ export function isValidRecipeId(recipeId: string): recipeId is RecipeIdForUser { } export async function getUserForRecipeId( - userId: string, + recipeUserId: RecipeUserId, recipeId: string ): Promise<{ - user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined; + user: UserWithFirstAndLastName | undefined; + recipe: + | "emailpassword" + | "thirdparty" + | "passwordless" + | "thirdpartyemailpassword" + | "thirdpartypasswordless" + | undefined; +}> { + let userResponse = await _getUserForRecipeId(recipeUserId, recipeId); + let user: UserWithFirstAndLastName | undefined = undefined; + if (userResponse.user !== undefined) { + user = { + ...userResponse.user, + firstName: "", + lastName: "", + }; + } + return { + user, + recipe: userResponse.recipe, + }; +} + +async function _getUserForRecipeId( + recipeUserId: RecipeUserId, + recipeId: string +): Promise<{ + user: User | undefined; recipe: | "emailpassword" | "thirdparty" @@ -84,7 +108,6 @@ export async function getUserForRecipeId( | "thirdpartypasswordless" | undefined; }> { - let user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined; let recipe: | "emailpassword" | "thirdparty" @@ -93,123 +116,90 @@ export async function getUserForRecipeId( | "thirdpartypasswordless" | undefined; + const user = await AccountLinking.getInstance().recipeInterfaceImpl.getUser({ + userId: recipeUserId.getAsString(), + userContext: {}, + }); + + if (user === undefined) { + return { + user: undefined, + recipe: undefined, + }; + } + + const loginMethod = user.loginMethods.find( + (m) => m.recipeId === recipeId && m.recipeUserId.getAsString() === recipeUserId.getAsString() + ); + + if (loginMethod === undefined) { + return { + user: undefined, + recipe: undefined, + }; + } + if (recipeId === EmailPasswordRecipe.RECIPE_ID) { try { - const userResponse = await EmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "emailpassword"; - } + // we detect if this recipe has been init or not.. + EmailPasswordRecipe.getInstanceOrThrowError(); + recipe = "emailpassword"; } catch (e) { // No - op } - if (user === undefined) { + if (recipe === undefined) { try { - const userResponse = await ThirdPartyEmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdpartyemailpassword"; - } + // we detect if this recipe has been init or not.. + ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); + recipe = "thirdpartyemailpassword"; } catch (e) { // No - op } } } else if (recipeId === ThirdPartyRecipe.RECIPE_ID) { try { - const userResponse = await ThirdParty.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdparty"; - } + ThirdPartyRecipe.getInstanceOrThrowError(); + recipe = "thirdparty"; } catch (e) { // No - op } - if (user === undefined) { + if (recipe === undefined) { try { - const userResponse = await ThirdPartyEmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdpartyemailpassword"; - } + // we detect if this recipe has been init or not.. + ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); + recipe = "thirdpartyemailpassword"; } catch (e) { // No - op } } - if (user === undefined) { + if (recipe === undefined) { try { - const userResponse = await ThirdPartyPasswordless.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdpartypasswordless"; - } + ThirdPartyPasswordlessRecipe.getInstanceOrThrowError(); + recipe = "thirdpartypasswordless"; } catch (e) { // No - op } } } else if (recipeId === PasswordlessRecipe.RECIPE_ID) { try { - const userResponse = await Passwordless.getUserById({ - userId, - }); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "passwordless"; - } + PasswordlessRecipe.getInstanceOrThrowError(); + recipe = "passwordless"; } catch (e) { // No - op } - if (user === undefined) { + if (recipe === undefined) { try { - const userResponse = await ThirdPartyPasswordless.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdpartypasswordless"; - } + ThirdPartyPasswordlessRecipe.getInstanceOrThrowError(); + recipe = "thirdpartypasswordless"; } catch (e) { // No - op } } } - return { user, recipe, diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index 8ea034d36..f8b4f0e2d 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -1,17 +1,19 @@ -import { APIInterface, APIOptions, User } from "../"; +import { APIInterface, APIOptions } from "../"; import { logDebugMessage } from "../../../logger"; import Session from "../../session"; import { SessionContainerInterface } from "../../session/types"; -import { GeneralErrorResponse } from "../../../types"; -import { getPasswordResetLink } from "../utils"; +import { GeneralErrorResponse, User } from "../../../types"; +import { listUsersByAccountInfo, getUser } from "../../../"; +import AccountLinking from "../../accountlinking/recipe"; +import EmailVerification from "../../emailverification/recipe"; +import { RecipeLevelUser } from "../../accountlinking/types"; +import RecipeUserId from "../../../recipeUserId"; export default function getAPIImplementation(): APIInterface { return { emailExistsGET: async function ({ email, tenantId, - options, - userContext, }: { email: string; tenantId: string; @@ -24,11 +26,27 @@ export default function getAPIImplementation(): APIInterface { } | GeneralErrorResponse > { - let user = await options.recipeImplementation.getUserByEmail({ email, tenantId, userContext }); + // even if the above returns true, we still need to check if there + // exists an email password user with the same email cause the function + // above does not check for that. + let users = await listUsersByAccountInfo( + tenantId, + { + email, + }, + false + ); + let emailPasswordUserExists = + users.find((u) => { + return ( + u.loginMethods.find((lm) => lm.recipeId === "emailpassword" && lm.hasSameEmailAs(email)) !== + undefined + ); + }) !== undefined; return { status: "OK", - exists: user !== undefined, + exists: emailPasswordUserExists, }; }, generatePasswordResetTokenPOST: async function ({ @@ -48,48 +66,269 @@ export default function getAPIImplementation(): APIInterface { | { status: "OK"; } + | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } | GeneralErrorResponse > { - let email = formFields.filter((f) => f.id === "email")[0].value; + const email = formFields.filter((f) => f.id === "email")[0].value; + + // this function will be reused in different parts of the flow below.. + async function generateAndSendPasswordResetToken( + primaryUserId: string, + recipeUserId: RecipeUserId | undefined + ): Promise< + | { + status: "OK"; + } + | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } + | GeneralErrorResponse + > { + // the user ID here can be primary or recipe level. + let response = await options.recipeImplementation.createResetPasswordToken({ + tenantId, + userId: recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString(), + email, + userContext, + }); + if (response.status === "UNKNOWN_USER_ID_ERROR") { + logDebugMessage( + `Password reset email not sent, unknown user id: ${ + recipeUserId === undefined ? primaryUserId : recipeUserId.getAsString() + }` + ); + return { + status: "OK", + }; + } + + let passwordResetLink = + options.appInfo.websiteDomain.getAsStringDangerous() + + options.appInfo.websiteBasePath.getAsStringDangerous() + + "/reset-password?token=" + + response.token + + "&rid=" + + options.recipeId; + + logDebugMessage(`Sending password reset email to ${email}`); + await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + tenantId, + type: "PASSWORD_RESET", + user: { + id: primaryUserId, + recipeUserId, + email, + }, + passwordResetLink, + userContext, + }); - let user = await options.recipeImplementation.getUserByEmail({ email, tenantId, userContext }); - if (user === undefined) { return { status: "OK", }; } - let response = await options.recipeImplementation.createResetPasswordToken({ - userId: user.id, + /** + * check if primaryUserId is linked with this email + */ + let users = await listUsersByAccountInfo( tenantId, - userContext, - }); - if (response.status === "UNKNOWN_USER_ID_ERROR") { - logDebugMessage(`Password reset email not sent, unknown user id: ${user.id}`); - return { - status: "OK", - }; + { + email, + }, + false + ); + + // we find the recipe user ID of the email password account from the user's list + // for later use. + let emailPasswordAccount: RecipeLevelUser | undefined = undefined; + for (let i = 0; i < users.length; i++) { + let emailPasswordAccountTmp = users[i].loginMethods.find( + (l) => l.recipeId === "emailpassword" && l.hasSameEmailAs(email) + ); + if (emailPasswordAccountTmp !== undefined) { + emailPasswordAccount = emailPasswordAccountTmp; + break; + } } - let passwordResetLink = getPasswordResetLink({ - appInfo: options.appInfo, - token: response.token, - recipeId: options.recipeId, - tenantId, - }); + // we find the primary user ID from the user's list for later use. + let primaryUserAssociatedWithEmail = users.find((u) => u.isPrimaryUser); - logDebugMessage(`Sending password reset email to ${email}`); - await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORD_RESET", - user, - passwordResetLink, + // first we check if there even exists a primary user that has the input email + // if not, then we do the regular flow for password reset. + if (primaryUserAssociatedWithEmail === undefined) { + if (emailPasswordAccount === undefined) { + logDebugMessage(`Password reset email not sent, unknown user email: ${email}`); + return { + status: "OK", + }; + } + return await generateAndSendPasswordResetToken( + emailPasswordAccount.recipeUserId.getAsString(), + emailPasswordAccount.recipeUserId + ); + } + + let shouldDoAccountLinkingResponse = await AccountLinking.getInstance().config.shouldDoAutomaticAccountLinking( + emailPasswordAccount !== undefined + ? emailPasswordAccount + : { + recipeId: "emailpassword", + email, + }, + primaryUserAssociatedWithEmail, tenantId, - userContext, - }); + userContext + ); - return { - status: "OK", - }; + // Now we need to check that if there exists any email password user at all + // for the input email. If not, then it implies that when the token is consumed, + // then we will create a new user - so we should only generate the token if + // the criteria for the new user is met. + if (emailPasswordAccount === undefined) { + // this means that there is no email password user that exists for the input email. + // So we check for the sign up condition and only go ahead if that condition is + // met. + + // But first we must check if account linking is enabled at all - cause if it's + // not, then the new email password user that will be created in password reset + // code consume cannot be linked to the primary user - therefore, we should + // not generate a password reset token + if (!shouldDoAccountLinkingResponse.shouldAutomaticallyLink) { + logDebugMessage( + `Password reset email not sent, since email password user didn't exist, and account linking not enabled` + ); + return { + status: "OK", + }; + } + + let isSignUpAllowed = await AccountLinking.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "emailpassword", + email, + }, + isVerified: true, // cause when the token is consumed, we will mark the email as verified + tenantId, + userContext, + }); + if (isSignUpAllowed) { + // notice that we pass in the primary user ID here. This means that + // we will be creating a new email password account when the token + // is consumed and linking it to this primary user. + return await generateAndSendPasswordResetToken(primaryUserAssociatedWithEmail.id, undefined); + } else { + logDebugMessage( + `Password reset email not sent, isSignUpAllowed returned false for email: ${email}` + ); + return { + status: "OK", + }; + } + } + + // At this point, we know that some email password user exists with this email + // and also some primary user ID exist. We now need to find out if they are linked + // together or not. If they are linked together, then we can just generate the token + // else we check for more security conditions (since we will be linking them post token generation) + let areTheTwoAccountsLinked = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.recipeUserId.getAsString() === emailPasswordAccount!.recipeUserId.getAsString(); + }) !== undefined; + + if (areTheTwoAccountsLinked) { + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } + + // Here we know that the two accounts are NOT linked. We now need to check for an + // extra security measure here to make sure that the input email in the primary user + // is verified, and if not, we need to make sure that there is no other email / phone number + // associated with the primary user account. If there is, then we do not proceed. + + /* + This security measure helps prevent the following attack: + An attacker has email A and they create an account using TP and it doesn't matter if A is verified or not. Now they create another account using EP with email A and verifies it. Both these accounts are linked. Now the attacker changes the email for EP recipe to B which makes the EP account unverified, but it's still linked. + + If the real owner of B tries to signup using EP, it will say that the account already exists so they may try to reset password which should be denied because then they will end up getting access to attacker's account and verify the EP account. + + The problem with this situation is if the EP account is verified, it will allow further sign-ups with email B which will also be linked to this primary account (that the attacker had created with email A). + + It is important to realize that the attacker had created another account with A because if they hadn't done that, then they wouldn't have access to this account after the real user resets the password which is why it is important to check there is another non-EP account linked to the primary such that the email is not the same as B. + + Exception to the above is that, if there is a third recipe account linked to the above two accounts and has B as verified, then we should allow reset password token generation because user has already proven that the owns the email B + */ + + // But first, this only matters it the user cares about checking for email verification status.. + + if (!shouldDoAccountLinkingResponse.shouldAutomaticallyLink) { + // here we will go ahead with the token generation cause + // even when the token is consumed, we will not be linking the accounts + // so no need to check for anything + return await generateAndSendPasswordResetToken( + emailPasswordAccount.recipeUserId.getAsString(), + emailPasswordAccount.recipeUserId + ); + } + + if (!shouldDoAccountLinkingResponse.shouldRequireVerification) { + // the checks below are related to email verification, and if the user + // does not care about that, then we should just continue with token generation + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } + + // Now we start the required security checks. First we check if the primary user + // it has just one linked account. And if that's true, then we continue + // cause then there is no scope for account takeover + if (primaryUserAssociatedWithEmail.loginMethods.length === 1) { + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } + + // Next we check if there is any login method in which the input email is verified. + // If that is the case, then it's proven that the user owns the email and we can + // trust linking of the email password account. + let emailVerified = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.hasSameEmailAs(email) && lm.verified; + }) !== undefined; + + if (emailVerified) { + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } + + // finally, we check if the primary user has any other email / phone number + // associated with this account - and if it does, then it means that + // there is a risk of account takeover, so we do not allow the token to be generated + let hasOtherEmailOrPhone = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + // we do the extra undefined check below cause + // hasSameEmailAs returns false if the lm.email is undefined, and + // we want to check that the email is different as opposed to email + // not existing in lm. + return (lm.email !== undefined && !lm.hasSameEmailAs(email)) || lm.phoneNumber !== undefined; + }) !== undefined; + if (hasOtherEmailOrPhone) { + return { + status: "PASSWORD_RESET_NOT_ALLOWED", + reason: + "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)", + }; + } else { + return await generateAndSendPasswordResetToken( + primaryUserAssociatedWithEmail.id, + emailPasswordAccount.recipeUserId + ); + } }, passwordResetPOST: async function ({ formFields, @@ -109,25 +348,210 @@ export default function getAPIImplementation(): APIInterface { }): Promise< | { status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; + user: User; + email: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } + | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } | GeneralErrorResponse > { + async function markEmailAsVerified(recipeUserId: RecipeUserId, email: string) { + const emailVerificationInstance = EmailVerification.getInstance(); + if (emailVerificationInstance) { + const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( + { + tenantId, + recipeUserId, + email, + userContext, + } + ); + + if (tokenResponse.status === "OK") { + await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ + tenantId, + token: tokenResponse.token, + attemptAccountLinking: false, // we pass false here cause + // we anyway do account linking in this API after this function is + // called. + userContext, + }); + } + } + } + + async function doUpdatePassword( + recipeUserId: RecipeUserId + ): Promise< + | { + status: "OK"; + user: User; + email: string; + } + | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } + | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } + | GeneralErrorResponse + > { + let updateResponse = await options.recipeImplementation.updateEmailOrPassword({ + tenantIdForPasswordPolicy: tenantId, + // we can treat userIdForWhomTokenWasGenerated as a recipe user id cause + // whenever this function is called, + recipeUserId, + password: newPassword, + userContext, + }); + if ( + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + throw new Error("This should never come here because we are not updating the email"); + } else if (updateResponse.status === "UNKNOWN_USER_ID_ERROR") { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + return { + status: "PASSWORD_POLICY_VIOLATED_ERROR", + failureReason: updateResponse.failureReason, + }; + } else { + // status: "OK" + return { + status: "OK", + user: existingUser!, + email: emailForWhomTokenWasGenerated, + }; + } + } + let newPassword = formFields.filter((f) => f.id === "password")[0].value; - let response = await options.recipeImplementation.resetPasswordUsingToken({ + let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ token, - newPassword, tenantId, userContext, }); - return response; + if (tokenConsumptionResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { + return tokenConsumptionResponse; + } + + let userIdForWhomTokenWasGenerated = tokenConsumptionResponse.userId; + let emailForWhomTokenWasGenerated = tokenConsumptionResponse.email; + + let existingUser = await getUser(tokenConsumptionResponse.userId, userContext); + + if (existingUser === undefined) { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + // Also note that this being undefined doesn't mean that the email password + // user does not exist, but it means that there is no reicpe or primary user + // for whom the token was generated. + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } + + // We start by checking if the existingUser is a primary user or not. If it is, + // then we will try and create a new email password user and link it to the primary user (if required) + + if (existingUser.isPrimaryUser) { + // If this user contains an email password account for whom the token was generated, + // then we update that user's password. + let emailPasswordUserIsLinkedToExistingUser = + existingUser.loginMethods.find((lm) => { + // we check based on user ID and not email because the only time + // the primary user ID is used for token generation is if the email password + // user did not exist - in which case the value of emailPasswordUserExists will + // resolve to false anyway, and that's what we want. + + // there is an edge case where if the email password recipe user was created + // after the password reset token generation, and it was linked to the + // primary user id (userIdForWhomTokenWasGenerated), in this case, + // we still don't allow password update, cause the user should try again + // and the token should be regenerated for the right recipe user. + return ( + lm.recipeUserId.getAsString() === userIdForWhomTokenWasGenerated && + lm.recipeId === "emailpassword" + ); + }) !== undefined; + + if (emailPasswordUserIsLinkedToExistingUser) { + return doUpdatePassword(new RecipeUserId(userIdForWhomTokenWasGenerated)); + } else { + // this means that the existingUser does not have an emailpassword user associated + // with it. It could now mean that no emailpassword user exists, or it could mean that + // the the ep user exists, but it's not linked to the current account. + // If no ep user doesn't exists, we will create one, and link it to the existing account. + // If ep user exists, then it means there is some race condition cause + // then the token should have been generated for that user instead of the primary user, + // and it shouldn't have come into this branch. So we can simply send a password reset + // invalid error and the user can try again. + + // NOTE: We do not ask the dev if we should do account linking or not here + // cause we already have asked them this when generating an password reset token. + // In the edge case that the dev changes account linking allowance from true to false + // when it comes here, only a new recipe user id will be created and not linked + // cause createPrimaryUserIdOrLinkAccounts will disallow linking. This doesn't + // really cause any security issue. + + let createUserResponse = await options.recipeImplementation.createNewRecipeUser({ + tenantId, + email: tokenConsumptionResponse.email, + password: newPassword, + userContext, + }); + if (createUserResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // this means that the user already existed and we can just return an invalid + // token (see the above comment) + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } else { + // we mark the email as verified because password reset also requires + // access to the email to work.. This has a good side effect that + // any other login method with the same email in existingAccount will also get marked + // as verified. + await markEmailAsVerified( + createUserResponse.user.loginMethods[0].recipeUserId, + tokenConsumptionResponse.email + ); + const updatedUser = await getUser(createUserResponse.user.id, userContext); + if (updatedUser === undefined) { + throw new Error("Should never happen - user deleted after during password reset"); + } + createUserResponse.user = updatedUser; + // Now we try and link the accounts. The function below will try and also + // create a primary user of the new account, and if it does that, it's OK.. + // But in most cases, it will end up linking to existing account since the + // email is shared. + let linkedToUser = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: createUserResponse.user, + userContext, + }); + if (linkedToUser.id !== existingUser.id) { + // this means that the account we just linked to + // was not the one we had expected to link it to. This can happen + // due to some race condition or the other.. Either way, this + // is not an issue and we can just return OK + } + return { + status: "OK", + email: tokenConsumptionResponse.email, + user: linkedToUser, + }; + } + } + } else { + // This means that the existing user is not a primary account, which implies that + // it must be a non linked email password account. In this case, we simply update the password. + // Linking to an existing account will be done after the user goes through the email + // verification flow once they log in (if applicable). + return doUpdatePassword(new RecipeUserId(userIdForWhomTokenWasGenerated)); + } }, signInPOST: async function ({ formFields, @@ -151,6 +575,10 @@ export default function getAPIImplementation(): APIInterface { | { status: "WRONG_CREDENTIALS_ERROR"; } + | { + status: "SIGN_IN_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse > { let email = formFields.filter((f) => f.id === "email")[0].value; @@ -160,13 +588,48 @@ export default function getAPIImplementation(): APIInterface { if (response.status === "WRONG_CREDENTIALS_ERROR") { return response; } - let user = response.user; + + let emailPasswordRecipeUser = response.user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.hasSameEmailAs(email) + ); + + if (emailPasswordRecipeUser === undefined) { + // this can happen cause of some race condition, but it's not a big deal. + throw new Error("Race condition error - please call this API again"); + } + + // Here we do this check after sign in is done cause: + // - We first want to check if the credentials are correct first or not + // - The above recipe function marks the email as verified if other linked users + // with the same email are verified. The function below checks for the email verification + // so we want to call it only once this is up to date, + + let isSignInAllowed = await AccountLinking.getInstance().isSignInAllowed({ + user: response.user, + tenantId, + userContext, + }); + + if (!isSignInAllowed) { + return { + status: "SIGN_IN_NOT_ALLOWED", + reason: + "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)", + }; + } + + // the above sign in recipe function does not do account linking - so we do it here. + response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); let session = await Session.createNewSession( options.req, options.res, tenantId, - user.id, + emailPasswordRecipeUser.recipeUserId, {}, {}, userContext @@ -174,7 +637,7 @@ export default function getAPIImplementation(): APIInterface { return { status: "OK", session, - user, + user: response.user, }; }, @@ -197,6 +660,10 @@ export default function getAPIImplementation(): APIInterface { session: SessionContainerInterface; user: User; } + | { + status: "SIGN_UP_NOT_ALLOWED"; + reason: string; + } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } @@ -205,17 +672,74 @@ export default function getAPIImplementation(): APIInterface { let email = formFields.filter((f) => f.id === "email")[0].value; let password = formFields.filter((f) => f.id === "password")[0].value; - let response = await options.recipeImplementation.signUp({ email, password, tenantId, userContext }); + // Here we do this check because if the input email already exists with a primary user, + // then we do not allow sign up, cause even though we do not link this and the existing + // account right away, and we send an email verification link, the user + // may click on it by mistake assuming it's for their existing account - resulting + // in account take over. In this case, we return an EMAIL_ALREADY_EXISTS_ERROR + // and if the user goes through the forgot password flow, it will create + // an account there and it will work fine cause there the email is also verified. + + let isSignUpAllowed = await AccountLinking.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "emailpassword", + email, + }, + isVerified: false, + tenantId, + userContext, + }); + + if (!isSignUpAllowed) { + const conflictingUsers = await AccountLinking.getInstance().recipeInterfaceImpl.listUsersByAccountInfo({ + tenantId, + accountInfo: { + email, + }, + doUnionOfAccountInfo: false, + userContext, + }); + if ( + conflictingUsers.some((u) => + u.loginMethods.some((lm) => lm.recipeId === "emailpassword" && lm.hasSameEmailAs(email)) + ) + ) { + return { + status: "EMAIL_ALREADY_EXISTS_ERROR", + }; + } + + return { + status: "SIGN_UP_NOT_ALLOWED", + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + }; + } + + // this function also does account linking + let response = await options.recipeImplementation.signUp({ + tenantId, + email, + password, + userContext, + }); if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { return response; } - let user = response.user; + let emailPasswordRecipeUser = response.user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.hasSameEmailAs(email) + ); + + if (emailPasswordRecipeUser === undefined) { + // this can happen cause of some race condition, but it's not a big deal. + throw new Error("Race condition error - please call this API again"); + } let session = await Session.createNewSession( options.req, options.res, tenantId, - user.id, + emailPasswordRecipeUser.recipeUserId, {}, {}, userContext @@ -223,7 +747,7 @@ export default function getAPIImplementation(): APIInterface { return { status: "OK", session, - user, + user: response.user, }; }, }; diff --git a/lib/ts/recipe/emailpassword/api/passwordReset.ts b/lib/ts/recipe/emailpassword/api/passwordReset.ts index 8b5303e07..3cecacfe4 100644 --- a/lib/ts/recipe/emailpassword/api/passwordReset.ts +++ b/lib/ts/recipe/emailpassword/api/passwordReset.ts @@ -30,7 +30,10 @@ export default async function passwordReset( return false; } - // step 1 + // step 1: We need to do this here even though the update emailpassword recipe function would do this cause: + // - we want to throw this error before consuming the token, so that the user can try again + // - there is a case in the api impl where we create a new user, and we want to assign + // a password that meets the password policy. let formFields: { id: string; value: string; @@ -62,6 +65,21 @@ export default async function passwordReset( userContext, }); + if (result.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + // this error will be caught by the recipe error handler, just + // like it's done in the validateFormFieldsOrThrowError function above. + throw new STError({ + type: STError.FIELD_ERROR, + payload: [ + { + id: "password", + error: result.failureReason, + }, + ], + message: "Error in input formFields", + }); + } + send200Response( options.res, result.status === "OK" diff --git a/lib/ts/recipe/emailpassword/api/signin.ts b/lib/ts/recipe/emailpassword/api/signin.ts index 75f68878d..58b3edfd5 100644 --- a/lib/ts/recipe/emailpassword/api/signin.ts +++ b/lib/ts/recipe/emailpassword/api/signin.ts @@ -13,7 +13,7 @@ * under the License. */ -import { send200Response } from "../../../utils"; +import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; @@ -48,7 +48,7 @@ export default async function signInAPI( if (result.status === "OK") { send200Response(options.res, { status: "OK", - user: result.user, + ...getBackwardsCompatibleUserInfo(options.req, result), }); } else { send200Response(options.res, result); diff --git a/lib/ts/recipe/emailpassword/api/signup.ts b/lib/ts/recipe/emailpassword/api/signup.ts index 1ae42d9ac..5df847aef 100644 --- a/lib/ts/recipe/emailpassword/api/signup.ts +++ b/lib/ts/recipe/emailpassword/api/signup.ts @@ -13,7 +13,7 @@ * under the License. */ -import { send200Response } from "../../../utils"; +import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; import STError from "../error"; @@ -49,11 +49,11 @@ export default async function signUpAPI( if (result.status === "OK") { send200Response(options.res, { status: "OK", - user: result.user, + ...getBackwardsCompatibleUserInfo(options.req, result), }); } else if (result.status === "GENERAL_ERROR") { send200Response(options.res, result); - } else { + } else if (result.status === "EMAIL_ALREADY_EXISTS_ERROR") { throw new STError({ type: STError.FIELD_ERROR, payload: [ @@ -64,6 +64,8 @@ export default async function signUpAPI( ], message: "Error in input formFields", }); + } else { + send200Response(options.res, result); } return true; } diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts index ef38911fd..3f975e117 100644 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts @@ -12,41 +12,35 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { TypeEmailPasswordEmailDeliveryInput, RecipeInterface } from "../../../types"; +import { TypeEmailPasswordEmailDeliveryInput } from "../../../types"; import { createAndSendEmailUsingSupertokensService } from "../../../passwordResetFunctions"; import { NormalisedAppinfo } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { - private recipeInterfaceImpl: RecipeInterface; private isInServerlessEnv: boolean; private appInfo: NormalisedAppinfo; - constructor(recipeInterfaceImpl: RecipeInterface, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean) { - this.recipeInterfaceImpl = recipeInterfaceImpl; + constructor(appInfo: NormalisedAppinfo, isInServerlessEnv: boolean) { this.isInServerlessEnv = isInServerlessEnv; this.appInfo = appInfo; } sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: any }) => { - let user = await this.recipeInterfaceImpl.getUserById({ - userId: input.user.id, - userContext: input.userContext, - }); - if (user === undefined) { - throw Error("this should never come here"); - } // we add this here cause the user may have overridden the sendEmail function // to change the input email and if we don't do this, the input email // will get reset by the getUserById call above. - user.email = input.user.email; try { if (!this.isInServerlessEnv) { - createAndSendEmailUsingSupertokensService(this.appInfo, user, input.passwordResetLink).catch((_) => {}); + createAndSendEmailUsingSupertokensService( + this.appInfo, + input.user, + input.passwordResetLink + ).catch((_) => {}); } else { // see https://github.com/supertokens/supertokens-node/pull/135 - await createAndSendEmailUsingSupertokensService(this.appInfo, user, input.passwordResetLink); + await createAndSendEmailUsingSupertokensService(this.appInfo, input.user, input.passwordResetLink); } } catch (_) {} }; diff --git a/lib/ts/recipe/emailpassword/index.ts b/lib/ts/recipe/emailpassword/index.ts index 0da835eaa..303f6def8 100644 --- a/lib/ts/recipe/emailpassword/index.ts +++ b/lib/ts/recipe/emailpassword/index.ts @@ -15,9 +15,11 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, User, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; +import { RecipeInterface, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; +import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; import { getPasswordResetLink } from "./utils"; +import { getUser } from "../.."; export default class Wrapper { static init = Recipe.init; @@ -42,40 +44,52 @@ export default class Wrapper { }); } - static getUserById(userId: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ + /** + * We do not make email optional here cause we want to + * allow passing in primaryUserId. If we make email optional, + * and if the user provides a primaryUserId, then it may result in two problems: + * - there is no recipeUserId = input primaryUserId, in this case, + * this function will throw an error + * - There is a recipe userId = input primaryUserId, but that recipe has no email, + * or has wrong email compared to what the user wanted to generate a reset token for. + * + * And we want to allow primaryUserId being passed in. + */ + static createResetPasswordToken(tenantId: string, userId: string, email: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static getUserByEmail(tenantId: string, email: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ email, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, userContext: userContext === undefined ? {} : userContext, }); } - static createResetPasswordToken(tenantId: string, userId: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ - userId, - tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + static async resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext?: any) { + const consumeResp = await Wrapper.consumePasswordResetToken(tenantId, token, userContext); + + if (consumeResp.status !== "OK") { + return consumeResp; + } + + return await Wrapper.updateEmailOrPassword({ + recipeUserId: new RecipeUserId(consumeResp.userId), + email: consumeResp.email, + password: newPassword, + tenantIdForPasswordPolicy: tenantId, + userContext, }); } - static resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + static consumePasswordResetToken(tenantId: string, token: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, - newPassword, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, userContext: userContext === undefined ? {} : userContext, }); } static updateEmailOrPassword(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext?: any; @@ -83,8 +97,8 @@ export default class Wrapper { tenantIdForPasswordPolicy?: string; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, tenantIdForPasswordPolicy: input.tenantIdForPasswordPolicy === undefined ? DEFAULT_TENANT_ID : input.tenantIdForPasswordPolicy, }); @@ -93,9 +107,10 @@ export default class Wrapper { static async createResetPasswordLink( tenantId: string, userId: string, - userContext?: any + email: string, + userContext: any = {} ): Promise<{ status: "OK"; link: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { - let token = await createResetPasswordToken(tenantId, userId, userContext); + let token = await createResetPasswordToken(tenantId, userId, email, userContext); if (token.status === "UNKNOWN_USER_ID_ERROR") { return token; } @@ -115,16 +130,32 @@ export default class Wrapper { static async sendResetPasswordEmail( tenantId: string, userId: string, - userContext?: any + email: string, + userContext: any = {} ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }> { - let link = await createResetPasswordLink(tenantId, userId, userContext); + const user = await getUser(userId, userContext); + if (!user) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + + const loginMethod = user.loginMethods.find((m) => m.recipeId === "emailpassword" && m.hasSameEmailAs(email)); + if (!loginMethod) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + + let link = await createResetPasswordLink(tenantId, userId, email, userContext); if (link.status === "UNKNOWN_USER_ID_ERROR") { return link; } + await sendEmail({ passwordResetLink: link.link, type: "PASSWORD_RESET", - user: (await getUserById(userId, userContext))!, + user: { + id: user.id, + recipeUserId: loginMethod.recipeUserId, + email: loginMethod.email!, + }, tenantId, userContext, }); @@ -152,17 +183,15 @@ export let signUp = Wrapper.signUp; export let signIn = Wrapper.signIn; -export let getUserById = Wrapper.getUserById; - -export let getUserByEmail = Wrapper.getUserByEmail; - export let createResetPasswordToken = Wrapper.createResetPasswordToken; export let resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; +export let consumePasswordResetToken = Wrapper.consumePasswordResetToken; + export let updateEmailOrPassword = Wrapper.updateEmailOrPassword; -export type { RecipeInterface, User, APIOptions, APIInterface }; +export type { RecipeInterface, APIOptions, APIInterface }; export let createResetPasswordLink = Wrapper.createResetPasswordLink; diff --git a/lib/ts/recipe/emailpassword/passwordResetFunctions.ts b/lib/ts/recipe/emailpassword/passwordResetFunctions.ts index db1d78d11..d935f07be 100644 --- a/lib/ts/recipe/emailpassword/passwordResetFunctions.ts +++ b/lib/ts/recipe/emailpassword/passwordResetFunctions.ts @@ -13,13 +13,15 @@ * under the License. */ -import { User } from "./types"; import { NormalisedAppinfo } from "../../types"; import { postWithFetch } from "../../utils"; export async function createAndSendEmailUsingSupertokensService( appInfo: NormalisedAppinfo, - user: User, + user: { + id: string; + email: string; + }, passwordResetURLWithToken: string ) { // related issue: https://github.com/supertokens/supertokens-node/issues/38 diff --git a/lib/ts/recipe/emailpassword/recipe.ts b/lib/ts/recipe/emailpassword/recipe.ts index c4fe75f03..d5f194b51 100644 --- a/lib/ts/recipe/emailpassword/recipe.ts +++ b/lib/ts/recipe/emailpassword/recipe.ts @@ -32,16 +32,13 @@ import generatePasswordResetTokenAPI from "./api/generatePasswordResetToken"; import passwordResetAPI from "./api/passwordReset"; import { send200Response } from "../../utils"; import emailExistsAPI from "./api/emailExists"; -import EmailVerificationRecipe from "../emailverification/recipe"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; import { Querier } from "../../querier"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypeEmailPasswordEmailDeliveryInput } from "./types"; -import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -87,17 +84,8 @@ export default class Recipe extends RecipeModule { */ this.emailDelivery = ingredients.emailDelivery === undefined - ? new EmailDeliveryIngredient( - this.config.getEmailDeliveryConfig(this.recipeInterfaceImpl, this.isInServerlessEnv) - ) + ? new EmailDeliveryIngredient(this.config.getEmailDeliveryConfig(this.isInServerlessEnv)) : ingredients.emailDelivery; - - PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = EmailVerificationRecipe.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); } static getInstanceOrThrowError(): Recipe { @@ -219,18 +207,4 @@ export default class Recipe extends RecipeModule { isErrorFromThisRecipe = (err: any): err is STError => { return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; - - // extra instance functions below............... - getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - let userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }; } diff --git a/lib/ts/recipe/emailpassword/recipeImplementation.ts b/lib/ts/recipe/emailpassword/recipeImplementation.ts index 31265e005..3b4bdaf3c 100644 --- a/lib/ts/recipe/emailpassword/recipeImplementation.ts +++ b/lib/ts/recipe/emailpassword/recipeImplementation.ts @@ -1,168 +1,206 @@ -import { RecipeInterface, TypeNormalisedInput, User } from "./types"; +import { RecipeInterface, TypeNormalisedInput } from "./types"; +import AccountLinking from "../accountlinking/recipe"; import { Querier } from "../../querier"; import NormalisedURLPath from "../../normalisedURLPath"; +import { getUser } from "../.."; import { FORM_FIELD_PASSWORD_ID } from "./constants"; +import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; +import { User as UserType } from "../../types"; +import { LoginMethod, User } from "../../user"; export default function getRecipeInterface( querier: Querier, getEmailPasswordConfig: () => TypeNormalisedInput ): RecipeInterface { return { - signUp: async function ({ - email, - password, - tenantId, - }: { + signUp: async function ( + this: RecipeInterface, + { + email, + password, + tenantId, + userContext, + }: { + email: string; + password: string; + tenantId: string; + userContext: any; + } + ): Promise< + { status: "OK"; user: UserType; recipeUserId: RecipeUserId } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { + const response = await this.createNewRecipeUser({ + email, + password, + tenantId, + userContext, + }); + if (response.status !== "OK") { + return response; + } + + let updatedUser = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); + + return { + status: "OK", + user: updatedUser, + recipeUserId: response.recipeUserId, + }; + }, + + createNewRecipeUser: async function (input: { + tenantId: string; email: string; password: string; - tenantId: string; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { - let response = await querier.sendPostRequest( - new NormalisedURLPath(`/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/signup`), + userContext: any; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { + const resp = await querier.sendPostRequest( + new NormalisedURLPath( + `/${input.tenantId === undefined ? DEFAULT_TENANT_ID : input.tenantId}/recipe/signup` + ), { - email, - password, + email: input.email, + password: input.password, } ); - if (response.status === "OK") { - return response; - } else { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; + if (resp.status === "OK") { + resp.user = new User(resp.user); + resp.recipeUserId = new RecipeUserId(resp.recipeUserId); } + return resp; + + // we do not do email verification here cause it's a new user and email password + // users are always initially unverified. }, signIn: async function ({ email, password, tenantId, + userContext, }: { email: string; password: string; tenantId: string; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }> { - let response = await querier.sendPostRequest( + userContext: any; + }): Promise< + { status: "OK"; user: UserType; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" } + > { + const response = await querier.sendPostRequest( new NormalisedURLPath(`/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/signin`), { email, password, } ); - if (response.status === "OK") { - return response; - } else { - return { - status: "WRONG_CREDENTIALS_ERROR", - }; - } - }, - getUserById: async function ({ userId }: { userId: string }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user"), { - userId, - }); if (response.status === "OK") { - return { - ...response.user, - }; - } else { - return undefined; - } - }, + response.user = new User(response.user); + response.recipeUserId = new RecipeUserId(response.recipeUserId); - getUserByEmail: async function ({ - email, - tenantId, - }: { - email: string; - tenantId: string; - }): Promise { - let response = await querier.sendGetRequest( - new NormalisedURLPath(`/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/user`), - { - email, + const loginMethod: LoginMethod = response.user.loginMethods.find( + (lm: LoginMethod) => lm.recipeUserId.getAsString() === response.recipeUserId.getAsString() + ); + + if (!loginMethod.verified) { + await AccountLinking.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: response.user, + recipeUserId: response.recipeUserId, + userContext, + }); + + // Unlike in the sign up recipe function, we do not do account linking here + // cause we do not want sign in to change the potentially user ID of a user + // due to linking when this function is called by the dev in their API - + // for example in their update password API. If we did account linking + // then we would have to ask the dev to also change the session + // in such API calls. + // In the case of sign up, since we are creating a new user, it's fine + // to link there since there is no user id change really from the dev's + // point of view who is calling the sign up recipe function. + + // We do this so that we get the updated user (in case the above + // function updated the verification status) and can return that + response.user = (await getUser(response.recipeUserId!.getAsString(), userContext))!; } - ); - if (response.status === "OK") { - return { - ...response.user, - }; - } else { - return undefined; } + + return response; }, createResetPasswordToken: async function ({ userId, + email, tenantId, }: { userId: string; + email: string; tenantId: string; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { - let response = await querier.sendPostRequest( + // the input user ID can be a recipe or a primary user ID. + return await querier.sendPostRequest( new NormalisedURLPath( `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/user/password/reset/token` ), { userId, + email, } ); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } }, - resetPasswordUsingToken: async function ({ + consumePasswordResetToken: async function ({ token, - newPassword, tenantId, }: { token: string; - newPassword: string; tenantId: string; }): Promise< | { status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; + userId: string; + email: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } > { - let response = await querier.sendPostRequest( + return await querier.sendPostRequest( new NormalisedURLPath( - `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/user/password/reset` + `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/user/password/reset/token/consume` ), { method: "token", token, - newPassword, } ); - return response; }, updateEmailOrPassword: async function (input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy: string; + userContext: any; }): Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } > { if (input.applyPasswordPolicy || input.applyPasswordPolicy === undefined) { @@ -178,24 +216,39 @@ export default function getRecipeInterface( } } } - let response = await querier.sendPutRequest(new NormalisedURLPath("/recipe/user"), { - userId: input.userId, - email: input.email, - password: input.password, - }); + + // We do not check for AccountLinking.isEmailChangeAllowed here cause + // that may return false if the user's email is not verified, and this + // function should not fail due to lack of email verification - since it's + // really up to the developer to decide what should be the pre condition for + // a change in email. The check for email verification should actually go in + // an update email API (post login update). + + let response = await querier.sendPutRequest( + new NormalisedURLPath(`${input.tenantIdForPasswordPolicy}/recipe/user`), + { + recipeUserId: input.recipeUserId.getAsString(), + email: input.email, + password: input.password, + } + ); + if (response.status === "OK") { - return { - status: "OK", - }; - } else if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; + const user = await getUser(input.recipeUserId.getAsString(), input.userContext); + if (user === undefined) { + // This means that the user was deleted between the put and get requests + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + } + await AccountLinking.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user, + recipeUserId: input.recipeUserId, + userContext: input.userContext, + }); } + + return response; }, }; } diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index 5f9e88aa3..e8483de9f 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -13,7 +13,7 @@ * under the License. */ -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; import { @@ -21,13 +21,13 @@ import { TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; signInFeature: TypeNormalisedInputSignIn; getEmailDeliveryConfig: ( - recipeImpl: RecipeInterface, isInServerlessEnv: boolean ) => EmailDeliveryTypeInputWithService; resetPasswordUsingTokenFeature: TypeNormalisedInputResetPasswordUsingTokenFeature; @@ -71,13 +71,6 @@ export type TypeNormalisedInputResetPasswordUsingTokenFeature = { formFieldsForPasswordResetForm: NormalisedFormField[]; }; -export type User = { - id: string; - email: string; - timeJoined: number; - tenantIds: string[]; -}; - export type TypeInput = { signUpFeature?: TypeInputSignUp; emailDelivery?: EmailDeliveryTypeInput; @@ -96,44 +89,69 @@ export type RecipeInterface = { password: string; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + >; - signIn(input: { + // this function is meant only for creating the recipe in the core and nothing else. + // we added this even though signUp exists cause devs may override signup expecting it + // to be called just during sign up. But we also need a version of signing up which can be + // called during operations like creating a user during password reset flow. + createNewRecipeUser(input: { email: string; password: string; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>; - - getUserById(input: { userId: string; userContext: any }): Promise; + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + >; - getUserByEmail(input: { email: string; tenantId: string; userContext: any }): Promise; + signIn(input: { + email: string; + password: string; + tenantId: string; + userContext: any; + }): Promise<{ status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" }>; + /** + * We pass in the email as well to this function cause the input userId + * may not be associated with an emailpassword account. In this case, we + * need to know which email to use to create an emailpassword account later on. + */ createResetPasswordToken(input: { - userId: string; + userId: string; // the id can be either recipeUserId or primaryUserId + email: string; tenantId: string; userContext: any; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; - resetPasswordUsingToken(input: { + consumePasswordResetToken(input: { token: string; - newPassword: string; tenantId: string; userContext: any; }): Promise< | { status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; + email: string; + userId: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } >; updateEmailOrPassword(input: { - userId: string; + recipeUserId: RecipeUserId; // the id should only be a recipeUserId cause if we give just an id + // and a password, and if there are multiple emailpassword accounts, we do not know + // for which one to update the password for. email?: string; password?: string; userContext: any; @@ -143,6 +161,10 @@ export type RecipeInterface = { | { status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } >; }; @@ -188,6 +210,10 @@ export type APIInterface = { | { status: "OK"; } + | { + status: "PASSWORD_RESET_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); @@ -205,11 +231,13 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - userId?: string; + email: string; + user: User; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; } + | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } | GeneralErrorResponse >); @@ -229,6 +257,10 @@ export type APIInterface = { user: User; session: SessionContainerInterface; } + | { + status: "SIGN_IN_NOT_ALLOWED"; + reason: string; + } | { status: "WRONG_CREDENTIALS_ERROR"; } @@ -251,6 +283,10 @@ export type APIInterface = { user: User; session: SessionContainerInterface; } + | { + status: "SIGN_UP_NOT_ALLOWED"; + reason: string; + } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } @@ -262,6 +298,7 @@ export type TypeEmailPasswordPasswordResetEmailDeliveryInput = { type: "PASSWORD_RESET"; user: { id: string; + recipeUserId: RecipeUserId | undefined; email: string; }; passwordResetLink: string; diff --git a/lib/ts/recipe/emailpassword/utils.ts b/lib/ts/recipe/emailpassword/utils.ts index 47c02c934..56ada8f62 100644 --- a/lib/ts/recipe/emailpassword/utils.ts +++ b/lib/ts/recipe/emailpassword/utils.ts @@ -50,15 +50,14 @@ export function validateAndNormaliseUserInput( ...config?.override, }; - function getEmailDeliveryConfig(recipeImpl: RecipeInterface, isInServerlessEnv: boolean) { + function getEmailDeliveryConfig(isInServerlessEnv: boolean) { let emailService = config?.emailDelivery?.service; /** - * following code is for backward compatibility. - * if user has not passed emailService config, we use the default - * createAndSendEmailUsingSupertokensService implementation which calls our supertokens API + * If the user has not passed even that config, we use the default + * createAndSendCustomEmail implementation which calls our supertokens API */ if (emailService === undefined) { - emailService = new BackwardCompatibilityService(recipeImpl, appInfo, isInServerlessEnv); + emailService = new BackwardCompatibilityService(appInfo, isInServerlessEnv); } return { ...config?.emailDelivery, diff --git a/lib/ts/recipe/emailverification/api/emailVerify.ts b/lib/ts/recipe/emailverification/api/emailVerify.ts index e75ce2de8..09e38e82e 100644 --- a/lib/ts/recipe/emailverification/api/emailVerify.ts +++ b/lib/ts/recipe/emailverification/api/emailVerify.ts @@ -62,6 +62,9 @@ export default async function emailVerify( userContext, }); if (response.status === "OK") { + // if there is a new session, it will be + // automatically added to the response by the createNewSession function call + // inside the verifyEmailPOST function. result = { status: "OK" }; } else { result = response; @@ -79,7 +82,7 @@ export default async function emailVerify( ); result = await apiImplementation.isEmailVerifiedGET({ options, - session: session!, + session, userContext, }); } diff --git a/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts b/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts index dd942dd45..ed53e92bd 100644 --- a/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts +++ b/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts @@ -36,7 +36,7 @@ export default async function generateEmailVerifyToken( const result = await apiImplementation.generateEmailVerifyTokenPOST({ options, - session: session!, + session: session, userContext, }); diff --git a/lib/ts/recipe/emailverification/api/implementation.ts b/lib/ts/recipe/emailverification/api/implementation.ts index 3d4cddfc3..2dd7c808e 100644 --- a/lib/ts/recipe/emailverification/api/implementation.ts +++ b/lib/ts/recipe/emailverification/api/implementation.ts @@ -1,127 +1,192 @@ -import { APIInterface, User } from "../"; +import { APIInterface, UserEmailInfo } from "../"; import { logDebugMessage } from "../../../logger"; import EmailVerificationRecipe from "../recipe"; import { GeneralErrorResponse } from "../../../types"; import { EmailVerificationClaim } from "../emailVerificationClaim"; import SessionError from "../../session/error"; import { getEmailVerifyLink } from "../utils"; +import { SessionContainerInterface } from "../../session/types"; export default function getAPIInterface(): APIInterface { return { - verifyEmailPOST: async function ({ - token, - tenantId, - options, - session, - userContext, - }): Promise< - { status: "OK"; user: User } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } | GeneralErrorResponse + verifyEmailPOST: async function ( + this: APIInterface, + { token, tenantId, options, session, userContext } + ): Promise< + | { status: "OK"; user: UserEmailInfo; newSession?: SessionContainerInterface } + | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } + | GeneralErrorResponse > { - const res = await options.recipeImplementation.verifyEmailUsingToken({ tenantId, token, userContext }); + const verifyTokenResponse = await options.recipeImplementation.verifyEmailUsingToken({ + token, + tenantId, + attemptAccountLinking: true, + userContext, + }); - if (res.status === "OK" && session !== undefined) { - try { - await session.fetchAndSetClaim(EmailVerificationClaim, userContext); - } catch (err) { - // This should never happen, since we've just set the status above. - if ((err as Error).message === "UNKNOWN_USER_ID") { - logDebugMessage( - "verifyEmailPOST: Returning UNAUTHORISED because the user id provided is unknown" - ); - throw new SessionError({ - type: SessionError.UNAUTHORISED, - message: "Unknown User ID provided", - }); - } - throw err; - } + if (verifyTokenResponse.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { + return verifyTokenResponse; } - return res; + + // status: "OK" + let newSession = await EmailVerificationRecipe.getInstanceOrThrowError().updateSessionIfRequiredPostEmailVerification( + { + req: options.req, + res: options.res, + session, + recipeUserIdWhoseEmailGotVerified: verifyTokenResponse.user.recipeUserId, + userContext, + } + ); + + return { + status: "OK", + user: verifyTokenResponse.user, + newSession, + }; }, - isEmailVerifiedGET: async function ({ - userContext, - session, - }): Promise< + isEmailVerifiedGET: async function ( + this: APIInterface, + { userContext, session, options } + ): Promise< | { status: "OK"; isVerified: boolean; + newSession?: SessionContainerInterface; } | GeneralErrorResponse > { - if (session === undefined) { - throw new Error("Session is undefined. Should not come here."); - } + // In this API, we will check if the session's recipe user id's email is verified or not. - try { - await session.fetchAndSetClaim(EmailVerificationClaim, userContext); - } catch (err) { - if ((err as Error).message === "UNKNOWN_USER_ID") { - logDebugMessage( - "isEmailVerifiedGET: Returning UNAUTHORISED because the user id provided is unknown" - ); - throw new SessionError({ - type: SessionError.UNAUTHORISED, - message: "Unknown User ID provided", - }); - } - throw err; - } - const isVerified = await session.getClaimValue(EmailVerificationClaim, userContext); + const emailInfo = await EmailVerificationRecipe.getInstanceOrThrowError().getEmailForRecipeUserId( + undefined, + session.getRecipeUserId(), + userContext + ); - if (isVerified === undefined) { - throw new Error("Should never come here: EmailVerificationClaim failed to set value"); - } + if (emailInfo.status === "OK") { + const isVerified = await options.recipeImplementation.isEmailVerified({ + recipeUserId: session.getRecipeUserId(), + email: emailInfo.email, + userContext, + }); - return { - status: "OK", - isVerified, - }; - }, + if (isVerified) { + // here we do the same things we do for post email verification + // cause email verification could happen in a different browser + // whilst the first browser is polling this API - in this case, + // we want to have the same effect to the session as if the + // email was opened on the original browser itself. + let newSession = await EmailVerificationRecipe.getInstanceOrThrowError().updateSessionIfRequiredPostEmailVerification( + { + req: options.req, + res: options.res, + session, + recipeUserIdWhoseEmailGotVerified: session.getRecipeUserId(), + userContext, + } + ); + return { + status: "OK", + isVerified: true, + newSession, + }; + } else { + if ((await session.getClaimValue(EmailVerificationClaim)) !== false) { + await session.setClaimValue(EmailVerificationClaim, false, userContext); + } - generateEmailVerifyTokenPOST: async function ({ - options, - userContext, - session, - }): Promise<{ status: "OK" | "EMAIL_ALREADY_VERIFIED_ERROR" } | GeneralErrorResponse> { - if (session === undefined) { - throw new Error("Session is undefined. Should not come here."); + return { + status: "OK", + isVerified: false, + }; + } + } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { + // We consider people without email addresses as validated + return { + status: "OK", + isVerified: true, + }; + } else { + // this means that the user ID is not known to supertokens. This could + // happen if the current session's user ID is not an auth user, + // or if it belong to a recipe user ID that got deleted. Either way, + // we logout the user. + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Unknown User ID provided", + }); } + }, - const userId = session.getUserId(); + generateEmailVerifyTokenPOST: async function ( + this: APIInterface, + { options, userContext, session } + ): Promise< + | { status: "OK" } + | { status: "EMAIL_ALREADY_VERIFIED_ERROR"; newSession?: SessionContainerInterface } + | GeneralErrorResponse + > { + // In this API, we generate the email verification token for session's recipe user ID. const tenantId = session.getTenantId(); - const emailInfo = await EmailVerificationRecipe.getInstanceOrThrowError().getEmailForUserId( - userId, + const emailInfo = await EmailVerificationRecipe.getInstanceOrThrowError().getEmailForRecipeUserId( + undefined, + session.getRecipeUserId(), userContext ); if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { logDebugMessage( - `Email verification email not sent to user ${userId} because it doesn't have an email address.` + `Email verification email not sent to user ${session + .getRecipeUserId() + .getAsString()} because it doesn't have an email address.` + ); + // this can happen if the user ID was found, but it has no email. In this + // case, we treat it as a success case. + let newSession = await EmailVerificationRecipe.getInstanceOrThrowError().updateSessionIfRequiredPostEmailVerification( + { + req: options.req, + res: options.res, + session, + recipeUserIdWhoseEmailGotVerified: session.getRecipeUserId(), + userContext, + } ); return { status: "EMAIL_ALREADY_VERIFIED_ERROR", + newSession, }; } else if (emailInfo.status === "OK") { let response = await options.recipeImplementation.createEmailVerificationToken({ - userId, + recipeUserId: session.getRecipeUserId(), email: emailInfo.email, tenantId, userContext, }); + // In case the email is already verified, we do the same thing + // as what happens in the verifyEmailPOST API post email verification (cause maybe the session is outdated). if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - if ((await session.getClaimValue(EmailVerificationClaim)) !== true) { - // this can happen if the email was verified in another browser - // and this session is still outdated - and the user has not - // called the get email verification API yet. - await session.fetchAndSetClaim(EmailVerificationClaim, userContext); - } logDebugMessage( - `Email verification email not sent to ${emailInfo.email} because it is already verified.` + `Email verification email not sent to user ${session + .getRecipeUserId() + .getAsString()} because it is already verified.` + ); + let newSession = await EmailVerificationRecipe.getInstanceOrThrowError().updateSessionIfRequiredPostEmailVerification( + { + req: options.req, + res: options.res, + session, + recipeUserIdWhoseEmailGotVerified: session.getRecipeUserId(), + userContext, + } ); - return response; + return { + status: "EMAIL_ALREADY_VERIFIED_ERROR", + newSession, + }; } if ((await session.getClaimValue(EmailVerificationClaim)) !== false) { @@ -142,7 +207,8 @@ export default function getAPIInterface(): APIInterface { await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ type: "EMAIL_VERIFICATION", user: { - id: userId, + id: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: emailInfo.email, }, emailVerifyLink, @@ -154,6 +220,10 @@ export default function getAPIInterface(): APIInterface { status: "OK", }; } else { + // this means that the user ID is not known to supertokens. This could + // happen if the current session's user ID is not an auth user, + // or if it belong to a recipe user ID that got deleted. Either way, + // we logout the user. logDebugMessage( "generateEmailVerifyTokenPOST: Returning UNAUTHORISED because the user id provided is unknown" ); diff --git a/lib/ts/recipe/emailverification/emailVerificationClaim.ts b/lib/ts/recipe/emailverification/emailVerificationClaim.ts index aade6bc46..abec3146d 100644 --- a/lib/ts/recipe/emailverification/emailVerificationClaim.ts +++ b/lib/ts/recipe/emailverification/emailVerificationClaim.ts @@ -9,12 +9,16 @@ export class EmailVerificationClaimClass extends BooleanClaim { constructor() { super({ key: "st-ev", - async fetchValue(userId, _, userContext) { + async fetchValue(_userId, recipeUserId, __tenantId, userContext) { const recipe = EmailVerificationRecipe.getInstanceOrThrowError(); - let emailInfo = await recipe.getEmailForUserId(userId, userContext); + let emailInfo = await recipe.getEmailForRecipeUserId(undefined, recipeUserId, userContext); if (emailInfo.status === "OK") { - return recipe.recipeInterfaceImpl.isEmailVerified({ userId, email: emailInfo.email, userContext }); + return recipe.recipeInterfaceImpl.isEmailVerified({ + recipeUserId, + email: emailInfo.email, + userContext, + }); } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { // We consider people without email addresses as validated return true; diff --git a/lib/ts/recipe/emailverification/emailVerificationFunctions.ts b/lib/ts/recipe/emailverification/emailVerificationFunctions.ts index 06b2e0554..ff8dc9ec1 100644 --- a/lib/ts/recipe/emailverification/emailVerificationFunctions.ts +++ b/lib/ts/recipe/emailverification/emailVerificationFunctions.ts @@ -13,13 +13,13 @@ * under the License. */ -import { User } from "./types"; +import { UserEmailInfo } from "./types"; import { NormalisedAppinfo } from "../../types"; import { postWithFetch } from "../../utils"; export async function createAndSendEmailUsingSupertokensService( appInfo: NormalisedAppinfo, - user: User, + user: UserEmailInfo, emailVerifyURLWithToken: string ) { if (process.env.TEST_MODE === "testing") { diff --git a/lib/ts/recipe/emailverification/index.ts b/lib/ts/recipe/emailverification/index.ts index 679438081..2e7f55f07 100644 --- a/lib/ts/recipe/emailverification/index.ts +++ b/lib/ts/recipe/emailverification/index.ts @@ -15,8 +15,15 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, APIOptions, APIInterface, User, TypeEmailVerificationEmailDeliveryInput } from "./types"; +import { + RecipeInterface, + APIOptions, + APIInterface, + UserEmailInfo, + TypeEmailVerificationEmailDeliveryInput, +} from "./types"; import { EmailVerificationClaim } from "./emailVerificationClaim"; +import RecipeUserId from "../../recipeUserId"; import { getEmailVerifyLink } from "./utils"; export default class Wrapper { @@ -28,9 +35,9 @@ export default class Wrapper { static async createEmailVerificationToken( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, email?: string, - userContext?: any + userContext: any = {} ): Promise< | { status: "OK"; @@ -41,7 +48,7 @@ export default class Wrapper { const recipeInstance = Recipe.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -54,18 +61,18 @@ export default class Wrapper { } return await recipeInstance.recipeInterfaceImpl.createEmailVerificationToken({ - userId, + recipeUserId, email: email!, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext, }); } static async createEmailVerificationLink( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, email?: string, - userContext?: any + userContext: any = {} ): Promise< | { status: "OK"; @@ -76,7 +83,7 @@ export default class Wrapper { const recipeInstance = Recipe.getInstanceOrThrowError(); const appInfo = recipeInstance.getAppInfo(); - let emailVerificationToken = await createEmailVerificationToken(tenantId, userId, email, userContext); + let emailVerificationToken = await createEmailVerificationToken(tenantId, recipeUserId, email, userContext); if (emailVerificationToken.status === "EMAIL_ALREADY_VERIFIED_ERROR") { return { status: "EMAIL_ALREADY_VERIFIED_ERROR", @@ -97,8 +104,9 @@ export default class Wrapper { static async sendEmailVerificationEmail( tenantId: string, userId: string, + recipeUserId: RecipeUserId, email?: string, - userContext?: any + userContext: any = {} ): Promise< | { status: "OK"; @@ -108,7 +116,7 @@ export default class Wrapper { if (email === undefined) { const recipeInstance = Recipe.getInstanceOrThrowError(); - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -120,7 +128,7 @@ export default class Wrapper { } } - let emailVerificationLink = await this.createEmailVerificationLink(tenantId, userId, email, userContext); + let emailVerificationLink = await this.createEmailVerificationLink(tenantId, recipeUserId, email, userContext); if (emailVerificationLink.status === "EMAIL_ALREADY_VERIFIED_ERROR") { return { @@ -132,6 +140,7 @@ export default class Wrapper { type: "EMAIL_VERIFICATION", user: { id: userId, + recipeUserId: recipeUserId, email: email!, }, emailVerifyLink: emailVerificationLink.link, @@ -143,18 +152,24 @@ export default class Wrapper { }; } - static async verifyEmailUsingToken(tenantId: string, token: string, userContext?: any) { + static async verifyEmailUsingToken( + tenantId: string, + token: string, + attemptAccountLinking: boolean = true, + userContext: any = {} + ) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ token, tenantId, - userContext: userContext === undefined ? {} : userContext, + attemptAccountLinking, + userContext, }); } - static async isEmailVerified(userId: string, email?: string, userContext?: any) { + static async isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext: any = {}) { const recipeInstance = Recipe.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); if (emailInfo.status === "OK") { email = emailInfo.email; @@ -166,20 +181,25 @@ export default class Wrapper { } return await recipeInstance.recipeInterfaceImpl.isEmailVerified({ - userId, - email: email!, - userContext: userContext === undefined ? {} : userContext, + recipeUserId, + email, + userContext, }); } - static async revokeEmailVerificationTokens(tenantId: string, userId: string, email?: string, userContext?: any) { + static async revokeEmailVerificationTokens( + tenantId: string, + recipeUserId: RecipeUserId, + email?: string, + userContext: any = {} + ) { const recipeInstance = Recipe.getInstanceOrThrowError(); // If the dev wants to delete the tokens for an old email address of the user they can pass the address // but redeeming those tokens would have no effect on isEmailVerified called without the old address // so in general that is not necessary either. if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -193,19 +213,18 @@ export default class Wrapper { throw new global.Error("Unknown User ID provided without email"); } } - return await recipeInstance.recipeInterfaceImpl.revokeEmailVerificationTokens({ - userId, + recipeUserId, email: email!, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext, }); } - static async unverifyEmail(userId: string, email?: string, userContext?: any) { + static async unverifyEmail(recipeUserId: RecipeUserId, email?: string, userContext: any = {}) { const recipeInstance = Recipe.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -218,18 +237,17 @@ export default class Wrapper { } } return await recipeInstance.recipeInterfaceImpl.unverifyEmail({ - userId, - email: email!, - userContext: userContext === undefined ? {} : userContext, + recipeUserId, + email, + userContext, }); } static async sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: any }) { let recipeInstance = Recipe.getInstanceOrThrowError(); return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, ...input, - tenantId: input.tenantId, + userContext: input.userContext ?? {}, }); } } @@ -252,7 +270,7 @@ export let revokeEmailVerificationTokens = Wrapper.revokeEmailVerificationTokens export let unverifyEmail = Wrapper.unverifyEmail; -export type { RecipeInterface, APIOptions, APIInterface, User }; +export type { RecipeInterface, APIOptions, APIInterface, UserEmailInfo }; export let sendEmail = Wrapper.sendEmail; diff --git a/lib/ts/recipe/emailverification/recipe.ts b/lib/ts/recipe/emailverification/recipe.ts index 216746b56..1ebd6e5bf 100644 --- a/lib/ts/recipe/emailverification/recipe.ts +++ b/lib/ts/recipe/emailverification/recipe.ts @@ -14,7 +14,7 @@ */ import RecipeModule from "../../recipeModule"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, GetEmailForUserIdFunc } from "./types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, GetEmailForRecipeUserIdFunc } from "./types"; import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; import STError from "./error"; import { validateAndNormaliseUserInput } from "./utils"; @@ -25,13 +25,19 @@ import emailVerifyAPI from "./api/emailVerify"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; import { Querier } from "../../querier"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypeEmailVerificationEmailDeliveryInput } from "./types"; import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; import SessionRecipe from "../session/recipe"; import { EmailVerificationClaim } from "./emailVerificationClaim"; +import { SessionContainerInterface } from "../session/types"; +import SessionError from "../session/error"; +import Session from "../session"; +import { getUser } from "../.."; +import RecipeUserId from "../../recipeUserId"; +import { logDebugMessage } from "../../logger"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -47,8 +53,6 @@ export default class Recipe extends RecipeModule { emailDelivery: EmailDeliveryIngredient; - getEmailForUserIdFuncsFromOtherRecipes: GetEmailForUserIdFunc[] = []; - constructor( recipeId: string, appInfo: NormalisedAppinfo, @@ -63,7 +67,9 @@ export default class Recipe extends RecipeModule { this.isInServerlessEnv = isInServerlessEnv; { - let builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))); + let builder = new OverrideableBuilder( + RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId), this.getEmailForRecipeUserId) + ); this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); } { @@ -188,18 +194,37 @@ export default class Recipe extends RecipeModule { return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; - getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - if (this.config.getEmailForUserId !== undefined) { - const userRes = await this.config.getEmailForUserId(userId, userContext); + getEmailForRecipeUserId: GetEmailForRecipeUserIdFunc = async (user, recipeUserId, userContext) => { + if (this.config.getEmailForRecipeUserId !== undefined) { + const userRes = await this.config.getEmailForRecipeUserId(recipeUserId, userContext); if (userRes.status !== "UNKNOWN_USER_ID_ERROR") { return userRes; } } - for (const getEmailForUserId of this.getEmailForUserIdFuncsFromOtherRecipes) { - const res = await getEmailForUserId(userId, userContext); - if (res.status !== "UNKNOWN_USER_ID_ERROR") { - return res; + if (user === undefined) { + user = await getUser(recipeUserId.getAsString(), userContext); + + if (user === undefined) { + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + } + } + + for (let i = 0; i < user.loginMethods.length; i++) { + let currLM = user.loginMethods[i]; + if (currLM.recipeUserId.getAsString() === recipeUserId.getAsString()) { + if (currLM.email !== undefined) { + return { + email: currLM.email, + status: "OK", + }; + } else { + return { + status: "EMAIL_DOES_NOT_EXIST_ERROR", + }; + } } } @@ -208,7 +233,149 @@ export default class Recipe extends RecipeModule { }; }; - addGetEmailForUserIdFunc = (func: GetEmailForUserIdFunc): void => { - this.getEmailForUserIdFuncsFromOtherRecipes.push(func); + getPrimaryUserIdForRecipeUser = async (recipeUserId: RecipeUserId, userContext: any): Promise => { + // We extract this into its own function like this cause we want to make sure that + // this recipe does not get the email of the user ID from the getUser function. + // In fact, there is a test "email verification recipe uses getUser function only in getEmailForRecipeUserId" + // which makes sure that this function is only called in 3 places in this recipe: + // - this function + // - getEmailForRecipeUserId function (above) + // - after verification to get the updated user in verifyEmailUsingToken + // We want to isolate the result of calling this function as much as possible + // so that the consumer of the getUser function does not read the email + // from the primaryUser. Hence, this function only returns the string ID + // and nothing else from the primaryUser. + let primaryUser = await getUser(recipeUserId.getAsString(), userContext); + if (primaryUser === undefined) { + // This can come here if the user is using session + email verification + // recipe with a user ID that is not known to supertokens. In this case, + // we do not allow linking for such users. + return recipeUserId.getAsString(); + } + return primaryUser?.id; + }; + + updateSessionIfRequiredPostEmailVerification = async (input: { + req: BaseRequest; + res: BaseResponse; + session: SessionContainerInterface | undefined; + recipeUserIdWhoseEmailGotVerified: RecipeUserId; + userContext: any; + }): Promise => { + let primaryUserId = await this.getPrimaryUserIdForRecipeUser( + input.recipeUserIdWhoseEmailGotVerified, + input.userContext + ); + + // if a session exists in the API, then we can update the session + // claim related to email verification + if (input.session !== undefined) { + logDebugMessage("updateSessionIfRequiredPostEmailVerification got session"); + // Due to linking, we will have to correct the current + // session's user ID. There are four cases here: + // --> (Case 1) User signed up and did email verification and the new account + // became a primary user (user ID no change) + // --> (Case 2) User signed up and did email verification and the new account got linked + // to another primary user (user ID change) + // --> (Case 3) This is post login account linking, in which the account that got verified + // got linked to the session's account (user ID of account has changed to the session's user ID) + // --> (Case 4) This is post login account linking, in which the account that got verified + // got linked to ANOTHER primary account (user ID of account has changed to a different user ID != session.getUserId, but + // we should ignore this since it will result in the user's session changing.) + + if ( + input.session.getRecipeUserId().getAsString() === input.recipeUserIdWhoseEmailGotVerified.getAsString() + ) { + logDebugMessage( + "updateSessionIfRequiredPostEmailVerification the session belongs to the verified user" + ); + // this means that the session's login method's account is the + // one that just got verified and that we are NOT doing post login + // account linking. So this is only for (Case 1) and (Case 2) + + if (input.session.getUserId() === primaryUserId) { + logDebugMessage( + "updateSessionIfRequiredPostEmailVerification the session userId matches the primary user id, so we are only refreshing the claim" + ); + // if the session's primary user ID is equal to the + // primary user ID that the account was linked to, then + // this means that the new account became a primary user (Case 1) + // We also have the sub cases here that the account that just + // got verified was already linked to the session's primary user ID, + // but either way, we don't need to change any user ID. + + // In this case, all we do is to update the emailverification claim if it's + // not already set to true (it is ok to assume true cause this function + // is only called when the email is verified). + if ((await input.session.getClaimValue(EmailVerificationClaim)) !== true) { + try { + // EmailVerificationClaim will be based on the recipeUserId + // and not the primary user ID. + await input.session.fetchAndSetClaim(EmailVerificationClaim, input.userContext); + } catch (err) { + // This should never happen, since we've just set the status above. + if ((err as Error).message === "UNKNOWN_USER_ID") { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Unknown User ID provided", + }); + } + throw err; + } + } + + return; + } else { + logDebugMessage( + "updateSessionIfRequiredPostEmailVerification the session user id doesn't match the primary user id, so we are revoking all sessions and creating a new one" + ); + // if the session's primary user ID is NOT equal to the + // primary user ID that the account that it was linked to, then + // this means that the new account got linked to another primary user (Case 2) + + // In this case, we need to update the session's user ID by creating + // a new session + + // Revoke all session belonging to session.getRecipeUserId() + // We do not really need to do this, but we do it anyway.. no harm. + await Session.revokeAllSessionsForUser( + input.recipeUserIdWhoseEmailGotVerified.getAsString(), + false, + input.session.getTenantId(), + input.userContext + ); + + // create a new session and return that.. + return await Session.createNewSession( + input.req, + input.res, + input.session.getTenantId(), + input.session.getRecipeUserId(), + {}, + {}, + input.userContext + ); + } + } else { + logDebugMessage( + "updateSessionIfRequiredPostEmailVerification the verified user doesn't match the session" + ); + // this means that the session's login method's account was NOT the + // one that just got verified and that we ARE doing post login + // account linking. So this is only for (Case 3) and (Case 4) + + // In both case 3 and case 4, we do not want to change anything in the + // current session in terms of user ID or email verification claim (since + // both of these refer to the current logged in user and not the newly + // linked user's account). + + return undefined; + } + } else { + logDebugMessage("updateSessionIfRequiredPostEmailVerification got no session"); + // the session is updated when the is email verification GET API is called + // so we don't do anything in this API. + return undefined; + } }; } diff --git a/lib/ts/recipe/emailverification/recipeImplementation.ts b/lib/ts/recipe/emailverification/recipeImplementation.ts index 239ba5314..5736645b6 100644 --- a/lib/ts/recipe/emailverification/recipeImplementation.ts +++ b/lib/ts/recipe/emailverification/recipeImplementation.ts @@ -1,15 +1,22 @@ -import { RecipeInterface, User } from "./"; +import { RecipeInterface } from "./"; import { Querier } from "../../querier"; import NormalisedURLPath from "../../normalisedURLPath"; +import RecipeUserId from "../../recipeUserId"; +import { GetEmailForRecipeUserIdFunc, UserEmailInfo } from "./types"; +import type AccountLinkingRecipe from "../accountlinking/recipe"; +import { getUser } from "../.."; -export default function getRecipeInterface(querier: Querier): RecipeInterface { +export default function getRecipeInterface( + querier: Querier, + getEmailForRecipeUserId: GetEmailForRecipeUserIdFunc +): RecipeInterface { return { createEmailVerificationToken: async function ({ - userId, + recipeUserId, email, tenantId, }: { - userId: string; + recipeUserId: RecipeUserId; email: string; tenantId: string; }): Promise< @@ -22,7 +29,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { let response = await querier.sendPostRequest( new NormalisedURLPath(`/${tenantId}/recipe/user/email/verify/token`), { - userId, + userId: recipeUserId.getAsString(), email, } ); @@ -40,11 +47,15 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { verifyEmailUsingToken: async function ({ token, + attemptAccountLinking, tenantId, + userContext, }: { token: string; + attemptAccountLinking: boolean; tenantId: string; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }> { + userContext: any; + }): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }> { let response = await querier.sendPostRequest( new NormalisedURLPath(`/${tenantId}/recipe/user/email/verify`), { @@ -53,10 +64,34 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { } ); if (response.status === "OK") { + const recipeUserId = new RecipeUserId(response.userId); + if (attemptAccountLinking) { + // TODO: this should ideally come from the api response + const updatedUser = await getUser(recipeUserId.getAsString()); + + if (updatedUser) { + // before attempting this, we must check that the email that got verified + // from the ID is the one that is currently associated with the ID (since + // email verification can be done for any combination of (user id, email) + // and not necessarily the email that is currently associated with the ID) + let emailInfo = await getEmailForRecipeUserId(updatedUser, recipeUserId, userContext); + if (emailInfo.status === "OK" && emailInfo.email === response.email) { + // we do this here to prevent cyclic dependencies. + // TODO: Fix this. + let AccountLinking = require("../accountlinking/recipe").default.getInstance() as AccountLinkingRecipe; + await AccountLinking.createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: updatedUser, + userContext, + }); + } + } + } + return { status: "OK", user: { - id: response.userId, + recipeUserId, email: response.email, }, }; @@ -67,32 +102,41 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { } }, - isEmailVerified: async function ({ userId, email }: { userId: string; email: string }): Promise { + isEmailVerified: async function ({ + recipeUserId, + email, + }: { + recipeUserId: RecipeUserId; + email: string; + }): Promise { let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user/email/verify"), { - userId, + userId: recipeUserId.getAsString(), email, }); return response.isVerified; }, revokeEmailVerificationTokens: async function (input: { - userId: string; + recipeUserId: RecipeUserId; email: string; tenantId: string; }): Promise<{ status: "OK" }> { await querier.sendPostRequest( new NormalisedURLPath(`/${input.tenantId}/recipe/user/email/verify/token/remove`), { - userId: input.userId, + userId: input.recipeUserId.getAsString(), email: input.email, } ); return { status: "OK" }; }, - unverifyEmail: async function (input: { userId: string; email: string }): Promise<{ status: "OK" }> { + unverifyEmail: async function (input: { + recipeUserId: RecipeUserId; + email: string; + }): Promise<{ status: "OK" }> { await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/email/verify/remove"), { - userId: input.userId, + userId: input.recipeUserId.getAsString(), email: input.email, }); return { status: "OK" }; diff --git a/lib/ts/recipe/emailverification/types.ts b/lib/ts/recipe/emailverification/types.ts index f88821e91..06d3bd5fa 100644 --- a/lib/ts/recipe/emailverification/types.ts +++ b/lib/ts/recipe/emailverification/types.ts @@ -13,7 +13,7 @@ * under the License. */ -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { TypeInput as EmailDeliveryTypeInput, @@ -22,12 +22,14 @@ import { import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; import { SessionContainerInterface } from "../session/types"; +import RecipeUserId from "../../recipeUserId"; +import { User } from "../../types"; export type TypeInput = { mode: "REQUIRED" | "OPTIONAL"; emailDelivery?: EmailDeliveryTypeInput; - getEmailForUserId?: ( - userId: string, + getEmailForRecipeUserId?: ( + recipeUserId: RecipeUserId, userContext: any ) => Promise< | { @@ -50,8 +52,8 @@ export type TypeNormalisedInput = { getEmailDeliveryConfig: ( isInServerlessEnv: boolean ) => EmailDeliveryTypeInputWithService; - getEmailForUserId?: ( - userId: string, + getEmailForRecipeUserId?: ( + recipeUserId: RecipeUserId, userContext: any ) => Promise< | { @@ -69,14 +71,14 @@ export type TypeNormalisedInput = { }; }; -export type User = { - id: string; +export type UserEmailInfo = { + recipeUserId: RecipeUserId; email: string; }; export type RecipeInterface = { createEmailVerificationToken(input: { - userId: string; + recipeUserId: RecipeUserId; // must be a recipeUserId email: string; tenantId: string; userContext: any; @@ -90,20 +92,21 @@ export type RecipeInterface = { verifyEmailUsingToken(input: { token: string; + attemptAccountLinking: boolean; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>; + }): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>; - isEmailVerified(input: { userId: string; email: string; userContext: any }): Promise; + isEmailVerified(input: { recipeUserId: RecipeUserId; email: string; userContext: any }): Promise; revokeEmailVerificationTokens(input: { - userId: string; + recipeUserId: RecipeUserId; email: string; tenantId: string; userContext: any; }): Promise<{ status: "OK" }>; - unverifyEmail(input: { userId: string; email: string; userContext: any }): Promise<{ status: "OK" }>; + unverifyEmail(input: { recipeUserId: RecipeUserId; email: string; userContext: any }): Promise<{ status: "OK" }>; }; export type APIOptions = { @@ -127,7 +130,9 @@ export type APIInterface = { userContext: any; session?: SessionContainerInterface; }) => Promise< - { status: "OK"; user: User } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } | GeneralErrorResponse + | { status: "OK"; user: UserEmailInfo; newSession?: SessionContainerInterface } + | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } + | GeneralErrorResponse >); isEmailVerifiedGET: @@ -140,6 +145,7 @@ export type APIInterface = { | { status: "OK"; isVerified: boolean; + newSession?: SessionContainerInterface; } | GeneralErrorResponse >); @@ -150,21 +156,30 @@ export type APIInterface = { options: APIOptions; userContext: any; session: SessionContainerInterface; - }) => Promise<{ status: "EMAIL_ALREADY_VERIFIED_ERROR" | "OK" } | GeneralErrorResponse>); + }) => Promise< + | { status: "OK" } + | { status: "EMAIL_ALREADY_VERIFIED_ERROR"; newSession?: SessionContainerInterface } + | GeneralErrorResponse + >); }; export type TypeEmailVerificationEmailDeliveryInput = { type: "EMAIL_VERIFICATION"; user: { + // we have the id here cause when sending the email, we have + // the user's session. Therefore, it makes sense to also primary the + // primary user's ID. id: string; + recipeUserId: RecipeUserId; email: string; }; emailVerifyLink: string; tenantId: string; }; -export type GetEmailForUserIdFunc = ( - userId: string, +export type GetEmailForRecipeUserIdFunc = ( + user: User | undefined, + recipeUserId: RecipeUserId, userContext: any ) => Promise< | { diff --git a/lib/ts/recipe/emailverification/utils.ts b/lib/ts/recipe/emailverification/utils.ts index 83c4441e9..6d4ccebfa 100644 --- a/lib/ts/recipe/emailverification/utils.ts +++ b/lib/ts/recipe/emailverification/utils.ts @@ -57,7 +57,7 @@ export function validateAndNormaliseUserInput( } return { mode: config.mode, - getEmailForUserId: config.getEmailForUserId, + getEmailForRecipeUserId: config.getEmailForRecipeUserId, override, getEmailDeliveryConfig, }; diff --git a/lib/ts/recipe/jwt/recipe.ts b/lib/ts/recipe/jwt/recipe.ts index 148d509dc..52649ba72 100644 --- a/lib/ts/recipe/jwt/recipe.ts +++ b/lib/ts/recipe/jwt/recipe.ts @@ -15,7 +15,7 @@ import SuperTokensError from "../../error"; import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; import normalisedURLPath from "../../normalisedURLPath"; import { Querier } from "../../querier"; @@ -96,12 +96,12 @@ export default class Recipe extends RecipeModule { } handleAPIRequest = async ( - _: string, - ____: string | undefined, // TODO tenantId + _id: string, + _tenantId: string | undefined, req: BaseRequest, res: BaseResponse, - __: normalisedURLPath, - ___: HTTPMethod, + _path: normalisedURLPath, + _method: HTTPMethod, userContext: any ): Promise => { let options = { diff --git a/lib/ts/recipe/jwt/types.ts b/lib/ts/recipe/jwt/types.ts index e48ce01df..85453ff08 100644 --- a/lib/ts/recipe/jwt/types.ts +++ b/lib/ts/recipe/jwt/types.ts @@ -13,7 +13,7 @@ * under the License. */ -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { GeneralErrorResponse } from "../../types"; diff --git a/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts b/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts index 06877359e..52239b26a 100644 --- a/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts +++ b/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts @@ -8,7 +8,7 @@ export class AllowedDomainsClaimClass extends PrimitiveArrayClaim { constructor() { super({ key: "st-t-dmns", - async fetchValue(_, tenantId, userContext) { + async fetchValue(_userId, _recipeUserId, tenantId, userContext) { const recipe = Recipe.getInstanceOrThrowError(); if (recipe.getAllowedDomainsForTenantId === undefined) { diff --git a/lib/ts/recipe/multitenancy/index.ts b/lib/ts/recipe/multitenancy/index.ts index 71a23a199..ecb4c2632 100644 --- a/lib/ts/recipe/multitenancy/index.ts +++ b/lib/ts/recipe/multitenancy/index.ts @@ -17,6 +17,7 @@ import Recipe from "./recipe"; import { RecipeInterface, APIOptions, APIInterface } from "./types"; import { ProviderConfig } from "../thirdparty/types"; import { AllowedDomainsClaim } from "./allowedDomainsClaim"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init = Recipe.init; @@ -144,7 +145,7 @@ export default class Wrapper { static async associateUserToTenant( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, userContext?: any ): Promise< | { @@ -158,18 +159,22 @@ export default class Wrapper { | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"; } + | { + status: "ASSOCIATION_NOT_ALLOWED_ERROR"; + reason: string; + } > { const recipeInstance = Recipe.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.associateUserToTenant({ tenantId, - userId, + recipeUserId, userContext: userContext === undefined ? {} : userContext, }); } static async disassociateUserFromTenant( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, userContext?: any ): Promise<{ status: "OK"; @@ -178,7 +183,7 @@ export default class Wrapper { const recipeInstance = Recipe.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.disassociateUserFromTenant({ tenantId, - userId, + recipeUserId, userContext: userContext === undefined ? {} : userContext, }); } diff --git a/lib/ts/recipe/multitenancy/recipeImplementation.ts b/lib/ts/recipe/multitenancy/recipeImplementation.ts index e8b11dd86..8a7a7738f 100644 --- a/lib/ts/recipe/multitenancy/recipeImplementation.ts +++ b/lib/ts/recipe/multitenancy/recipeImplementation.ts @@ -73,25 +73,25 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { return response; }, - associateUserToTenant: async function ({ tenantId, userId }) { + associateUserToTenant: async function ({ tenantId, recipeUserId }) { let response = await querier.sendPostRequest( new NormalisedURLPath( `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/multitenancy/tenant/user` ), { - userId, + recipeUserId: recipeUserId.getAsString(), } ); return response; }, - disassociateUserFromTenant: async function ({ tenantId, userId }) { + disassociateUserFromTenant: async function ({ tenantId, recipeUserId }) { let response = await querier.sendPostRequest( new NormalisedURLPath( `/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/multitenancy/tenant/user/remove` ), { - userId, + recipeUserId: recipeUserId.getAsString(), } ); return response; diff --git a/lib/ts/recipe/multitenancy/types.ts b/lib/ts/recipe/multitenancy/types.ts index 62a228f32..6fdab1a19 100644 --- a/lib/ts/recipe/multitenancy/types.ts +++ b/lib/ts/recipe/multitenancy/types.ts @@ -17,6 +17,7 @@ import { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { ProviderConfig, ProviderInput } from "../thirdparty/types"; import { GeneralErrorResponse } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export type TypeInput = { getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; @@ -128,7 +129,7 @@ export type RecipeInterface = { // User tenant association associateUserToTenant: (input: { tenantId: string; - userId: string; + recipeUserId: RecipeUserId; userContext: any; }) => Promise< | { @@ -142,10 +143,14 @@ export type RecipeInterface = { | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"; } + | { + status: "ASSOCIATION_NOT_ALLOWED_ERROR"; + reason: string; + } >; disassociateUserFromTenant: (input: { tenantId: string; - userId: string; + recipeUserId: RecipeUserId; userContext: any; }) => Promise<{ status: "OK"; diff --git a/lib/ts/recipe/openid/recipe.ts b/lib/ts/recipe/openid/recipe.ts index 534beb6d4..27362aea7 100644 --- a/lib/ts/recipe/openid/recipe.ts +++ b/lib/ts/recipe/openid/recipe.ts @@ -13,7 +13,7 @@ * under the License. */ import STError from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; diff --git a/lib/ts/recipe/openid/types.ts b/lib/ts/recipe/openid/types.ts index b3ade8a79..e5e4c5124 100644 --- a/lib/ts/recipe/openid/types.ts +++ b/lib/ts/recipe/openid/types.ts @@ -13,7 +13,7 @@ * under the License. */ import OverrideableBuilder from "supertokens-js-override"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLDomain from "../../normalisedURLDomain"; import NormalisedURLPath from "../../normalisedURLPath"; import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface, JsonWebKey } from "../jwt/types"; diff --git a/lib/ts/recipe/passwordless/api/consumeCode.ts b/lib/ts/recipe/passwordless/api/consumeCode.ts index 2658e231d..944d665f8 100644 --- a/lib/ts/recipe/passwordless/api/consumeCode.ts +++ b/lib/ts/recipe/passwordless/api/consumeCode.ts @@ -13,7 +13,7 @@ * under the License. */ -import { send200Response } from "../../../utils"; +import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; @@ -80,6 +80,10 @@ export default async function consumeCode( ); if (result.status === "OK") { + result = { + ...result, + ...getBackwardsCompatibleUserInfo(options.req, result), + }; delete (result as any).session; } diff --git a/lib/ts/recipe/passwordless/api/implementation.ts b/lib/ts/recipe/passwordless/api/implementation.ts index a93ee35a8..4958f0c8f 100644 --- a/lib/ts/recipe/passwordless/api/implementation.ts +++ b/lib/ts/recipe/passwordless/api/implementation.ts @@ -1,11 +1,69 @@ import { APIInterface } from "../"; import { logDebugMessage } from "../../../logger"; -import EmailVerification from "../../emailverification/recipe"; +import AccountLinking from "../../accountlinking/recipe"; import Session from "../../session"; +import { listUsersByAccountInfo } from "../../.."; +import { RecipeLevelUser } from "../../accountlinking/types"; export default function getAPIImplementation(): APIInterface { return { consumeCodePOST: async function (input) { + const deviceInfo = await input.options.recipeImplementation.listCodesByPreAuthSessionId({ + tenantId: input.tenantId, + preAuthSessionId: input.preAuthSessionId, + userContext: input.userContext, + }); + + if (!deviceInfo) { + return { + status: "RESTART_FLOW_ERROR", + }; + } + + let existingUsers = await listUsersByAccountInfo( + input.tenantId, + { + phoneNumber: deviceInfo.phoneNumber, + email: deviceInfo.email, + }, + false, + input.userContext + ); + existingUsers = existingUsers.filter((u) => + u.loginMethods.some( + (m) => + m.recipeId === "passwordless" && + (m.hasSameEmailAs(deviceInfo.email) || m.hasSamePhoneNumberAs(m.phoneNumber)) + ) + ); + + if (existingUsers.length === 0) { + let isSignUpAllowed = await AccountLinking.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "passwordless", + email: deviceInfo.email, + phoneNumber: deviceInfo.phoneNumber, + }, + isVerified: true, + tenantId: input.tenantId, + userContext: input.userContext, + }); + + if (!isSignUpAllowed) { + // On the frontend, this should show a UI of asking the user + // to login using a different method. + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)", + }; + } + } else if (existingUsers.length > 1) { + throw new Error( + "You have found a bug. Please report it on https://github.com/supertokens/supertokens-node/issues" + ); + } + let response = await input.options.recipeImplementation.consumeCode( "deviceId" in input ? { @@ -27,35 +85,52 @@ export default function getAPIImplementation(): APIInterface { return response; } - let user = response.user; + let loginMethod: RecipeLevelUser | undefined = response.user.loginMethods.find( + (m) => + m.recipeId === "passwordless" && + (m.hasSameEmailAs(deviceInfo.email) || m.hasSamePhoneNumberAs(m.phoneNumber)) + ); - if (user.email !== undefined) { - const emailVerificationInstance = EmailVerification.getInstance(); - if (emailVerificationInstance) { - const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( - { - userId: user.id, - email: user.email, - tenantId: input.tenantId, - userContext: input.userContext, - } - ); + if (loginMethod === undefined) { + throw new Error("Should never come here"); + } - if (tokenResponse.status === "OK") { - await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ - token: tokenResponse.token, - tenantId: input.tenantId, - userContext: input.userContext, - }); - } + if (existingUsers.length > 0) { + // Here we do this check after sign in is done cause: + // - We first want to check if the credentials are correct first or not + // - The above recipe function marks the email as verified + // - Even though the above call to signInUp is state changing (it changes the email + // of the user), it's OK to do this check here cause the isSignInAllowed checks + // conditions related to account linking + + let isSignInAllowed = await AccountLinking.getInstance().isSignInAllowed({ + user: response.user, + tenantId: input.tenantId, + userContext: input.userContext, + }); + + if (!isSignInAllowed) { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_003)", + }; } + + // we do account linking only during sign in here cause during sign up, + // the recipe function above does account linking for us. + response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId: input.tenantId, + user: response.user, + userContext: input.userContext, + }); } const session = await Session.createNewSession( input.options.req, input.options.res, input.tenantId, - user.id, + loginMethod.recipeUserId, {}, {}, input.userContext @@ -63,12 +138,75 @@ export default function getAPIImplementation(): APIInterface { return { status: "OK", - createdNewUser: response.createdNewUser, + createdNewRecipeUser: response.createdNewRecipeUser, user: response.user, session, }; }, createCodePOST: async function (input) { + const accountInfo: { phoneNumber?: string; email?: string } = {}; + if ("email" in input) { + accountInfo.email = input.email; + } + if ("phoneNumber" in input) { + accountInfo.email = input.phoneNumber; + } + let existingUsers = await listUsersByAccountInfo(input.tenantId, accountInfo, false, input.userContext); + existingUsers = existingUsers.filter((u) => + u.loginMethods.some( + (m) => + m.recipeId === "passwordless" && + (m.hasSameEmailAs(accountInfo.email) || m.hasSamePhoneNumberAs(accountInfo.phoneNumber)) + ) + ); + + if (existingUsers.length === 0) { + let isSignUpAllowed = await AccountLinking.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "passwordless", + ...accountInfo, + }, + isVerified: true, + tenantId: input.tenantId, + userContext: input.userContext, + }); + + if (!isSignUpAllowed) { + // On the frontend, this should show a UI of asking the user + // to login using a different method. + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)", + }; + } + } else if (existingUsers.length === 1) { + let loginMethod: RecipeLevelUser | undefined = existingUsers[0].loginMethods.find( + (m) => + m.recipeId === "passwordless" && + (m.hasSameEmailAs(accountInfo.email) || m.hasSamePhoneNumberAs(accountInfo.phoneNumber)) + ); + + if (loginMethod === undefined) { + throw new Error("Should never come here"); + } + let isSignInAllowed = await AccountLinking.getInstance().isSignInAllowed({ + user: existingUsers[0], + tenantId: input.tenantId, + userContext: input.userContext, + }); + if (!isSignInAllowed) { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_003)", + }; + } + } else { + throw new Error( + "You have found a bug. Please report it on https://github.com/supertokens/supertokens-node/issues" + ); + } let response = await input.options.recipeImplementation.createCode( "email" in input ? { @@ -159,26 +297,34 @@ export default function getAPIImplementation(): APIInterface { }; }, emailExistsGET: async function (input) { - let response = await input.options.recipeImplementation.getUserByEmail({ - userContext: input.userContext, - email: input.email, - tenantId: input.tenantId, - }); + let users = await listUsersByAccountInfo( + input.tenantId, + { + email: input.email, + // tenantId: input.tenantId, + }, + false, + input.userContext + ); return { - exists: response !== undefined, + exists: users.length > 0, status: "OK", }; }, phoneNumberExistsGET: async function (input) { - let response = await input.options.recipeImplementation.getUserByPhoneNumber({ - userContext: input.userContext, - phoneNumber: input.phoneNumber, - tenantId: input.tenantId, - }); + let users = await listUsersByAccountInfo( + input.tenantId, + { + phoneNumber: input.phoneNumber, + // tenantId: input.tenantId, + }, + false, + input.userContext + ); return { - exists: response !== undefined, + exists: users.length > 0, status: "OK", }; }, diff --git a/lib/ts/recipe/passwordless/index.ts b/lib/ts/recipe/passwordless/index.ts index d755fb437..d688bdf1c 100644 --- a/lib/ts/recipe/passwordless/index.ts +++ b/lib/ts/recipe/passwordless/index.ts @@ -17,12 +17,12 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; import { RecipeInterface, - User, APIOptions, APIInterface, TypePasswordlessEmailDeliveryInput, TypePasswordlessSmsDeliveryInput, } from "./types"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init = Recipe.init; @@ -40,8 +40,8 @@ export default class Wrapper { ) & { tenantId: string; userInputCode?: string; userContext?: any } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -52,8 +52,8 @@ export default class Wrapper { userContext?: any; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -74,33 +74,13 @@ export default class Wrapper { } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ - userContext: {}, ...input, - }); - } - - static getUserById(input: { userId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userContext: {}, ...input }); - } - - static getUserByEmail(input: { email: string; tenantId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ - userContext: {}, - ...input, - tenantId: input.tenantId, - }); - } - - static getUserByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByPhoneNumber({ - userContext: {}, - ...input, - tenantId: input.tenantId, + userContext: input.userContext ?? {}, }); } static updateUser(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; userContext?: any; @@ -122,43 +102,43 @@ export default class Wrapper { } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static revokeCode(input: { codeId: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static listCodesByEmail(input: { email: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -176,8 +156,8 @@ export default class Wrapper { } ) { return Recipe.getInstanceOrThrowError().createMagicLink({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -195,22 +175,22 @@ export default class Wrapper { } ) { return Recipe.getInstanceOrThrowError().signInUp({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static async sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: any }) { return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: any }) { return await Recipe.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } } @@ -223,12 +203,6 @@ export let createCode = Wrapper.createCode; export let consumeCode = Wrapper.consumeCode; -export let getUserByEmail = Wrapper.getUserByEmail; - -export let getUserById = Wrapper.getUserById; - -export let getUserByPhoneNumber = Wrapper.getUserByPhoneNumber; - export let listCodesByDeviceId = Wrapper.listCodesByDeviceId; export let listCodesByEmail = Wrapper.listCodesByEmail; @@ -249,7 +223,7 @@ export let createMagicLink = Wrapper.createMagicLink; export let signInUp = Wrapper.signInUp; -export type { RecipeInterface, User, APIOptions, APIInterface }; +export type { RecipeInterface, APIOptions, APIInterface }; export let sendEmail = Wrapper.sendEmail; diff --git a/lib/ts/recipe/passwordless/recipe.ts b/lib/ts/recipe/passwordless/recipe.ts index 11b75bd97..741dda92b 100644 --- a/lib/ts/recipe/passwordless/recipe.ts +++ b/lib/ts/recipe/passwordless/recipe.ts @@ -19,11 +19,10 @@ import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from ". import STError from "./error"; import { validateAndNormaliseUserInput } from "./utils"; import NormalisedURLPath from "../../normalisedURLPath"; -import EmailVerificationRecipe from "../emailverification/recipe"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; import { Querier } from "../../querier"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import consumeCodeAPI from "./api/consumeCode"; import createCodeAPI from "./api/createCode"; @@ -40,8 +39,6 @@ import { import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypePasswordlessEmailDeliveryInput, TypePasswordlessSmsDeliveryInput } from "./types"; import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; -import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -95,13 +92,6 @@ export default class Recipe extends RecipeModule { ingredients.smsDelivery === undefined ? new SmsDeliveryIngredient(this.config.getSmsDeliveryConfig()) : ingredients.smsDelivery; - - PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = EmailVerificationRecipe.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); } static getInstanceOrThrowError(): Recipe { @@ -315,30 +305,12 @@ export default class Recipe extends RecipeModule { if (consumeCodeResponse.status === "OK") { return { status: "OK", - createdNewUser: consumeCodeResponse.createdNewUser, + createdNewRecipeUser: consumeCodeResponse.createdNewRecipeUser, + recipeUserId: consumeCodeResponse.recipeUserId, user: consumeCodeResponse.user, }; } else { throw new Error("Failed to create user. Please retry"); } }; - - // helper functions... - getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - let userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - if (userInfo.email !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "EMAIL_DOES_NOT_EXIST_ERROR", - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }; } diff --git a/lib/ts/recipe/passwordless/recipeImplementation.ts b/lib/ts/recipe/passwordless/recipeImplementation.ts index 79c960e1e..2ea91a25c 100644 --- a/lib/ts/recipe/passwordless/recipeImplementation.ts +++ b/lib/ts/recipe/passwordless/recipeImplementation.ts @@ -1,6 +1,11 @@ import { RecipeInterface } from "./types"; import { Querier } from "../../querier"; +import AccountLinking from "../accountlinking/recipe"; import NormalisedURLPath from "../../normalisedURLPath"; +import { logDebugMessage } from "../../logger"; +import { LoginMethod, User } from "../../user"; +import { getUser } from "../.."; +import RecipeUserId from "../../recipeUserId"; export default function getRecipeInterface(querier: Querier): RecipeInterface { function copyAndRemoveUserContextAndTenantId(input: any): any { @@ -9,16 +14,67 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { }; delete result.userContext; delete result.tenantId; + if (result.recipeUserId !== undefined && result.recipeUserId.getAsString !== undefined) { + result.recipeUserId = result.recipeUserId.getAsString(); + } return result; } return { - consumeCode: async function (input) { + consumeCode: async function (this: RecipeInterface, input) { let response = await querier.sendPostRequest( new NormalisedURLPath(`/${input.tenantId}/recipe/signinup/code/consume`), copyAndRemoveUserContextAndTenantId(input) ); - return response; + + if (response.status !== "OK") { + return response; + } + + logDebugMessage("Passwordless.consumeCode code consumed OK"); + response.user = new User(response.user); + response.recipeUserId = new RecipeUserId(response.recipeUserId); + + const loginMethod = response.user.loginMethods.find( + (lm: LoginMethod) => lm.recipeUserId.getAsString() === response.recipeUserId.getAsString() + )!; + if (loginMethod === undefined) { + throw new Error("This should never happen: login method not found after signin"); + } + + if (!response.createdNewUser) { + // Unlike in the sign up scenario, we do not do account linking here + // cause we do not want sign in to change the potentially user ID of a user + // due to linking when this function is called by the dev in their API. + // If we did account linking + // then we would have to ask the dev to also change the session + // in such API calls. + // In the case of sign up, since we are creating a new user, it's fine + // to link there since there is no user id change really from the dev's + // point of view who is calling the sign up recipe function. + return { + status: "OK", + createdNewRecipeUser: response.createdNewUser, + user: response.user, + recipeUserId: response.recipeUserId, + }; + } + + let updatedUser = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId: input.tenantId, + user: response.user, + userContext: input.userContext, + }); + + if (updatedUser === undefined) { + throw new Error("Should never come here."); + } + return { + status: "OK", + createdNewRecipeUser: response.createdNewUser, + user: updatedUser, + recipeUserId: response.recipeUserId, + }; }, createCode: async function (input) { let response = await querier.sendPostRequest( @@ -34,36 +90,6 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { ); return response; }, - getUserByEmail: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath(`/${input.tenantId}/recipe/user`), - copyAndRemoveUserContextAndTenantId(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }, - getUserById: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/user"), - copyAndRemoveUserContextAndTenantId(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }, - getUserByPhoneNumber: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath(`/${input.tenantId}/recipe/user`), - copyAndRemoveUserContextAndTenantId(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }, listCodesByDeviceId: async function (input) { let response = await querier.sendGetRequest( new NormalisedURLPath(`/${input.tenantId}/recipe/signinup/codes`), @@ -110,9 +136,24 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { }, updateUser: async function (input) { let response = await querier.sendPutRequest( - new NormalisedURLPath("/recipe/user"), + new NormalisedURLPath(`/recipe/user`), copyAndRemoveUserContextAndTenantId(input) ); + if (response.status !== "OK") { + return response; + } + const user = await getUser(input.recipeUserId.getAsString(), input.userContext); + if (user === undefined) { + // This means that the user was deleted between the put and get requests + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + } + await AccountLinking.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user, + recipeUserId: input.recipeUserId, + userContext: input.userContext, + }); return response; }, }; diff --git a/lib/ts/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts index 509564580..794f30723 100644 --- a/lib/ts/recipe/passwordless/types.ts +++ b/lib/ts/recipe/passwordless/types.ts @@ -13,7 +13,7 @@ * under the License. */ -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; import { @@ -26,18 +26,10 @@ import { TypeInputWithService as SmsDeliveryTypeInputWithService, } from "../../ingredients/smsdelivery/types"; import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; // As per https://github.com/supertokens/supertokens-core/issues/325 - -export type User = { - id: string; - email?: string; - phoneNumber?: string; - timeJoined: number; - tenantIds: string[]; -}; - export type TypeInput = ( | { contactMethod: "PHONE"; @@ -171,8 +163,9 @@ export type RecipeInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; + recipeUserId: RecipeUserId; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -182,22 +175,24 @@ export type RecipeInterface = { | { status: "RESTART_FLOW_ERROR" } >; - getUserById: (input: { userId: string; userContext: any }) => Promise; - getUserByEmail: (input: { email: string; tenantId: string; userContext: any }) => Promise; - getUserByPhoneNumber: (input: { - phoneNumber: string; - tenantId: string; - userContext: any; - }) => Promise; - updateUser: (input: { - userId: string; + recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; + }) => Promise< + | { + status: + | "OK" + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; revokeAllCodes: ( input: @@ -285,6 +280,10 @@ export type APIInterface = { preAuthSessionId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >; @@ -315,7 +314,7 @@ export type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; session: SessionContainerInterface; } @@ -326,6 +325,10 @@ export type APIInterface = { } | GeneralErrorResponse | { status: "RESTART_FLOW_ERROR" } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } >; emailExistsGET?: (input: { diff --git a/lib/ts/recipe/session/accessToken.ts b/lib/ts/recipe/session/accessToken.ts index cc4d1466f..c9f5bc637 100644 --- a/lib/ts/recipe/session/accessToken.ts +++ b/lib/ts/recipe/session/accessToken.ts @@ -17,6 +17,7 @@ import STError from "./error"; import { ParsedJWTInfo } from "./jwt"; import * as jose from "jose"; import { ProcessState, PROCESS_STATE } from "../../processState"; +import RecipeUserId from "../../recipeUserId"; import { logDebugMessage } from "../../logger"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; @@ -27,6 +28,7 @@ export async function getInfoFromAccessToken( ): Promise<{ sessionHandle: string; userId: string; + recipeUserId: RecipeUserId; refreshTokenHash1: string; parentRefreshTokenHash1: string | undefined; userData: any; @@ -78,6 +80,9 @@ export async function getInfoFromAccessToken( : sanitizeNumberInput(payload.iat)! * 1000; let userData = jwtInfo.version === 2 ? payload.userData : payload; let sessionHandle = sanitizeStringInput(payload.sessionHandle)!; + + // we use ?? below cause recipeUserId may be undefined for JWTs that are of an older version. + let recipeUserId = new RecipeUserId(sanitizeStringInput(payload.rsub) ?? userId); let refreshTokenHash1 = sanitizeStringInput(payload.refreshTokenHash1)!; let parentRefreshTokenHash1 = sanitizeStringInput(payload.parentRefreshTokenHash1); let antiCsrfToken = sanitizeStringInput(payload.antiCsrfToken); @@ -103,6 +108,7 @@ export async function getInfoFromAccessToken( antiCsrfToken, expiryTime, timeCreated, + recipeUserId, tenantId, }; } catch (err) { @@ -118,7 +124,34 @@ export async function getInfoFromAccessToken( } export function validateAccessTokenStructure(payload: any, version: number) { - if (version >= 3) { + if (version >= 5) { + if ( + typeof payload.sub !== "string" || + typeof payload.exp !== "number" || + typeof payload.iat !== "number" || + typeof payload.sessionHandle !== "string" || + typeof payload.refreshTokenHash1 !== "string" || + typeof payload.rsub !== "string" + ) { + logDebugMessage("validateAccessTokenStructure: Access token is using version >= 4"); + // The error message below will be logged by the error handler that translates this into a TRY_REFRESH_TOKEN_ERROR + // it would come here if we change the structure of the JWT. + throw Error("Access token does not contain all the information. Maybe the structure has changed?"); + } + } else if (version >= 4) { + if ( + typeof payload.sub !== "string" || + typeof payload.exp !== "number" || + typeof payload.iat !== "number" || + typeof payload.sessionHandle !== "string" || + typeof payload.refreshTokenHash1 !== "string" + ) { + logDebugMessage("validateAccessTokenStructure: Access token is using version >= 4"); + // The error message below will be logged by the error handler that translates this into a TRY_REFRESH_TOKEN_ERROR + // it would come here if we change the structure of the JWT. + throw Error("Access token does not contain all the information. Maybe the structure has changed?"); + } + } else if (version >= 3) { if ( typeof payload.sub !== "string" || typeof payload.exp !== "number" || diff --git a/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts b/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts index a30142b39..bf785d862 100644 --- a/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts +++ b/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts @@ -1,9 +1,11 @@ +import RecipeUserId from "../../../recipeUserId"; import { JSONPrimitive } from "../../../types"; import { SessionClaim, SessionClaimValidator } from "../types"; export class PrimitiveArrayClaim extends SessionClaim { public readonly fetchValue: ( userId: string, + recipeUserId: RecipeUserId, tenantId: string, userContext: any ) => Promise | T[] | undefined; diff --git a/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts b/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts index 3cb144377..9cfa97474 100644 --- a/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts +++ b/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts @@ -1,9 +1,11 @@ +import RecipeUserId from "../../../recipeUserId"; import { JSONPrimitive } from "../../../types"; import { SessionClaim, SessionClaimValidator } from "../types"; export class PrimitiveClaim extends SessionClaim { public readonly fetchValue: ( userId: string, + recipeUserId: RecipeUserId, tenantId: string, userContext: any ) => Promise | T | undefined; diff --git a/lib/ts/recipe/session/constants.ts b/lib/ts/recipe/session/constants.ts index d6e8ac58a..a5c903b81 100644 --- a/lib/ts/recipe/session/constants.ts +++ b/lib/ts/recipe/session/constants.ts @@ -21,3 +21,18 @@ export const SIGNOUT_API_PATH = "/signout"; export const availableTokenTransferMethods: TokenTransferMethod[] = ["cookie", "header"]; export const hundredYearsInMs = 3153600000000; + +export const JWKCacheCooldownInMs = 500; +export const JWKCacheMaxAgeInMs = 60000; + +export const protectedProps = [ + "sub", + "iat", + "exp", + "sessionHandle", + "parentRefreshTokenHash1", + "refreshTokenHash1", + "antiCsrfToken", + "rsub", + "tId", +]; diff --git a/lib/ts/recipe/session/cookieAndHeaders.ts b/lib/ts/recipe/session/cookieAndHeaders.ts index c41980f96..3b469a7ea 100644 --- a/lib/ts/recipe/session/cookieAndHeaders.ts +++ b/lib/ts/recipe/session/cookieAndHeaders.ts @@ -13,7 +13,7 @@ * under the License. */ import { HEADER_RID } from "../../constants"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import { logDebugMessage } from "../../logger"; import { availableTokenTransferMethods } from "./constants"; import { TokenTransferMethod, TokenType, TypeNormalisedInput } from "./types"; diff --git a/lib/ts/recipe/session/error.ts b/lib/ts/recipe/session/error.ts index 546364aa4..8812224f6 100644 --- a/lib/ts/recipe/session/error.ts +++ b/lib/ts/recipe/session/error.ts @@ -14,6 +14,7 @@ */ import STError from "../../error"; +import RecipeUserId from "../../recipeUserId"; import { ClaimValidationError } from "./types"; export default class SessionError extends STError { @@ -40,6 +41,7 @@ export default class SessionError extends STError { type: "TOKEN_THEFT_DETECTED"; payload: { userId: string; + recipeUserId: RecipeUserId; sessionHandle: string; }; } diff --git a/lib/ts/recipe/session/index.ts b/lib/ts/recipe/session/index.ts index 82d94c21b..d207cc97f 100644 --- a/lib/ts/recipe/session/index.ts +++ b/lib/ts/recipe/session/index.ts @@ -29,7 +29,10 @@ import Recipe from "./recipe"; import { JSONObject } from "../../types"; import { getRequiredClaimValidators } from "./utils"; import { createNewSessionInRequest, getSessionFromRequest, refreshSessionInRequest } from "./sessionRequestFunctions"; +import RecipeUserId from "../../recipeUserId"; +import { getUser } from "../.."; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; +import { protectedProps } from "./constants"; export default class SessionWrapper { static init = Recipe.init; @@ -40,7 +43,7 @@ export default class SessionWrapper { req: any, res: any, tenantId: string, - userId: string, + recipeUserId: RecipeUserId, accessTokenPayload: any = {}, sessionDataInDatabase: any = {}, userContext: any = {} @@ -49,6 +52,12 @@ export default class SessionWrapper { const config = recipeInstance.config; const appInfo = recipeInstance.getAppInfo(); + let user = await getUser(recipeUserId.getAsString(), userContext); + let userId = recipeUserId.getAsString(); + if (user !== undefined) { + userId = user.id; + } + return await createNewSessionInRequest({ req, res, @@ -56,6 +65,7 @@ export default class SessionWrapper { recipeInstance, accessTokenPayload, userId, + recipeUserId, config, appInfo, sessionDataInDatabase, @@ -65,7 +75,7 @@ export default class SessionWrapper { static async createNewSessionWithoutRequestResponse( tenantId: string, - userId: string, + recipeUserId: RecipeUserId, accessTokenPayload: any = {}, sessionDataInDatabase: any = {}, disableAntiCsrf: boolean = false, @@ -81,16 +91,26 @@ export default class SessionWrapper { iss: issuer, }; + for (const prop of protectedProps) { + delete finalAccessTokenPayload[prop]; + } + + let user = await getUser(recipeUserId.getAsString(), userContext); + let userId = recipeUserId.getAsString(); + if (user !== undefined) { + userId = user.id; + } + for (const claim of claimsAddedByOtherRecipes) { - const update = await claim.build(userId, tenantId, userContext); + const update = await claim.build(userId, recipeUserId, tenantId, userContext); finalAccessTokenPayload = { ...finalAccessTokenPayload, ...update, }; } - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewSession({ userId, + recipeUserId, accessTokenPayload: finalAccessTokenPayload, sessionDataInDatabase, disableAntiCsrf, @@ -131,6 +151,7 @@ export default class SessionWrapper { const claimValidatorsAddedByOtherRecipes = Recipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes(); const globalClaimValidators: SessionClaimValidator[] = await recipeImpl.getGlobalClaimValidators({ userId: sessionInfo.userId, + recipeUserId: sessionInfo.recipeUserId, tenantId: sessionInfo.tenantId, claimValidatorsAddedByOtherRecipes, userContext, @@ -143,6 +164,7 @@ export default class SessionWrapper { let claimValidationResponse = await recipeImpl.validateClaims({ userId: sessionInfo.userId, + recipeUserId: sessionInfo.recipeUserId, accessTokenPayload: sessionInfo.customClaimsInAccessTokenPayload, claimValidators, userContext, @@ -167,42 +189,6 @@ export default class SessionWrapper { }; } - static async validateClaimsInJWTPayload( - tenantId: string, - userId: string, - jwtPayload: JSONObject, - overrideGlobalClaimValidators?: ( - globalClaimValidators: SessionClaimValidator[], - userId: string, - userContext: any - ) => Promise | SessionClaimValidator[], - userContext: any = {} - ): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }> { - const recipeImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; - - const claimValidatorsAddedByOtherRecipes = Recipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators: SessionClaimValidator[] = await recipeImpl.getGlobalClaimValidators({ - tenantId, - userId, - claimValidatorsAddedByOtherRecipes, - userContext, - }); - - const claimValidators = - overrideGlobalClaimValidators !== undefined - ? await overrideGlobalClaimValidators(globalClaimValidators, userId, userContext) - : globalClaimValidators; - return recipeImpl.validateClaimsInJWTPayload({ - userId, - jwtPayload, - claimValidators, - userContext, - }); - } - static getSession(req: any, res: any): Promise; static getSession( req: any, @@ -233,7 +219,7 @@ export default class SessionWrapper { recipeInterfaceImpl, config, options, - userContext, + userContext, // userContext is normalized inside the function }); } @@ -333,20 +319,31 @@ export default class SessionWrapper { userContext, }); } - static revokeAllSessionsForUser(userId: string, tenantId?: string, userContext: any = {}) { + static revokeAllSessionsForUser( + userId: string, + revokeSessionsForLinkedAccounts: boolean = true, + tenantId?: string, + userContext: any = {} + ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllSessionsForUser({ userId, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, - revokeAcrossAllTenants: tenantId === undefined, + revokeSessionsForLinkedAccounts, userContext, }); } - static getAllSessionHandlesForUser(userId: string, tenantId?: string, userContext: any = {}) { + static getAllSessionHandlesForUser( + userId: string, + fetchSessionsForAllLinkedAccounts: boolean = true, + tenantId?: string, + userContext: any = {} + ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getAllSessionHandlesForUser({ userId, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, fetchAcrossAllTenants: tenantId === undefined, + fetchSessionsForAllLinkedAccounts, userContext, }); } @@ -484,7 +481,6 @@ export let fetchAndSetClaim = SessionWrapper.fetchAndSetClaim; export let setClaimValue = SessionWrapper.setClaimValue; export let getClaimValue = SessionWrapper.getClaimValue; export let removeClaim = SessionWrapper.removeClaim; -export let validateClaimsInJWTPayload = SessionWrapper.validateClaimsInJWTPayload; export let validateClaimsForSessionHandle = SessionWrapper.validateClaimsForSessionHandle; export let Error = SessionWrapper.Error; diff --git a/lib/ts/recipe/session/recipe.ts b/lib/ts/recipe/session/recipe.ts index 18f4ec4c5..dc9cb5ea6 100644 --- a/lib/ts/recipe/session/recipe.ts +++ b/lib/ts/recipe/session/recipe.ts @@ -37,7 +37,7 @@ import { import RecipeImplementation from "./recipeImplementation"; import { Querier } from "../../querier"; import APIImplementation from "./api/implementation"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { APIOptions } from "."; import OpenIdRecipe from "../openid/recipe"; @@ -96,7 +96,9 @@ export default class SessionRecipe extends RecipeModule { if (SessionRecipe.instance !== undefined) { return SessionRecipe.instance; } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); + throw new Error( + "Initialisation not done. Did you forget to call the SuperTokens.init or Session.init function?" + ); } static init(config?: TypeInput): RecipeListFunction { @@ -211,6 +213,7 @@ export default class SessionRecipe extends RecipeModule { return await this.config.errorHandlers.onTokenTheftDetected( err.payload.sessionHandle, err.payload.userId, + err.payload.recipeUserId, request, response ); diff --git a/lib/ts/recipe/session/recipeImplementation.ts b/lib/ts/recipe/session/recipeImplementation.ts index 54e06f3ae..94c4e8fba 100644 --- a/lib/ts/recipe/session/recipeImplementation.ts +++ b/lib/ts/recipe/session/recipeImplementation.ts @@ -20,7 +20,9 @@ import { logDebugMessage } from "../../logger"; import { ParsedJWTInfo, parseJWTWithoutSignatureVerification } from "./jwt"; import { validateAccessTokenStructure } from "./accessToken"; import SessionError from "./error"; +import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; +import { JWKCacheCooldownInMs, JWKCacheMaxAgeInMs, protectedProps } from "./constants"; export type Helpers = { querier: Querier; @@ -30,20 +32,6 @@ export type Helpers = { getRecipeImpl: () => RecipeInterface; }; -const JWKCacheCooldownInMs = 500; -export const JWKCacheMaxAgeInMs = 60000; - -export const protectedProps = [ - "sub", - "iat", - "exp", - "sessionHandle", - "parentRefreshTokenHash1", - "refreshTokenHash1", - "antiCsrfToken", - "tId", -]; - export default function getRecipeInterface( querier: Querier, config: TypeNormalisedInput, @@ -86,13 +74,14 @@ export default function getRecipeInterface( let obj: RecipeInterface = { createNewSession: async function ({ - userId, + recipeUserId, accessTokenPayload = {}, sessionDataInDatabase = {}, disableAntiCsrf, tenantId, }: { userId: string; + recipeUserId: RecipeUserId; disableAntiCsrf?: boolean; accessTokenPayload?: any; sessionDataInDatabase?: any; @@ -104,7 +93,7 @@ export default function getRecipeInterface( let response = await SessionFunctions.createNewSession( helpers, tenantId, - userId, + recipeUserId, disableAntiCsrf === true, accessTokenPayload, sessionDataInDatabase @@ -120,6 +109,7 @@ export default function getRecipeInterface( response.antiCsrfToken, response.session.handle, response.session.userId, + response.session.recipeUserId, payload, undefined, true, @@ -220,6 +210,7 @@ export default function getRecipeInterface( antiCsrfToken, response.session.handle, response.session.userId, + response.session.recipeUserId, payload, undefined, response.accessToken !== undefined, @@ -233,6 +224,7 @@ export default function getRecipeInterface( this: RecipeInterface, input: { userId: string; + recipeUserId: RecipeUserId; accessTokenPayload: any; claimValidators: SessionClaimValidator[]; userContext: any; @@ -251,6 +243,7 @@ export default function getRecipeInterface( logDebugMessage("updateClaimsInPayloadIfNeeded refetching " + validator.id); const value = await validator.claim.fetchValue( input.userId, + input.recipeUserId, accessTokenPayload.tId === undefined ? DEFAULT_TENANT_ID : accessTokenPayload.tId, input.userContext ); @@ -283,32 +276,6 @@ export default function getRecipeInterface( }; }, - validateClaimsInJWTPayload: async function ( - this: RecipeInterface, - input: { - userId: string; - jwtPayload: JSONObject; - claimValidators: SessionClaimValidator[]; - userContext: any; - } - ): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }> { - // We skip refetching here, because we have no way of updating the JWT payload here - // if we have access to the entire session other methods can be used to do validation while updating - const invalidClaims = await validateClaimsInPayload( - input.claimValidators, - input.jwtPayload, - input.userContext - ); - - return { - status: "OK", - invalidClaims, - }; - }, - getSessionInformation: async function ({ sessionHandle, }: { @@ -350,6 +317,7 @@ export default function getRecipeInterface( response.antiCsrfToken, response.session.handle, response.session.userId, + response.session.recipeUserId, payload, undefined, true, @@ -370,6 +338,7 @@ export default function getRecipeInterface( session: { handle: string; userId: string; + recipeUserId: RecipeUserId; userDataInJWT: any; tenantId: string; }; @@ -398,6 +367,7 @@ export default function getRecipeInterface( session: { handle: response.session.handle, userId: response.session.userId, + recipeUserId: new RecipeUserId(response.session.recipeUserId), userDataInJWT: response.session.userDataInJWT, tenantId: response.session.tenantId, }, @@ -409,24 +379,41 @@ export default function getRecipeInterface( userId, tenantId, revokeAcrossAllTenants, + revokeSessionsForLinkedAccounts, }: { userId: string; + revokeSessionsForLinkedAccounts: boolean; tenantId?: string; revokeAcrossAllTenants?: boolean; }) { - return SessionFunctions.revokeAllSessionsForUser(helpers, userId, tenantId, revokeAcrossAllTenants); + return SessionFunctions.revokeAllSessionsForUser( + helpers, + userId, + revokeSessionsForLinkedAccounts, + tenantId, + revokeAcrossAllTenants + ); }, getAllSessionHandlesForUser: function ({ userId, + fetchSessionsForAllLinkedAccounts, tenantId, fetchAcrossAllTenants, }: { userId: string; + fetchSessionsForAllLinkedAccounts: boolean; tenantId?: string; fetchAcrossAllTenants?: boolean; + userContext: any; }): Promise { - return SessionFunctions.getAllSessionHandlesForUser(helpers, userId, tenantId, fetchAcrossAllTenants); + return SessionFunctions.getAllSessionHandlesForUser( + helpers, + userId, + fetchSessionsForAllLinkedAccounts, + tenantId, + fetchAcrossAllTenants + ); }, revokeSession: function ({ sessionHandle }: { sessionHandle: string }): Promise { @@ -495,6 +482,7 @@ export default function getRecipeInterface( } const accessTokenPayloadUpdate = await input.claim.build( sessionInfo.userId, + sessionInfo.recipeUserId, sessionInfo.tenantId, input.userContext ); diff --git a/lib/ts/recipe/session/sessionClass.ts b/lib/ts/recipe/session/sessionClass.ts index 76f61c776..ba0df0349 100644 --- a/lib/ts/recipe/session/sessionClass.ts +++ b/lib/ts/recipe/session/sessionClass.ts @@ -15,10 +15,12 @@ import { buildFrontToken, clearSession, setAntiCsrfTokenInHeaders, setToken } from "./cookieAndHeaders"; import STError from "./error"; import { SessionClaim, SessionClaimValidator, SessionContainerInterface, ReqResInfo, TokenInfo } from "./types"; -import { Helpers, protectedProps } from "./recipeImplementation"; +import { Helpers } from "./recipeImplementation"; import { setAccessTokenInResponse } from "./utils"; import { parseJWTWithoutSignatureVerification } from "./jwt"; import { logDebugMessage } from "../../logger"; +import RecipeUserId from "../../recipeUserId"; +import { protectedProps } from "./constants"; export default class Session implements SessionContainerInterface { constructor( @@ -29,12 +31,17 @@ export default class Session implements SessionContainerInterface { protected antiCsrfToken: string | undefined, protected sessionHandle: string, protected userId: string, + protected recipeUserId: RecipeUserId, protected userDataInAccessToken: any, protected reqResInfo: ReqResInfo | undefined, protected accessTokenUpdated: boolean, protected tenantId: string ) {} + getRecipeUserId(_userContext?: any): RecipeUserId { + return this.recipeUserId; + } + async revokeSession(userContext?: any) { await this.helpers.getRecipeImpl().revokeSession({ sessionHandle: this.sessionHandle, @@ -209,6 +216,7 @@ export default class Session implements SessionContainerInterface { let validateClaimResponse = await this.helpers.getRecipeImpl().validateClaims({ accessTokenPayload: this.getAccessTokenPayload(userContext), userId: this.getUserId(userContext), + recipeUserId: this.getRecipeUserId(userContext), claimValidators, userContext, }); @@ -232,7 +240,12 @@ export default class Session implements SessionContainerInterface { // Any update to this function should also be reflected in the respective JWT version async fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise { - const update = await claim.build(this.getUserId(userContext), this.getTenantId(), userContext); + const update = await claim.build( + this.getUserId(userContext), + this.getRecipeUserId(userContext), + this.getTenantId(), + userContext + ); return this.mergeIntoAccessTokenPayload(update, userContext); } diff --git a/lib/ts/recipe/session/sessionFunctions.ts b/lib/ts/recipe/session/sessionFunctions.ts index 1c5232f8d..d2c1ab71c 100644 --- a/lib/ts/recipe/session/sessionFunctions.ts +++ b/lib/ts/recipe/session/sessionFunctions.ts @@ -18,10 +18,12 @@ import STError from "./error"; import { PROCESS_STATE, ProcessState } from "../../processState"; import { CreateOrRefreshAPIResponse, SessionInformation } from "./types"; import NormalisedURLPath from "../../normalisedURLPath"; -import { Helpers, JWKCacheMaxAgeInMs } from "./recipeImplementation"; +import { Helpers } from "./recipeImplementation"; import { maxVersion } from "../../utils"; import { logDebugMessage } from "../../logger"; +import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; +import { JWKCacheMaxAgeInMs } from "./constants"; /** * @description call this to "login" a user. @@ -29,7 +31,7 @@ import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; export async function createNewSession( helpers: Helpers, tenantId: string, - userId: string, + recipeUserId: RecipeUserId, disableAntiCsrf: boolean, accessTokenPayload: any = {}, sessionDataInDatabase: any = {} @@ -39,13 +41,13 @@ export async function createNewSession( sessionDataInDatabase === null || sessionDataInDatabase === undefined ? {} : sessionDataInDatabase; const requestBody = { - userId, - userDataInJWT: accessTokenPayload, + userId: recipeUserId.getAsString(), + userDataInJWT: { ...accessTokenPayload }, userDataInDatabase: sessionDataInDatabase, useDynamicSigningKey: helpers.config.useDynamicAccessTokenSigningKey, enableAntiCsrf: !disableAntiCsrf && helpers.config.antiCsrf === "VIA_TOKEN", }; - const response = await helpers.querier.sendPostRequest( + let response = await helpers.querier.sendPostRequest( new NormalisedURLPath(`/${tenantId}/recipe/session`), requestBody ); @@ -54,6 +56,7 @@ export async function createNewSession( session: { handle: response.session.handle, userId: response.session.userId, + recipeUserId: new RecipeUserId(response.session.recipeUserId), userDataInJWT: response.session.userDataInJWT, tenantId: response.session.tenantId, }, @@ -84,6 +87,7 @@ export async function getSession( session: { handle: string; userId: string; + recipeUserId: RecipeUserId; userDataInJWT: any; expiryTime: number; tenantId: string; @@ -210,6 +214,7 @@ export async function getSession( session: { handle: accessTokenInfo.sessionHandle, userId: accessTokenInfo.userId, + recipeUserId: accessTokenInfo.recipeUserId, userDataInJWT: accessTokenInfo.userData, expiryTime: accessTokenInfo.expiryTime, tenantId: accessTokenInfo.tenantId, @@ -228,6 +233,7 @@ export async function getSession( }; let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session/verify"), requestBody); + if (response.status === "OK") { delete response.status; return { @@ -235,6 +241,7 @@ export async function getSession( session: { handle: response.session.handle, userId: response.session.userId, + recipeUserId: new RecipeUserId(response.session.recipeUserId), expiryTime: response.accessToken?.expiry || // if we got a new accesstoken we take the expiry time from there accessTokenInfo?.expiryTime || // if we didn't get a new access token but could validate the token take that info (alwaysCheckCore === true, or parentRefreshTokenHash1 !== null) @@ -271,7 +278,6 @@ export async function getSessionInformation( if (maxVersion(apiVersion, "2.7") === "2.7") { throw new Error("Please use core version >= 3.5 to call this function."); } - let response = await helpers.querier.sendGetRequest(new NormalisedURLPath(`/recipe/session`), { sessionHandle, }); @@ -283,6 +289,7 @@ export async function getSessionInformation( timeCreated: response.timeCreated, expiry: response.expiry, userId: response.userId, + recipeUserId: new RecipeUserId(response.recipeUserId), sessionDataInDatabase: response.userDataInDatabase, customClaimsInAccessTokenPayload: response.userDataInJWT, tenantId: response.tenantId, @@ -319,11 +326,13 @@ export async function refreshSession( } let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session/refresh"), requestBody); + if (response.status === "OK") { return { session: { handle: response.session.handle, userId: response.session.userId, + recipeUserId: new RecipeUserId(response.session.recipeUserId), userDataInJWT: response.session.userDataInJWT, tenantId: response.session.tenantId, }, @@ -350,6 +359,7 @@ export async function refreshSession( throw new STError({ message: "Token theft detected", payload: { + recipeUserId: new RecipeUserId(response.session.recipeUserId), userId: response.session.userId, sessionHandle: response.session.handle, }, @@ -365,15 +375,16 @@ export async function refreshSession( export async function revokeAllSessionsForUser( helpers: Helpers, userId: string, + revokeSessionsForLinkedAccounts: boolean, tenantId?: string, revokeAcrossAllTenants?: boolean ): Promise { if (tenantId === undefined) { tenantId = DEFAULT_TENANT_ID; } - let response = await helpers.querier.sendPostRequest(new NormalisedURLPath(`/${tenantId}/recipe/session/remove`), { userId, + revokeSessionsForLinkedAccounts, revokeAcrossAllTenants, }); return response.sessionHandlesRevoked; @@ -385,15 +396,16 @@ export async function revokeAllSessionsForUser( export async function getAllSessionHandlesForUser( helpers: Helpers, userId: string, + fetchSessionsForAllLinkedAccounts: boolean, tenantId?: string, fetchAcrossAllTenants?: boolean ): Promise { if (tenantId === undefined) { tenantId = DEFAULT_TENANT_ID; } - let response = await helpers.querier.sendGetRequest(new NormalisedURLPath(`/${tenantId}/recipe/session/user`), { userId, + fetchSessionsForAllLinkedAccounts, fetchAcrossAllTenants, }); return response.sessionHandles; @@ -404,7 +416,7 @@ export async function getAllSessionHandlesForUser( * @returns true if session was deleted from db. Else false in case there was nothing to delete */ export async function revokeSession(helpers: Helpers, sessionHandle: string): Promise { - let response = await helpers.querier.sendPostRequest(new NormalisedURLPath(`/recipe/session/remove`), { + let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session/remove"), { sessionHandles: [sessionHandle], }); return response.sessionHandlesRevoked.length === 1; @@ -447,7 +459,7 @@ export async function updateAccessTokenPayload( ): Promise { newAccessTokenPayload = newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; - let response = await helpers.querier.sendPutRequest(new NormalisedURLPath(`/recipe/jwt/data`), { + let response = await helpers.querier.sendPutRequest(new NormalisedURLPath("/recipe/jwt/data"), { sessionHandle, userDataInJWT: newAccessTokenPayload, }); diff --git a/lib/ts/recipe/session/sessionRequestFunctions.ts b/lib/ts/recipe/session/sessionRequestFunctions.ts index c1f52cdbb..7ee13b775 100644 --- a/lib/ts/recipe/session/sessionRequestFunctions.ts +++ b/lib/ts/recipe/session/sessionRequestFunctions.ts @@ -11,12 +11,13 @@ import SuperTokens from "../../supertokens"; import { getRequiredClaimValidators } from "./utils"; import { getRidFromHeader, isAnIpAddress, normaliseHttpMethod, setRequestInUserContextIfNotDefined } from "../../utils"; import { logDebugMessage } from "../../logger"; -import { availableTokenTransferMethods } from "./constants"; +import { availableTokenTransferMethods, protectedProps } from "./constants"; import { clearSession, getAntiCsrfTokenFromHeaders, getToken, setCookie } from "./cookieAndHeaders"; import { ParsedJWTInfo, parseJWTWithoutSignatureVerification } from "./jwt"; import { validateAccessTokenStructure } from "./accessToken"; import { NormalisedAppinfo } from "../../types"; import SessionError from "./error"; +import RecipeUserId from "../../recipeUserId"; // We are defining this here (and not exporting it) to reduce the scope of legacy code const LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = "sIdRefreshToken"; @@ -321,6 +322,7 @@ export async function createNewSessionInRequest({ recipeInstance, accessTokenPayload, userId, + recipeUserId, config, appInfo, sessionDataInDatabase, @@ -332,6 +334,7 @@ export async function createNewSessionInRequest({ recipeInstance: Recipe; accessTokenPayload: any; userId: string; + recipeUserId: RecipeUserId; config: TypeNormalisedInput; appInfo: NormalisedAppinfo; sessionDataInDatabase: any; @@ -356,8 +359,12 @@ export async function createNewSessionInRequest({ iss: issuer, }; + for (const prop of protectedProps) { + delete finalAccessTokenPayload[prop]; + } + for (const claim of claimsAddedByOtherRecipes) { - const update = await claim.build(userId, tenantId, userContext); + const update = await claim.build(userId, recipeUserId, tenantId, userContext); finalAccessTokenPayload = { ...finalAccessTokenPayload, ...update, @@ -389,6 +396,7 @@ export async function createNewSessionInRequest({ const disableAntiCsrf = outputTransferMethod === "header"; const session = await recipeInstance.recipeInterfaceImpl.createNewSession({ userId, + recipeUserId, accessTokenPayload: finalAccessTokenPayload, sessionDataInDatabase, disableAntiCsrf, diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index 2b70f759f..8e81d5ca0 100644 --- a/lib/ts/recipe/session/types.ts +++ b/lib/ts/recipe/session/types.ts @@ -12,13 +12,14 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface } from "../jwt/types"; import OverrideableBuilder from "supertokens-js-override"; import { RecipeInterface as OpenIdRecipeInterface, APIInterface as OpenIdAPIInterface } from "../openid/types"; import { JSONObject, JSONValue } from "../../types"; import { GeneralErrorResponse } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export type KeyInfo = { publicKey: string; @@ -38,6 +39,7 @@ export type CreateOrRefreshAPIResponse = { session: { handle: string; userId: string; + recipeUserId: RecipeUserId; userDataInJWT: any; tenantId: string; }; @@ -161,7 +163,13 @@ export interface ErrorHandlerMiddleware { } export interface TokenTheftErrorHandlerMiddleware { - (sessionHandle: string, userId: string, request: BaseRequest, response: BaseResponse): Promise; + ( + sessionHandle: string, + userId: string, + recipeUserId: RecipeUserId, + request: BaseRequest, + response: BaseResponse + ): Promise; } export interface InvalidClaimErrorHandlerMiddleware { @@ -189,6 +197,7 @@ export interface VerifySessionOptions { export type RecipeInterface = { createNewSession(input: { userId: string; + recipeUserId: RecipeUserId; accessTokenPayload?: any; sessionDataInDatabase?: any; disableAntiCsrf?: boolean; @@ -199,6 +208,7 @@ export type RecipeInterface = { getGlobalClaimValidators(input: { tenantId: string; userId: string; + recipeUserId: RecipeUserId; claimValidatorsAddedByOtherRecipes: SessionClaimValidator[]; userContext: any; }): Promise | SessionClaimValidator[]; @@ -228,6 +238,7 @@ export type RecipeInterface = { revokeAllSessionsForUser(input: { userId: string; + revokeSessionsForLinkedAccounts: boolean; tenantId: string; revokeAcrossAllTenants?: boolean; userContext: any; @@ -235,6 +246,7 @@ export type RecipeInterface = { getAllSessionHandlesForUser(input: { userId: string; + fetchSessionsForAllLinkedAccounts: boolean; tenantId: string; fetchAcrossAllTenants?: boolean; userContext: any; @@ -270,6 +282,7 @@ export type RecipeInterface = { session: { handle: string; userId: string; + recipeUserId: RecipeUserId; userDataInJWT: any; tenantId: string; }; @@ -284,6 +297,7 @@ export type RecipeInterface = { validateClaims(input: { userId: string; + recipeUserId: RecipeUserId; accessTokenPayload: any; claimValidators: SessionClaimValidator[]; userContext: any; @@ -291,17 +305,6 @@ export type RecipeInterface = { invalidClaims: ClaimValidationError[]; accessTokenPayloadUpdate?: any; }>; - - validateClaimsInJWTPayload(input: { - userId: string; - jwtPayload: JSONObject; - claimValidators: SessionClaimValidator[]; - userContext: any; - }): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }>; - fetchAndSetClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; setClaimValue(input: { sessionHandle: string; @@ -336,6 +339,7 @@ export interface SessionContainerInterface { getUserId(userContext?: any): string; + getRecipeUserId(userContext?: any): RecipeUserId; getTenantId(userContext?: any): string; getAccessTokenPayload(userContext?: any): any; @@ -410,6 +414,7 @@ export type APIInterface = { export type SessionInformation = { sessionHandle: string; userId: string; + recipeUserId: RecipeUserId; sessionDataInDatabase: any; expiry: number; customClaimsInAccessTokenPayload: any; @@ -450,7 +455,12 @@ export abstract class SessionClaim { * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. */ - abstract fetchValue(userId: string, tenantId: string, userContext: any): Promise | T | undefined; + abstract fetchValue( + userId: string, + recipeUserId: RecipeUserId, + tenantId: string, + userContext: any + ): Promise | T | undefined; /** * Saves the provided value into the payload, by cloning and updating the entire object. @@ -480,8 +490,8 @@ export abstract class SessionClaim { */ abstract getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; - async build(userId: string, tenantId: string, userContext?: any): Promise { - const value = await this.fetchValue(userId, tenantId, userContext); + async build(userId: string, recipeUserId: RecipeUserId, tenantId: string, userContext?: any): Promise { + const value = await this.fetchValue(userId, recipeUserId, tenantId, userContext); if (value === undefined) { return {}; diff --git a/lib/ts/recipe/session/utils.ts b/lib/ts/recipe/session/utils.ts index 2480b3025..3179e0c1b 100644 --- a/lib/ts/recipe/session/utils.ts +++ b/lib/ts/recipe/session/utils.ts @@ -30,9 +30,10 @@ import NormalisedURLPath from "../../normalisedURLPath"; import { NormalisedAppinfo } from "../../types"; import { isAnIpAddress } from "../../utils"; import { RecipeInterface, APIInterface } from "./types"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import { sendNon200ResponseWithMessage, sendNon200Response } from "../../utils"; import { logDebugMessage } from "../../logger"; +import RecipeUserId from "../../recipeUserId"; export async function sendTryRefreshTokenResponse( recipeInstance: SessionRecipe, @@ -68,7 +69,8 @@ export async function sendTokenTheftDetectedResponse( recipeInstance: SessionRecipe, sessionHandle: string, _: string, - __: BaseRequest, + __: RecipeUserId, + ___: BaseRequest, response: BaseResponse ) { await recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext: {} }); @@ -176,10 +178,18 @@ export function validateAndNormaliseUserInput( onTokenTheftDetected: async ( sessionHandle: string, userId: string, + recipeUserId: RecipeUserId, request: BaseRequest, response: BaseResponse ) => { - return await sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, userId, request, response); + return await sendTokenTheftDetectedResponse( + recipeInstance, + sessionHandle, + userId, + recipeUserId, + request, + response + ); }, onTryRefreshToken: async (message: string, request: BaseRequest, response: BaseResponse) => { return await sendTryRefreshTokenResponse(recipeInstance, message, request, response); @@ -284,6 +294,7 @@ export async function getRequiredClaimValidators( const globalClaimValidators: SessionClaimValidator[] = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getGlobalClaimValidators( { userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), tenantId: session.getTenantId(), claimValidatorsAddedByOtherRecipes, userContext, diff --git a/lib/ts/recipe/thirdparty/api/implementation.ts b/lib/ts/recipe/thirdparty/api/implementation.ts index f2765d6ce..08a291b36 100644 --- a/lib/ts/recipe/thirdparty/api/implementation.ts +++ b/lib/ts/recipe/thirdparty/api/implementation.ts @@ -1,7 +1,12 @@ import { APIInterface } from "../"; import Session from "../../session"; +import AccountLinking from "../../accountlinking/recipe"; -import EmailVerification from "../../emailverification/recipe"; +import { RecipeLevelUser } from "../../accountlinking/types"; +import { listUsersByAccountInfo } from "../../.."; +import RecipeUserId from "../../../recipeUserId"; +import EmailVerification from "../../emailverification"; +import EmailVerificationRecipe from "../../emailverification/recipe"; export default function getAPIInterface(): APIInterface { return { @@ -50,53 +55,228 @@ export default function getAPIInterface(): APIInterface { status: "NO_EMAIL_GIVEN_BY_PROVIDER", }; } + let existingUsers = await listUsersByAccountInfo( + tenantId, + { + thirdParty: { + id: provider.id, + userId: userInfo.thirdPartyUserId, + }, + }, + false, + userContext + ); + + if (existingUsers.length === 0) { + let isSignUpAllowed = await AccountLinking.getInstance().isSignUpAllowed({ + newUser: { + recipeId: "thirdparty", + email: emailInfo.id, + thirdParty: { + id: provider.id, + userId: userInfo.thirdPartyUserId, + }, + }, + isVerified: emailInfo.isVerified, + tenantId, + userContext, + }); + + if (!isSignUpAllowed) { + // On the frontend, this should show a UI of asking the user + // to login using a different method. + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_006)", + }; + } + } else { + // this is a sign in. So before we proceed, we need to check if an email change + // is allowed since the email could have changed from the social provider's side. + // We do this check here and not in the recipe function cause we want to keep the + // recipe function checks to a minimum so that the dev has complete control of + // what they can do. + + // The isEmailChangeAllowed function takes in a isVerified boolean. Now, even though + // we already have that from the input, that's just what the provider says. If the + // provider says that the email is NOT verified, it could have been that the email + // is verified on the user's account via supertokens on a previous sign in / up. + // So we just check that as well before calling isEmailChangeAllowed + + if (existingUsers.length > 1) { + throw new Error( + "You have found a bug. Please report it on https://github.com/supertokens/supertokens-node/issues" + ); + } + + let recipeUserId: RecipeUserId | undefined = undefined; + existingUsers[0].loginMethods.forEach((lM) => { + if ( + lM.hasSameThirdPartyInfoAs({ + id: provider.id, + userId: userInfo.thirdPartyUserId, + }) + ) { + recipeUserId = lM.recipeUserId; + } + }); + + if (!emailInfo.isVerified && EmailVerificationRecipe.getInstance() !== undefined) { + emailInfo.isVerified = await EmailVerification.isEmailVerified( + recipeUserId!, + emailInfo.id, + userContext + ); + } + + /** + * In this API, during only a sign in, we check for isEmailChangeAllowed first, then + * change the email by calling the recipe function, then check if is sign in allowed. + * This may result in a few states where email change is allowed, but still, sign in + * is not allowed: + * + * Various outcomes of isSignInAllowed vs isEmailChangeAllowed + * isSignInAllowed result: + * - is primary user -> TRUE + * - is recipe user + * - other recipe user exists + * - no -> TRUE + * - yes + * - email verified -> TRUE + * - email unverified -> FALSE + * - other primary user exists + * - no -> TRUE + * - yes + * - email verification status + * - this && primary -> TRUE + * - !this && !primary -> FALSE + * - this && !primary -> FALSE + * - !this && primary -> FALSE + * + * isEmailChangeAllowed result: + * - is primary user -> TRUE + * - is recipe user + * - other recipe user exists + * - no -> TRUE + * - yes + * - email verified -> TRUE + * - email unverified -> TRUE + * - other primary user exists + * - no -> TRUE + * - yes + * - email verification status + * - this && primary -> TRUE + * - !this && !primary -> FALSE + * - this && !primary -> TRUE + * - !this && primary -> FALSE + * + * Based on the above, isEmailChangeAllowed can return true, but isSignInAllowed will return false + * in the following situations: + * - If a recipe user is signing in with a new email, other recipe users with the same email exist, + * and one of them is unverfied. In this case, the email change will happen in the social login + * recipe, but the user will not be able to login anyway. + * + * - If the recipe user is signing in with a new email, there exists a primary user with the same + * email, but this new email is verified for the recipe user already, but the primary user's email + * is not verified. + */ + + let isEmailChangeAllowed = await AccountLinking.getInstance().isEmailChangeAllowed({ + user: existingUsers[0], + isVerified: emailInfo.isVerified, + newEmail: emailInfo.id, + userContext, + }); + + if (!isEmailChangeAllowed) { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)", + }; + } + } let response = await options.recipeImplementation.signInUp({ thirdPartyId: provider.id, thirdPartyUserId: userInfo.thirdPartyUserId, email: emailInfo.id, + + isVerified: emailInfo.isVerified, oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, tenantId, userContext, }); - // we set the email as verified if already verified by the OAuth provider. - // This block was added because of https://github.com/supertokens/supertokens-core/issues/295 - if (emailInfo.isVerified) { - const emailVerificationInstance = EmailVerification.getInstance(); - if (emailVerificationInstance) { - const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( - { - userId: response.user.id, - email: response.user.email, - tenantId, - userContext, - } - ); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + return response; + } - if (tokenResponse.status === "OK") { - await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ - token: tokenResponse.token, - tenantId, - userContext, - }); - } + let loginMethod: RecipeLevelUser | undefined = undefined; + for (let i = 0; i < response.user.loginMethods.length; i++) { + if ( + response.user.loginMethods[i].hasSameThirdPartyInfoAs({ + id: provider.id, + userId: userInfo.thirdPartyUserId, + }) + ) { + loginMethod = response.user.loginMethods[i]; } } + if (loginMethod === undefined) { + throw new Error("Should never come here"); + } + + if (existingUsers.length > 0) { + // Here we do this check after sign in is done cause: + // - We first want to check if the credentials are correct first or not + // - The above recipe function marks the email as verified if other linked users + // with the same email are verified. The function below checks for the email verification + // so we want to call it only once this is up to date, + // - Even though the above call to signInUp is state changing (it changes the email + // of the user), it's OK to do this check here cause the isSignInAllowed checks + // conditions related to account linking and not related to email change. Email change + // condition checking happens before calling the recipe function anyway. + + let isSignInAllowed = await AccountLinking.getInstance().isSignInAllowed({ + user: response.user, + tenantId, + userContext, + }); + + if (!isSignInAllowed) { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)", + }; + } + + // we do account linking only during sign in here cause during sign up, + // the recipe function above does account linking for us. + response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); + } + let session = await Session.createNewSession( options.req, options.res, tenantId, - response.user.id, + loginMethod.recipeUserId, {}, {}, userContext ); + return { status: "OK", - createdNewUser: response.createdNewUser, + createdNewRecipeUser: response.createdNewRecipeUser, user: response.user, session, oAuthTokens: oAuthTokensToUse, diff --git a/lib/ts/recipe/thirdparty/api/signinup.ts b/lib/ts/recipe/thirdparty/api/signinup.ts index a56ef1a13..698f5dda0 100644 --- a/lib/ts/recipe/thirdparty/api/signinup.ts +++ b/lib/ts/recipe/thirdparty/api/signinup.ts @@ -14,7 +14,7 @@ */ import STError from "../error"; -import { send200Response } from "../../../utils"; +import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; import { APIInterface, APIOptions } from "../"; export default async function signInUpAPI( @@ -92,12 +92,7 @@ export default async function signInUpAPI( if (result.status === "OK") { send200Response(options.res, { status: result.status, - user: result.user, - createdNewUser: result.createdNewUser, - }); - } else if (result.status === "NO_EMAIL_GIVEN_BY_PROVIDER") { - send200Response(options.res, { - status: "NO_EMAIL_GIVEN_BY_PROVIDER", + ...getBackwardsCompatibleUserInfo(options.req, result), }); } else { send200Response(options.res, result); diff --git a/lib/ts/recipe/thirdparty/index.ts b/lib/ts/recipe/thirdparty/index.ts index ec5e68b5f..ff104b08e 100644 --- a/lib/ts/recipe/thirdparty/index.ts +++ b/lib/ts/recipe/thirdparty/index.ts @@ -15,7 +15,7 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, User, APIInterface, APIOptions, TypeProvider } from "./types"; +import { RecipeInterface, APIInterface, APIOptions, TypeProvider } from "./types"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; export default class Wrapper { @@ -42,6 +42,7 @@ export default class Wrapper { thirdPartyId: string, thirdPartyUserId: string, email: string, + isVerified: boolean, userContext: any = {} ) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.manuallyCreateOrUpdateUser({ @@ -49,32 +50,7 @@ export default class Wrapper { thirdPartyUserId, email, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, - userContext, - }); - } - - static getUserById(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - - static getUsersByEmail(tenantId: string, email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ - email, - tenantId, - userContext, - }); - } - - static getUserByThirdPartyInfo( - tenantId: string, - thirdPartyId: string, - thirdPartyUserId: string, - userContext: any = {} - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, - tenantId, + isVerified, userContext, }); } @@ -88,10 +64,4 @@ export let getProvider = Wrapper.getProvider; export let manuallyCreateOrUpdateUser = Wrapper.manuallyCreateOrUpdateUser; -export let getUserById = Wrapper.getUserById; - -export let getUsersByEmail = Wrapper.getUsersByEmail; - -export let getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; - -export type { RecipeInterface, User, APIInterface, APIOptions, TypeProvider }; +export type { RecipeInterface, APIInterface, APIOptions, TypeProvider }; diff --git a/lib/ts/recipe/thirdparty/recipe.ts b/lib/ts/recipe/thirdparty/recipe.ts index b534298f4..8d57780a4 100644 --- a/lib/ts/recipe/thirdparty/recipe.ts +++ b/lib/ts/recipe/thirdparty/recipe.ts @@ -17,7 +17,6 @@ import RecipeModule from "../../recipeModule"; import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, ProviderInput } from "./types"; import { validateAndNormaliseUserInput } from "./utils"; -import EmailVerificationRecipe from "../emailverification/recipe"; import MultitenancyRecipe from "../multitenancy/recipe"; import STError from "./error"; @@ -28,11 +27,10 @@ import authorisationUrlAPI from "./api/authorisationUrl"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; import { Querier } from "../../querier"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import appleRedirectHandler from "./api/appleRedirect"; import OverrideableBuilder from "supertokens-js-override"; import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -74,11 +72,6 @@ export default class Recipe extends RecipeModule { } PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = EmailVerificationRecipe.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - const mtRecipe = MultitenancyRecipe.getInstance(); if (mtRecipe !== undefined) { mtRecipe.staticThirdPartyProviders = this.config.signInAndUpFeature.providers; @@ -183,18 +176,4 @@ export default class Recipe extends RecipeModule { isErrorFromThisRecipe = (err: any): err is STError => { return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; - - // helper functions... - getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - let userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }; } diff --git a/lib/ts/recipe/thirdparty/recipeImplementation.ts b/lib/ts/recipe/thirdparty/recipeImplementation.ts index f32290029..760680231 100644 --- a/lib/ts/recipe/thirdparty/recipeImplementation.ts +++ b/lib/ts/recipe/thirdparty/recipeImplementation.ts @@ -1,115 +1,166 @@ -import { RecipeInterface, User, ProviderInput } from "./types"; +import { RecipeInterface, ProviderInput } from "./types"; import { Querier } from "../../querier"; import NormalisedURLPath from "../../normalisedURLPath"; import { findAndCreateProviderInstance, mergeProvidersFromCoreAndStatic } from "./providers/configUtils"; +import AccountLinking from "../accountlinking/recipe"; import MultitenancyRecipe from "../multitenancy/recipe"; +import RecipeUserId from "../../recipeUserId"; +import { getUser } from "../.."; +import { User as UserType } from "../../types"; +import { User } from "../../user"; export default function getRecipeImplementation(querier: Querier, providers: ProviderInput[]): RecipeInterface { return { - signInUp: async function ({ - thirdPartyId, - thirdPartyUserId, - email, - oAuthTokens, - rawUserInfoFromProvider, - tenantId, - }: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - tenantId: string; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }> { - let response = await querier.sendPostRequest(new NormalisedURLPath(`/${tenantId}/recipe/signinup`), { + manuallyCreateOrUpdateUser: async function ( + this: RecipeInterface, + { thirdPartyId, thirdPartyUserId, - email: { id: email }, - }); - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - oAuthTokens, - rawUserInfoFromProvider, - }; - }, - - manuallyCreateOrUpdateUser: async function ({ - thirdPartyId, - thirdPartyUserId, - email, - tenantId, - }: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - tenantId: string; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { + email, + isVerified, + tenantId, + userContext, + }: { + thirdPartyId: string; + thirdPartyUserId: string; + email: string; + isVerified: boolean; + tenantId: string; + userContext: any; + } + ): Promise< + | { status: "OK"; createdNewRecipeUser: boolean; user: UserType; recipeUserId: RecipeUserId } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { let response = await querier.sendPostRequest(new NormalisedURLPath(`/${tenantId}/recipe/signinup`), { thirdPartyId, thirdPartyUserId, - email: { id: email }, + email: { id: email, isVerified }, }); - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - }; - }, - getUserById: async function ({ userId }: { userId: string }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user"), { - userId, + if (response.status !== "OK") { + return response; + } + + response.user = new User(response.user); + response.recipeUserId = new RecipeUserId(response.recipeUserId); + + await AccountLinking.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: response.user, + recipeUserId: response.recipeUserId, + userContext, }); - if (response.status === "OK") { - return response.user; - } else { - return undefined; + + // we do this so that we get the updated user (in case the above + // function updated the verification status) and can return that + response.user = (await getUser(response.recipeUserId.getAsString(), userContext))!; + + if (!response.createdNewUser) { + // Unlike in the sign up scenario, we do not do account linking here + // cause we do not want sign in to change the potentially user ID of a user + // due to linking when this function is called by the dev in their API. + // If we did account linking + // then we would have to ask the dev to also change the session + // in such API calls. + // In the case of sign up, since we are creating a new user, it's fine + // to link there since there is no user id change really from the dev's + // point of view who is calling the sign up recipe function. + return { + status: "OK", + createdNewRecipeUser: response.createdNewUser, + user: response.user, + recipeUserId: response.recipeUserId, + }; } - }, - getUsersByEmail: async function ({ email, tenantId }: { email: string; tenantId: string }): Promise { - let users: User[] = []; - users = ( - await querier.sendGetRequest(new NormalisedURLPath(`/${tenantId}/recipe/users/by-email`), { - email, - }) - ).users; + let updatedUser = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); - return users; + return { + status: "OK", + createdNewRecipeUser: response.createdNewUser, + user: updatedUser, + recipeUserId: response.recipeUserId, + }; }, - getUserByThirdPartyInfo: async function ({ - thirdPartyId, - thirdPartyUserId, - tenantId, - }: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath(`/${tenantId}/recipe/user`), { + signInUp: async function ( + this: RecipeInterface, + { + thirdPartyId, + thirdPartyUserId, + email, + isVerified, + tenantId, + userContext, + oAuthTokens, + rawUserInfoFromProvider, + }: { + thirdPartyId: string; + thirdPartyUserId: string; + email: string; + isVerified: boolean; + tenantId: string; + userContext: any; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + ): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: UserType; + recipeUserId: RecipeUserId; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { + let response = await this.manuallyCreateOrUpdateUser({ thirdPartyId, thirdPartyUserId, + email, + tenantId, + isVerified, + userContext, }); + + if (response.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + return { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)", + }; + } + if (response.status === "OK") { - return response.user; - } else { - return undefined; + return { + ...response, + oAuthTokens, + rawUserInfoFromProvider, + }; } + + return response; }, getProvider: async function ({ thirdPartyId, tenantId, clientType, userContext }) { diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index adcb96423..f27bed3ed 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -13,11 +13,12 @@ * under the License. */ -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import { NormalisedAppinfo } from "../../types"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export type UserInfo = { thirdPartyUserId: string; @@ -92,18 +93,6 @@ export type TypeProvider = { getUserInfo: (input: { oAuthTokens: any; userContext: any }) => Promise; }; -export type User = { - // https://github.com/supertokens/core-driver-interface/wiki#third-party-user - id: string; - timeJoined: number; - email: string; - thirdParty: { - id: string; - userId: string; - }; - tenantIds: string[]; -}; - export type ProviderConfig = CommonProviderConfig & { clients?: ProviderClientConfig[]; }; @@ -144,17 +133,6 @@ export type TypeNormalisedInput = { }; export type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - - getUsersByEmail(input: { email: string; tenantId: string; userContext: any }): Promise; - - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise; - getProvider(input: { thirdPartyId: string; tenantId: string; @@ -166,6 +144,7 @@ export type RecipeInterface = { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; @@ -173,24 +152,47 @@ export type RecipeInterface = { }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + recipeUserId: RecipeUserId; + user: User; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; manuallyCreateOrUpdateUser(input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; }; export type APIOptions = { @@ -245,7 +247,7 @@ export type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; session: SessionContainerInterface; oAuthTokens: { [key: string]: any }; @@ -255,6 +257,10 @@ export type APIInterface = { }; } | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); diff --git a/lib/ts/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts index eb5066d51..f2f91324b 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts @@ -2,30 +2,9 @@ import { APIInterface } from "../../thirdparty"; import { APIInterface as ThirdPartyEmailPasswordAPIInterface } from "../"; export default function getIterfaceImpl(apiImplmentation: ThirdPartyEmailPasswordAPIInterface): APIInterface { - const signInUpPOSTFromThirdPartyEmailPassword = apiImplmentation.thirdPartySignInUpPOST?.bind(apiImplmentation); return { authorisationUrlGET: apiImplmentation.authorisationUrlGET?.bind(apiImplmentation), appleRedirectHandlerPOST: apiImplmentation.appleRedirectHandlerPOST?.bind(apiImplmentation), - signInUpPOST: - signInUpPOSTFromThirdPartyEmailPassword === undefined - ? undefined - : async function (input) { - let result = await signInUpPOSTFromThirdPartyEmailPassword(input); - if (result.status === "OK") { - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return { - ...result, - user: { - ...result.user, - thirdParty: { - ...result.user.thirdParty, - }, - }, - }; - } - return result; - }, + signInUpPOST: apiImplmentation.thirdPartySignInUpPOST?.bind(apiImplmentation), }; } diff --git a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts index c585bcb58..bd04ddf2e 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts @@ -13,7 +13,6 @@ * under the License. */ import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; -import { RecipeInterface as EmailPasswordRecipeInterface } from "../../../../emailpassword"; import { NormalisedAppinfo } from "../../../../../types"; import EmailPasswordBackwardCompatibilityService from "../../../../emailpassword/emaildelivery/services/backwardCompatibility"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; @@ -22,14 +21,9 @@ export default class BackwardCompatibilityService implements EmailDeliveryInterface { private emailPasswordBackwardCompatibilityService: EmailPasswordBackwardCompatibilityService; - constructor( - emailPasswordRecipeInterfaceImpl: EmailPasswordRecipeInterface, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean - ) { + constructor(appInfo: NormalisedAppinfo, isInServerlessEnv: boolean) { { this.emailPasswordBackwardCompatibilityService = new EmailPasswordBackwardCompatibilityService( - emailPasswordRecipeInterfaceImpl, appInfo, isInServerlessEnv ); diff --git a/lib/ts/recipe/thirdpartyemailpassword/index.ts b/lib/ts/recipe/thirdpartyemailpassword/index.ts index 2ff9f9c0f..425d1f132 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/index.ts @@ -15,11 +15,13 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions } from "./types"; +import { RecipeInterface, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions } from "./types"; import { TypeProvider } from "../thirdparty/types"; import { TypeEmailPasswordEmailDeliveryInput } from "../emailpassword/types"; +import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; import { getPasswordResetLink } from "../emailpassword/utils"; +import { getUser } from "../.."; export default class Wrapper { static init = Recipe.init; @@ -45,26 +47,14 @@ export default class Wrapper { thirdPartyId: string, thirdPartyUserId: string, email: string, + isVerified: boolean, userContext: any = {} ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ thirdPartyId, thirdPartyUserId, email, - tenantId, - userContext, - }); - } - - static getUserByThirdPartyInfo( - tenantId: string, - thirdPartyId: string, - thirdPartyUserId: string, - userContext: any = {} - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, + isVerified, tenantId, userContext, }); @@ -88,37 +78,41 @@ export default class Wrapper { }); } - static getUserById(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - - static getUsersByEmail(tenantId: string, email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ + static createResetPasswordToken(tenantId: string, userId: string, email: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ + userId, email, tenantId, userContext, }); } - static createResetPasswordToken(tenantId: string, userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ - userId, - tenantId, + static async resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext?: any) { + const consumeResp = await Wrapper.consumePasswordResetToken(tenantId, token, userContext); + + if (consumeResp.status !== "OK") { + return consumeResp; + } + + return await Wrapper.updateEmailOrPassword({ + recipeUserId: new RecipeUserId(consumeResp.userId), + email: consumeResp.email, + password: newPassword, + tenantIdForPasswordPolicy: tenantId, userContext, }); } - static resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + static consumePasswordResetToken(tenantId: string, token: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, - newPassword, tenantId, userContext, }); } static updateEmailOrPassword(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext?: any; @@ -126,8 +120,8 @@ export default class Wrapper { tenantIdForPasswordPolicy?: string; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, tenantIdForPasswordPolicy: input.tenantIdForPasswordPolicy === undefined ? DEFAULT_TENANT_ID : input.tenantIdForPasswordPolicy, }); @@ -136,9 +130,10 @@ export default class Wrapper { static async createResetPasswordLink( tenantId: string, userId: string, - userContext?: any + email: string, + userContext: any = {} ): Promise<{ status: "OK"; link: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { - let token = await createResetPasswordToken(userId, tenantId, userContext); + let token = await createResetPasswordToken(userId, tenantId, email, userContext); if (token.status === "UNKNOWN_USER_ID_ERROR") { return token; } @@ -158,16 +153,32 @@ export default class Wrapper { static async sendResetPasswordEmail( tenantId: string, userId: string, - userContext?: any + email: string, + userContext: any = {} ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }> { - let link = await createResetPasswordLink(userId, tenantId, userContext); + const user = await getUser(userId, userContext); + if (!user) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + + const loginMethod = user.loginMethods.find((m) => m.recipeId === "emailpassword" && m.hasSameEmailAs(email)); + if (!loginMethod) { + return { status: "UNKNOWN_USER_ID_ERROR" }; + } + + let link = await createResetPasswordLink(userId, tenantId, email, userContext); if (link.status === "UNKNOWN_USER_ID_ERROR") { return link; } + await sendEmail({ passwordResetLink: link.link, type: "PASSWORD_RESET", - user: (await getUserById(userId, userContext))!, + user: { + email: loginMethod.email!, + id: user.id, + recipeUserId: loginMethod.recipeUserId, + }, tenantId, userContext, }); @@ -179,8 +190,8 @@ export default class Wrapper { static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: any }) { return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, tenantId: input.tenantId === undefined ? DEFAULT_TENANT_ID : input.tenantId, }); } @@ -198,19 +209,15 @@ export let thirdPartyGetProvider = Wrapper.thirdPartyGetProvider; export let thirdPartyManuallyCreateOrUpdateUser = Wrapper.thirdPartyManuallyCreateOrUpdateUser; -export let getUserById = Wrapper.getUserById; - -export let getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; - -export let getUsersByEmail = Wrapper.getUsersByEmail; - export let createResetPasswordToken = Wrapper.createResetPasswordToken; export let resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; +export let consumePasswordResetToken = Wrapper.consumePasswordResetToken; + export let updateEmailOrPassword = Wrapper.updateEmailOrPassword; -export type { RecipeInterface, TypeProvider, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions }; +export type { RecipeInterface, TypeProvider, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions }; export let createResetPasswordLink = Wrapper.createResetPasswordLink; diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipe.ts b/lib/ts/recipe/thirdpartyemailpassword/recipe.ts index b4bc49a41..a40a6bcdf 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipe.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipe.ts @@ -16,7 +16,7 @@ import RecipeModule from "../../recipeModule"; import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; import EmailPasswordRecipe from "../emailpassword/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import STError from "./error"; import { TypeInput, @@ -97,9 +97,7 @@ export default class Recipe extends RecipeModule { */ this.emailDelivery = ingredients.emailDelivery === undefined - ? new EmailDeliveryIngredient( - this.config.getEmailDeliveryConfig(emailPasswordRecipeImplementation, this.isInServerlessEnv) - ) + ? new EmailDeliveryIngredient(this.config.getEmailDeliveryConfig(this.isInServerlessEnv)) : ingredients.emailDelivery; this.emailPasswordRecipe = diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts index 0d2c7a2e3..d7337efbd 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts @@ -1,5 +1,7 @@ -import { RecipeInterface, User } from "../../emailpassword/types"; +import { RecipeInterface } from "../../emailpassword/types"; +import { User } from "../../../types"; import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; +import RecipeUserId from "../../../recipeUserId"; export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface { return { @@ -8,7 +10,9 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw password: string; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { + }): Promise< + { status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { return await recipeInterface.emailPasswordSignUp(input); }, @@ -17,52 +21,40 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw password: string; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }> { + }): Promise<{ status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" }> { return recipeInterface.emailPasswordSignIn(input); }, - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user = await recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty !== undefined) { - // either user is undefined or it's a thirdparty user. - return undefined; - } - return user; - }, - - getUserByEmail: async function (input: { - email: string; - tenantId: string; - userContext: any; - }): Promise { - let result = await recipeInterface.getUsersByEmail(input); - for (let i = 0; i < result.length; i++) { - if (result[i].thirdParty === undefined) { - return result[i]; - } - } - return undefined; - }, - createResetPasswordToken: async function (input: { userId: string; + email: string; tenantId: string; userContext: any; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { return recipeInterface.createResetPasswordToken(input); }, - resetPasswordUsingToken: async function (input: { - token: string; - newPassword: string; - tenantId: string; + consumePasswordResetToken: async function (input: { token: string; tenantId: string; userContext: any }) { + return recipeInterface.consumePasswordResetToken(input); + }, + + createNewRecipeUser: async function (input: { + email: string; + password: string; userContext: any; - }) { - return recipeInterface.resetPasswordUsingToken(input); + }): Promise< + | { + status: "OK"; + user: User; + recipeUserId: RecipeUserId; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { + return recipeInterface.createNewEmailPasswordRecipeUser(input); }, updateEmailOrPassword: async function (input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext: any; @@ -72,6 +64,10 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw | { status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } > { return recipeInterface.updateEmailOrPassword(input); diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts index 6f0fdeb95..1c780c9a8 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts @@ -1,4 +1,4 @@ -import { RecipeInterface, User } from "../types"; +import { RecipeInterface } from "../types"; import EmailPasswordImplemenation from "../../emailpassword/recipeImplementation"; import ThirdPartyImplemenation from "../../thirdparty/recipeImplementation"; @@ -6,6 +6,9 @@ import { RecipeInterface as ThirdPartyRecipeInterface, TypeProvider } from "../. import { Querier } from "../../../querier"; import DerivedEP from "./emailPasswordRecipeImplementation"; import DerivedTP from "./thirdPartyRecipeImplementation"; +import { getUser } from "../../../"; +import { User } from "../../../types"; +import RecipeUserId from "../../../recipeUserId"; import { TypeNormalisedInput } from "../../emailpassword/types"; import { ProviderInput } from "../../thirdparty/types"; @@ -23,12 +26,24 @@ export default function getRecipeInterface( ); return { + createNewEmailPasswordRecipeUser: async function (input: { + email: string; + password: string; + tenantId: string; + userContext: any; + }): Promise< + { status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { + return await originalEmailPasswordImplementation.createNewRecipeUser.bind(DerivedEP(this))(input); + }, emailPasswordSignUp: async function (input: { email: string; password: string; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { + }): Promise< + { status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { return await originalEmailPasswordImplementation.signUp.bind(DerivedEP(this))(input); }, @@ -37,7 +52,7 @@ export default function getRecipeInterface( password: string; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }> { + }): Promise<{ status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" }> { return originalEmailPasswordImplementation.signIn.bind(DerivedEP(this))(input); }, @@ -45,6 +60,7 @@ export default function getRecipeInterface( thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; @@ -52,16 +68,23 @@ export default function getRecipeInterface( }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }> { + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { return originalThirdPartyImplementation.signInUp.bind(DerivedTP(this))(input); }, @@ -69,9 +92,20 @@ export default function getRecipeInterface( thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { + }): Promise< + | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { return originalThirdPartyImplementation.manuallyCreateOrUpdateUser.bind(DerivedTP(this))(input); }, @@ -84,69 +118,23 @@ export default function getRecipeInterface( return originalThirdPartyImplementation.getProvider.bind(DerivedTP(this))(input); }, - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user: User | undefined = await originalEmailPasswordImplementation.getUserById.bind(DerivedEP(this))( - input - ); - if (user !== undefined) { - return user; - } - return await originalThirdPartyImplementation.getUserById.bind(DerivedTP(this))(input); - }, - - getUsersByEmail: async function ({ - email, - tenantId, - userContext, - }: { - email: string; - tenantId: string; - userContext: any; - }): Promise { - let userFromEmailPass: User | undefined = await originalEmailPasswordImplementation.getUserByEmail.bind( - DerivedEP(this) - )({ email, tenantId, userContext }); - - let usersFromThirdParty: User[] = await originalThirdPartyImplementation.getUsersByEmail.bind( - DerivedTP(this) - )({ email, tenantId, userContext }); - - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }, - - getUserByThirdPartyInfo: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise { - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind(DerivedTP(this))(input); - }, - createResetPasswordToken: async function (input: { userId: string; + email: string; tenantId: string; userContext: any; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { return originalEmailPasswordImplementation.createResetPasswordToken.bind(DerivedEP(this))(input); }, - resetPasswordUsingToken: async function (input: { - token: string; - newPassword: string; - tenantId: string; - userContext: any; - }) { - return originalEmailPasswordImplementation.resetPasswordUsingToken.bind(DerivedEP(this))(input); + consumePasswordResetToken: async function (input: { token: string; tenantId: string; userContext: any }) { + return originalEmailPasswordImplementation.consumePasswordResetToken.bind(DerivedEP(this))(input); }, updateEmailOrPassword: async function ( this: RecipeInterface, input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext: any; @@ -157,14 +145,27 @@ export default function getRecipeInterface( | { status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } > { - let user = await this.getUserById({ userId: input.userId, userContext: input.userContext }); + let user = await getUser(input.recipeUserId.getAsString(), input.userContext); if (user === undefined) { return { status: "UNKNOWN_USER_ID_ERROR", }; - } else if (user.thirdParty !== undefined) { + } + let inputUserIdIsPointingToEmailPasswordUser = + user.loginMethods.find((lM) => { + return ( + lM.recipeId === "emailpassword" && + lM.recipeUserId.getAsString() === input.recipeUserId.getAsString() + ); + }) !== undefined; + + if (!inputUserIdIsPointingToEmailPasswordUser) { throw new Error("Cannot update email or password of a user who signed up using third party login."); } return originalEmailPasswordImplementation.updateEmailOrPassword.bind(DerivedEP(this))(input); diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts index 7cd69b2d4..dc736efde 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts @@ -1,31 +1,15 @@ -import { RecipeInterface, TypeProvider, User } from "../../thirdparty/types"; +import { RecipeInterface, TypeProvider } from "../../thirdparty/types"; import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; +import { User } from "../../../types"; +import RecipeUserId from "../../../recipeUserId"; export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface { return { - getUserByThirdPartyInfo: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise { - let user = await recipeInterface.getUserByThirdPartyInfo(input); - if (user === undefined || user.thirdParty === undefined) { - return undefined; - } - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - tenantIds: user.tenantIds, - }; - }, - signInUp: async function (input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; @@ -33,56 +17,62 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }> { - let result = await recipeInterface.thirdPartySignInUp(input); - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: { - email: result.user.email, - id: result.user.id, - timeJoined: result.user.timeJoined, - thirdParty: result.user.thirdParty, - tenantIds: result.user.tenantIds, - }, - oAuthTokens: result.oAuthTokens, - rawUserInfoFromProvider: result.rawUserInfoFromProvider, - }; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { + return await recipeInterface.thirdPartySignInUp(input); }, manuallyCreateOrUpdateUser: async function (input: { + tenantId: string; thirdPartyId: string; thirdPartyUserId: string; email: string; - tenantId: string; + isVerified: boolean; userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { let result = await recipeInterface.thirdPartyManuallyCreateOrUpdateUser(input); + if (result.status !== "OK") { + return result; + } + if (result.user.thirdParty === undefined) { throw new Error("Should never come here"); } return { status: "OK", - createdNewUser: result.createdNewUser, - user: { - email: result.user.email, - id: result.user.id, - timeJoined: result.user.timeJoined, - thirdParty: result.user.thirdParty, - tenantIds: result.user.tenantIds, - }, + createdNewRecipeUser: result.createdNewRecipeUser, + user: result.user, + recipeUserId: result.recipeUserId, }; }, @@ -94,33 +84,5 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw }): Promise { return await recipeInterface.thirdPartyGetProvider(input); }, - - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user = await recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty === undefined) { - // either user is undefined or it's an email password user. - return undefined; - } - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - tenantIds: user.tenantIds, - }; - }, - - getUsersByEmail: async function (input: { - email: string; - tenantId: string; - userContext: any; - }): Promise { - let users = await recipeInterface.getUsersByEmail(input); - - // we filter out all non thirdparty users. - return users.filter((u) => { - return u.thirdParty !== undefined; - }) as User[]; - }, }; } diff --git a/lib/ts/recipe/thirdpartyemailpassword/types.ts b/lib/ts/recipe/thirdpartyemailpassword/types.ts index bc8306576..52930a251 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/types.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/types.ts @@ -22,11 +22,9 @@ import { } from "../thirdparty/types"; import { NormalisedFormField, - TypeFormField, TypeInputFormField, APIOptions as EmailPasswordAPIOptionsOriginal, TypeEmailPasswordEmailDeliveryInput, - RecipeInterface as EPRecipeInterface, } from "../emailpassword/types"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; @@ -34,32 +32,8 @@ import { TypeInput as EmailDeliveryTypeInput, TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; -import { GeneralErrorResponse } from "../../types"; - -export type User = { - id: string; - timeJoined: number; - email: string; - thirdParty?: { - id: string; - userId: string; - }; - tenantIds: string[]; -}; - -export type TypeContextEmailPasswordSignUp = { - loginType: "emailpassword"; - formFields: TypeFormField[]; -}; - -export type TypeContextEmailPasswordSignIn = { - loginType: "emailpassword"; -}; - -export type TypeContextThirdParty = { - loginType: "thirdparty"; - thirdPartyAuthCodeResponse: any; -}; +import { GeneralErrorResponse, User as GlobalUser, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export type TypeInputSignUp = { formFields?: TypeInputFormField[]; @@ -86,7 +60,6 @@ export type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; providers: ProviderInput[]; getEmailDeliveryConfig: ( - emailPasswordRecipeImpl: EPRecipeInterface, isInServerlessEnv: boolean ) => EmailDeliveryTypeInputWithService; override: { @@ -99,17 +72,6 @@ export type TypeNormalisedInput = { }; export type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - - getUsersByEmail(input: { email: string; tenantId: string; userContext: any }): Promise; - - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise; - thirdPartyGetProvider(input: { thirdPartyId: string; clientType?: string; @@ -121,6 +83,7 @@ export type RecipeInterface = { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; @@ -128,64 +91,111 @@ export type RecipeInterface = { }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; thirdPartyManuallyCreateOrUpdateUser(input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: GlobalUser; + recipeUserId: RecipeUserId; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; emailPasswordSignUp(input: { email: string; password: string; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>; + }): Promise< + | { + status: "OK"; + user: GlobalUser; + recipeUserId: RecipeUserId; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + >; + + createNewEmailPasswordRecipeUser(input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: GlobalUser; + recipeUserId: RecipeUserId; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + >; emailPasswordSignIn(input: { email: string; password: string; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>; + }): Promise< + | { + status: "OK"; + user: GlobalUser; + recipeUserId: RecipeUserId; + } + | { status: "WRONG_CREDENTIALS_ERROR" } + >; createResetPasswordToken(input: { userId: string; + email: string; tenantId: string; userContext: any; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; - resetPasswordUsingToken(input: { + consumePasswordResetToken(input: { token: string; - newPassword: string; tenantId: string; userContext: any; }): Promise< | { status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; + email: string; + userId: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } >; updateEmailOrPassword(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string; password?: string; userContext: any; @@ -195,6 +205,10 @@ export type RecipeInterface = { | { status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } >; }; @@ -249,6 +263,10 @@ export type APIInterface = { | { status: "OK"; } + | { + status: "PASSWORD_RESET_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); @@ -266,11 +284,13 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - userId?: string; + email: string; + user: GlobalUser; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; } + | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } | GeneralErrorResponse >); @@ -297,8 +317,8 @@ export type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; - user: User; + createdNewRecipeUser: boolean; + user: GlobalUser; session: SessionContainerInterface; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { @@ -307,6 +327,10 @@ export type APIInterface = { }; } | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); @@ -323,9 +347,13 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - user: User; + user: GlobalUser; session: SessionContainerInterface; } + | { + status: "SIGN_IN_NOT_ALLOWED"; + reason: string; + } | { status: "WRONG_CREDENTIALS_ERROR"; } @@ -345,9 +373,13 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - user: User; + user: GlobalUser; session: SessionContainerInterface; } + | { + status: "SIGN_UP_NOT_ALLOWED"; + reason: string; + } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } diff --git a/lib/ts/recipe/thirdpartyemailpassword/utils.ts b/lib/ts/recipe/thirdpartyemailpassword/utils.ts index d23f86134..1abdc5f69 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/utils.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/utils.ts @@ -20,7 +20,6 @@ import Recipe from "./recipe"; import { normaliseSignUpFormFields } from "../emailpassword/utils"; import { RecipeInterface, APIInterface } from "./types"; import BackwardCompatibilityService from "./emaildelivery/services/backwardCompatibility"; -import { RecipeInterface as EPRecipeInterface } from "../emailpassword/types"; export function validateAndNormaliseUserInput( recipeInstance: Recipe, @@ -41,7 +40,7 @@ export function validateAndNormaliseUserInput( ...config?.override, }; - function getEmailDeliveryConfig(emailPasswordRecipeImpl: EPRecipeInterface, isInServerlessEnv: boolean) { + function getEmailDeliveryConfig(isInServerlessEnv: boolean) { let emailService = config?.emailDelivery?.service; /** * following code is for backward compatibility. @@ -49,7 +48,7 @@ export function validateAndNormaliseUserInput( * createAndSendEmailUsingSupertokensService implementation */ if (emailService === undefined) { - emailService = new BackwardCompatibilityService(emailPasswordRecipeImpl, appInfo, isInServerlessEnv); + emailService = new BackwardCompatibilityService(appInfo, isInServerlessEnv); } return { ...config?.emailDelivery, diff --git a/lib/ts/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts index 99295860e..5e7ef41a2 100644 --- a/lib/ts/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts +++ b/lib/ts/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts @@ -2,30 +2,9 @@ import { APIInterface } from "../../thirdparty"; import { APIInterface as ThirdPartyPasswordlessAPIInterface } from "../types"; export default function getIterfaceImpl(apiImplmentation: ThirdPartyPasswordlessAPIInterface): APIInterface { - const signInUpPOSTFromThirdPartyPasswordless = apiImplmentation.thirdPartySignInUpPOST?.bind(apiImplmentation); return { authorisationUrlGET: apiImplmentation.authorisationUrlGET?.bind(apiImplmentation), appleRedirectHandlerPOST: apiImplmentation.appleRedirectHandlerPOST?.bind(apiImplmentation), - signInUpPOST: - signInUpPOSTFromThirdPartyPasswordless === undefined - ? undefined - : async function (input) { - let result = await signInUpPOSTFromThirdPartyPasswordless(input); - if (result.status === "OK") { - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return { - ...result, - user: { - ...result.user, - thirdParty: { - ...result.user.thirdParty, - }, - }, - }; - } - return result; - }, + signInUpPOST: apiImplmentation.thirdPartySignInUpPOST?.bind(apiImplmentation), }; } diff --git a/lib/ts/recipe/thirdpartypasswordless/index.ts b/lib/ts/recipe/thirdpartypasswordless/index.ts index 5ef040a32..503d02194 100644 --- a/lib/ts/recipe/thirdpartypasswordless/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/index.ts @@ -17,7 +17,6 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; import { RecipeInterface, - User, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions, @@ -25,6 +24,7 @@ import { } from "./types"; import { TypeProvider } from "../thirdparty/types"; import { TypePasswordlessSmsDeliveryInput } from "../passwordless/types"; +import RecipeUserId from "../../recipeUserId"; export default class Wrapper { static init = Recipe.init; @@ -50,38 +50,14 @@ export default class Wrapper { thirdPartyId: string, thirdPartyUserId: string, email: string, + isVerified: boolean, userContext: any = {} ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ thirdPartyId, thirdPartyUserId, email, - tenantId, - userContext, - }); - } - - static getUserByThirdPartyInfo( - tenantId: string, - thirdPartyId: string, - thirdPartyUserId: string, - userContext: any = {} - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, - tenantId, - userContext, - }); - } - - static getUserById(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - - static getUsersByEmail(tenantId: string, email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ - email, + isVerified, tenantId, userContext, }); @@ -98,8 +74,8 @@ export default class Wrapper { ) & { userInputCode?: string; tenantId: string; userContext?: any } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -110,8 +86,8 @@ export default class Wrapper { userContext?: any; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -132,27 +108,20 @@ export default class Wrapper { } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ - userContext: {}, - ...input, - }); - } - - static getUserByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByPhoneNumber({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static updatePasswordlessUser(input: { - userId: string; + recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; userContext?: any; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updatePasswordlessUser({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -170,43 +139,43 @@ export default class Wrapper { } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static revokeCode(input: { codeId: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static listCodesByEmail(input: { email: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext?: any }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -224,8 +193,8 @@ export default class Wrapper { } ) { return Recipe.getInstanceOrThrowError().passwordlessRecipe.createMagicLink({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } @@ -243,22 +212,22 @@ export default class Wrapper { } ) { return Recipe.getInstanceOrThrowError().passwordlessRecipe.signInUp({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static async sendEmail(input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext?: any }) { return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: any }) { return await Recipe.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms({ - userContext: {}, ...input, + userContext: input.userContext ?? {}, }); } } @@ -273,18 +242,10 @@ export let thirdPartyManuallyCreateOrUpdateUser = Wrapper.thirdPartyManuallyCrea export let passwordlessSignInUp = Wrapper.passwordlessSignInUp; -export let getUserById = Wrapper.getUserById; - -export let getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; - -export let getUsersByEmail = Wrapper.getUsersByEmail; - export let createCode = Wrapper.createCode; export let consumeCode = Wrapper.consumeCode; -export let getUserByPhoneNumber = Wrapper.getUserByPhoneNumber; - export let listCodesByDeviceId = Wrapper.listCodesByDeviceId; export let listCodesByEmail = Wrapper.listCodesByEmail; @@ -303,7 +264,7 @@ export let revokeCode = Wrapper.revokeCode; export let createMagicLink = Wrapper.createMagicLink; -export type { RecipeInterface, TypeProvider, User, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions }; +export type { RecipeInterface, TypeProvider, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions }; export let sendEmail = Wrapper.sendEmail; diff --git a/lib/ts/recipe/thirdpartypasswordless/recipe.ts b/lib/ts/recipe/thirdpartypasswordless/recipe.ts index b094ee0e2..cc2f1922d 100644 --- a/lib/ts/recipe/thirdpartypasswordless/recipe.ts +++ b/lib/ts/recipe/thirdpartypasswordless/recipe.ts @@ -16,7 +16,7 @@ import RecipeModule from "../../recipeModule"; import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; import PasswordlessRecipe from "../passwordless/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import STError from "./error"; import { TypeInput, diff --git a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts index fc251c766..a50a87934 100644 --- a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts @@ -1,4 +1,4 @@ -import { RecipeInterface, User } from "../types"; +import { RecipeInterface } from "../types"; import PasswordlessImplemenation from "../../passwordless/recipeImplementation"; import ThirdPartyImplemenation from "../../thirdparty/recipeImplementation"; @@ -6,6 +6,8 @@ import { RecipeInterface as ThirdPartyRecipeInterface, TypeProvider } from "../. import { Querier } from "../../../querier"; import DerivedPwdless from "./passwordlessRecipeImplementation"; import DerivedTP from "./thirdPartyRecipeImplementation"; +import { User } from "../../../types"; +import { RecipeUserId, getUser } from "../../../"; import { ProviderInput } from "../../thirdparty/types"; export default function getRecipeInterface( @@ -29,9 +31,6 @@ export default function getRecipeInterface( createNewCodeForDevice: async function (input) { return originalPasswordlessImplementation.createNewCodeForDevice.bind(DerivedPwdless(this))(input); }, - getUserByPhoneNumber: async function (input) { - return originalPasswordlessImplementation.getUserByPhoneNumber.bind(DerivedPwdless(this))(input); - }, listCodesByDeviceId: async function (input) { return originalPasswordlessImplementation.listCodesByDeviceId.bind(DerivedPwdless(this))(input); }, @@ -52,16 +51,26 @@ export default function getRecipeInterface( }, updatePasswordlessUser: async function (this: RecipeInterface, input) { - let user = await this.getUserById({ userId: input.userId, userContext: input.userContext }); + let user = await getUser(input.recipeUserId.getAsString(), input.userContext); if (user === undefined) { return { status: "UNKNOWN_USER_ID_ERROR", }; - } else if ("thirdParty" in user) { + } + let inputUserIdIsPointingToPasswordlessUser = + user.loginMethods.find((lM) => { + return ( + lM.recipeId === "passwordless" && + lM.recipeUserId.getAsString() === input.recipeUserId.getAsString() + ); + }) !== undefined; + + if (!inputUserIdIsPointingToPasswordlessUser) { throw new Error( - "Cannot update passwordless user info for those who signed up using third party login." + "Cannot update a user who signed up using third party login using updatePasswordlessUser." ); } + return originalPasswordlessImplementation.updateUser.bind(DerivedPwdless(this))(input); }, @@ -69,6 +78,7 @@ export default function getRecipeInterface( thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; @@ -76,16 +86,23 @@ export default function getRecipeInterface( }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }> { + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { return originalThirdPartyImplementation.signInUp.bind(DerivedTP(this))(input); }, @@ -93,9 +110,20 @@ export default function getRecipeInterface( thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { + }): Promise< + | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { return originalThirdPartyImplementation.manuallyCreateOrUpdateUser.bind(DerivedTP(this))(input); }, @@ -107,47 +135,5 @@ export default function getRecipeInterface( }): Promise { return originalThirdPartyImplementation.getProvider.bind(DerivedTP(this))(input); }, - - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user: User | undefined = await originalPasswordlessImplementation.getUserById.bind( - DerivedPwdless(this) - )(input); - if (user !== undefined) { - return user; - } - return await originalThirdPartyImplementation.getUserById.bind(DerivedTP(this))(input); - }, - - getUsersByEmail: async function ({ - email, - tenantId, - userContext, - }: { - email: string; - tenantId: string; - userContext: any; - }): Promise { - let userFromEmailPass: User | undefined = await originalPasswordlessImplementation.getUserByEmail.bind( - DerivedPwdless(this) - )({ email, tenantId, userContext }); - - let usersFromThirdParty: User[] = await originalThirdPartyImplementation.getUsersByEmail.bind( - DerivedTP(this) - )({ email, tenantId, userContext }); - - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }, - - getUserByThirdPartyInfo: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise { - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind(DerivedTP(this))(input); - }, }; } diff --git a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts index 676384fc8..b7d6e9f4e 100644 --- a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts @@ -12,32 +12,6 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordle createNewCodeForDevice: async function (input) { return await recipeInterface.createNewCodeForDevice(input); }, - getUserByEmail: async function (input) { - let users = await recipeInterface.getUsersByEmail(input); - for (let i = 0; i < users.length; i++) { - let u = users[i]; - if (!("thirdParty" in u)) { - return u; - } - } - return undefined; - }, - getUserById: async function (input) { - let user = await recipeInterface.getUserById(input); - if (user !== undefined && "thirdParty" in user) { - // this is a thirdparty user. - return undefined; - } - return user; - }, - getUserByPhoneNumber: async function (input) { - let user = await recipeInterface.getUserByPhoneNumber(input); - if (user !== undefined && "thirdParty" in user) { - // this is a thirdparty user. - return undefined; - } - return user; - }, listCodesByDeviceId: async function (input) { return await recipeInterface.listCodesByDeviceId(input); }, diff --git a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts index 06a93943b..a50977465 100644 --- a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts @@ -1,25 +1,15 @@ -import { RecipeInterface, TypeProvider, User } from "../../thirdparty/types"; +import { RecipeInterface, TypeProvider } from "../../thirdparty/types"; import { RecipeInterface as ThirdPartyPasswordlessRecipeInterface } from "../types"; +import { User } from "../../../types"; +import RecipeUserId from "../../../recipeUserId"; export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordlessRecipeInterface): RecipeInterface { return { - getUserByThirdPartyInfo: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise { - let user = await recipeInterface.getUserByThirdPartyInfo(input); - if (user === undefined || !("thirdParty" in user)) { - return undefined; - } - return user; - }, - signInUp: async function (input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; @@ -27,43 +17,55 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordle }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }> { - let result = await recipeInterface.thirdPartySignInUp(input); - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: result.user, - oAuthTokens: result.oAuthTokens, - rawUserInfoFromProvider: result.rawUserInfoFromProvider, - }; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { + return await recipeInterface.thirdPartySignInUp(input); }, manuallyCreateOrUpdateUser: async function (input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { + }): Promise< + | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + > { let result = await recipeInterface.thirdPartyManuallyCreateOrUpdateUser(input); + if (result.status !== "OK") { + return result; + } if (!("thirdParty" in result.user)) { throw new Error("Should never come here"); } return { status: "OK", - createdNewUser: result.createdNewUser, + createdNewRecipeUser: result.createdNewRecipeUser, + recipeUserId: result.recipeUserId, user: result.user, }; }, @@ -76,27 +78,5 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordle }): Promise { return await recipeInterface.thirdPartyGetProvider(input); }, - - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user = await recipeInterface.getUserById(input); - if (user === undefined || !("thirdParty" in user)) { - // either user is undefined or it's an email password user. - return undefined; - } - return user; - }, - - getUsersByEmail: async function (input: { - email: string; - tenantId: string; - userContext: any; - }): Promise { - let users = await recipeInterface.getUsersByEmail(input); - - // we filter out all non thirdparty users. - return users.filter((u) => { - return "thirdParty" in u; - }) as User[]; - }, }; } diff --git a/lib/ts/recipe/thirdpartypasswordless/types.ts b/lib/ts/recipe/thirdpartypasswordless/types.ts index 1275be0be..5b5e6e75f 100644 --- a/lib/ts/recipe/thirdpartypasswordless/types.ts +++ b/lib/ts/recipe/thirdpartypasswordless/types.ts @@ -36,30 +36,11 @@ import { TypeInput as SmsDeliveryTypeInput, TypeInputWithService as SmsDeliveryTypeInputWithService, } from "../../ingredients/smsdelivery/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, User } from "../../types"; +import RecipeUserId from "../../recipeUserId"; export type DeviceType = DeviceTypeOriginal; -export type User = ( - | { - // passwordless user properties - email?: string; - phoneNumber?: string; - } - | { - // third party user properties - email: string; - thirdParty: { - id: string; - userId: string; - }; - } -) & { - id: string; - timeJoined: number; - tenantIds: string[]; -}; - export type TypeInput = ( | { contactMethod: "PHONE"; @@ -140,27 +121,11 @@ export type TypeNormalisedInput = ( }; export type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - - getUsersByEmail(input: { email: string; tenantId: string; userContext: any }): Promise; - - getUserByPhoneNumber: (input: { - phoneNumber: string; - tenantId: string; - userContext: any; - }) => Promise; - - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - tenantId: string; - userContext: any; - }): Promise; - thirdPartySignInUp(input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; @@ -168,24 +133,42 @@ export type RecipeInterface = { }; tenantId: string; userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }>; + }): Promise< + | { + status: "OK"; + createdNewRecipeUser: boolean; + user: User; + recipeUserId: RecipeUserId; + oAuthTokens: { [key: string]: any }; + rawUserInfoFromProvider: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; thirdPartyManuallyCreateOrUpdateUser(input: { thirdPartyId: string; thirdPartyUserId: string; email: string; + isVerified: boolean; tenantId: string; userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }>; + }): Promise< + | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } + >; thirdPartyGetProvider(input: { thirdPartyId: string; @@ -251,8 +234,9 @@ export type RecipeInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; + recipeUserId: RecipeUserId; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -263,13 +247,23 @@ export type RecipeInterface = { >; updatePasswordlessUser: (input: { - userId: string; + recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; + }) => Promise< + | { + status: + | "OK" + | "UNKNOWN_USER_ID_ERROR" + | "EMAIL_ALREADY_EXISTS_ERROR" + | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; revokeAllCodes: ( input: @@ -360,7 +354,7 @@ export type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; session: SessionContainerInterface; oAuthTokens: { [key: string]: any }; @@ -370,6 +364,10 @@ export type APIInterface = { }; } | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); @@ -396,6 +394,10 @@ export type APIInterface = { preAuthSessionId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } | GeneralErrorResponse >); @@ -430,7 +432,7 @@ export type APIInterface = { ) => Promise< | { status: "OK"; - createdNewUser: boolean; + createdNewRecipeUser: boolean; user: User; session: SessionContainerInterface; } @@ -441,6 +443,10 @@ export type APIInterface = { } | GeneralErrorResponse | { status: "RESTART_FLOW_ERROR" } + | { + status: "SIGN_IN_UP_NOT_ALLOWED"; + reason: string; + } >); passwordlessUserEmailExistsGET: diff --git a/lib/ts/recipe/usermetadata/recipe.ts b/lib/ts/recipe/usermetadata/recipe.ts index 036b99d97..93d7e41d1 100644 --- a/lib/ts/recipe/usermetadata/recipe.ts +++ b/lib/ts/recipe/usermetadata/recipe.ts @@ -15,7 +15,7 @@ import SuperTokensError from "../../error"; import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import { Querier } from "../../querier"; import RecipeModule from "../../recipeModule"; @@ -83,7 +83,7 @@ export default class Recipe extends RecipeModule { // This stub is required to implement RecipeModule handleAPIRequest = async ( _: string, - ______: string | undefined, // TODO tenantId + _tenantId: string | undefined, __: BaseRequest, ___: BaseResponse, ____: normalisedURLPath, diff --git a/lib/ts/recipe/userroles/permissionClaim.ts b/lib/ts/recipe/userroles/permissionClaim.ts index bf8645f79..7fa66a77d 100644 --- a/lib/ts/recipe/userroles/permissionClaim.ts +++ b/lib/ts/recipe/userroles/permissionClaim.ts @@ -8,7 +8,7 @@ export class PermissionClaimClass extends PrimitiveArrayClaim { constructor() { super({ key: "st-perm", - async fetchValue(userId, tenantId, userContext) { + async fetchValue(userId, _recipeUserId, tenantId, userContext) { const recipe = UserRoleRecipe.getInstanceOrThrowError(); // We fetch the roles because the rolesClaim may not be present in the payload diff --git a/lib/ts/recipe/userroles/recipe.ts b/lib/ts/recipe/userroles/recipe.ts index 8bafc1e19..0f0176c7b 100644 --- a/lib/ts/recipe/userroles/recipe.ts +++ b/lib/ts/recipe/userroles/recipe.ts @@ -15,7 +15,7 @@ import SuperTokensError from "../../error"; import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; +import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import { Querier } from "../../querier"; import RecipeModule from "../../recipeModule"; @@ -96,7 +96,7 @@ export default class Recipe extends RecipeModule { // This stub is required to implement RecipeModule handleAPIRequest = async ( _: string, - ______: string | undefined, // TODO tenantId + _tenantId: string | undefined, __: BaseRequest, ___: BaseResponse, ____: normalisedURLPath, diff --git a/lib/ts/recipe/userroles/userRoleClaim.ts b/lib/ts/recipe/userroles/userRoleClaim.ts index ffefa6d18..e7f689bad 100644 --- a/lib/ts/recipe/userroles/userRoleClaim.ts +++ b/lib/ts/recipe/userroles/userRoleClaim.ts @@ -8,7 +8,7 @@ export class UserRoleClaimClass extends PrimitiveArrayClaim { constructor() { super({ key: "st-role", - async fetchValue(userId, tenantId, userContext) { + async fetchValue(userId, _recipeUserId, tenantId, userContext) { const recipe = UserRoleRecipe.getInstanceOrThrowError(); const res = await recipe.recipeInterfaceImpl.getRolesForUser({ userId, diff --git a/lib/ts/recipeUserId.ts b/lib/ts/recipeUserId.ts new file mode 100644 index 000000000..9199e55bd --- /dev/null +++ b/lib/ts/recipeUserId.ts @@ -0,0 +1,13 @@ +export default class RecipeUserId { + private recipeUserId: string; + constructor(recipeUserId: string) { + if (recipeUserId === undefined) { + throw new Error("recipeUserId cannot be undefined. Please check for bugs in code"); + } + this.recipeUserId = recipeUserId; + } + + public getAsString = () => { + return this.recipeUserId; + }; +} diff --git a/lib/ts/supertokens.ts b/lib/ts/supertokens.ts index e9c302d68..bbf65492a 100644 --- a/lib/ts/supertokens.ts +++ b/lib/ts/supertokens.ts @@ -26,8 +26,8 @@ import RecipeModule from "./recipeModule"; import { HEADER_RID, HEADER_FDI } from "./constants"; import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; -import { BaseRequest, BaseResponse } from "./framework"; -import { TypeFramework } from "./framework/types"; +import type { BaseRequest, BaseResponse } from "./framework"; +import type { TypeFramework } from "./framework/types"; import STError from "./error"; import { logDebugMessage } from "./logger"; import { PostSuperTokensInitCallbacks } from "./postSuperTokensInitCallbacks"; @@ -172,60 +172,6 @@ export default class SuperTokens { return Number(response.count); }; - getUsers = async (input: { - timeJoinedOrder: "ASC" | "DESC"; - limit?: number; - paginationToken?: string; - includeRecipeIds?: string[]; - query?: object; - tenantId: string; - }): Promise<{ - users: { recipeId: string; user: any }[]; - nextPaginationToken?: string; - }> => { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error( - "Please use core version >= 3.5 to call this function. Otherwise, you can call .getUsersOldestFirst() or .getUsersNewestFirst() instead (for example, EmailPassword.getUsersOldestFirst())" - ); - } - let includeRecipeIdsStr = undefined; - if (input.includeRecipeIds !== undefined) { - includeRecipeIdsStr = input.includeRecipeIds.join(","); - } - let response = await querier.sendGetRequest(new NormalisedURLPath(`/${input.tenantId}/users`), { - ...input.query, - includeRecipeIds: includeRecipeIdsStr, - timeJoinedOrder: input.timeJoinedOrder, - limit: input.limit, - paginationToken: input.paginationToken, - }); - - const users: { recipeId: string; user: any }[] = response.users; - return { - users, - nextPaginationToken: response.nextPaginationToken, - }; - }; - - deleteUser = async (input: { userId: string }): Promise<{ status: "OK" }> => { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = await querier.getAPIVersion(); - if (maxVersion("2.10", cdiVersion) === cdiVersion) { - // delete user is only available >= CDI 2.10 - await querier.sendPostRequest(new NormalisedURLPath("/user/remove"), { - userId: input.userId, - }); - - return { - status: "OK", - }; - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.7.0"); - } - }; - createUserIdMapping = async function (input: { superTokensUserId: string; externalUserId: string; diff --git a/lib/ts/types.ts b/lib/ts/types.ts index 22a278ac3..c3fd99584 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -17,6 +17,7 @@ import RecipeModule from "./recipeModule"; import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; import { TypeFramework } from "./framework/types"; +import { RecipeLevelUser } from "./recipe/accountlinking/types"; export type AppInfo = { appName: string; @@ -73,3 +74,28 @@ export type GeneralErrorResponse = { status: "GENERAL_ERROR"; message: string; }; + +export type User = { + id: string; // primaryUserId or recipeUserId + timeJoined: number; // minimum timeJoined value from linkedRecipes + isPrimaryUser: boolean; + tenantIds: string[]; + emails: string[]; + phoneNumbers: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: (RecipeLevelUser & { + verified: boolean; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + toJson: () => any; + })[]; + + // this function will be used in the send200Response function in utils to + // convert this object to JSON before sending it to the client. So that in RecipeLevelUser + // the recipeUserId can be converted to string from the RecipeUserId object type. + toJson: () => any; +}; diff --git a/lib/ts/user.ts b/lib/ts/user.ts new file mode 100644 index 000000000..365e0f846 --- /dev/null +++ b/lib/ts/user.ts @@ -0,0 +1,153 @@ +import { RecipeLevelUser } from "./recipe/accountlinking/types"; +import RecipeUserId from "./recipeUserId"; +import parsePhoneNumber from "libphonenumber-js/max"; +import { JSONObject, User as UserType } from "./types"; + +export class LoginMethod implements RecipeLevelUser { + public readonly recipeId: RecipeLevelUser["recipeId"]; + public readonly recipeUserId: RecipeUserId; + public readonly tenantIds: string[]; + + public readonly email?: string; + public readonly phoneNumber?: string; + public readonly thirdParty?: RecipeLevelUser["thirdParty"]; + public readonly verified: boolean; + + public readonly timeJoined: number; + + constructor(loginMethod: UserWithoutHelperFunctions["loginMethods"][number]) { + this.recipeId = loginMethod.recipeId; + this.recipeUserId = new RecipeUserId(loginMethod.recipeUserId); + this.tenantIds = loginMethod.tenantIds; + + this.email = loginMethod.email; + this.phoneNumber = loginMethod.phoneNumber; + this.thirdParty = loginMethod.thirdParty; + + this.timeJoined = loginMethod.timeJoined; + this.verified = loginMethod.verified; + } + + hasSameEmailAs(email: string | undefined): boolean { + if (email === undefined) { + return false; + } + // this needs to be the same as what's done in the core. + email = email.toLowerCase().trim(); + return this.email !== undefined && this.email === email; + } + + hasSamePhoneNumberAs(phoneNumber: string | undefined): boolean { + if (phoneNumber === undefined) { + return false; + } + const parsedPhoneNumber = parsePhoneNumber(phoneNumber.trim(), { extract: false }); + if (parsedPhoneNumber === undefined) { + // this means that the phone number is not valid according to the E.164 standard. + // but we still just trim it. + phoneNumber = phoneNumber.trim(); + } else { + phoneNumber = parsedPhoneNumber.format("E.164"); + } + return this.phoneNumber !== undefined && this.phoneNumber === phoneNumber; + } + + hasSameThirdPartyInfoAs(thirdParty?: { id: string; userId: string }): boolean { + if (thirdParty === undefined) { + return false; + } + thirdParty.id = thirdParty.id.trim(); + thirdParty.userId = thirdParty.userId.trim(); + return ( + this.thirdParty !== undefined && + this.thirdParty.id === thirdParty.id && + this.thirdParty.userId === thirdParty.userId + ); + } + + toJson(): JSONObject { + return { + recipeId: this.recipeId, + recipeUserId: this.recipeUserId.getAsString(), + tenantIds: this.tenantIds, + email: this.email, + phoneNumber: this.phoneNumber, + thirdParty: this.thirdParty, + timeJoined: this.timeJoined, + verified: this.verified, + }; + } +} + +export class User implements UserType { + public readonly id: string; // primaryUserId or recipeUserId + public readonly isPrimaryUser: boolean; + public readonly tenantIds: string[]; + + public readonly emails: string[]; + public readonly phoneNumbers: string[]; + public readonly thirdParty: { + id: string; + userId: string; + }[]; + public readonly loginMethods: LoginMethod[]; + + public readonly timeJoined: number; // minimum timeJoined value from linkedRecipes + + constructor(user: UserWithoutHelperFunctions) { + this.id = user.id; + this.isPrimaryUser = user.isPrimaryUser; + this.tenantIds = user.tenantIds; + + this.emails = user.emails; + this.phoneNumbers = user.phoneNumbers; + this.thirdParty = user.thirdParty; + + this.timeJoined = user.timeJoined; + + this.loginMethods = user.loginMethods.map((m) => new LoginMethod(m)); + } + + toJson(): JSONObject { + return { + id: this.id, + isPrimaryUser: this.isPrimaryUser, + tenantIds: this.tenantIds, + + emails: this.emails, + phoneNumbers: this.phoneNumbers, + thirdParty: this.thirdParty, + loginMethods: this.loginMethods.map((m) => m.toJson()), + + timeJoined: this.timeJoined, + }; + } +} + +export type UserWithoutHelperFunctions = { + id: string; // primaryUserId or recipeUserId + timeJoined: number; // minimum timeJoined value from linkedRecipes + isPrimaryUser: boolean; + emails: string[]; + phoneNumbers: string[]; + tenantIds: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: { + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + recipeUserId: string; + tenantIds: string[]; + + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; + + verified: boolean; + timeJoined: number; + }[]; +}; diff --git a/lib/ts/utils.ts b/lib/ts/utils.ts index 415cdfd55..3ccb32f2c 100644 --- a/lib/ts/utils.ts +++ b/lib/ts/utils.ts @@ -5,8 +5,10 @@ import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; import type { BaseRequest, BaseResponse } from "./framework"; import { logDebugMessage } from "./logger"; -import { HEADER_RID } from "./constants"; +import { HEADER_FDI, HEADER_RID } from "./constants"; import fetch from "cross-fetch"; +import { User } from "./user"; +import { SessionContainer } from "./recipe/session"; export function getLargestVersionFromIntersection(v1: string[], v2: string[]): string | undefined { let intersection = v1.filter((value) => v2.indexOf(value) !== -1); @@ -100,16 +102,101 @@ export function sendNon200Response(res: BaseResponse, statusCode: number, body: export function send200Response(res: BaseResponse, responseJson: any) { logDebugMessage("Sending response to client with status code: 200"); + responseJson = deepTransform(responseJson); res.setStatusCode(200); res.sendJSONResponse(responseJson); } +// this function tries to convert the json response based on the toJson function +// defined in the objects in the input. This is primarily used to convert the RecipeUserId +// type to a string type before sending it to the client. +function deepTransform(obj: { [key: string]: any }): { [key: string]: any } { + let out: { [key: string]: any } = Array.isArray(obj) ? [] : {}; + + for (let key in obj) { + let val = obj[key]; + if (val && typeof val === "object" && val["toJson"] !== undefined && typeof val["toJson"] === "function") { + out[key] = val.toJson(); + } else if (val && typeof val === "object") { + out[key] = deepTransform(val); + } else { + out[key] = val; + } + } + + return out; +} + export function isAnIpAddress(ipaddress: string) { return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( ipaddress ); } +export function getBackwardsCompatibleUserInfo( + req: BaseRequest, + result: { + user: User; + session: SessionContainer; + createdNewRecipeUser?: boolean; + } +) { + let resp: JSONObject; + if (doesRequestSupportFDI(req, "1.18")) { + resp = { + user: result.user.toJson(), + }; + + if (result.createdNewRecipeUser !== undefined) { + resp.createdNewRecipeUser = result.createdNewRecipeUser; + } + return resp; + } else { + const loginMethod = result.user.loginMethods.find( + (lm) => lm.recipeUserId.getAsString() === result.session.getRecipeUserId().getAsString() + ); + + if (loginMethod === undefined) { + throw new Error("This should never happen: session and user mismatch"); + } + + const userObj: JSONObject = { + id: result.session.getUserId(), + timeJoined: result.user.timeJoined, + }; + if (loginMethod.thirdParty) { + userObj.thirdParty = loginMethod.thirdParty; + } + if (loginMethod.email) { + userObj.email = loginMethod.email; + } + if (loginMethod.phoneNumber) { + userObj.phoneNumber = loginMethod.phoneNumber; + } + + resp = { + user: userObj, + }; + + if (result.createdNewRecipeUser !== undefined) { + resp.createdNewUser = result.createdNewRecipeUser; + } + } + return resp; +} + +export function doesRequestSupportFDI(req: BaseRequest, version: string) { + let requestFDI = req.getHeaderValue(HEADER_FDI); + if (requestFDI === undefined) { + // By default we assume they want to use the latest FDI, this also helps with tests + return true; + } + if (requestFDI === version || maxVersion(version, requestFDI) !== version) { + return true; + } + return false; +} + export function getRidFromHeader(req: BaseRequest): string | undefined { return req.getHeaderValue(HEADER_RID); } diff --git a/lib/ts/version.ts b/lib/ts/version.ts index fd15aa982..f2ada3aee 100644 --- a/lib/ts/version.ts +++ b/lib/ts/version.ts @@ -12,9 +12,9 @@ * License for the specific language governing permissions and limitations * under the License. */ -export const version = "15.2.0"; +export const version = "16.0.0"; -export const cdiSupported = ["3.0"]; +export const cdiSupported = ["4.0"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} -export const dashboardVersion = "0.7"; +export const dashboardVersion = "0.8"; diff --git a/lib/tsconfig.json b/lib/tsconfig.json index 537bb3a43..8c5d608b2 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2016", + "target": "ES2017", "strictNullChecks": true, "declaration": true, "module": "commonJS", diff --git a/package-lock.json b/package-lock.json index ff4950582..3367b1f3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supertokens-node", - "version": "15.2.0", + "version": "16.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "supertokens-node", - "version": "15.2.0", + "version": "16.0.0", "license": "Apache-2.0", "dependencies": { "content-type": "^1.0.5", @@ -49,7 +49,7 @@ "koa": "^2.13.3", "lambda-tester": "^4.0.1", "loopback-datasource-juggler": "^4.26.0", - "mocha": "6.1.4", + "mocha": "^10.2.0", "next": "11.1.3", "next-test-api-route-handler": "^3.1.8", "nock": "11.7.0", @@ -1473,38 +1473,6 @@ } } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.3.tgz", - "integrity": "sha512-TwP4krjhs+uU9pesDYCShEXZrLSbJr78p12e7XnLBBaNf20SgWLlVmQUT9gX9KbWan5V0sUbJfmcS8MRNHgYuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.3.tgz", - "integrity": "sha512-ZSWmkg/PxccHFNUSeBdrfaH8KwSkoeUtewXKvuYYt7Ph0yRsbqSyNIvhUezDua96lApiXXq6EL2d1THfeWomvw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-linux-x64-gnu": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.3.tgz", @@ -1521,22 +1489,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.3.tgz", - "integrity": "sha512-mRwbscVjRoHk+tDY7XbkT5d9FCwujFIQJpGp0XNb1i5OHCSDO8WW/C9cLEWS4LxKRbIZlTLYg1MTXqLQkvva8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/helper": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.2.1.tgz", @@ -2001,9 +1953,9 @@ "dev": true }, "node_modules/ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true, "engines": { "node": ">=6" @@ -2221,15 +2173,6 @@ "traverse": "^0.6.6" } }, - "node_modules/aws-sdk-mock/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/aws-sdk-mock/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2704,12 +2647,15 @@ } }, "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/caniuse-lite": { @@ -2865,35 +2811,14 @@ "dev": true }, "node_modules/cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, "node_modules/clone-deep": { @@ -2920,15 +2845,6 @@ "node": ">= 0.12.0" } }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3186,6 +3102,25 @@ "node-fetch": "^2.6.12" } }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3317,12 +3252,15 @@ } }, "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/deep-eql": { @@ -3409,9 +3347,9 @@ } }, "node_modules/diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true, "engines": { "node": ">=0.3.1" @@ -3528,9 +3466,9 @@ "dev": true }, "node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "node_modules/emojis-list": { @@ -3680,19 +3618,6 @@ "node": ">=0.8.0" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4093,52 +4018,30 @@ } }, "node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "locate-path": "^3.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, - "dependencies": { - "is-buffer": "~2.0.3" - }, "bin": { "flat": "cli.js" } }, - "node_modules/flat/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, "node_modules/flatstr": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", @@ -4219,9 +4122,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -4381,15 +4284,6 @@ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", "dev": true }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -4645,7 +4539,7 @@ "node_modules/inflation": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", - "integrity": "sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=", "engines": { "node": ">= 0.8.0" } @@ -4809,12 +4703,12 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/is-generator-function": { @@ -4896,6 +4790,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -4994,6 +4897,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -5129,30 +5044,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jmespath": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", @@ -5560,16 +5451,18 @@ } }, "node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -5590,64 +5483,138 @@ "dev": true }, "node_modules/log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "dependencies": { - "chalk": "^2.0.1" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loopback-connector": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-5.0.1.tgz", - "integrity": "sha512-aSPT6x5WZdoW9ylyNE4CxGqFbIqC9cSEZJwWkCincso27PXlZPj52POoF6pgxug9mkH7MrbXuP3SSDLkLq5oQQ==", + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "async": "^3.2.0", - "bluebird": "^3.7.2", - "debug": "^4.1.1", - "msgpack5": "^4.2.0", - "strong-globalize": "^6.0.4", - "uuid": "^8.3.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/loopback-datasource-juggler": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-4.26.0.tgz", - "integrity": "sha512-/R40jUGDrnRBgTh121L4Y7sHDF0KxbgSAN4gLJKp8xGNQ6KpkSQyqZkmap98eN7B75RES78DS3MGghsYMvAJ3Q==", + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "async": "^3.1.0", - "change-case": "^4.1.1", - "debug": "^4.1.0", - "depd": "^2.0.0", - "inflection": "^1.6.0", - "lodash": "^4.17.11", - "loopback-connector": "^5.0.0", - "minimatch": "^3.0.3", - "qs": "^6.5.0", - "shortid": "^2.2.6", - "strong-globalize": "^6.0.5", - "traverse": "^0.6.6", - "uuid": "^8.3.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/loopback-datasource-juggler/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "engines": { + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loopback-connector": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-5.0.1.tgz", + "integrity": "sha512-aSPT6x5WZdoW9ylyNE4CxGqFbIqC9cSEZJwWkCincso27PXlZPj52POoF6pgxug9mkH7MrbXuP3SSDLkLq5oQQ==", + "dev": true, + "dependencies": { + "async": "^3.2.0", + "bluebird": "^3.7.2", + "debug": "^4.1.1", + "msgpack5": "^4.2.0", + "strong-globalize": "^6.0.4", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/loopback-datasource-juggler": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-4.26.0.tgz", + "integrity": "sha512-/R40jUGDrnRBgTh121L4Y7sHDF0KxbgSAN4gLJKp8xGNQ6KpkSQyqZkmap98eN7B75RES78DS3MGghsYMvAJ3Q==", + "dev": true, + "dependencies": { + "async": "^3.1.0", + "change-case": "^4.1.1", + "debug": "^4.1.0", + "depd": "^2.0.0", + "inflection": "^1.6.0", + "lodash": "^4.17.11", + "loopback-connector": "^5.0.0", + "minimatch": "^3.0.3", + "qs": "^6.5.0", + "shortid": "^2.2.6", + "strong-globalize": "^6.0.5", + "traverse": "^0.6.6", + "uuid": "^8.3.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/loopback-datasource-juggler/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { "node": ">= 0.8" } }, @@ -5906,66 +5873,88 @@ } }, "node_modules/mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dev": true, "dependencies": { - "ansi-colors": "3.2.3", + "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" }, "bin": { "_mocha": "bin/_mocha", - "mocha": "bin/mocha" + "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" } }, - "node_modules/mocha/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/mocha/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { - "sprintf-js": "~1.0.2" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/mocha/node_modules/debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "dependencies": { - "ms": "^2.1.1" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/mocha/node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -5977,6 +5966,9 @@ }, "engines": { "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/glob/node_modules/minimatch": { @@ -5991,37 +5983,45 @@ "node": "*" } }, - "node_modules/mocha/node_modules/js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dev": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "brace-expansion": "^2.0.1" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "balanced-match": "^1.0.0" } }, "node_modules/mocha/node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/mocha/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -6089,9 +6089,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -6231,6 +6231,26 @@ "node": ">= 0.6" } }, + "node_modules/next-test-api-route-handler/node_modules/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/next/node_modules/buffer": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", @@ -6257,15 +6277,6 @@ "node": ">= 0.6" } }, - "node_modules/next/node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true, - "engines": { - "node": "4.x || >=6.0.0" - } - }, "node_modules/next/node_modules/raw-body": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", @@ -6290,12 +6301,6 @@ "node": ">=0.6" } }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "node_modules/nise": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", @@ -6351,42 +6356,13 @@ "node": ">= 8.0" } }, - "node_modules/node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", - "dev": true, - "dependencies": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node_modules/node-environment-flags/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true, "engines": { "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } } }, "node_modules/node-html-parser": { @@ -6598,15 +6574,6 @@ "node": ">=8" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6649,38 +6616,6 @@ "node": ">= 0.4" } }, - "node_modules/object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", - "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6813,27 +6748,15 @@ } }, "node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6925,12 +6848,12 @@ } }, "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-is-absolute": { @@ -7026,9 +6949,9 @@ "dev": true }, "node_modules/pkce-challenge": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-3.0.0.tgz", - "integrity": "sha512-sQ8sJJJuLhA5pFnoxayMCrFnBMNj7DDpa+TWxOXl4B24oXHlVSADi/3Bowm66QuzWkBuF6DhmaelCdlC2JKwsg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-3.1.0.tgz", + "integrity": "sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==", "dependencies": { "crypto-js": "^4.1.1" } @@ -7097,15 +7020,6 @@ "node": ">=8" } }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", @@ -7293,15 +7207,6 @@ "node": ">=8" } }, - "node_modules/pretty-quick/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/pretty-quick/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7586,7 +7491,7 @@ "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "engines": { "node": ">=0.10.0" @@ -7601,12 +7506,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -7827,6 +7726,15 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -7842,12 +7750,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, "node_modules/set-cookie-parser": { "version": "2.4.8", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz", @@ -7990,15 +7892,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "node_modules/sinon/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/sinon/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8210,37 +8103,29 @@ "dev": true }, "node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=4" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/string-width/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/string.prototype.trimend": { @@ -8281,15 +8166,6 @@ "node": ">=8" } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -8300,12 +8176,15 @@ } }, "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strong-error-handler": { @@ -8483,15 +8362,27 @@ "integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==" }, "node_modules/supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-color/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/timers-browserify": { @@ -8927,18 +8818,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -8955,12 +8834,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, "node_modules/which-typed-array": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", @@ -8981,75 +8854,62 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true }, "node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "dependencies": { - "number-is-nan": "^1.0.0" + "node": ">=10" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=0.10.0" + "node": ">=7.0.0" } }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -9091,471 +8951,86 @@ } }, "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" - } - }, - "node_modules/yamljs/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", - "dev": true, - "dependencies": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - } - }, - "node_modules/yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", - "dev": true, - "dependencies": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/yargs-unparser/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "node_modules/yargs-unparser/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs-unparser/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs-unparser/node_modules/lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "dependencies": { - "invert-kv": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs-unparser/node_modules/os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "dependencies": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs-unparser/node_modules/require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "node_modules/yargs-unparser/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/yargs-unparser/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs-unparser/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs-unparser/node_modules/yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "dependencies": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "node_modules/yargs-unparser/node_modules/yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/yargs/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "dependencies": { - "invert-kv": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "dependencies": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/yargs/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "engines": { - "node": ">=4" + "node": ">= 6" } }, - "node_modules/yargs/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, "bin": { - "semver": "bin/semver" + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" } }, - "node_modules/yargs/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "sprintf-js": "~1.0.2" } }, - "node_modules/yargs/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/yargs/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "dependencies": { - "ansi-regex": "^4.1.0" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" } }, "node_modules/ylru": { @@ -10751,20 +10226,6 @@ "dev": true, "requires": {} }, - "@next/swc-darwin-arm64": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.3.tgz", - "integrity": "sha512-TwP4krjhs+uU9pesDYCShEXZrLSbJr78p12e7XnLBBaNf20SgWLlVmQUT9gX9KbWan5V0sUbJfmcS8MRNHgYuA==", - "dev": true, - "optional": true - }, - "@next/swc-darwin-x64": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.3.tgz", - "integrity": "sha512-ZSWmkg/PxccHFNUSeBdrfaH8KwSkoeUtewXKvuYYt7Ph0yRsbqSyNIvhUezDua96lApiXXq6EL2d1THfeWomvw==", - "dev": true, - "optional": true - }, "@next/swc-linux-x64-gnu": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.3.tgz", @@ -10772,13 +10233,6 @@ "dev": true, "optional": true }, - "@next/swc-win32-x64-msvc": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.3.tgz", - "integrity": "sha512-mRwbscVjRoHk+tDY7XbkT5d9FCwujFIQJpGp0XNb1i5OHCSDO8WW/C9cLEWS4LxKRbIZlTLYg1MTXqLQkvva8w==", - "dev": true, - "optional": true - }, "@node-rs/helper": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.2.1.tgz", @@ -11229,9 +10683,9 @@ "dev": true }, "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, "ansi-regex": { @@ -11417,12 +10871,6 @@ "traverse": "^0.6.6" }, "dependencies": { - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -11807,9 +11255,9 @@ } }, "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, "caniuse-lite": { @@ -11937,31 +11385,14 @@ "dev": true }, "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, "clone-deep": { @@ -11981,12 +11412,6 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -12209,6 +11634,16 @@ "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", "requires": { "node-fetch": "^2.6.12" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "requires": { + "whatwg-url": "^5.0.0" + } + } } }, "cross-spawn": { @@ -12307,9 +11742,9 @@ } }, "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, "deep-eql": { @@ -12377,9 +11812,9 @@ "dev": true }, "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, "diffie-hellman": { @@ -12482,9 +11917,9 @@ } }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "emojis-list": { @@ -12605,12 +12040,6 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -12948,30 +12377,20 @@ } }, "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, "flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true - } - } + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true }, "flatstr": { "version": "1.0.12", @@ -13026,9 +12445,9 @@ "dev": true }, "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "optional": true }, @@ -13145,12 +12564,6 @@ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", "dev": true }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -13342,7 +12755,7 @@ "inflation": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", - "integrity": "sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==" + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" }, "inflection": { "version": "1.13.2", @@ -13455,9 +12868,9 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "is-generator-function": { @@ -13509,6 +12922,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -13571,6 +12990,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -13670,23 +13095,6 @@ "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jmespath": { @@ -14030,13 +13438,12 @@ } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "^5.0.0" } }, "lodash": { @@ -14057,12 +13464,64 @@ "dev": true }, "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { - "chalk": "^2.0.1" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "loopback-connector": { @@ -14312,58 +13771,60 @@ } }, "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dev": true, "requires": { - "ansi-colors": "3.2.3", + "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { - "sprintf-js": "~1.0.2" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" } }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -14385,30 +13846,40 @@ } } }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } } } }, @@ -14475,9 +13946,9 @@ } }, "nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true }, "native-url": { @@ -14580,12 +14051,6 @@ "toidentifier": "1.0.0" } }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true - }, "raw-body": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", @@ -14621,15 +14086,18 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true + }, + "node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } } } }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "nise": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", @@ -14684,31 +14152,11 @@ "propagate": "^2.0.0" } }, - "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", - "requires": { - "whatwg-url": "^5.0.0" - } + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true }, "node-html-parser": { "version": "1.4.9", @@ -14907,12 +14355,6 @@ "path-key": "^3.0.0" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -14940,29 +14382,6 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", - "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -15059,23 +14478,12 @@ } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "requires": { - "p-limit": "^2.0.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } + "p-limit": "^3.0.2" } }, "p-timeout": { @@ -15155,9 +14563,9 @@ } }, "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "path-is-absolute": { @@ -15232,9 +14640,9 @@ "dev": true }, "pkce-challenge": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-3.0.0.tgz", - "integrity": "sha512-sQ8sJJJuLhA5pFnoxayMCrFnBMNj7DDpa+TWxOXl4B24oXHlVSADi/3Bowm66QuzWkBuF6DhmaelCdlC2JKwsg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-3.1.0.tgz", + "integrity": "sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==", "requires": { "crypto-js": "^4.1.1" } @@ -15284,12 +14692,6 @@ "requires": { "p-limit": "^2.2.0" } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true } } }, @@ -15424,12 +14826,6 @@ "p-limit": "^2.2.0" } }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -15658,7 +15054,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, "require-from-string": { @@ -15667,12 +15063,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -15871,6 +15261,15 @@ "upper-case-first": "^2.0.2" } }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -15883,12 +15282,6 @@ "send": "0.18.0" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, "set-cookie-parser": { "version": "2.4.8", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz", @@ -16014,12 +15407,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -16205,28 +15592,23 @@ "dev": true }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "dependencies": { - "ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true - }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.1" } } } @@ -16260,12 +15642,6 @@ "ansi-regex": "^5.0.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -16273,9 +15649,9 @@ "dev": true }, "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "strong-error-handler": { @@ -16424,12 +15800,20 @@ "integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==" }, "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + } } }, "timers-browserify": { @@ -16781,15 +16165,6 @@ "webidl-conversions": "^3.0.0" } }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -16803,12 +16178,6 @@ "is-symbol": "^1.0.3" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, "which-typed-array": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", @@ -16823,59 +16192,46 @@ "is-typed-array": "^1.1.7" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "number-is-nan": "^1.0.0" + "color-convert": "^2.0.1" } }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "color-name": "~1.1.4" } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true } } }, @@ -16914,9 +16270,9 @@ "dev": true }, "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yaml": { @@ -16947,347 +16303,36 @@ } }, "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true }, "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" } }, "ylru": { diff --git a/package.json b/package.json index 6fec3e531..949ca99c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supertokens-node", - "version": "15.2.0", + "version": "16.0.0", "description": "NodeJS driver for SuperTokens core", "main": "index.js", "scripts": { @@ -74,7 +74,7 @@ "koa": "^2.13.3", "lambda-tester": "^4.0.1", "loopback-datasource-juggler": "^4.26.0", - "mocha": "6.1.4", + "mocha": "^10.2.0", "next": "11.1.3", "next-test-api-route-handler": "^3.1.8", "nock": "11.7.0", diff --git a/recipe/accountlinking/index.d.ts b/recipe/accountlinking/index.d.ts new file mode 100644 index 000000000..f8123db75 --- /dev/null +++ b/recipe/accountlinking/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../lib/build/recipe/accountlinking"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../lib/build/recipe/accountlinking"; +export default _default; diff --git a/recipe/accountlinking/index.js b/recipe/accountlinking/index.js new file mode 100644 index 000000000..6eb1cca66 --- /dev/null +++ b/recipe/accountlinking/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../lib/build/recipe/accountlinking")); diff --git a/recipe/accountlinking/types/index.d.ts b/recipe/accountlinking/types/index.d.ts new file mode 100644 index 000000000..0e14a7177 --- /dev/null +++ b/recipe/accountlinking/types/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../../lib/build/recipe/accountlinking/types"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../../lib/build/recipe/accountlinking/types"; +export default _default; diff --git a/recipe/accountlinking/types/index.js b/recipe/accountlinking/types/index.js new file mode 100644 index 000000000..37778409f --- /dev/null +++ b/recipe/accountlinking/types/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../../lib/build/recipe/accountlinking/types")); diff --git a/test/accountlinking/emailpassword.test.js b/test/accountlinking/emailpassword.test.js new file mode 100644 index 000000000..8cc69d075 --- /dev/null +++ b/test/accountlinking/emailpassword.test.js @@ -0,0 +1,748 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let EmailVerification = require("../../recipe/emailverification"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpassword.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("sign up tests", function () { + it("sign up without account linking does not make primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(!user.isPrimaryUser); + }); + + it("sign up with account linking makes primary user if email verification is not require", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser); + }); + + it("sign up with account linking does not make primary user if email verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(!user.isPrimaryUser); + }); + + it("sign up allowed even if account linking is on and email already used by another recipe (cause in recipe level, it is allowed), but no linking happens if email verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user.id)); + + let response = await EmailPassword.signUp("public", "test@example.com", "password123"); + + assert(response.status === "OK"); + assert(response.user.id !== user.id); + assert(response.user.isPrimaryUser === false); + }); + + it("sign up allowed if account linking is on, email verification is off, and email already used by another recipe", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test@example.com", "password123"); + + assert(response.status === "OK"); + assert(response.user.id === user.id); + assert(response.user.loginMethods.length === 2); + }); + + it("sign up allowed if account linking is off, and email already used by another recipe", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user.id)); + + let response = await EmailPassword.signUp("public", "test@example.com", "password123"); + + assert(response.status === "OK"); + }); + + it("sign up doesn't link user to existing account if email verification is needed", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + if (newAccountInfo.recipeId === "emailpassword") { + let existingUser = await supertokens.listUsersByAccountInfo("public", { + email: newAccountInfo.email, + }); + let doesEmailPasswordUserExist = existingUser.length > 1; + if (!doesEmailPasswordUserExist) { + return { + shouldAutomaticallyLink: false, + }; + } + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user.id)); + + let response = await EmailPassword.signUp("public", "test@example.com", "password123"); + + assert(response.status === "OK"); + assert(response.user.id !== user.id); + assert(!response.user.isPrimaryUser); + }); + }); + + describe("sign in tests", function () { + it("sign in recipe function does not do account linking", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + assert(!user.isPrimaryUser); + + user = (await EmailPassword.signIn("public", "test@example.com", "password123")).user; + assert(!user.isPrimaryUser); + }); + + it("sign in recipe function marks email as verified if linked accounts has email as verified and uses the same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "abc", "abcd", "test@example.com", true) + ).user; + assert(tpUser.isPrimaryUser); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(!user.isPrimaryUser); + assert(user.loginMethods[0].verified === false); + + await AccountLinking.linkAccounts(user.loginMethods[0].recipeUserId, tpUser.id); + + user = (await EmailPassword.signIn("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === true); + assert(user.loginMethods[0].verified === true); + assert(user.loginMethods[1].verified === true); + }); + + it("sign in returns the primary user even if accountlinking was later disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const email1 = `test+${Date.now()}@example.com`; + let user = (await EmailPassword.signUp("public", email1, "password123")).user; + const email2 = `test+${Date.now()}@example.com`; + let user2 = (await EmailPassword.signUp("public", email2, "password123", { doNotLink: true })).user; + + const linkResp = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + assert.strictEqual(linkResp.status, "OK"); + + const primUser = linkResp.user; + + resetAll(); + + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + const signInResp1 = await EmailPassword.signIn("public", email1, "password123"); + const signInResp2 = await EmailPassword.signIn("public", email2, "password123"); + + assert.deepStrictEqual(signInResp1.user.toJson(), primUser.toJson()); + assert.deepStrictEqual(signInResp2.user.toJson(), primUser.toJson()); + }); + }); + + describe("update email or password tests", function () { + it("update email which belongs to other primary account, and current user is also a primary user should not work", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser); + assert(response.status === "OK"); + + let isAllowed = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false + ); + assert(isAllowed === false); + + response = await EmailPassword.updateEmailOrPassword({ + recipeUserId: response.user.loginMethods[0].recipeUserId, + email: "test@example.com", + }); + + assert(response.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR"); + }); + + it("update email which belongs to other primary account should work if email password user is not a primary user or is not linked, and account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user.id)); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + + let isAllowed = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false + ); + assert(isAllowed === true); + + response = await EmailPassword.updateEmailOrPassword({ + recipeUserId: response.user.loginMethods[0].recipeUserId, + email: "test@example.com", + }); + + assert(response.status === "OK"); + let isVerified = await EmailVerification.isEmailVerified(recipeUserId); + assert(!isVerified); + }); + + it("update email which belongs to linked user should mark email as verified of email password user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + await AccountLinking.linkAccounts(recipeUserId, user.id); + + let isAllowed = await AccountLinking.isEmailChangeAllowed(recipeUserId, "test@example.com", false); + assert(isAllowed === true); + + response = await EmailPassword.updateEmailOrPassword({ + recipeUserId: recipeUserId, + email: "test@example.com", + }); + + assert(response.status === "OK"); + let isVerified = await EmailVerification.isEmailVerified(recipeUserId); + assert(isVerified); + }); + }); +}); diff --git a/test/accountlinking/emailpasswordapis.test.js b/test/accountlinking/emailpasswordapis.test.js new file mode 100644 index 000000000..bca715e0b --- /dev/null +++ b/test/accountlinking/emailpasswordapis.test.js @@ -0,0 +1,1911 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + killAllST, + cleanST, + extractInfoFromResponse, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let EmailVerification = require("../../recipe/emailverification"); +const express = require("express"); +let { middleware, errorHandler } = require("../../framework/express"); +const request = require("supertest"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpasswordapis.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("signUpPOST tests", function () { + it("calling signUpPOST returns email already exists if an EP user exsits with the same email even with account linking turned on", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + const responseInfo = res.body; + + assert(responseInfo.status === "FIELD_ERROR"); + assert(responseInfo.formFields.length === 1); + assert(responseInfo.formFields[0].id === "email"); + assert(responseInfo.formFields[0].error === "This email already exists. Please sign in instead."); + }); + + it("calling signUpPOST fails if email exists in some non email password primary user - account linking enabled and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + status: "SIGN_UP_NOT_ALLOWED", + }); + }); + + it("calling signUpPOST succeeds, but not linked account, if email exists in some non email password, non primary user, verified account with account linking enabled, and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true, + { + doNotLink: true, + } + ); + assert(tpUser.user.isPrimaryUser === false); + assert(tpUser.user.loginMethods[0].verified === true); + + let res = await new Promise((resolve, reject) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + let epUser = await supertokens.getUser(res.body.user.id); + assert(epUser.isPrimaryUser === false); + assert(epUser.loginMethods.length === 1); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert(session.getUserId() !== tpUser.user.id); + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("calling signUpPOST fails but not linked account, if email exists in some non email password, non primary user, with account linking enabled, and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(!tpUser.user.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + status: "SIGN_UP_NOT_ALLOWED", + }); + }); + + it("calling signUpPOST fails but not linked account, if email exists in some non email password, primary user, with account linking enabled, and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(tpUser.user.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + status: "SIGN_UP_NOT_ALLOWED", + }); + }); + + it("calling signUpPOST succeeds, and linked account, if email exists in some non email password primary user - account linking enabled and email verification not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.user.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + let epUser = await supertokens.getUser(res.body.user.id); + assert(epUser.isPrimaryUser === true); + assert(epUser.loginMethods.length === 2); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert(session.getUserId() === tpUser.user.id); + assert(session.getUserId() !== session.getRecipeUserId().getAsString()); + let didAsserts = false; + for (let i = 0; i < epUser.loginMethods.length; i++) { + if (epUser.loginMethods[i].recipeId === "emailpassword") { + didAsserts = true; + assert( + epUser.loginMethods[i].recipeUserId.getAsString() === session.getRecipeUserId().getAsString() + ); + assert(epUser.loginMethods[i].email === "test@example.com"); + } + } + assert(didAsserts); + }); + + it("calling signUpPOST succeeds, and not linked account, but is a primary user, if email exists in some non email password, non primary user - account linking enabled, and email verification not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false, + { + doNotLink: true, + } + ); + assert(tpUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + let epUser = await supertokens.getUser(res.body.user.id); + assert(epUser.isPrimaryUser === true); + assert(epUser.loginMethods.length === 1); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert(session.getUserId() !== tpUser.user.id); + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("calling signUpPOST succeeds if email exists in some non email password primary user - account linking disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + let epUser = await supertokens.getUser(res.body.user.id); + assert(epUser.isPrimaryUser === false); + assert(epUser.loginMethods.length === 1); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert(session.getUserId() !== tpUser.user.id); + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("calling signUpPOST succeeds if email exists in some non email password, non primary user - account linking disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + let epUser = await supertokens.getUser(res.body.user.id); + assert(epUser.isPrimaryUser === false); + assert(epUser.loginMethods.length === 1); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert(session.getUserId() !== tpUser.user.id); + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("calling signUpPOST fails if email exists in email password primary user - account linking enabled and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(epUser.user.id)); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + status: "SIGN_UP_NOT_ALLOWED", + }); + }); + + it("calling signUpPOST fails if email exists in email password user, non primary user - account linking enabled, and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + assert(!epUser.user.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + status: "SIGN_UP_NOT_ALLOWED", + }); + }); + + it("calling signUpPOST fails if email exists in email password primary user - account linking enabled and email verification not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(epUser.user.id)); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + status: "FIELD_ERROR", + formFields: [ + { + id: "email", + error: "This email already exists. Please sign in instead.", + }, + ], + }); + }); + + it("calling signUpPOST fails if email exists in email password, non primary user - account linking enabled, and email verification not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + assert(!epUser.user.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + status: "FIELD_ERROR", + formFields: [ + { + id: "email", + error: "This email already exists. Please sign in instead.", + }, + ], + }); + }); + + it("calling signUpPOST fails if email exists in email password primary user - account linking disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(epUser.user.id)); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + status: "FIELD_ERROR", + formFields: [ + { + id: "email", + error: "This email already exists. Please sign in instead.", + }, + ], + }); + }); + + it("calling signUpPOST fails if email exists in email password, non primary user - account linking disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + assert(epUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.deepStrictEqual(res.body, { + status: "FIELD_ERROR", + formFields: [ + { + id: "email", + error: "This email already exists. Please sign in instead.", + }, + ], + }); + }); + }); + + describe("signInPOST tests", function () { + it("calling signInPOST creates session with correct userId and recipeUserId in case accounts are linked", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, tpUser.user.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password1234", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + let epUserFromResponse = await supertokens.getUser(res.body.user.id); + assert(epUserFromResponse.isPrimaryUser === true); + assert(epUserFromResponse.loginMethods.length === 2); + assert(epUserFromResponse.id === tpUser.user.id); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert(session.getUserId() === tpUser.user.id); + assert(session.getRecipeUserId().getAsString() === epUser.user.id); + }); + + it("calling signInPOST creates session with correct userId and recipeUserId in case accounts are not linked", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doLink) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false, + { + doLink: true, + } + ); + assert(!tpUser.user.isPrimaryUser); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doLink: true, + }); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password1234", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + let epUserFromResponse = await supertokens.getUser(res.body.user.id); + assert(epUserFromResponse.isPrimaryUser === false); + assert(epUserFromResponse.loginMethods.length === 1); + assert(epUserFromResponse.id === epUser.user.id); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert(session.getUserId() === epUser.user.id); + assert(session.getRecipeUserId().getAsString() === epUser.user.id); + }); + + it("calling signInPOST calls isSignInAllowed and returns SIGN_IN_NOT_ALLOWED in case that function returns false.", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password1234", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "SIGN_IN_NOT_ALLOWED"); + + let sessionTokens = extractInfoFromResponse(res); + assert(sessionTokens.accessTokenFromAny === undefined); + + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED)) !== + undefined + ); + }); + + it("calling signInPOST links account if needed", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(tpUser.user.isPrimaryUser); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + assert(epUser.user.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.user.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password1234", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert.notStrictEqual(res, undefined); + assert.strictEqual(res.body.status, "OK"); + let epUserFromResponse = await supertokens.getUser(res.body.user.id); + assert.strictEqual(epUserFromResponse.isPrimaryUser, true); + assert.strictEqual(epUserFromResponse.loginMethods.length, 2); + assert.strictEqual(epUserFromResponse.id, tpUser.user.id); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert.strictEqual(session.getUserId(), tpUser.user.id); + assert.strictEqual(session.getRecipeUserId().getAsString(), epUser.user.id); + }); + + it("calling signInPOST allows sign-in with a fresh user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + assert(epUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password1234", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert.notStrictEqual(res, undefined); + assert.strictEqual(res.body.status, "OK"); + let epUserFromResponse = await supertokens.getUser(res.body.user.id); + assert.strictEqual(epUserFromResponse.isPrimaryUser, false); + assert.strictEqual(epUserFromResponse.loginMethods.length, 1); + assert.strictEqual(epUserFromResponse.id, epUser.user.id); + + let sessionTokens = extractInfoFromResponse(res); + let session = await Session.getSessionWithoutRequestResponse(sessionTokens.accessTokenFromAny); + assert.strictEqual(session.getUserId(), epUser.user.id); + assert.strictEqual(session.getRecipeUserId().getAsString(), epUser.user.id); + }); + }); +}); diff --git a/test/accountlinking/emailpasswordapis2.test.js b/test/accountlinking/emailpasswordapis2.test.js new file mode 100644 index 000000000..031841743 --- /dev/null +++ b/test/accountlinking/emailpasswordapis2.test.js @@ -0,0 +1,4317 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + extractInfoFromResponse, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let EmailVerification = require("../../recipe/emailverification"); +const express = require("express"); +let { middleware, errorHandler } = require("../../framework/express"); +const request = require("supertest"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpasswordapis2.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("generatePasswordResetTokenPOST tests", function () { + it("calling generatePasswordResetTokenPOST with no primary user and no email password user should be OK, and not send any email", async function () { + let sendEmailCallbackCalled = false; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function () { + sendEmailCallbackCalled = true; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailCallbackCalled === false); + }); + + it("calling generatePasswordResetTokenPOST with no primary user and existing email password user should be OK, and should send an email", async function () { + let sendEmailToUserId = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === epUser.user.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and no email password user, and email is in unverified state of primary user, and email verification is required, should return OK, but should not send an email", async function () { + let sendEmailCallbackCalled = false; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function () { + sendEmailCallbackCalled = true; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailCallbackCalled === false); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and no email password user, and email is in unverified state of primary user, and email verification is NOT required, should return OK, and should send an email", async function () { + let sendEmailToUserId = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and no email password user, account linking enabled, and email verification required, should return OK, and should send an email", async function () { + let sendEmailToUserId = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + }, + }; + }, + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(tpUser.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and no email password user, account linking disabled, should return OK, but should not send an email", async function () { + let sendEmailCallbackCalled = false; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function () { + sendEmailCallbackCalled = true; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailCallbackCalled === false); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and email password user existing, where both accounts are linked, should send email if account linking is enabled", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, tpUser.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and email password user existing, where both accounts are linked, should send email if account linking is disabled", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, tpUser.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and no email password user existing, primary user is not verified, and email verification is required, should not send email", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test2@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2.body, { + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + status: "SIGN_UP_NOT_ALLOWED", + }); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test2@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === undefined); + assert(sendEmailToUserId === undefined); + }); + + it("calling generatePasswordResetTokenPOST with recipe user existing, and no email password user existing, primary user is not verified, and email verification is required, should not send email", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "email", + value: "test2@example.com", + }, + { + id: "password", + value: "password123", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2.body, { + reason: + "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", + status: "SIGN_UP_NOT_ALLOWED", + }); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test2@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === undefined); + assert(sendEmailToUserId === undefined); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and no email password user existing, primary user is not verified, and email verification is not required, should send email", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + assert(tpUser.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test2@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test2@example.com"); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with recipe user existing, and no email password user existing, primary user is not verified, and email verification is not required, should not send email - cause no primary user exists", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false, + { + doNotLink: true, + } + ); + assert(tpUser.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test2@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === undefined); + assert(sendEmailToUserId === undefined); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and email password user existing, but account linking is disabled should send email, but for email password user", async function () { + let sendEmailToUserId = undefined; + let sendEmailToRecipeUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + sendEmailToRecipeUserId = input.user.recipeUserId; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + assert(epUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToRecipeUserId.getAsString() === epUser.user.id); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and email password user existing, account linking enabled, but email verification not required should send email, for primary user", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + assert(epUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, and email password user existing, account linking enabled, email verification required should send email, for primary user", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + assert(epUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, with multiple login methods, and email is verified in one of those methods, and email password user existing, account linking enabled, email verification required should send email, for primary user", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(tpUser.isPrimaryUser); + + let epUser2 = await EmailPassword.signUp("public", "test2@example.com", "password1234", { + doNotLink: true, + }); + assert(epUser2.user.isPrimaryUser === false); + await AccountLinking.linkAccounts(epUser2.user.loginMethods[0].recipeUserId, tpUser.id); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + assert(epUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, with multiple login methods, and email right is not verified in the login methods, and email password user existing, account linking enabled, email verification required should say not allowed", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser2 = await EmailPassword.signUp("public", "test2@example.com", "password1234", { + doNotLink: true, + }); + assert(epUser2.user.isPrimaryUser === false); + await AccountLinking.linkAccounts(epUser2.user.loginMethods[0].recipeUserId, tpUser.id); + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(epUser2.user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + assert(epUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert.strictEqual(res.body.status, "PASSWORD_RESET_NOT_ALLOWED"); + assert.strictEqual( + res.body.reason, + "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)" + ); + assert.strictEqual(sendEmailToUserId, undefined); + assert.strictEqual(sendEmailToUserEmail, undefined); + }); + + it("calling generatePasswordResetTokenPOST with primary user existing, with multiple login methods, and all of them having the same email, but none are verified, and email password user existing, account linking enabled, email verification required should send email with primary user", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + }, + }; + }, + }, + }), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let tpUser2 = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + false + ); + assert(tpUser2.user.isPrimaryUser === false); + await AccountLinking.linkAccounts(supertokens.convertToRecipeUserId(tpUser2.user.id), tpUser.id); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + assert(epUser.user.isPrimaryUser === false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === tpUser.id); + assert(sendEmailToUserEmail === "test@example.com"); + }); + }); + + describe("passwordResetPOST tests", function () { + it("calling passwordResetPOST with no primary user and existing email password user should change password and not link account, and not mark email as verified.", async function () { + let sendEmailToUserId = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === epUser.user.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + assert.strictEqual(emailPostPasswordReset, "test@example.com"); + assert(!userPostPasswordReset.isPrimaryUser); + assert.strictEqual(userPostPasswordReset.loginMethods.length, 1); + assert.strictEqual(userPostPasswordReset.id, epUser.user.id); + }); + + it("calling passwordResetPOST with bad password returns a PASSWORD_POLICY_VIOLATED_ERROR", async function () { + let sendEmailToUserId = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === epUser.user.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "valid", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "FIELD_ERROR", + formFields: [ + { + error: "Password must contain at least 8 characters, including a number", + id: "password", + }, + ], + }); + }); + + it("calling passwordResetPOST with primary user existing, and no email password user, and email is in unverified state of primary user, and email verification is NOT required, should create an email password user and link to the existing primary user.", async function () { + let sendEmailToUserId = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "OPTIONAL", + }), + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === tpUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + assert(emailPostPasswordReset === "test@example.com"); + assert(userPostPasswordReset.isPrimaryUser); + assert(userPostPasswordReset.loginMethods.length === 2); + assert(userPostPasswordReset.id === tpUser.id); + for (let i = 0; i < userPostPasswordReset.loginMethods.length; i++) { + if (userPostPasswordReset.loginMethods[i].recipeUserId.getAsString() !== tpUser.id) { + assert(userPostPasswordReset.loginMethods[i].recipeId === "emailpassword"); + assert(userPostPasswordReset.loginMethods[i].verified); + } else { + assert(userPostPasswordReset.loginMethods[i].verified === false); + } + assert(userPostPasswordReset.loginMethods[i].email === "test@example.com"); + } + }); + + it("calling passwordResetPOST with primary user existing, and no email password user, and email is in unverified state of primary user, and email verification is NOT required, but right before the token is consumed, account linking is switched off, should only create an email password user and not link it, but very it.", async function () { + let sendEmailToUserId = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "OPTIONAL", + }), + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST({ + ...input, + userContext: { + ...input.userContext, + doNotLink: true, + }, + }); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === tpUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + assert(emailPostPasswordReset === "test@example.com"); + assert(!userPostPasswordReset.isPrimaryUser); + assert(userPostPasswordReset.loginMethods.length === 1); + assert(userPostPasswordReset.id !== tpUser.id); + for (let i = 0; i < userPostPasswordReset.loginMethods.length; i++) { + assert(userPostPasswordReset.loginMethods[i].recipeId === "emailpassword"); + assert(userPostPasswordReset.loginMethods[i].verified); + assert(userPostPasswordReset.loginMethods[i].email === "test@example.com"); + } + }); + + it("calling passwordResetPOST with primary user existing, and email password user existing (not linked), but the ep user was created after the reset password token was generated should result in an invalid token error, even though the token was consumed just fine.", async function () { + let sendEmailToUserId = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "OPTIONAL", + }), + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === tpUser.id); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + doNotLink: true, + }); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }); + let user = await supertokens.getUser(epUser.user.id); + assert(user !== undefined); + assert(user.id === epUser.user.id); + assert(user.loginMethods.length === 1); + }); + + it("calling passwordResetPOST with primary user existing, and email password user existing (not linked), but the ep user was created and linked after the reset password token was generated should result in invalid token error", async function () { + let sendEmailToUserId = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "OPTIONAL", + }), + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === tpUser.id); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + assert(epUser.user.isPrimaryUser === true); + assert(epUser.user.id === tpUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }); + }); + + it("calling passwordResetPOST with primary user existing, and no email password user, and email is in unverified state of primary user, and email verification is required, should create an email password user and link to the existing primary user.", async function () { + let sendEmailToUserId = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "OPTIONAL", + }), + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(tpUser.isPrimaryUser); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserId === tpUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + assert(emailPostPasswordReset === "test@example.com"); + assert(userPostPasswordReset.isPrimaryUser); + assert(userPostPasswordReset.loginMethods.length === 2); + assert(userPostPasswordReset.id === tpUser.id); + for (let i = 0; i < userPostPasswordReset.loginMethods.length; i++) { + if (userPostPasswordReset.loginMethods[i].recipeUserId.getAsString() !== tpUser.id) { + assert(userPostPasswordReset.loginMethods[i].recipeId === "emailpassword"); + } + assert(userPostPasswordReset.loginMethods[i].verified); + assert(userPostPasswordReset.loginMethods[i].email === "test@example.com"); + } + }); + + it("calling passwordResetPOST with primary user existing, and email password user existing, where both accounts are linked, should change existing email password account's password if account linking is enabled, and NOT mark both as verified", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, tpUser.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + assert(emailPostPasswordReset === "test@example.com"); + assert(userPostPasswordReset.isPrimaryUser); + assert(userPostPasswordReset.loginMethods.length === 2); + assert(userPostPasswordReset.id === tpUser.id); + for (let i = 0; i < userPostPasswordReset.loginMethods.length; i++) { + if (userPostPasswordReset.loginMethods[i].recipeUserId.getAsString() !== tpUser.id) { + assert(userPostPasswordReset.loginMethods[i].recipeId === "emailpassword"); + assert(userPostPasswordReset.loginMethods[i].email === "test@example.com"); + } else { + assert(userPostPasswordReset.loginMethods[i].email === "test2@example.com"); + } + assert(!userPostPasswordReset.loginMethods[i].verified); + } + + let signInResp = await EmailPassword.signIn("public", "test@example.com", "validpass123"); + assert(signInResp.status === "OK"); + assert(signInResp.user.id === tpUser.id); + }); + + it("calling passwordResetPOST with primary user existing, and email password user existing, where both accounts are linked, should change existing email password account's password if account linking is disabled, and NOT mark both as verified", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, tpUser.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + assert(emailPostPasswordReset === "test@example.com"); + assert(userPostPasswordReset.isPrimaryUser); + assert(userPostPasswordReset.loginMethods.length === 2); + assert(userPostPasswordReset.id === tpUser.id); + for (let i = 0; i < userPostPasswordReset.loginMethods.length; i++) { + if (userPostPasswordReset.loginMethods[i].recipeUserId.getAsString() !== tpUser.id) { + assert(userPostPasswordReset.loginMethods[i].recipeId === "emailpassword"); + assert(userPostPasswordReset.loginMethods[i].email === "test@example.com"); + } else { + assert(userPostPasswordReset.loginMethods[i].email === "test2@example.com"); + } + assert(!userPostPasswordReset.loginMethods[i].verified); + } + + let signInResp = await EmailPassword.signIn("public", "test@example.com", "validpass123"); + assert(signInResp.status === "OK"); + assert(signInResp.user.id === tpUser.id); + }); + + it("calling passwordResetPOST with primary user existing, and multiple email password user existing, where all accounts are linked, should change the right email password account's password, and NOT mark both as verified", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, tpUser.id); + + let epUser2 = await EmailPassword.signUp("public", "test2@example.com", "password1234"); + await AccountLinking.linkAccounts(epUser2.user.loginMethods[0].recipeUserId, tpUser.id); + + let pUser = await supertokens.getUser(epUser.user.id); + assert(pUser.loginMethods.length === 3); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + assert(emailPostPasswordReset === "test@example.com"); + assert(userPostPasswordReset.isPrimaryUser); + assert(userPostPasswordReset.loginMethods.length === 3); + assert(userPostPasswordReset.id === tpUser.id); + for (let i = 0; i < userPostPasswordReset.loginMethods.length; i++) { + if (userPostPasswordReset.loginMethods[i].recipeUserId.getAsString() !== tpUser.id) { + assert(userPostPasswordReset.loginMethods[i].recipeId === "emailpassword"); + assert( + userPostPasswordReset.loginMethods[i].email === "test@example.com" || + userPostPasswordReset.loginMethods[i].email === "test2@example.com" + ); + } else { + assert(userPostPasswordReset.loginMethods[i].email === "test2@example.com"); + } + assert(!userPostPasswordReset.loginMethods[i].verified); + } + + { + let signInResp = await EmailPassword.signIn("public", "test@example.com", "validpass123"); + assert(signInResp.status === "OK"); + assert(signInResp.user.id === tpUser.id); + } + { + let signInResp = await EmailPassword.signIn("public", "test2@example.com", "password1234"); + assert(signInResp.status === "OK"); + assert(signInResp.user.id === tpUser.id); + } + }); + + it("calling passwordResetPOST with primary user existing, and email password user existing, where both accounts are linked, when the token was created, but then get unlinked right before token consumption, should result in OK, but no linking done", async function () { + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let { user: tpUser } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test2@example.com", + false + ); + assert(tpUser.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.id)); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, tpUser.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === "test@example.com"); + assert(sendEmailToUserId === tpUser.id); + + { + await AccountLinking.unlinkAccount(epUser.user.loginMethods[0].recipeUserId); + let user = await supertokens.getUser(tpUser.id); + assert(user !== undefined); + assert(user.loginMethods.length === 1); + } + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + let user = await supertokens.getUser(tpUser.id); + assert(user !== undefined); + assert(user.loginMethods.length === 1); + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + + let signInResp = await EmailPassword.signIn("public", "test@example.com", "validpass123"); + assert(signInResp.status === "OK"); + assert(signInResp.user.id === epUser.user.id); + assert(signInResp.user.loginMethods.length === 1); + assert(signInResp.user.isPrimaryUser === false); + }); + + it("should create a linked user if a primary user exists with a verified and an unverified thirdparty sign in method with the same email", async function () { + let date = Date.now(); + let email = `john.doe+${date}@supertokens.com`; + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let tpUser = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd" + date, email, true)) + .user; + await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); + + let tpUserUnverified = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd2" + date, email, false) + ).user; + + const linkRes = await AccountLinking.linkAccounts(tpUserUnverified.loginMethods[0].recipeUserId, tpUser.id); + + assert.strictEqual(linkRes.status, "OK"); + + await EmailVerification.unverifyEmail(tpUserUnverified.loginMethods[0].recipeUserId); + const primUser = await supertokens.getUser(linkRes.user.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: email, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === email); + assert(sendEmailToUserId === primUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + let user = await supertokens.getUser(tpUser.id); + assert(user !== undefined); + assert(user.loginMethods.length === 3); + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + + let signInResp = await EmailPassword.signIn("public", email, "validpass123"); + assert(signInResp.status === "OK"); + assert(signInResp.user.id === primUser.id); + assert(signInResp.user.loginMethods.length === 3); + assert(signInResp.user.isPrimaryUser === true); + }); + + it("should reset the password if a primary user exists with a verified emailpassword and an unverified thirdparty sign in method with the same email", async function () { + let date = Date.now(); + let email = `john.doe+${date}@supertokens.com`; + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = (await EmailPassword.signUp("public", email, "differentvalidpass123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let evToken = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(epUser.id) + ); + assert.strictEqual(evToken.status, "OK"); + await EmailVerification.verifyEmailUsingToken("public", evToken.token); + + let tpUserUnverified = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd2" + date, email, false) + ).user; + + const linkRes = await AccountLinking.linkAccounts(tpUserUnverified.loginMethods[0].recipeUserId, epUser.id); + + assert.strictEqual(linkRes.status, "OK"); + + await EmailVerification.unverifyEmail(tpUserUnverified.loginMethods[0].recipeUserId); + const primUser = await supertokens.getUser(linkRes.user.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: email, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === email); + assert(sendEmailToUserId === primUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + let user = await supertokens.getUser(epUser.id); + assert(user !== undefined); + assert.strictEqual(user.loginMethods.length, 2); + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + + let signInResp = await EmailPassword.signIn("public", email, "validpass123"); + assert.strictEqual(signInResp.status, "OK"); + assert.strictEqual(signInResp.user.id, primUser.id); + assert.strictEqual(signInResp.user.loginMethods.length, 2); + assert.strictEqual(signInResp.user.isPrimaryUser, true); + }); + + it("should reset the password if a primary user exists with an unverified emailpassword and a verified thirdparty sign in method with the same email", async function () { + let date = Date.now(); + let email = `john.doe+${date}@supertokens.com`; + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = (await EmailPassword.signUp("public", email, "differentvalidpass123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let tpUserUnverified = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd2" + date, email, true) + ).user; + + const linkRes = await AccountLinking.linkAccounts(tpUserUnverified.loginMethods[0].recipeUserId, epUser.id); + + assert.strictEqual(linkRes.status, "OK"); + + const primUser = await supertokens.getUser(linkRes.user.id); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: email, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === email); + assert(sendEmailToUserId === primUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + let user = await supertokens.getUser(epUser.id); + assert(user !== undefined); + assert.strictEqual(user.loginMethods.length, 2); + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + + let signInResp = await EmailPassword.signIn("public", email, "validpass123"); + assert.strictEqual(signInResp.status, "OK"); + assert.strictEqual(signInResp.user.id, primUser.id); + assert.strictEqual(signInResp.user.loginMethods.length, 2); + assert.strictEqual(signInResp.user.isPrimaryUser, true); + }); + + it("should create a linked user after password reset flow if the main user was deleted", async function () { + let date = Date.now(); + let email = `john.doe+${date}@supertokens.com`; + let sendEmailToUserId = undefined; + let sendEmailToUserEmail = undefined; + let token = undefined; + let userPostPasswordReset = undefined; + let emailPostPasswordReset = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + let response = await oI.passwordResetPOST(input); + if (response.status === "OK") { + emailPostPasswordReset = response.email; + userPostPasswordReset = response.user; + } + return response; + }, + }; + }, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + sendEmailToUserId = input.user.id; + sendEmailToUserEmail = input.user.email; + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = (await EmailPassword.signUp("public", email, "differentvalidpass123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let tpUser = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd2" + date, email, true)) + .user; + + const linkRes = await AccountLinking.linkAccounts(tpUser.loginMethods[0].recipeUserId, epUser.id); + assert.strictEqual(linkRes.status, "OK"); + + const deleteResp = await supertokens.deleteUser(epUser.id, false); + assert.strictEqual(deleteResp.status, "OK"); + + const primUser = await supertokens.getUser(linkRes.user.id); + assert.strictEqual(primUser.id, epUser.id); // This should still be the id of the deleted ep user + assert.strictEqual(primUser.loginMethods.length, 1); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset/token") + .send({ + formFields: [ + { + id: "email", + value: email, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res !== undefined); + assert(res.body.status === "OK"); + assert(sendEmailToUserEmail === email); + assert(sendEmailToUserId === primUser.id); + + let res2 = await new Promise((resolve) => + request(app) + .post("/auth/user/password/reset") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + let user = await supertokens.getUser(tpUser.id); + assert(user !== undefined); + assert(user.loginMethods.length === 2); + assert(res2 !== undefined); + assert.deepStrictEqual(res2, { + status: "OK", + }); + + let signInResp = await EmailPassword.signIn("public", email, "validpass123"); + assert(signInResp.status === "OK"); + assert(signInResp.user.id === primUser.id); + assert(signInResp.user.loginMethods.length === 2); + assert(signInResp.user.isPrimaryUser === true); + }); + }); +}); diff --git a/test/accountlinking/emailverification.test.js b/test/accountlinking/emailverification.test.js new file mode 100644 index 000000000..77544ade0 --- /dev/null +++ b/test/accountlinking/emailverification.test.js @@ -0,0 +1,749 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let EmailVerification = require("../../recipe/emailverification"); + +describe(`emailverificationTests: ${printPath("[test/accountlinking/emailverification.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("verifyEmailUsingToken tests", function () { + it("verifyEmailUsingToken links account if required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + false + ); + assert(tpUser.user.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + + let userFromVerification = await EmailVerification.verifyEmailUsingToken("public", token.token); + assert(userFromVerification.user.recipeUserId.getAsString() === epUser.id); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === true); + assert(user.id === tpUser.user.id); + } + }); + + it("verifyEmailUsingToken links account only if the associated email is verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + false + ); + assert(tpUser.user.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + { + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token, false); + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId, + "test2@example.com" + ); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + + let userFromVerification = await EmailVerification.verifyEmailUsingToken("public", token.token); + assert(userFromVerification.user.recipeUserId.getAsString() === epUser.id); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + assert(user.id === epUser.id); + } + }); + + it("verifyEmailUsingToken creates primary user if required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + + let userFromVerification = await EmailVerification.verifyEmailUsingToken("public", token.token); + assert(userFromVerification.user.recipeUserId.getAsString() === epUser.id); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === true); + } + }); + + it("verifyEmailUsingToken does not link account if account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + false + ); + assert(tpUser.user.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + + let userFromVerification = await EmailVerification.verifyEmailUsingToken("public", token.token); + assert(userFromVerification.user.recipeUserId.getAsString() === epUser.id); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + assert(user.id === epUser.id); + } + }); + + it("verifyEmailUsingToken does not create a primary user if account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + + let userFromVerification = await EmailVerification.verifyEmailUsingToken("public", token.token); + assert(userFromVerification.user.recipeUserId.getAsString() === epUser.id); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + }); + + it("verifyEmailUsingToken does not create a primary user if account linking is disabled and attemptAccountLinking is false", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + + let userFromVerification = await EmailVerification.verifyEmailUsingToken("public", token.token, false); + assert(userFromVerification.user.recipeUserId.getAsString() === epUser.id); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + }); + + it("verifyEmailUsingToken does not link accounts if attemptAccountLinking is false even if account linking callback allows it", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + false + ); + assert(tpUser.user.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + } + + let userFromVerification = await EmailVerification.verifyEmailUsingToken("public", token.token, false); + assert(userFromVerification.user.recipeUserId.getAsString() === epUser.id); + + { + let user = await supertokens.getUser(epUser.id); + assert(user.isPrimaryUser === false); + assert(user.id === epUser.id); + } + }); + }); + + describe("isEmailVerified tests", function () { + it("isEmailVerified checks recipe level user and not primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + true + ); + assert(tpUser.user.isPrimaryUser); + let epUser = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + await AccountLinking.linkAccounts(epUser.loginMethods[0].recipeUserId, tpUser.user.id); + + epUser = await supertokens.getUser(epUser.id); + assert(epUser.isPrimaryUser === true); + assert(epUser.loginMethods.length === 2); + + for (let i = 0; i < epUser.loginMethods.length; i++) { + let lm = epUser.loginMethods[i]; + if (lm.recipeId === "emailpassword") { + assert(!(await EmailVerification.isEmailVerified(lm.recipeUserId))); + } else { + assert(await EmailVerification.isEmailVerified(supertokens.convertToRecipeUserId(epUser.id))); + } + } + }); + }); + + describe("unverifyEmail tests", function () { + it("unverifyEmail unverifies only recipe level user and has no effect on account linking", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + true + ); + assert(tpUser.user.isPrimaryUser); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + { + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(epUser.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + } + + { + let linkedUser = await supertokens.getUser(epUser.id); + assert(linkedUser.isPrimaryUser === true); + assert(linkedUser.loginMethods.length === 2); + + assert(await EmailVerification.isEmailVerified(supertokens.convertToRecipeUserId(tpUser.user.id))); + + for (let i = 0; i < linkedUser.loginMethods.length; i++) { + let lm = linkedUser.loginMethods[i]; + if (lm.recipeId === "emailpassword") { + assert(await EmailVerification.isEmailVerified(lm.recipeUserId)); + } else { + assert( + await EmailVerification.isEmailVerified(supertokens.convertToRecipeUserId(linkedUser.id)) + ); + } + } + } + + await EmailVerification.unverifyEmail(supertokens.convertToRecipeUserId(epUser.id)); + + { + let linkedUser = await supertokens.getUser(epUser.id); + assert(linkedUser.isPrimaryUser === true); + assert(linkedUser.loginMethods.length === 2); + + assert(await EmailVerification.isEmailVerified(supertokens.convertToRecipeUserId(tpUser.user.id))); + + for (let i = 0; i < linkedUser.loginMethods.length; i++) { + let lm = linkedUser.loginMethods[i]; + if (lm.recipeId === "emailpassword") { + assert(!(await EmailVerification.isEmailVerified(lm.recipeUserId))); + } else { + assert( + await EmailVerification.isEmailVerified(supertokens.convertToRecipeUserId(linkedUser.id)) + ); + } + } + } + }); + }); +}); diff --git a/test/accountlinking/emailverificationapis.test.js b/test/accountlinking/emailverificationapis.test.js new file mode 100644 index 000000000..6216a3f67 --- /dev/null +++ b/test/accountlinking/emailverificationapis.test.js @@ -0,0 +1,1948 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + extractInfoFromResponse, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let EmailVerification = require("../../recipe/emailverification"); +let EmailVerificationRecipe = require("../../lib/build/recipe/emailverification/recipe").default; +const express = require("express"); +const request = require("supertest"); +let { middleware, errorHandler } = require("../../framework/express"); +let fs = require("fs"); +let path = require("path"); + +describe(`emailverificationapiTests: ${printPath("[test/accountlinking/emailverificationapi.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("updateSessionIfRequiredPostEmailVerification tests", function () { + it("updateSessionIfRequiredPostEmailVerification throws unauthorised error in case user does not exist and session exists", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = ( + await EmailVerification.createEmailVerificationToken("public", epUser.loginMethods[0].recipeUserId) + ).token; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + await supertokens.deleteUser(epUser.id); + + try { + await EmailVerificationRecipe.getInstance().updateSessionIfRequiredPostEmailVerification({ + req: undefined, + res: undefined, + session, + recipeUserIdWhoseEmailGotVerified: epUser.loginMethods[0].recipeUserId, + }); + assert(false); + } catch (err) { + assert.strictEqual(err.type, "UNAUTHORISED"); + } + }); + + it("updateSessionIfRequiredPostEmailVerification does not throws unauthorised error in case user does not exist and session does not exists", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = ( + await EmailVerification.createEmailVerificationToken("public", epUser.loginMethods[0].recipeUserId) + ).token; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + await supertokens.deleteUser(epUser.id); + + let res = await EmailVerificationRecipe.getInstance().updateSessionIfRequiredPostEmailVerification({ + req: undefined, + res: undefined, + session: undefined, + recipeUserIdWhoseEmailGotVerified: epUser.loginMethods[0].recipeUserId, + }); + assert(res === undefined); + }); + + it("updateSessionIfRequiredPostEmailVerification sets the right claim in the session post verification of the current logged in user, if it did not get linked to another user ", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = ( + await EmailVerification.createEmailVerificationToken("public", epUser.loginMethods[0].recipeUserId) + ).token; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let payloadBefore = session.getAccessTokenPayload(); + assert(payloadBefore["st-ev"]["v"] === false); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .send({ + method: "token", + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + let tokens = extractInfoFromResponse(response); + let accessToken = tokens.accessTokenFromAny; + + let sessionAfter = await Session.getSessionWithoutRequestResponse(accessToken); + let payloadAfter = sessionAfter.getAccessTokenPayload(); + assert(payloadAfter["st-ev"]["v"] === true); + assert(payloadAfter["sub"] === payloadBefore["sub"]); + }); + + it("updateSessionIfRequiredPostEmailVerification creates a new session if the user is linked to another user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = ( + await EmailVerification.createEmailVerificationToken("public", epUser.loginMethods[0].recipeUserId) + ).token; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let payloadBefore = session.getAccessTokenPayload(); + assert(payloadBefore["st-ev"]["v"] === false); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false + ); + assert(tpUser.user.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + await AccountLinking.linkAccounts(epUser.loginMethods[0].recipeUserId, tpUser.user.id); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .send({ + method: "token", + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + let tokens = extractInfoFromResponse(response); + let accessToken = tokens.accessTokenFromHeader; + + let sessionAfter = await Session.getSessionWithoutRequestResponse(accessToken); + let payloadAfter = sessionAfter.getAccessTokenPayload(); + assert(payloadAfter["st-ev"]["v"] === true); + assert(sessionAfter.getUserId() === tpUser.user.id); + assert(sessionAfter.getRecipeUserId().getAsString() === epUser.id); + + // check that old session is revoked + let sessionInformation = await Session.getSessionInformation(session.getHandle()); + assert(sessionInformation === undefined); + }); + + it("updateSessionIfRequiredPostEmailVerification works fine if session does not exist for user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = ( + await EmailVerification.createEmailVerificationToken("public", epUser.loginMethods[0].recipeUserId) + ).token; + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify") + .send({ + method: "token", + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + let isVerified = await EmailVerification.isEmailVerified(epUser.loginMethods[0].recipeUserId); + assert(isVerified === true); + let tokens = extractInfoFromResponse(response); + let accessToken = tokens.accessTokenFromAny; + assert(accessToken === undefined); + }); + }); + + describe("isEmailVerifiedGET tests", function () { + it("calling isEmailVerifiedGET gives false for currently logged in user if email is not verified, and does not update session", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .get("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(response.body.isVerified === false); + + let tokens = extractInfoFromResponse(response); + assert(tokens.accessToken === undefined); + assert(tokens.accessTokenFromAny === undefined); + assert(tokens.accessTokenFromHeader === undefined); + }); + + it("calling isEmailVerifiedGET gives true for currently logged in user if email is verified, and does not update session", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .get("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(response.body.isVerified === true); + + let tokens = extractInfoFromResponse(response); + assert(tokens.accessToken === undefined); + assert(tokens.accessTokenFromAny === undefined); + assert(tokens.accessTokenFromHeader === undefined); + }); + + it("calling isEmailVerifiedGET gives false for currently logged in user if email is not verified, and updates session if needed", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + await EmailVerification.unverifyEmail(epUser.loginMethods[0].recipeUserId); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .get("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(response.body.isVerified === false); + + let tokens = extractInfoFromResponse(response); + assert(tokens.accessToken !== undefined); + assert(tokens.accessTokenFromAny !== undefined); + let newSession = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + let claimValue = await newSession.getClaimValue(EmailVerification.EmailVerificationClaim); + assert(claimValue === false); + }); + + it("calling isEmailVerifiedGET gives true for currently logged in user if email is verified, and updates session if needed", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .get("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(response.body.isVerified === true); + + let tokens = extractInfoFromResponse(response); + assert(tokens.accessToken !== undefined); + assert(tokens.accessTokenFromAny !== undefined); + let newSession = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + let claimValue = await newSession.getClaimValue(EmailVerification.EmailVerificationClaim); + assert(claimValue === true); + }); + }); + + describe("generateEmailVerifyTokenPOST tests", function () { + it("calling generateEmailVerifyTokenPOST generates for currently logged in user if email is not verified, and does not update session", async function () { + let userInCallback = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + userInCallback = input.user; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify/token") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + + let tokens = extractInfoFromResponse(response); + assert(tokens.accessToken === undefined); + assert(tokens.accessTokenFromAny === undefined); + assert(tokens.accessTokenFromHeader === undefined); + + assert(userInCallback.id === epUser.id); + assert(userInCallback.email === "test@example.com"); + assert(userInCallback.recipeUserId.getAsString() === epUser.loginMethods[0].recipeUserId.getAsString()); + }); + + it("calling generateEmailVerifyTokenPOST gives already verified for currently logged in user if email is verified, and does not update session", async function () { + let userInCallback = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + userInCallback = input.user; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify/token") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "EMAIL_ALREADY_VERIFIED_ERROR"); + + let tokens = extractInfoFromResponse(response); + assert(tokens.accessToken === undefined); + assert(tokens.accessTokenFromAny === undefined); + assert(tokens.accessTokenFromHeader === undefined); + + assert(userInCallback === undefined); + }); + + it("calling generateEmailVerifyTokenPOST sends email for currently logged in user if email is not verified, and updates session if needed", async function () { + let userInCallback = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + userInCallback = input.user; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + await EmailVerification.unverifyEmail(epUser.loginMethods[0].recipeUserId); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify/token") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + + let tokens = extractInfoFromResponse(response); + assert(tokens.accessToken !== undefined); + assert(tokens.accessTokenFromAny !== undefined); + let newSession = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + let claimValue = await newSession.getClaimValue(EmailVerification.EmailVerificationClaim); + assert(claimValue === false); + + assert(userInCallback.id === epUser.id); + assert(userInCallback.email === "test@example.com"); + assert(userInCallback.recipeUserId.getAsString() === epUser.loginMethods[0].recipeUserId.getAsString()); + }); + + it("calling generateEmailVerifyTokenPOST gives email already verified for currently logged in user if email is verified, and updates session if needed", async function () { + let userInCallback = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + userInCallback = input.user; + }, + }; + }, + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (input) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(epUser.isPrimaryUser === false); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify/token") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "EMAIL_ALREADY_VERIFIED_ERROR"); + + let tokens = extractInfoFromResponse(response); + assert(tokens.accessToken !== undefined); + assert(tokens.accessTokenFromAny !== undefined); + let newSession = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + let claimValue = await newSession.getClaimValue(EmailVerification.EmailVerificationClaim); + assert(claimValue === true); + + assert(userInCallback === undefined); + }); + }); + + describe("getEmailForRecipeUserId tests", function () { + it("calling getEmailForRecipeUserId returns email provided from the config", async function () { + let email; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + getEmailForRecipeUserId: async function (recipeUserId) { + return { + status: "OK", + email: "random@example.com", + }; + }, + override: { + functions: (oI) => ({ + ...oI, + isEmailVerified: (input) => { + email = input.email; + return oI.isEmailVerified(input); + }, + }), + }, + }), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (_, __, _tenantId, userContext) { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = await EmailPassword.signUp("public", "random2@example.com", "password1234"); + + await EmailVerification.isEmailVerified(epUser.user.loginMethods[0].recipeUserId); + + assert.strictEqual(email, "random@example.com"); + }); + + it("calling getEmailForRecipeUserId falls back on default method of getting email if UNKNOWN_USER_ID_ERROR is returned", async function () { + let email; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + getEmailForRecipeUserId: async function (recipeUserId) { + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + }, + override: { + functions: (oI) => ({ + ...oI, + isEmailVerified: (input) => { + email = input.email; + return oI.isEmailVerified(input); + }, + }), + }, + }), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (_, __, _tenantId, userContext) { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = await EmailPassword.signUp("public", "random@example.com", "password1234"); + + await EmailVerification.isEmailVerified(epUser.user.loginMethods[0].recipeUserId); + assert.strictEqual(email, "random@example.com"); + }); + + it("calling getEmailForRecipeUserId with recipe user id that has many other linked recipe user ids returns the right email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + getEmailForRecipeUserId: async function (recipeUserId) { + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + }, + override: { + functions: (oI) => ({ + ...oI, + isEmailVerified: (input) => { + email = input.email; + return oI.isEmailVerified(input); + }, + }), + }, + }), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (_, __, _tenantId, userContext) { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = await EmailPassword.signUp("public", "random@example.com", "password1234"); + await AccountLinking.createPrimaryUser(epUser.user.loginMethods[0].recipeUserId); + + let epUser2 = await EmailPassword.signUp("public", "random2@example.com", "password1234"); + await AccountLinking.linkAccounts(epUser2.user.loginMethods[0].recipeUserId, epUser.user.id); + + let pUser = await supertokens.getUser(epUser.user.id); + assert(pUser.isPrimaryUser === true); + assert(pUser.loginMethods.length === 2); + assert(pUser.id === epUser.user.id); + + await EmailVerification.isEmailVerified(epUser2.user.loginMethods[0].recipeUserId); + assert.strictEqual(email, "random2@example.com"); + }); + }); + + it("email verification recipe uses getUser function only in getEmailForRecipeUserId", async function () { + // search through all files in directory for a string + let files = await new Promise((resolve, reject) => { + recursive("./lib/ts/recipe/emailverification", (err, files) => { + if (err) { + reject(err); + } + resolve(files); + }); + }); + let getUserCount = 0; + for (let i = 0; i < files.length; i++) { + let file = files[i]; + let content = fs.readFileSync(file).toString(); + let count = content.split("getUser(").length - 1; + getUserCount += count; + } + + assert.strictEqual(getUserCount, 3); + + let listUsersCount = 0; + for (let i = 0; i < files.length; i++) { + let file = files[i]; + let content = fs.readFileSync(file).toString(); + let count = content.split("listUsersByAccountInfo(").length - 1; + listUsersCount += count; + } + assert.strictEqual(listUsersCount, 0); + + // define recursive function used above + function recursive(dir, done) { + let results = []; + fs.readdir(dir, function (err, list) { + if (err) return done(err); + let pending = list.length; + if (!pending) return done(null, results); + list.forEach(function (file) { + file = path.resolve(dir, file); + fs.stat(file, function (err, stat) { + if (stat && stat.isDirectory()) { + recursive(file, function (err, res) { + results = results.concat(res); + if (!--pending) done(null, results); + }); + } else { + results.push(file); + if (!--pending) done(null, results); + } + }); + }); + }); + } + }); + + it("email and session flow work with random user ID", async function () { + let token = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "REQUIRED", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + token = input.emailVerifyLink.split("?token=")[1].split("&rid=")[0]; + }, + }; + }, + }, + getEmailForRecipeUserId: async function (recipeUserId) { + if (recipeUserId.getAsString() === "random") { + return { + status: "OK", + email: "test@example.com", + }; + } else { + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + } + }, + }), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (_, __, _tenantId, userContext) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + // now we check if the email is verified or not + { + let response = await new Promise((resolve) => + request(app) + .get("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(response.body.isVerified === false); + } + + // we generate an email verification token + { + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify/token") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(token !== undefined); + } + + // now we verify the token + { + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify") + .send({ + method: "token", + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(err); + } else { + resolve(res); + } + }) + ); + + assert(JSON.parse(response.text).status === "OK"); + } + + // now we check if the email is verified or not + { + let response = await new Promise((resolve) => + request(app) + .get("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(response.body.isVerified === true); + } + }); + + it("email and session flow work with random user ID, with session during verify email", async function () { + let token = undefined; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "REQUIRED", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async function (input) { + token = input.emailVerifyLink.split("?token=")[1].split("&rid=")[0]; + }, + }; + }, + }, + getEmailForRecipeUserId: async function (recipeUserId) { + if (recipeUserId.getAsString() === "random") { + return { + status: "OK", + email: "test@example.com", + }; + } else { + return { + status: "UNKNOWN_USER_ID_ERROR", + }; + } + }, + }), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async function (_, __, _tenantId, userContext) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + // now we check if the email is verified or not + { + let response = await new Promise((resolve) => + request(app) + .get("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(response.body.isVerified === false); + } + + // we generate an email verification token + { + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify/token") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(token !== undefined); + } + + // now we verify the token + { + let response = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .send({ + method: "token", + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(err); + } else { + resolve(res); + } + }) + ); + + assert(JSON.parse(response.text).status === "OK"); + } + + // now we check if the email is verified or not + { + let response = await new Promise((resolve) => + request(app) + .get("/auth/user/email/verify") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(response !== undefined); + assert(response.body.status === "OK"); + assert(response.body.isVerified === true); + } + }); + + describe("verifyEmailPOST tests", function () { + it("verifyEmailPOST links accounts if required for new user post sign up", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + { + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + } + + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let newUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + + let pUser = await supertokens.getUser(epUser.id); + assert(pUser.loginMethods.length === 1); + + let token = ( + await EmailVerification.createEmailVerificationToken("public", newUser.loginMethods[0].recipeUserId) + ).token; + + let response2 = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify") + .send({ + method: "token", + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + let tokens = extractInfoFromResponse(response2); + assert(tokens.accessTokenFromAny === undefined); + + pUser = await supertokens.getUser(epUser.id); + assert(pUser.loginMethods.length === 2); + assert(pUser.emails.length === 1); + }); + + it("verifyEmailPOST does not link accounts if account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + { + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + } + + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let newUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + + let token = ( + await EmailVerification.createEmailVerificationToken("public", newUser.loginMethods[0].recipeUserId) + ).token; + + let response2 = await new Promise((resolve) => + request(app) + .post("/auth/user/email/verify") + .send({ + method: "token", + token, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + let tokens = extractInfoFromResponse(response2); + assert(tokens.accessTokenFromAny === undefined); + + let pUser = await supertokens.getUser(epUser.id); + assert(pUser.loginMethods.length === 1); + assert(pUser.emails.length === 1); + }); + }); +}); diff --git a/test/accountlinking/helperFunctions.test.js b/test/accountlinking/helperFunctions.test.js new file mode 100644 index 000000000..e01417138 --- /dev/null +++ b/test/accountlinking/helperFunctions.test.js @@ -0,0 +1,3026 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startSTWithMultitenancyAndAccountLinking, + stopS, + startSTWithMultitenancyAndAccountLinkingT, + killAllST, + cleanST, + resetAll, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let EmailVerification = require("../../recipe/emailverification"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let AccountLinkingRecipe = require("../../lib/build/recipe/accountlinking/recipe").default; + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/helperFunctions.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("createPrimaryUserIdOrLinkAccounts tests", function () { + it("calling createPrimaryUserIdOrLinkAccounts with primary user returns the same user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + assert(user.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + user = await supertokens.getUser(user.id); + assert(user.isPrimaryUser === true); + assert(user.loginMethods[0].verified === true); + + let response = await AccountLinking.createPrimaryUserIdOrLinkAccounts( + "public", + user.loginMethods[0].recipeUserId + ); + + assert.strictEqual(response.id, user.id); + }); + + it("calling createPrimaryUserIdOrLinkAccounts should create a primary user if possible", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token, false); + + user = await supertokens.getUser(user.id); + assert(user.isPrimaryUser === false); + + let response = await AccountLinking.createPrimaryUserIdOrLinkAccounts( + "public", + user.loginMethods[0].recipeUserId + ); + + assert.strictEqual(response.id, user.id); + let userObj = await supertokens.getUser(user.id); + assert(userObj.isPrimaryUser); + assert(userObj.id === user.id); + assert(userObj.loginMethods.length === 1); + }); + + it("calling createPrimaryUserIdOrLinkAccounts with account linking disabled should not create a primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token, false); + user = await supertokens.getUser(user.id); + + assert(user.isPrimaryUser === false); + + let response = await AccountLinking.createPrimaryUserIdOrLinkAccounts( + "public", + user.loginMethods[0].recipeUserId + ); + + assert.strictEqual(response.id, user.id); + let userObj = await supertokens.getUser(user.id); + assert(!userObj.isPrimaryUser); + assert(userObj.id === user.id); + assert(userObj.loginMethods.length === 1); + }); + + it("calling createPrimaryUserIdOrLinkAccounts with account linking enabled by require verification should not create a primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + + assert(user.isPrimaryUser === false); + assert(user.loginMethods[0].verified === false); + + let response = await AccountLinking.createPrimaryUserIdOrLinkAccounts( + "public", + user.loginMethods[0].recipeUserId + ); + + assert.strictEqual(response.id, user.id); + let userObj = await supertokens.getUser(user.id); + assert(!userObj.isPrimaryUser); + assert(userObj.id === user.id); + assert(userObj.loginMethods.length === 1); + }); + + it("calling createPrimaryUserIdOrLinkAccounts should link accounts if possible", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let primaryUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false, { + doNotLink: true, + }) + ).user; + + assert(primaryUser.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(primaryUser.id)); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token, false); + user = await supertokens.getUser(user.id); + + assert(user.isPrimaryUser === false); + assert(user.loginMethods[0].verified === true); + + let response = await AccountLinking.createPrimaryUserIdOrLinkAccounts( + "public", + user.loginMethods[0].recipeUserId + ); + + assert(response.isPrimaryUser); + assert(response.id === primaryUser.id); + assert(response.loginMethods.length === 2); + }); + + it("calling createPrimaryUserIdOrLinkAccounts should not link accounts if account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let primaryUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false, { + doNotLink: true, + }) + ).user; + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(primaryUser.id)); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token, false); + user = await supertokens.getUser(user.id); + + assert(user.isPrimaryUser === false); + assert(user.loginMethods[0].verified === true); + + let response = await AccountLinking.createPrimaryUserIdOrLinkAccounts( + "public", + user.loginMethods[0].recipeUserId + ); + + assert.strictEqual(response.id, user.id); + let userObj = await supertokens.getUser(user.id); + assert(!userObj.isPrimaryUser); + assert(userObj.id === user.id); + assert(userObj.loginMethods.length === 1); + }); + + it("calling createPrimaryUserIdOrLinkAccounts should not link accounts if account linking is enabled, but verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let primaryUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false, { + doNotLink: true, + }) + ).user; + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(primaryUser.id)); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + + assert(user.isPrimaryUser === false); + assert(user.loginMethods[0].verified === false); + + let response = await AccountLinking.createPrimaryUserIdOrLinkAccounts( + "public", + user.loginMethods[0].recipeUserId + ); + + assert.strictEqual(response.id, user.id); + let userObj = await supertokens.getUser(user.id); + assert(!userObj.isPrimaryUser); + assert(userObj.id === user.id); + assert(userObj.loginMethods.length === 1); + }); + }); + + describe("getPrimaryUserThatCanBeLinkedToRecipeUserId tests", function () { + it("calling getPrimaryUserThatCanBeLinkedToRecipeUserId returns undefined if nothing can be linked", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let primaryUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test2@example.com", false, { + doNotLink: true, + }) + ).user; + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(primaryUser.id)); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + + assert(user.isPrimaryUser === false); + + let response = await AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId( + "public", + user.loginMethods[0].recipeUserId + ); + + assert(response === undefined); + }); + + it("calling getPrimaryUserThatCanBeLinkedToRecipeUserId returns the right primary user if it can be linked", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let primaryUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false, { + doNotLink: true, + }) + ).user; + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(primaryUser.id)); + + let user = ( + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }) + ).user; + + assert(user.isPrimaryUser === false); + + let response = await AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId( + "public", + user.loginMethods[0].recipeUserId + ); + + assert(response.id === primaryUser.id); + }); + }); + + describe("isSignUpAllowed tests", function () { + it("calling isSignUpAllowed returns true if the email is unique", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + true + ); + + assert(isAllowed); + }); + + it("calling isSignUpAllowed throws an error if email and phone number is provided to it.", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + try { + await AccountLinking.isSignUpAllowed( + "public", + { + phoneNumber: "", + email: "test@example.com", + }, + true + ); + assert(false); + } catch (err) { + assert(err.message === "Please pass one of email or phone number, not both"); + } + }); + + it("calling isSignUpAllowed returns true if user exists with same email, but is not a primary user, and email verification not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + false + ); + + assert(isAllowed); + }); + + it("calling isSignUpAllowed returns true if user exists with same email, but is not a primary user, and account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + false + ); + + assert(isAllowed); + }); + + it("calling isSignUpAllowed returns true if user exists with same email, but is not a primary user, and email verification is not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + false + ); + + assert(isAllowed); + }); + + it("calling isSignUpAllowed returns false if user exists with same email, but is not a primary user, and email verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + false + ); + + assert(!isAllowed); + assert( + (await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS + )) !== undefined + ); + }); + + it("calling isSignUpAllowed returns false if 2 users exists with same email, are not primary users, one of them has email verified, and one of them not, and email verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + assert(!epUser.user.isPrimaryUser); + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(epUser.user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token, false); + { + let user = await supertokens.getUser(epUser.user.id); + assert(user.isPrimaryUser === false); + } + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false, + { + doNotLink: true, + } + ); + assert(!tpUser.user.isPrimaryUser); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + false + ); + + assert(!isAllowed); + assert( + (await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS + )) !== undefined + ); + }); + + it("calling isSignUpAllowed returns false if user exists with same email, but primary user's email is not verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + false + ); + + assert(!isAllowed); + }); + + it("calling isSignUpAllowed returns true if user exists with same email, and primary user's email is not verified, but automatic account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + false + ); + + assert(isAllowed); + }); + + it("calling isSignUpAllowed returns true if primary user's email is verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + let token = await EmailVerification.createEmailVerificationToken( + "public", + pUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + true + ); + + assert(isAllowed); + }); + + it("calling isSignUpAllowed returns true if primary user's email is verified and other recipe user's email is not verified, with the same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + let token = await EmailVerification.createEmailVerificationToken( + "public", + pUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + await ThirdParty.manuallyCreateOrUpdateUser("public", "abcd", "abcd", "test@example.com", false); + + let isAllowed = await AccountLinking.isSignUpAllowed( + "public", + { + email: "test@example.com", + }, + true + ); + + assert(isAllowed); + }); + }); + + describe("listUsersByAccountInfo tests", function () { + it("listUsersByAccountInfo does and properly", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + Session.init(), + ], + }); + + await EmailPassword.signUp("public", "test@example.com", "password123"); + + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false); + + let users = await supertokens.listUsersByAccountInfo("public", { + email: "test@example.com", + thirdParty: { + id: "google", + userId: "abc", + }, + }); + + assert(users.length === 1); + }); + + it("listUsersByAccountInfo does OR properly", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + Session.init(), + ], + }); + + await EmailPassword.signUp("public", "test@example.com", "password123"); + + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false); + + let users = await supertokens.listUsersByAccountInfo( + "public", + { + email: "test@example.com", + thirdParty: { + id: "google", + userId: "abc", + }, + }, + true + ); + + assert(users.length === 2); + }); + }); + + describe("isEmailChangeAllowed tests", function () { + it("isEmailChangeAllowed returns false if checking for email which belongs to other primary and if email password user is not a primary user or is not linked, and account linking is enabled and email verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser === false); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + + response = await AccountLinking.isEmailChangeAllowed(recipeUserId, "test@example.com", false); + + assert(response === false); + }); + + it("isEmailChangeAllowed returns true when updating email which belongs to other primary account and if email password user is not a primary user or is not linked, and account linking is enabled and email verification is not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123", { + doNotLink: true, + }); + assert(response.user.isPrimaryUser === false); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + + let isAllowed = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false + ); + + assert(isAllowed === true); + }); + + it("isEmailChangeAllowed returns false if checking for email which belongs to other primary and if email password user is also a primary user, and account linking is enabled and email verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + await AccountLinking.createPrimaryUser(response.user.loginMethods[0].recipeUserId); + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false + ); + + assert(response === false); + }); + + it("isEmailChangeAllowed returns true if checking for email which does not belong to other primary and if email password user is also a primary user, and account linking is enabled and email verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + await AccountLinking.createPrimaryUser(response.user.loginMethods[0].recipeUserId); + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false + ); + + assert(response === true); + }); + + it("isEmailChangeAllowed returns false if checking for email which belongs to other primary and if email password user is also a primary user, and account linking is enabled and email verification is not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser === true); + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false + ); + + assert(response === false); + }); + + it("isEmailChangeAllowed returns true if checking for email which does not belong to other primary and if email password user is also a primary user, and account linking is enabled and email verification is not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + await AccountLinking.createPrimaryUser(response.user.loginMethods[0].recipeUserId); + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false + ); + + assert(response === true); + }); + + it("isEmailChangeAllowed returns false if checking for email which belongs to other primary and if email password user is also a primary user, and account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldDoAutomaticAccountLinking: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser === true); + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false, + { + doNotLink: true, + } + ); + + assert(response === false); + }); + + it("isEmailChangeAllowed returns true if checking for email which does not belong to other primary and if email password user is also a primary user, and account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldDoAutomaticAccountLinking: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser === true); + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false, + { + doNotLink: true, + } + ); + + assert(response === true); + }); + + it("isEmailChangeAllowed returns true if it's verified, even though email exist for other primary user and this user is a recipe user.", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser === false); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + true + ); + + assert(response === true); + }); + + it("isEmailChangeAllowed returns true if email has not changed and is not a primary user, even though another primary user exists with the same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test@example.com", "password123"); + assert(response.user.isPrimaryUser === false); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false + ); + + assert(response === true); + }); + + it("isEmailChangeAllowed returns true if checking for email which belongs to other primary and if email password user is not a primary user or is not linked, and account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser === false); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false, + { + doNotLink: true, + } + ); + + assert(response === true); + }); + + it("isEmailChangeAllowed returns true if checking for email which belongs to other primary and if email password user is not a primary user or is not linked, and account linking is enabled but email verification is not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + true + ); + assert(user.isPrimaryUser); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser === false); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test@example.com", + false, + { + doNotLink: true, + } + ); + + assert(response === true); + }); + + it("isEmailChangeAllowed returns true recipe user id is changing email with no primary user id having that email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let response = await EmailPassword.signUp("public", "test2@example.com", "password123"); + assert(response.user.isPrimaryUser === false); + assert(response.status === "OK"); + let recipeUserId = response.user.loginMethods[0].recipeUserId; + + response = await AccountLinking.isEmailChangeAllowed( + response.user.loginMethods[0].recipeUserId, + "test2@example.com", + false + ); + + assert(response === true); + }); + }); + + describe("isSignInAllowed tests", function () { + it("calling isSignInAllowed returns true if the email is unique", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "abcd1234"); + + let isAllowed = await AccountLinking.isSignInAllowed("public", user.user.loginMethods[0].recipeUserId); + + assert(isAllowed); + }); + + it("calling isSignInAllowed returns true if user exists with same email, but is not a primary user, and email verification not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + let isAllowed = await AccountLinking.isSignInAllowed("public", user.user.loginMethods[0].recipeUserId); + + assert(isAllowed); + }); + + it("calling isSignInAllowed returns true if user exists with same email, but is not a primary user, and account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + let isAllowed = await AccountLinking.isSignInAllowed("public", user.user.loginMethods[0].recipeUserId); + + assert(isAllowed); + }); + + it("calling isSignInAllowed returns true if user exists with same email, but is not a primary user, and email verification is not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + let isAllowed = await AccountLinking.isSignInAllowed("public", user.user.loginMethods[0].recipeUserId); + + assert(isAllowed); + }); + + it("calling isSignInAllowed returns true for non-verified non-primary user if no other user exists with the same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + let isAllowed = await AccountLinking.isSignInAllowed("public", user.user.loginMethods[0].recipeUserId); + + assert(isAllowed); + assert.strictEqual( + await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS + ), + undefined + ); + }); + + it("calling isSignInAllowed returns false if 2 users exists with same email, are not primary users, one of them has email verified, and one of them not, and email verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + assert(!epUser.user.isPrimaryUser); + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(epUser.user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token, false); + { + let user = await supertokens.getUser(epUser.user.id); + assert(user.isPrimaryUser === false); + } + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false, + { + doNotLink: true, + } + ); + assert(!tpUser.user.isPrimaryUser); + + let isAllowed = await AccountLinking.isSignInAllowed("public", tpUser.user.loginMethods[0].recipeUserId); + + assert(!isAllowed); + assert( + (await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_NO_PRIMARY_USER_EXISTS + )) !== undefined + ); + }); + + it("calling isSignInAllowed returns false if user exists with same email, but primary user's email is not verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + + let isAllowed = await AccountLinking.isSignInAllowed("public", pUser.loginMethods[0].recipeUserId); + + assert(isAllowed); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "abcd", "abcd", "test@example.com", false) + ).user; + assert(tpUser.isPrimaryUser === false); + + isAllowed = await AccountLinking.isSignInAllowed("public", tpUser.loginMethods[0].recipeUserId); + + assert(!isAllowed); + }); + + it("calling isSignInAllowed returns true if user exists with same email, and primary user's email is not verified, but automatic account linking is disabled", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + + let isAllowed = await AccountLinking.isSignInAllowed("public", pUser.loginMethods[0].recipeUserId); + + assert(isAllowed); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "abcd", "abcd", "test@example.com", false) + ).user; + assert(tpUser.isPrimaryUser === false); + + isAllowed = await AccountLinking.isSignInAllowed("public", tpUser.loginMethods[0].recipeUserId); + + assert(isAllowed); + }); + + it("calling isSignInAllowed returns true if recipe user's email is verified and primary user's email is verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext?.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + let token = await EmailVerification.createEmailVerificationToken( + "public", + pUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "abcd", "abcd", "test@example.com", true, { + doNotLink: true, + }) + ).user; + assert(tpUser.isPrimaryUser === false); + assert(tpUser.loginMethods[0].verified); + + isAllowed = await AccountLinking.isSignInAllowed("public", tpUser.loginMethods[0].recipeUserId); + + assert(isAllowed); + }); + + it("calling isSignInAllowed returns true if primary user's email is verified and other recipe user's email is not verified, with the same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + let token = await EmailVerification.createEmailVerificationToken( + "public", + pUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let user = await ThirdParty.manuallyCreateOrUpdateUser("public", "abcd", "abcd", "test@example.com", false); + + let user2 = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "abcd1", + "abcd", + "test@example.com", + true + ); + + let isAllowed = await AccountLinking.isSignInAllowed("public", user2.user.loginMethods[0].recipeUserId); + + assert(isAllowed); + }); + + it("calling isSignInAllowed with primary user does not call the helper function", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let pUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(pUser.loginMethods[0].recipeUserId); + pUser = await supertokens.getUser(pUser.id); + assert(pUser.isPrimaryUser); + + let isAllowed = await AccountLinking.isSignInAllowed("public", pUser.loginMethods[0].recipeUserId); + + assert(isAllowed); + + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED)) === + undefined + ); + }); + }); + + describe("verifyEmailForRecipeUserIfLinkedAccountsAreVerified tests", function () { + it("verifyEmailForRecipeUserIfLinkedAccountsAreVerified should not crash if email verification is not defined", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password123", { + doNotLink: true, + }); + + await AccountLinkingRecipe.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: user.user, + recipeUserId: user.user.loginMethods[0].recipeUserId, + }); + }); + + it("verifyEmailForRecipeUserIfLinkedAccountsAreVerified marks email as verified of linked user with same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password123"); + let tokens = await EmailVerification.createEmailVerificationToken( + "public", + user.user.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", tokens.token); + await AccountLinking.createPrimaryUser(user.user.loginMethods[0].recipeUserId); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "abc", + "abcd", + "test@example.com", + false + ); + await AccountLinking.linkAccounts(tpUser.user.loginMethods[0].recipeUserId, user.user.id); + + await AccountLinkingRecipe.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user, + recipeUserId: tpUser.user.loginMethods[0].recipeUserId, + }); + + let pUser = await supertokens.getUser(user.user.id); + assert(pUser.loginMethods[0].verified === true); + assert(pUser.loginMethods[1].verified === true); + + assert(pUser.loginMethods[0].recipeUserId.getAsString() === user.user.id); + assert(pUser.loginMethods[1].recipeUserId.getAsString() === tpUser.user.id); + }); + + it("verifyEmailForRecipeUserIfLinkedAccountsAreVerified does not mark email as verified of linked user that has different email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password123"); + let tokens = await EmailVerification.createEmailVerificationToken( + "public", + user.user.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", tokens.token); + await AccountLinking.createPrimaryUser(user.user.loginMethods[0].recipeUserId); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "abc", + "abcd", + "test2@example.com", + false + ); + await AccountLinking.linkAccounts(tpUser.user.loginMethods[0].recipeUserId, user.user.id); + + await AccountLinkingRecipe.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: tpUser.user, + recipeUserId: tpUser.user.loginMethods[0].recipeUserId, + }); + + let pUser = await supertokens.getUser(user.user.id); + assert(pUser.loginMethods[0].verified === true); + assert(pUser.loginMethods[1].verified === false); + + assert(pUser.loginMethods[0].recipeUserId.getAsString() === user.user.id); + assert(pUser.loginMethods[1].recipeUserId.getAsString() === tpUser.user.id); + }); + + it("verifyEmailForRecipeUserIfLinkedAccountsAreVerified does not mark email as verified of linked user with same email if no other linked user has email as verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password123"); + await AccountLinking.createPrimaryUser(user.user.loginMethods[0].recipeUserId); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "abc", + "abcd", + "test2@example.com", + false + ); + await AccountLinking.linkAccounts(tpUser.user.loginMethods[0].recipeUserId, user.user.id); + + await AccountLinkingRecipe.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: tpUser.user, + recipeUserId: tpUser.user.loginMethods[0].recipeUserId, + }); + + let pUser = await supertokens.getUser(user.user.id); + assert(pUser.loginMethods[0].verified === false); + assert(pUser.loginMethods[1].verified === false); + + assert(pUser.loginMethods[0].recipeUserId.getAsString() === user.user.id); + assert(pUser.loginMethods[1].recipeUserId.getAsString() === tpUser.user.id); + }); + + it("verifyEmailForRecipeUserIfLinkedAccountsAreVerified does not change email verification status of non linked user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "abc", + "abcd", + "test2@example.com", + false + ); + + await AccountLinkingRecipe.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: tpUser.user, + recipeUserId: tpUser.user.loginMethods[0].recipeUserId, + }); + + let pUser = await supertokens.getUser(tpUser.user.id); + assert(pUser.loginMethods[0].verified === false); + + assert(pUser.loginMethods[0].recipeUserId.getAsString() === tpUser.user.id); + }); + }); +}); diff --git a/test/accountlinking/multiRecipe.test.js b/test/accountlinking/multiRecipe.test.js new file mode 100644 index 000000000..57cc20e43 --- /dev/null +++ b/test/accountlinking/multiRecipe.test.js @@ -0,0 +1,181 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + killAllST, + cleanST, + extractInfoFromResponse, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../.."); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let Passwordless = require("../../recipe/passwordless"); +let EmailVerification = require("../../recipe/emailverification"); +const express = require("express"); +let { middleware, errorHandler } = require("../../framework/express"); +const request = require("supertest"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/multiRecipe.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("migration tests", function () { + it("allows sign in with verified recipe user even if there is an unverified one w/ the same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + const shouldDoLinking = { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + const app = initST(connectionURI, shouldDoLinking); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + assert.strictEqual(epUser.user.isPrimaryUser, false); + + let pwlessUser = await Passwordless.signInUp({ + tenantId: "public", + email: "test@example.com", + userContext: { doNotLink: true }, + }); + assert.strictEqual(pwlessUser.user.isPrimaryUser, false); + + const code = await Passwordless.createCode({ tenantId: "public", email: "test@example.com" }); + + let consumeCodeResponse = await request(app).post("/auth/signinup/code/consume").send({ + preAuthSessionId: code.preAuthSessionId, + deviceId: code.deviceId, + userInputCode: code.userInputCode, + }); + + assert.strictEqual(consumeCodeResponse.body.status, "OK"); + }); + + it("should not allow sign in with unverified recipe user when there is a verified one w/ the same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + const shouldDoLinking = { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + const app = initST(connectionURI, shouldDoLinking); + + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234"); + assert.strictEqual(epUser.user.isPrimaryUser, false); + + let pwlessUser = await Passwordless.signInUp({ + tenantId: "public", + email: "test@example.com", + userContext: { doNotLink: true }, + }); + assert.strictEqual(pwlessUser.user.isPrimaryUser, false); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "email", + value: "test@example.com", + }, + { + id: "password", + value: "password1234", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert.notStrictEqual(res, undefined); + assert.strictEqual(res.body.status, "SIGN_IN_NOT_ALLOWED"); + }); + }); +}); + +function initST(connectionURI, shouldDoLinking) { + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return shouldDoLinking; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + return app; +} diff --git a/test/accountlinking/multitenancy.test.js b/test/accountlinking/multitenancy.test.js new file mode 100644 index 000000000..6996bf6c3 --- /dev/null +++ b/test/accountlinking/multitenancy.test.js @@ -0,0 +1,1039 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../.."); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let EmailVerification = require("../../recipe/emailverification"); +let AccountLinking = require("../../recipe/accountlinking"); +let Passwordless = require("../../recipe/passwordless"); +let ThirdParty = require("../../recipe/thirdparty"); +let MultiTenancy = require("../../recipe/multitenancy"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/multitenancy.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("user sharing", function () { + it("should work fine for primary users", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let user = (await EmailPassword.signUp("public", email, password)).user; + assert(user.isPrimaryUser); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", user.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const signInRes = await EmailPassword.signIn("tenant1", email, password); + assert.strictEqual(signInRes.status, "OK"); + }); + + it("should not share linked users when sharing primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ email, tenantId: "public" }); + const linkRes = await AccountLinking.linkAccounts(pwlessSignUpResp.recipeUserId, primUser.id); + assert.strictEqual(linkRes.status, "OK"); + assert.strictEqual(linkRes.user.id, primUser.id); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const signInRes = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + assert.strictEqual(signInRes.status, "OK"); + assert.strictEqual(signInRes.user.loginMethods.length, 1); + assert.notStrictEqual(signInRes.user.id, primUser.id); + assert.notStrictEqual(signInRes.recipeUserId.getAsString(), pwlessSignUpResp.recipeUserId.getAsString()); + }); + + it("should not share linked users when sharing recipe user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ email, tenantId: "public" }); + + const shareRes = await MultiTenancy.associateUserToTenant( + "tenant1", + pwlessSignUpResp.user.loginMethods.find((lm) => lm.recipeId === "passwordless").recipeUserId + ); + assert.strictEqual(shareRes.status, "OK"); + + const signInRes = await EmailPassword.signIn("tenant1", email, password); + + assert.strictEqual(signInRes.status, "WRONG_CREDENTIALS_ERROR"); + }); + + it("should not share linked users if linked after sharing was done", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ email, tenantId: "public" }); + const linkRes = await AccountLinking.linkAccounts(pwlessSignUpResp.recipeUserId, primUser.id); + assert.strictEqual(linkRes.status, "OK"); + assert.strictEqual(linkRes.user.id, primUser.id); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const signInRes = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + assert.strictEqual(signInRes.status, "OK"); + assert.notStrictEqual(signInRes.user.id, primUser.id); + assert.strictEqual(signInRes.user.loginMethods.length, 1); + assert.notStrictEqual(signInRes.recipeUserId.getAsString(), pwlessSignUpResp.recipeUserId.getAsString()); + }); + + it("should not allow sharing if there is a conflicting primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + const createPrimRes = await AccountLinking.createPrimaryUser(pwlessSignUpResp.recipeUserId); + assert.strictEqual(createPrimRes.status, "OK"); + assert(createPrimRes.user.isPrimaryUser); + assert.notStrictEqual(createPrimRes.user.id, primUser.id); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.notStrictEqual(shareRes.status, "OK"); + }); + }); + + describe("getPrimaryUserThatCanBeLinkedToRecipeUserId", () => { + it("should not suggest linking users on separate tenants", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const toLink = await AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId( + "tenant1", + pwlessSignUpResp.recipeUserId + ); + assert.strictEqual(toLink, undefined); + }); + + it("should not check if recipeUser is associated with tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const toLink = await AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId( + "public", + pwlessSignUpResp.recipeUserId + ); + assert.deepStrictEqual(toLink.toJson(), primUser.toJson()); + }); + }); + + describe("canCreatePrimaryUser", () => { + it("should return ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR if a conflicting user was shared on the tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const resp = await AccountLinking.canCreatePrimaryUser(pwlessSignUpResp.recipeUserId); + assert.deepStrictEqual(resp.status, "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + }); + + it("should return OK if a conflicting user is only on a different tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const resp = await AccountLinking.canCreatePrimaryUser(pwlessSignUpResp.recipeUserId); + assert.deepStrictEqual(resp.status, "OK"); + }); + }); + + describe("createPrimaryUser", () => { + it("should return ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR if a conflicting user was shared on the tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const resp = await AccountLinking.createPrimaryUser(pwlessSignUpResp.recipeUserId); + assert.deepStrictEqual(resp.status, "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + }); + + it("should return OK if a conflicting user is only on a different tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const resp = await AccountLinking.createPrimaryUser(pwlessSignUpResp.recipeUserId); + assert.deepStrictEqual(resp.status, "OK"); + }); + }); + + describe("linkAccounts", () => { + it("should be able to link to a shared user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const resp = await AccountLinking.linkAccounts(pwlessSignUpResp.recipeUserId, primUser.id); + assert.deepStrictEqual(resp.status, "OK"); + }); + + it("should return OK even if the primary user is only on a different tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const resp = await AccountLinking.linkAccounts(pwlessSignUpResp.recipeUserId, primUser.id); + assert.deepStrictEqual(resp.status, "OK"); + }); + }); + + describe("isEmailChangeAllowed", () => { + it("should return false for primary user if a conflicting user was shared on the tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const email2 = `test2+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ email: email2, tenantId: "tenant1" }); + await AccountLinking.createPrimaryUser(pwlessSignUpResp.recipeUserId); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const resp = await AccountLinking.isEmailChangeAllowed(pwlessSignUpResp.recipeUserId, email, false); + + assert.deepStrictEqual(resp, false); + }); + + it("should return false for recipe user if a conflicting user was shared on the tenant and verification is required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkIfVerified, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const email2 = `test2+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + primUser = (await AccountLinking.createPrimaryUser(primUser.loginMethods[0].recipeUserId)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ email: email2, tenantId: "tenant1" }); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const resp = await AccountLinking.isEmailChangeAllowed(pwlessSignUpResp.recipeUserId, email, false); + + assert.deepStrictEqual(resp, false); + }); + + it("should return true if a conflicting user is only present on another tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkNoVerify, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const email2 = `test2+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ email: email2, tenantId: "tenant1" }); + await AccountLinking.createPrimaryUser(pwlessSignUpResp.recipeUserId); + + const resp = await AccountLinking.isEmailChangeAllowed(pwlessSignUpResp.recipeUserId, email, false); + + assert.deepStrictEqual(resp, true); + }); + }); + + describe("isSignUpAllowed", () => { + it("should return false if a conflicting user was shared on the tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Session.init(), + EmailVerification.init({ + mode: "REQUIRED", + }), + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkIfVerified, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + primUser = (await AccountLinking.createPrimaryUser(primUser.loginMethods[0].recipeUserId)).user; + assert(primUser.isPrimaryUser); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const resp = await AccountLinking.isSignUpAllowed("tenant1", { + email, + recipeId: "passwordless", + }); + assert.deepStrictEqual(resp, false); + }); + + it("should return true if a conflicting user is only on a different tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkIfVerified, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + primUser = (await AccountLinking.createPrimaryUser(primUser.loginMethods[0].recipeUserId)).user; + assert(primUser.isPrimaryUser); + + const resp = await AccountLinking.isSignUpAllowed( + "tenant1", + { + email, + recipeId: "passwordless", + }, + false + ); + assert.deepStrictEqual(resp, true); + }); + }); + + describe("isSignInAllowed", () => { + it("should return false if a conflicting user was shared on the tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Session.init(), + EmailVerification.init({ + mode: "REQUIRED", + }), + ThirdParty.init(), + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkIfVerified, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + thirdPartyEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + primUser = (await AccountLinking.createPrimaryUser(primUser.loginMethods[0].recipeUserId)).user; + assert(primUser.isPrimaryUser); + + const tpSignUpResp = await ThirdParty.manuallyCreateOrUpdateUser("tenant1", "tpid", "asdf", email, false); + + const shareRes = await MultiTenancy.associateUserToTenant("tenant1", primUser.loginMethods[0].recipeUserId); + assert.strictEqual(shareRes.status, "OK"); + + const resp = await AccountLinking.isSignInAllowed("tenant1", tpSignUpResp.recipeUserId); + assert.deepStrictEqual(resp, false); + }); + + it("should return true if a conflicting user is only on a different tenant", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Session.init(), + EmailVerification.init({ + mode: "REQUIRED", + }), + MultiTenancy.init(), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: automaticallyLinkIfVerified, + }), + ], + }); + + const createTenant = await MultiTenancy.createOrUpdateTenant("tenant1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + }); + assert.strictEqual(createTenant.status, "OK"); + + const email = `test+${Date.now()}@example.com`; + const password = "password123"; + let primUser = (await EmailPassword.signUp("public", email, password)).user; + primUser = (await AccountLinking.createPrimaryUser(primUser.loginMethods[0].recipeUserId)).user; + assert(primUser.isPrimaryUser); + + const pwlessSignUpResp = await Passwordless.signInUp({ + email, + tenantId: "tenant1", + userContext: { doNotLink: true }, + }); + + const resp = await AccountLinking.isSignInAllowed("tenant1", pwlessSignUpResp.recipeUserId); + assert.deepStrictEqual(resp, true); + }); + }); +}); + +const automaticallyLinkNoVerify = async (_accountInfo, _user, _tenantId, userContext) => { + if (userContext.doNotLink === true) { + return { shouldAutomaticallyLink: false }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; +}; + +const automaticallyLinkIfVerified = async (_accountInfo, _user, _tenantId, userContext) => { + if (userContext?.doNotLink === true) { + return { shouldAutomaticallyLink: false }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; +}; diff --git a/test/accountlinking/passwordlessapis.test.js b/test/accountlinking/passwordlessapis.test.js new file mode 100644 index 000000000..c8f2fde3c --- /dev/null +++ b/test/accountlinking/passwordlessapis.test.js @@ -0,0 +1,1089 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + extractInfoFromResponse, + assertJSONEquals, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../.."); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let Passwordless = require("../../recipe/passwordless"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let EmailVerification = require("../../recipe/emailverification"); +const express = require("express"); +let { middleware, errorHandler } = require("../../framework/express"); +const request = require("supertest"); + +// phoneNumber based accounts can't exist in other recipes now, +const createCodeBehaviours = [ + // calling signUpPOST fails but not linked account, if email exists in some non email password, primary user, with account linking enabled, and email verification required + // calling signUpPOST fails if email exists in some non email password primary user - account linking enabled and email verification + { + // only: true, + pwlessUser: undefined, + otherRecipeUser: { verified: false, primary: true }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "SIGN_IN_UP_NOT_ALLOWED" }, + }, + // calling signUpPOST succeeds, but not linked account, if email exists in some non email password, non primary user, verified account with account linking enabled, and email verification required + { + // only: true, + pwlessUser: undefined, + otherRecipeUser: { verified: true, primary: false }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK" }, + }, + // calling signUpPOST fails but not linked account, if email exists in some non email password, non primary user, with account linking enabled, and email verification required + { + // only: true, + pwlessUser: undefined, + otherRecipeUser: { verified: false, primary: false }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "SIGN_IN_UP_NOT_ALLOWED" }, + }, + // calling signUpPOST succeeds, and linked account, if email exists in some non email password primary user - account linking enabled and email verification not required + { + // only: true, + pwlessUser: undefined, + otherRecipeUser: { verified: false, primary: true }, + accountLinking: { enabled: true, requiresVerification: false }, + expect: { status: "OK" }, + }, + // calling signUpPOST succeeds, and not linked account, but is a primary user, if email exists in some non email password, non primary user - account linking enabled, and email verification not required + { + // only: true, + pwlessUser: undefined, + otherRecipeUser: { verified: false, primary: false }, + accountLinking: { enabled: true, requiresVerification: false }, + expect: { status: "OK" }, + }, + // calling signUpPOST succeeds if email exists in some non email password primary user - account linking disabled + { + // only: true, + pwlessUser: undefined, + otherRecipeUser: { verified: false, primary: true }, + accountLinking: { enabled: false }, + expect: { status: "OK" }, + }, + // calling signUpPOST succeeds if email exists in some non email password, non primary user - account linking disabled + { + // only: true, + pwlessUser: undefined, + otherRecipeUser: { verified: false, primary: false }, + accountLinking: { enabled: false }, + expect: { status: "OK" }, + }, + + // calling signInPOST creates session with correct userId and recipeUserId in case accounts are linked + { + // only: true, + pwlessUser: { exists: true, linked: true }, + otherRecipeUser: { verified: false, primary: true }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK" }, + }, + // calling signInPOST creates session with correct userId and recipeUserId in case accounts are not + { + // only: true, + pwlessUser: { exists: true, linked: false }, + otherRecipeUser: { verified: true, primary: false }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK" }, + }, + + // calling signInPOST calls isSignInAllowed and returns wrong credentials in case that function returns false +]; + +const consumeCodeBehaviours = [ + { + pwlessUser: undefined, + otherRecipeUser: { verified: true, primary: true }, + accountLinking: { enabled: false }, + expect: { status: "OK", isPrimary: false, userId: "self" }, + }, + { + pwlessUser: undefined, + otherRecipeUser: { verified: true, primary: false }, + accountLinking: { enabled: false }, + expect: { status: "OK", isPrimary: false, userId: "self" }, + }, + { + pwlessUser: { exists: true, linked: true }, + otherRecipeUser: { verified: true, primary: true }, + accountLinking: { enabled: false }, + expect: { status: "OK", isPrimary: true, userId: "other" }, + }, + { + pwlessUser: { exists: true, linked: false }, + otherRecipeUser: { verified: true, primary: false }, + accountLinking: { enabled: false }, + expect: { status: "OK", isPrimary: false, userId: "self" }, + }, + + { + pwlessUser: undefined, + otherRecipeUser: { verified: true, primary: true }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK", isPrimary: true, userId: "other" }, + }, + { + pwlessUser: undefined, + otherRecipeUser: { verified: true, primary: false }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK", isPrimary: true, userId: "self" }, + }, + { + pwlessUser: { exists: true, linked: false }, + otherRecipeUser: { verified: true, primary: false }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK", isPrimary: true, userId: "self" }, + }, + { + pwlessUser: { exists: true, linked: true }, + otherRecipeUser: { verified: true, primary: true }, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK", isPrimary: true, userId: "other" }, + }, + + { + pwlessUser: undefined, + otherRecipeUser: undefined, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK", isPrimary: true, userId: "self" }, + }, + { + pwlessUser: { exists: true, linked: false }, + otherRecipeUser: undefined, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK", isPrimary: true, userId: "self" }, + }, + { + pwlessUser: { exists: true, linked: true }, + otherRecipeUser: undefined, + accountLinking: { enabled: true, requiresVerification: true }, + expect: { status: "OK", isPrimary: true, userId: "self" }, + }, + + { + pwlessUser: undefined, + otherRecipeUser: undefined, + accountLinking: { enabled: false }, + expect: { status: "OK", isPrimary: false, userId: "self" }, + }, + { + pwlessUser: { exists: true, linked: false }, + otherRecipeUser: undefined, + accountLinking: { enabled: false }, + expect: { status: "OK", isPrimary: false, userId: "self" }, + }, + { + pwlessUser: { exists: true, linked: true }, + otherRecipeUser: undefined, + accountLinking: { enabled: false }, + expect: { status: "OK", isPrimary: true, userId: "self" }, + }, +]; + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/passwordlessapis.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("createCodePOST tests", function () { + it("calling createCodePOST fails if email exists in some non passwordless primary user - account linking enabled and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + createAndSendCustomTextMessage: (input) => { + return; + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode; + return; + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const email = "test@example.com"; + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", email, false); + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + + // createCodeAPI with email + let createCodeResponse = await new Promise((resolve) => + request(app) + .post("/auth/signinup/code") + .send({ + email: email, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert(createCodeResponse !== undefined); + assert.deepStrictEqual(createCodeResponse, { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)", + }); + }); + + it("calling createCodePOST succeeds, if email exists in some non passwordless, non primary user, verified account with account linking enabled, and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + createAndSendCustomTextMessage: (input) => { + return; + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode; + return; + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const email = "test@example.com"; + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", email, true, { + doNotLink: true, + }); + + // createCodeAPI with email + let createCodeResponse = await new Promise((resolve) => + request(app) + .post("/auth/signinup/code") + .send({ + email: email, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert.notEqual(createCodeResponse, undefined); + assert.strictEqual(createCodeResponse.status, "OK"); + }); + + it("calling createCodePOST fails during sign up, if email exists in some non passwordless, non primary user, with account linking enabled, and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + createAndSendCustomTextMessage: (input) => { + return; + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode; + return; + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const email = "test@example.com"; + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", email, false); + + // createCodeAPI with email + let createCodeResponse = await new Promise((resolve) => + request(app) + .post("/auth/signinup/code") + .send({ + email: email, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert(createCodeResponse !== undefined); + assert.deepStrictEqual(createCodeResponse, { + status: "SIGN_IN_UP_NOT_ALLOWED", + reason: + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)", + }); + }); + + it("calling createCodePOST returns OK during sign in, if email exists in some non passwordless, non primary user, with account linking enabled, and email verification required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + createAndSendCustomTextMessage: (input) => { + return; + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode; + return; + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_newAccountInfo, _user, _tenantId, userContext) => { + if (userContext?.doNotLink === true) { + return { shouldAutomaticallyLink: false }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const email = "test@example.com"; + let tpUser = await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", email, false); + await Passwordless.signInUp({ email, tenantId: "public", userContext: { doNotLink: true } }); + + // createCodeAPI with email + let createCodeResponse = await new Promise((resolve) => + request(app) + .post("/auth/signinup/code") + .send({ + email: email, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert.notEqual(createCodeResponse, undefined); + assert.strictEqual(createCodeResponse.status, "OK"); + }); + + describe("signup", () => { + for (const b of createCodeBehaviours.filter((b) => b.pwlessUser === undefined)) { + const otherUserSegment = + b.otherRecipeUser === undefined + ? "there is no other user with the same email" + : `a non-pwless ${b.otherRecipeUser.primary ? "primary" : "non-primary"} ${ + b.otherRecipeUser.verified ? "verified" : "non-verified" + } user exists with the same email`; + const accountLinkingSegment = `account linking ${ + b.accountLinking.enabled ? "enabled" : "disabled" + } and email verification ${b.accountLinking.requiresVerification ? "required" : "not-required"}`; + it(`should return status: ${b.expect.status} if ${otherUserSegment} with ${accountLinkingSegment}`, () => + getCreateCodeTestCase(b)); + } + }); + + describe("signin", () => { + for (const b of createCodeBehaviours.filter((b) => b.pwlessUser !== undefined)) { + const otherUserSegment = + b.otherRecipeUser === undefined + ? "there is no other user with the same email" + : `a non-pwless ${b.otherRecipeUser.primary ? "primary" : "non-primary"} ${ + b.otherRecipeUser.verified ? "verified" : "non-verified" + } user exists with the same email`; + const accountLinkingSegment = `account linking ${ + b.accountLinking.enabled ? "enabled" : "disabled" + } and email verification ${b.accountLinking.requiresVerification ? "required" : "not-required"}`; + it(`should return status: ${b.expect.status} for a ${ + b.pwlessUser.linked ? "primary" : "not-linked" + } user if ${otherUserSegment} with ${accountLinkingSegment}`, () => getCreateCodeTestCase(b)); + } + }); + }); + + describe("createCodePOST tests", function () { + describe("signup", () => { + for (const b of consumeCodeBehaviours.filter((b) => b.pwlessUser === undefined)) { + const otherUserSegment = + b.otherRecipeUser === undefined + ? "there is no other user with the same email" + : `a non-pwless ${b.otherRecipeUser.primary ? "primary" : "non-primary"} ${ + b.otherRecipeUser.verified ? "verified" : "non-verified" + } user exists with the same email`; + const accountLinkingSegment = `account linking ${ + b.accountLinking.enabled ? "enabled" : "disabled" + } and email verification ${b.accountLinking.requiresVerification ? "required" : "not-required"}`; + it(`should return status: ${b.expect.status} if ${otherUserSegment} with ${accountLinkingSegment}`, () => + getConsumeCodeTestCase(b)); + } + }); + + describe("signin", () => { + for (const b of consumeCodeBehaviours.filter((b) => b.pwlessUser !== undefined)) { + const otherUserSegment = + b.otherRecipeUser === undefined + ? "there is no other user with the same email" + : `a non-pwless ${b.otherRecipeUser.primary ? "primary" : "non-primary"} ${ + b.otherRecipeUser.verified ? "verified" : "non-verified" + } user exists with the same email`; + const accountLinkingSegment = `account linking ${ + b.accountLinking.enabled ? "enabled" : "disabled" + } and email verification ${b.accountLinking.requiresVerification ? "required" : "not-required"}`; + it(`should return status: ${b.expect.status} for a ${ + b.pwlessUser.linked ? "primary" : "not-linked" + } user if ${otherUserSegment} with ${accountLinkingSegment}`, () => getConsumeCodeTestCase(b)); + } + }); + + describe("SIGN_IN_UP_NOT_ALLOWED", () => { + it("should be returned if another (non-primary, unverified) user signs up after the code was created for a pwless sign up", async () => { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + createAndSendCustomTextMessage: (input) => { + return; + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode; + return; + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "REQUIRED", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (userInfo, __, _tenantId, userContext) => { + if (userContext.doNotLink || userInfo.email?.includes("doNotLink") === true) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const email = `test-${Date.now()}@example.com`; + + let tpUser; + + const code = await Passwordless.createCode({ tenantId: "public", email }); + + tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc" + Date.now(), + email, + false, + { + doNotLink: true, + } + ); + + assert.strictEqual(tpUser.status, "OK"); + + let consumeCodeResponse = await request(app).post("/auth/signinup/code/consume").send({ + preAuthSessionId: code.preAuthSessionId, + deviceId: code.deviceId, + userInputCode: code.userInputCode, + }); + + assert.strictEqual(consumeCodeResponse.body.status, "SIGN_IN_UP_NOT_ALLOWED"); + }); + + it("should be returned if another (primary, unverified) user signs up after the code was created for a pwless sign up", async () => { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + createAndSendCustomTextMessage: (input) => { + return; + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode; + return; + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "REQUIRED", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (userInfo, __, _tenantId, userContext) => { + if (userContext.doNotLink || userInfo.email?.includes("doNotLink") === true) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const email = `test-${Date.now()}@example.com`; + + let tpUser; + + const code = await Passwordless.createCode({ tenantId: "public", email }); + + tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc" + Date.now(), + email, + false, + { + doNotLink: true, + } + ); + + assert.strictEqual(tpUser.status, "OK"); + const resp = await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + assert.strictEqual(resp.status, "OK"); + + let consumeCodeResponse = await request(app).post("/auth/signinup/code/consume").send({ + preAuthSessionId: code.preAuthSessionId, + deviceId: code.deviceId, + userInputCode: code.userInputCode, + }); + + assert.strictEqual(consumeCodeResponse.body.status, "SIGN_IN_UP_NOT_ALLOWED"); + }); + }); + }); +}); + +/* + Setup emails: + tp-nonprimary-verified@example.com + tp-nonprimary-unverified@example.com + tp-primary-verified@example.com +*/ + +async function getCreateCodeTestCase({ pwlessUser, otherRecipeUser, accountLinking, expect }) { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + createAndSendCustomTextMessage: (input) => { + return; + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode; + return; + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "REQUIRED", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (userInfo, __, _tenantId, userContext) => { + if (userContext.doNotLink || userInfo.email?.includes("doNotLink") === true) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: accountLinking.enabled, + shouldRequireVerification: accountLinking.requiresVerification, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const email = "test@example.com"; + + let tpUser; + if (otherRecipeUser) { + tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + email, + otherRecipeUser.verified, + { + doNotLink: !otherRecipeUser.primary, + } + ); + + assert.strictEqual(tpUser.status, "OK"); + if (otherRecipeUser.primary) { + const resp = await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + assert.strictEqual(resp.status, "OK"); + } + } + + if (pwlessUser?.exists === true) { + const code = await Passwordless.createCode({ tenantId: "public", email }); + assert.strictEqual(code.status, "OK"); + const consumeResp = await Passwordless.consumeCode( + { + tenantId: "public", + preAuthSessionId: code.preAuthSessionId, + deviceId: code.deviceId, + userInputCode: code.userInputCode, + }, + { + doNotLink: pwlessUser.linked !== true, + } + ); + assert.strictEqual(consumeResp.status, "OK"); + if (pwlessUser.linked === true) { + const linkResp = await AccountLinking.linkAccounts( + consumeResp.user.loginMethods[0].recipeUserId, + tpUser.user.id + ); + assert.strictEqual(linkResp.status, "OK"); + } + } + + let createCodeResponse = await new Promise((resolve) => + request(app) + .post("/auth/signinup/code") + .send({ + email: email, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + assert.notEqual(createCodeResponse, undefined); + assert.strictEqual(createCodeResponse.status, expect.status); +} + +async function getConsumeCodeTestCase({ pwlessUser, otherRecipeUser, accountLinking, expect }) { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + createAndSendCustomTextMessage: (input) => { + return; + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode; + return; + }, + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + EmailVerification.init({ + mode: "REQUIRED", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (userInfo, __, _tenantId, userContext) => { + if (userContext.doNotLink || userInfo.email?.includes("doNotLink") === true) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: accountLinking.enabled, + shouldRequireVerification: accountLinking.requiresVerification, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const email = `test-${Date.now()}@example.com`; + + let tpUser; + if (otherRecipeUser) { + tpUser = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc" + Date.now(), + email, + otherRecipeUser.verified, + { + doNotLink: !otherRecipeUser.primary, + } + ); + + assert.strictEqual(tpUser.status, "OK"); + if (otherRecipeUser.primary) { + const resp = await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(tpUser.user.id)); + assert.strictEqual(resp.status, "OK"); + } + } + + if (pwlessUser?.exists === true) { + const code = await Passwordless.createCode({ tenantId: "public", email }); + assert.strictEqual(code.status, "OK"); + const consumeResp = await Passwordless.consumeCode( + { + tenantId: "public", + preAuthSessionId: code.preAuthSessionId, + deviceId: code.deviceId, + userInputCode: code.userInputCode, + }, + { + doNotLink: pwlessUser.linked !== true, + } + ); + assert.strictEqual(consumeResp.status, "OK"); + if (pwlessUser.linked === true) { + if (tpUser) { + const linkResp = await AccountLinking.linkAccounts( + consumeResp.user.loginMethods[0].recipeUserId, + tpUser.user.id + ); + assert.strictEqual(linkResp.status, "OK"); + } else { + const primResp = await AccountLinking.createPrimaryUser(consumeResp.user.loginMethods[0].recipeUserId); + assert.strictEqual(primResp.status, "OK"); + } + } + } + + const code = await Passwordless.createCode({ tenantId: "public", email }); + + let consumeCodeResponse = await request(app).post("/auth/signinup/code/consume").send({ + preAuthSessionId: code.preAuthSessionId, + deviceId: code.deviceId, + userInputCode: code.userInputCode, + }); + + assert.strictEqual(consumeCodeResponse.body.status, expect.status); + if (expect.status === "OK") { + const user = consumeCodeResponse.body.user; + const userFromGetUser = await supertokens.getUser(user.id); + + assertJSONEquals(user, userFromGetUser.toJson()); + assert.strictEqual(user.isPrimaryUser, expect.isPrimary); + if (expect.userId === "other") { + assert.strictEqual(user.id, tpUser.user.id); + } else if (tpUser !== undefined) { + assert.notStrictEqual(user.id, tpUser.user.id); + } + } +} diff --git a/test/accountlinking/recipeFunction.test.js b/test/accountlinking/recipeFunction.test.js new file mode 100644 index 000000000..40c9214b3 --- /dev/null +++ b/test/accountlinking/recipeFunction.test.js @@ -0,0 +1,1017 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + assertJSONEquals, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let EmailVerification = require("../../recipe/emailverification"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/recipeFunction.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("make primary user success", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + assert(user.isPrimaryUser === false); + + let response = await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + assert(response.status === "OK"); + assert(response.user.isPrimaryUser === true); + assert(response.wasAlreadyAPrimaryUser === false); + + assert(response.user.id === user.id); + assert((response.user.emails = user.emails)); + assert((response.user.loginMethods.length = 1)); + + let refetchedUser = await supertokens.getUser(user.id); + + // we do the json parse/stringify to remove the toJson and other functions in the login + // method array in each of the below user objects. + assertJSONEquals(refetchedUser.toJson(), response.user.toJson()); + }); + + it("make primary user succcess - already is a primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + assert(user.isPrimaryUser === false); + + let response = await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + assert(response.status === "OK"); + + let response2 = await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + assert(response2.status === "OK"); + assert(response2.user.id === user.id); + assert(response2.wasAlreadyAPrimaryUser); + }); + + it("make primary user failure - recipe user already linked to another user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + let response = await AccountLinking.createPrimaryUser(user2.loginMethods[0].recipeUserId); + assert(response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"); + assert(response.primaryUserId === user.id); + }); + + it("make primary user failure - account info user already associated with a primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + Session.init(), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false)) + .user; + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + + let response = await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user2.id)); + assert(response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + assert(response.primaryUserId === user.id); + }); + + it("link accounts success", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let primaryUserInCallback; + let newAccountInfoInCallback; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + onAccountLinked: (primaryUser, newAccountInfo) => { + primaryUserInCallback = primaryUser; + newAccountInfoInCallback = newAccountInfo; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + + // we create a new session to check that the session has not been revoked + // when we link accounts, cause these users are already linked. + await Session.createNewSessionWithoutRequestResponse("public", user2.loginMethods[0].recipeUserId); + let sessions = await Session.getAllSessionHandlesForUser(user2.loginMethods[0].recipeUserId.getAsString()); + assert(sessions.length === 1); + + let response = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + assert(response.status === "OK"); + assert(response.accountsAlreadyLinked === false); + + let linkedUser = await supertokens.getUser(user.id); + // we do the json parse/stringify to remove the toJson and other functions in the login + // method array in each of the below user objects. + assertJSONEquals(linkedUser, primaryUserInCallback); + assertJSONEquals(linkedUser, response.user); + + assert(newAccountInfoInCallback.recipeId === "emailpassword"); + assert(newAccountInfoInCallback.email === "test2@example.com"); + sessions = await Session.getAllSessionHandlesForUser(user2.loginMethods[0].recipeUserId.getAsString()); + assert(sessions.length === 0); + }); + + it("link accounts success, even if using recipe user id that is linked to the primary user id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let primaryUserInCallback; + let newAccountInfoInCallback; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + onAccountLinked: (primaryUser, newAccountInfo) => { + primaryUserInCallback = primaryUser; + newAccountInfoInCallback = newAccountInfo; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + let response = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + assert(response.status === "OK"); + assert(response.accountsAlreadyLinked === false); + + let user3 = (await EmailPassword.signUp("public", "test3@example.com", "password123")).user; + assert(user3.isPrimaryUser === false); + + response = await AccountLinking.linkAccounts(user3.loginMethods[0].recipeUserId, user2.id); + assert(response.status === "OK"); + assert(response.accountsAlreadyLinked === false); + + let linkedUser = await supertokens.getUser(user.id); + assert(linkedUser.loginMethods.length === 3); + assertJSONEquals(linkedUser, response.user); + }); + + it("link accounts success - already linked", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let primaryUserInCallback; + let newAccountInfoInCallback; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + onAccountLinked: (primaryUser, newAccountInfo) => { + primaryUserInCallback = primaryUser; + newAccountInfoInCallback = newAccountInfo; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + const initialResp = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + assert.strictEqual(initialResp.status, "OK"); + assert.notStrictEqual(initialResp.user, undefined); + + primaryUserInCallback = undefined; + newAccountInfoInCallback = undefined; + + // we create a new session to check that the session has not been revoked + // when we link accounts, cause these users are already linked. + await Session.createNewSessionWithoutRequestResponse("public", user2.loginMethods[0].recipeUserId); + let sessions = await Session.getAllSessionHandlesForUser(user2.loginMethods[0].recipeUserId.getAsString()); + assert.strictEqual(sessions.length, 1); + + let response = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + assert.strictEqual(response.status, "OK"); + assert(response.accountsAlreadyLinked); + assertJSONEquals(response.user.toJson(), initialResp.user.toJson()); + + assert.strictEqual(primaryUserInCallback, undefined); + assert.strictEqual(newAccountInfoInCallback, undefined); + + sessions = await Session.getAllSessionHandlesForUser(user2.loginMethods[0].recipeUserId.getAsString()); + assert.strictEqual(sessions.length, 1); + }); + + it("link accounts failure - recipe user id already linked with another primary user id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let primaryUserInCallback; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + onAccountLinked: (primaryUser, newAccountInfo) => { + primaryUserInCallback = primaryUser; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + let otherPrimaryUser = (await EmailPassword.signUp("public", "test3@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(otherPrimaryUser.loginMethods[0].recipeUserId); + + primaryUserInCallback = undefined; + + let response = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, otherPrimaryUser.id); + + assert(response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + assert(response.primaryUserId === user.id); + + assert(primaryUserInCallback === undefined); + }); + + it("link accounts failure - input user is not a primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let primaryUserInCallback; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + onAccountLinked: (primaryUser, newAccountInfo) => { + primaryUserInCallback = primaryUser; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + let resp = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + assert(resp.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER"); + }); + + it("account linking failure - account info user already associated with a primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + Session.init(), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + + let user2 = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false)) + .user; + let otherPrimaryUser = (await EmailPassword.signUp("public", "test3@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(otherPrimaryUser.loginMethods[0].recipeUserId); + + let response = await AccountLinking.linkAccounts( + supertokens.convertToRecipeUserId(user2.id), + otherPrimaryUser.id + ); + + assert.strictEqual(response.status, "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + assert.strictEqual(response.primaryUserId, user.id); + }); + + it("unlinking accounts success and removes session", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + + await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + // we create a new session to check that the session has not been revoked + // when we link accounts, cause these users are already linked. + await Session.createNewSessionWithoutRequestResponse("public", user2.loginMethods[0].recipeUserId); + let sessions = await Session.getAllSessionHandlesForUser(user2.loginMethods[0].recipeUserId.getAsString()); + assert(sessions.length === 1); + + let response = await AccountLinking.unlinkAccount(user2.loginMethods[0].recipeUserId); + assert(response.wasRecipeUserDeleted === false); + assert(response.wasLinked === true); + + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser !== undefined); + assert(primaryUser.loginMethods.length === 1); + assert(primaryUser.isPrimaryUser); + + let recipeUser = await supertokens.getUser(user2.id); + assert(recipeUser !== undefined); + assert(recipeUser.loginMethods.length === 1); + assert(!recipeUser.isPrimaryUser); + + sessions = await Session.getAllSessionHandlesForUser(user2.loginMethods[0].recipeUserId.getAsString()); + assert(sessions.length === 0); + }); + + it("unlinking account of primary user causes it to become a recipe user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + + // we create a new session to check that the session has not been revoked + // when we link accounts, cause these users are already linked. + await Session.createNewSessionWithoutRequestResponse("public", user.loginMethods[0].recipeUserId); + let sessions = await Session.getAllSessionHandlesForUser(user.loginMethods[0].recipeUserId.getAsString()); + assert(sessions.length === 1); + + let response = await AccountLinking.unlinkAccount(user.loginMethods[0].recipeUserId); + assert(response.wasRecipeUserDeleted === false); + assert(response.wasLinked === true); + + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser !== undefined); + assert(primaryUser.loginMethods.length === 1); + assert(!primaryUser.isPrimaryUser); + + sessions = await Session.getAllSessionHandlesForUser(user.loginMethods[0].recipeUserId.getAsString()); + assert(sessions.length === 0); + }); + + it("unlinking accounts where user id is primary user causes that user id to be deleted", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === false); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.createPrimaryUser(user.loginMethods[0].recipeUserId); + + await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + // we create a new session to check that the session has not been revoked + // when we link accounts, cause these users are already linked. + await Session.createNewSessionWithoutRequestResponse("public", user.loginMethods[0].recipeUserId); + let sessions = await Session.getAllSessionHandlesForUser(user.loginMethods[0].recipeUserId.getAsString()); + assert(sessions.length === 1); + + let response = await AccountLinking.unlinkAccount(user.loginMethods[0].recipeUserId); + assert(response.wasRecipeUserDeleted === true); + assert(response.wasLinked === true); + + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser !== undefined); + assert(primaryUser.loginMethods.length === 1); + assert(primaryUser.isPrimaryUser); + + let recipeUser = await supertokens.getUser(user2.id); + + // we do the json parse/stringify to remove the toJson and other functions in the login + // method array in each of the below user objects. + assertJSONEquals(recipeUser.toJson(), primaryUser.toJson()); + + sessions = await Session.getAllSessionHandlesForUser(user.loginMethods[0].recipeUserId.getAsString()); + assert(sessions.length === 0); + }); + + it("delete user successful", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + if (newAccountInfo.email === "test2@example.com" && user === undefined) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === true); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + { + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser !== undefined); + assert(primaryUser.loginMethods.length === 2); + assert(primaryUser.isPrimaryUser); + } + await supertokens.deleteUser(user2.id, false); + + { + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser !== undefined); + assert(primaryUser.loginMethods.length === 1); + assert(primaryUser.loginMethods[0].recipeUserId.getAsString() === primaryUser.id); + assert(primaryUser.isPrimaryUser); + } + }); + + it("delete user successful - primary user being deleted", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + if (newAccountInfo.email === "test2@example.com" && user === undefined) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === true); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + { + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser !== undefined); + assert(primaryUser.loginMethods.length === 2); + assert(primaryUser.isPrimaryUser); + } + await supertokens.deleteUser(user.id, false); + + { + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser !== undefined); + assert(primaryUser.loginMethods.length === 1); + assert(primaryUser.loginMethods[0].recipeUserId.getAsString() === user2.id); + assert(primaryUser.isPrimaryUser); + } + }); + + it("delete user successful - remove all linked accounts", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo, user) => { + if (newAccountInfo.email === "test2@example.com" && user === undefined) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ], + }); + + let user = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + assert(user.isPrimaryUser === true); + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + { + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser !== undefined); + assert(primaryUser.loginMethods.length === 2); + assert(primaryUser.isPrimaryUser); + } + await supertokens.deleteUser(user.id); + + { + let primaryUser = await supertokens.getUser(user.id); + assert(primaryUser === undefined); + } + + { + let user = await supertokens.getUser(user2.id); + assert(user === undefined); + } + }); + + it("link accounts success causes new account's email to be verified if same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false)) + .user; + assert(user.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let user2 = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user.id)); + + let response = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + assert(response.status === "OK"); + assert(response.accountsAlreadyLinked === false); + + let isVerified = await EmailVerification.isEmailVerified(user2.loginMethods[0].recipeUserId); + assert(isVerified === true); + }); + + it("link accounts success does not cause primary user's account's email to be verified if same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false)) + .user; + + let user2 = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + let user3 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user2.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user.id)); + + let response = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + assert(response.status === "OK"); + assert(response.accountsAlreadyLinked === false); + + await AccountLinking.linkAccounts(user3.loginMethods[0].recipeUserId, user.id); + + { + let isVerified = await EmailVerification.isEmailVerified(supertokens.convertToRecipeUserId(user.id)); + assert(isVerified === false); + } + + { + let isVerified = await EmailVerification.isEmailVerified(supertokens.convertToRecipeUserId(user3.id)); + assert(isVerified === false); + } + }); + + it("link accounts success does not cause new account's email to be verified if different email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false)) + .user; + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user.id)); + + let response = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + assert(response.status === "OK"); + assert(response.accountsAlreadyLinked === false); + + let isVerified = await EmailVerification.isEmailVerified(user2.loginMethods[0].recipeUserId); + assert(isVerified === false); + }); + + it("link accounts does not cause primary user's account's email to be verified if different email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false)) + .user; + + let user2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + let token = await EmailVerification.createEmailVerificationToken( + "public", + supertokens.convertToRecipeUserId(user2.id) + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(user.id)); + + let response = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + assert(response.status === "OK"); + assert(response.accountsAlreadyLinked === false); + + let isVerified = await EmailVerification.isEmailVerified(supertokens.convertToRecipeUserId(user.id)); + assert(isVerified === false); + }); +}); diff --git a/test/accountlinking/session.test.js b/test/accountlinking/session.test.js new file mode 100644 index 000000000..22cec8318 --- /dev/null +++ b/test/accountlinking/session.test.js @@ -0,0 +1,1467 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + extractInfoFromResponse, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let AccountLinking = require("../../recipe/accountlinking").default; +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let EmailVerification = require("../../recipe/emailverification"); +const express = require("express"); +const request = require("supertest"); +let { middleware, errorHandler } = require("../../framework/express"); +let { protectedProps } = require("../../lib/build/recipe/session/constants"); +let { PrimitiveClaim } = require("../../lib/build/recipe/session/claimBaseClasses/primitiveClaim"); + +describe(`sessionTests: ${printPath("[test/accountlinking/session.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("createNewSessionWithoutRequestResponse tests", function () { + it("create new session with no linked accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("create new session with linked accounts should have different user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + assert(session.getUserId() !== session.getRecipeUserId().getAsString()); + assert(session.getUserId() === epUser.id); + assert(session.getRecipeUserId().getAsString() === epUser2.id); + }); + + it("create new session with no linked and no auth recipe accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + assert(session.getUserId() === "random"); + }); + }); + + describe("createNewSession tests", function () { + it("create new session with no linked accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + const app = express(); + + app.use(middleware()); + + app.post("/create", async (req, res) => { + await Session.createNewSession(req, res, "public", epUser.loginMethods[0].recipeUserId, {}, {}); + res.status(200).send(""); + }); + + app.use(errorHandler()); + + let res = await new Promise((resolve) => + request(app) + .post("/create") + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + let tokens = extractInfoFromResponse(res); + assert(tokens.accessTokenFromAny !== undefined); + + let session = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("create new session with linked accounts should have different user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + const app = express(); + + app.use(middleware()); + + app.post("/create", async (req, res) => { + await Session.createNewSession(req, res, "public", epUser2.loginMethods[0].recipeUserId, {}, {}); + res.status(200).send(""); + }); + + app.use(errorHandler()); + + let res = await new Promise((resolve) => + request(app) + .post("/create") + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + let tokens = extractInfoFromResponse(res); + assert(tokens.accessTokenFromAny !== undefined); + + let session = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + + assert(session.getUserId() !== session.getRecipeUserId().getAsString()); + assert(session.getUserId() === epUser.id); + assert(session.getRecipeUserId().getAsString() === epUser2.id); + }); + + it("create new session with no linked accounts and no auth recipe should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + const app = express(); + + app.use(middleware()); + + app.post("/create", async (req, res) => { + await Session.createNewSession(req, res, "public", supertokens.convertToRecipeUserId("random"), {}, {}); + res.status(200).send(""); + }); + + app.use(errorHandler()); + + let res = await new Promise((resolve) => + request(app) + .post("/create") + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + let tokens = extractInfoFromResponse(res); + assert(tokens.accessTokenFromAny !== undefined); + + let session = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + assert(session.getUserId() === "random"); + }); + }); + + describe("getSessionWithoutRequestResponse tests", function () { + it("getSessionWithoutRequestResponse with no linked accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + session = await Session.getSessionWithoutRequestResponse(session.getAccessToken()); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("getSessionWithoutRequestResponse with linked accounts should have different user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + session = await Session.getSessionWithoutRequestResponse(session.getAccessToken()); + + assert(session.getUserId() !== session.getRecipeUserId().getAsString()); + assert(session.getUserId() === epUser.id); + assert(session.getRecipeUserId().getAsString() === epUser2.id); + }); + + it("getSessionWithoutRequestResponse with no linked and no auth recipe accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + session = await Session.getSessionWithoutRequestResponse(session.getAccessToken()); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + assert(session.getUserId() === "random"); + }); + + it("getSessionWithoutRequestResponse with no linked accounts should have same user id and recipe id, with check db", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + session = await Session.getSessionWithoutRequestResponse(session.getAccessToken(), undefined, { + checkDatabase: true, + }); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("getSessionWithoutRequestResponse with linked accounts should have different user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + session = await Session.getSessionWithoutRequestResponse(session.getAccessToken(), undefined, { + checkDatabase: true, + }); + + assert(session.getUserId() !== session.getRecipeUserId().getAsString()); + assert(session.getUserId() === epUser.id); + assert(session.getRecipeUserId().getAsString() === epUser2.id); + }); + + it("getSessionWithoutRequestResponse with no linked and no auth recipe accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + session = await Session.getSessionWithoutRequestResponse(session.getAccessToken(), undefined, { + checkDatabase: true, + }); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + assert(session.getUserId() === "random"); + }); + }); + + describe("getSession tests", function () { + it("get session with no linked accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let userId = undefined; + let recipeUserId = undefined; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId, + {}, + {} + ); + + const app = express(); + + app.use(middleware()); + + app.post("/getsession", async (req, res) => { + let session = await Session.getSession(req, res); + userId = session.getUserId(); + recipeUserId = session.getRecipeUserId().getAsString(); + res.status(200).send(""); + }); + + app.use(errorHandler()); + + let res = await new Promise((resolve) => + request(app) + .post("/getsession") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(userId === recipeUserId && userId !== undefined); + }); + + it("get session with linked accounts should have different user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let userId = undefined; + let recipeUserId = undefined; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId, + {}, + {} + ); + + const app = express(); + + app.use(middleware()); + + app.post("/getsession", async (req, res) => { + let session = await Session.getSession(req, res); + userId = session.getUserId(); + recipeUserId = session.getRecipeUserId().getAsString(); + res.status(200).send(""); + }); + + app.use(errorHandler()); + + let res = await new Promise((resolve) => + request(app) + .post("/getsession") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(userId !== recipeUserId); + assert(userId === epUser.id); + assert(recipeUserId === epUser2.id); + }); + + it("get session with no linked accounts and no auth recipe should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let userId = undefined; + let recipeUserId = undefined; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random"), + {}, + {} + ); + + const app = express(); + + app.use(middleware()); + + app.post("/getsession", async (req, res) => { + let session = await Session.getSession(req, res); + userId = session.getUserId(); + recipeUserId = session.getRecipeUserId().getAsString(); + res.status(200).send(""); + }); + + app.use(errorHandler()); + + let res = await new Promise((resolve) => + request(app) + .post("/getsession") + .set("Cookie", ["sAccessToken=" + session.getAccessToken()]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(userId === recipeUserId && userId !== undefined); + assert(userId === "random"); + }); + }); + + describe("getSessionInformation tests", function () { + it("getSessionInformation with no linked accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + info = await Session.getSessionInformation(session.getHandle()); + + assert(info.userId === info.recipeUserId.getAsString()); + }); + + it("getSessionInformation with linked accounts should have different user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + info = await Session.getSessionInformation(session.getHandle()); + + assert(info.userId !== info.recipeUserId.getAsString()); + assert(session.userId === epUser.id); + assert(session.recipeUserId.getAsString() === epUser2.id); + }); + + it("getSessionInformation with no linked and no auth recipe accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + info = await Session.getSessionInformation(session.getHandle()); + + assert(info.userId === info.recipeUserId.getAsString()); + assert(session.userId === "random"); + }); + }); + + describe("refreshSessionWithoutRequestResponse tests", function () { + it("refreshSessionWithoutRequestResponse with no linked accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + session = await Session.refreshSessionWithoutRequestResponse( + session.getAllSessionTokensDangerously().refreshToken + ); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + }); + + it("refreshSessionWithoutRequestResponse with linked accounts should have different user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + session = await Session.refreshSessionWithoutRequestResponse( + session.getAllSessionTokensDangerously().refreshToken + ); + + assert(session.getUserId() !== session.getRecipeUserId().getAsString()); + assert(session.getUserId() === epUser.id); + assert(session.getRecipeUserId().getAsString() === epUser2.id); + }); + + it("refreshSessionWithoutRequestResponse with no linked and no auth recipe accounts should have same user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + session = await Session.refreshSessionWithoutRequestResponse( + session.getAllSessionTokensDangerously().refreshToken + ); + + assert(session.getUserId() === session.getRecipeUserId().getAsString()); + assert(session.getUserId() === "random"); + }); + + it("refreshSessionWithoutRequestResponse with token theft uses the right recipe user id and session user id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + let refreshToken = session.getAllSessionTokensDangerously().refreshToken; + + session = await Session.refreshSessionWithoutRequestResponse(refreshToken); + + await Session.getSessionWithoutRequestResponse(session.getAccessToken()); + + try { + await Session.refreshSessionWithoutRequestResponse(refreshToken); + assert(fail); + } catch (err) { + assert(err.type === "TOKEN_THEFT_DETECTED"); + assert(err.payload.recipeUserId.getAsString() === epUser2.loginMethods[0].recipeUserId.getAsString()); + assert(err.payload.userId === epUser.id); + } + }); + }); + + describe("refreshSession tests", function () { + it("refreshSession with linked accounts should have different user id and recipe id", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + const app = express(); + + app.use(middleware()); + + app.post("/refreshsession", async (req, res) => { + let session = await Session.refreshSession(req, res); + userId = session.getUserId(); + recipeUserId = session.getRecipeUserId().getAsString(); + res.status(200).send(""); + }); + + app.use(errorHandler()); + + let res = await new Promise((resolve) => + request(app) + .post("/refreshsession") + .set("Cookie", ["sRefreshToken=" + session.getAllSessionTokensDangerously().refreshToken]) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + session = await Session.refreshSessionWithoutRequestResponse( + session.getAllSessionTokensDangerously().refreshToken + ); + + assert(session.getUserId() !== session.getRecipeUserId().getAsString()); + assert(session.getUserId() === epUser.id); + assert(session.getRecipeUserId().getAsString() === epUser2.id); + }); + }); + + describe("revokeAllSessionsForUser test", function () { + it("revokeAllSessionsForUser with linked accounts should delete all the sessions if revokeSessionsForLinkedAccounts is true", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let epuser2session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + let epuser1session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let result = await Session.revokeAllSessionsForUser(epUser2.id); + assert(result.length === 2); + + assert((await Session.getSessionInformation(epuser2session.getHandle())) === undefined); + assert((await Session.getSessionInformation(epuser1session.getHandle())) === undefined); + }); + + it("revokeAllSessionsForUser with linked accounts should delete only specific account's sessions if revokeSessionsForLinkedAccounts is false", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let epuser2session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + let epuser1session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let result = await Session.revokeAllSessionsForUser(epUser2.id, false); + assert(result.length === 1); + + assert((await Session.getSessionInformation(epuser2session.getHandle())) === undefined); + assert((await Session.getSessionInformation(epuser1session.getHandle())) !== undefined); + }); + + it("revokeAllSessionsForUser with linked accounts should delete only the primary user's session if that id is passed and if revokeSessionsForLinkedAccounts is false", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let epuser2session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + let epuser1session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let result = await Session.revokeAllSessionsForUser(epUser.id, false); + assert(result.length === 1); + + assert((await Session.getSessionInformation(epuser2session.getHandle())) !== undefined); + assert((await Session.getSessionInformation(epuser1session.getHandle())) === undefined); + }); + }); + + describe("getAllSessionHandlesForUser test", function () { + it("getAllSessionHandlesForUser with linked accounts should return all the sessions if fetchSessionsForAllLinkedAccounts is true", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let epuser2session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + let epuser1session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let result = await Session.getAllSessionHandlesForUser(epUser2.id); + assert(result.length === 2); + + assert.deepStrictEqual(new Set(result), new Set([epuser1session.getHandle(), epuser2session.getHandle()])); + }); + + it("getAllSessionHandlesForUser with linked accounts should return only specific account's sessions if fetchSessionsForAllLinkedAccounts is false", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let epuser2session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + let epuser1session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let result = await Session.getAllSessionHandlesForUser(epUser2.id, false); + assert(result.length === 1); + + assert(result[0] === epuser2session.getHandle()); + }); + + it("getAllSessionHandlesForUser with linked accounts should return only the primary user's session if that id is passed and if fetchSessionsForAllLinkedAccounts is false", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let epuser2session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + let epuser1session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser.loginMethods[0].recipeUserId + ); + + let result = await Session.getAllSessionHandlesForUser(epUser.id, false); + assert(result.length === 1); + + assert(result[0] === epuser1session.getHandle()); + }); + }); + + describe("protected props tests", function () { + it("mergeIntoAccessTokenPayload should not allow rsub since it's a protected claim", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + try { + await session.mergeIntoAccessTokenPayload({ + sessionHandle: "new string", + }); + assert(false); + } catch (err) { + assert.strictEqual( + err.message, + "SuperTokens core threw an error for a POST request to path: '/recipe/session/regenerate' with status code: 400 and message: The user payload contains protected field\n" + ); + } + + try { + await session.mergeIntoAccessTokenPayload({ + rsub: "new string", + }); + assert(false); + } catch (err) { + assert.strictEqual( + err.message, + "SuperTokens core threw an error for a POST request to path: '/recipe/session/regenerate' with status code: 400 and message: The user payload contains protected field\n" + ); + } + }); + + it("mergeIntoAccessTokenPayload with session handle not allow rsub since it's a protected claim", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("random") + ); + + try { + await Session.mergeIntoAccessTokenPayload(session.getHandle(), { + sessionHandle: "new string", + }); + assert(false); + } catch (err) { + assert.strictEqual( + err.message, + "SuperTokens core threw an error for a PUT request to path: '/recipe/jwt/data' with status code: 400 and message: The user payload contains protected field\n" + ); + } + + try { + await Session.mergeIntoAccessTokenPayload(session.getHandle(), { + rsub: "new string", + }); + assert(false); + } catch (err) { + assert.strictEqual( + err.message, + "SuperTokens core threw an error for a PUT request to path: '/recipe/jwt/data' with status code: 400 and message: The user payload contains protected field\n" + ); + } + }); + + it("protected props contains rsub", async function () { + assert(protectedProps.indexOf("rsub") !== -1); + }); + + it("createNewSession should not allow rsub since it's a protected claim", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + const session = await Session.createNewSessionWithoutRequestResponse( + "public", + supertokens.convertToRecipeUserId("testRecipeUserId"), + { + rsub: "new string", + } + ); + + assert.strictEqual(session.getAccessTokenPayload().rsub, "testRecipeUserId"); + }); + }); + + describe("fetch claim function tests", function () { + it("fetch callback in claim gets right recipeUserId and userId when using fetch and set claim with session object", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let userIdInCallback; + let recipeUserIdInCallback; + + let primitiveClaim = new PrimitiveClaim({ + key: "some-key", + fetchValue: async (userId, recipeUserId) => { + userIdInCallback = userId; + recipeUserIdInCallback = recipeUserId; + return undefined; + }, + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + await session.fetchAndSetClaim(primitiveClaim); + + assert(userIdInCallback === epUser.id); + assert(recipeUserIdInCallback.getAsString() === epUser2.loginMethods[0].recipeUserId.getAsString()); + }); + + it("fetch callback in claim gets right recipeUserId and userId when using fetch and set claim with session handle", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + + let userIdInCallback; + let recipeUserIdInCallback; + + let primitiveClaim = new PrimitiveClaim({ + key: "some-key", + fetchValue: async (userId, recipeUserId) => { + userIdInCallback = userId; + recipeUserIdInCallback = recipeUserId; + return undefined; + }, + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + await Session.fetchAndSetClaim(session.getHandle(), primitiveClaim); + + assert(userIdInCallback === epUser.id); + assert(recipeUserIdInCallback.getAsString() === epUser2.loginMethods[0].recipeUserId.getAsString()); + }); + + it("fetch callback in claim gets right recipeUserId and userId when creating a new session", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + + let userIdInCallback; + let recipeUserIdInCallback; + + let primitiveClaim = new PrimitiveClaim({ + key: "some-key", + fetchValue: async (userId, recipeUserId) => { + userIdInCallback = userId; + recipeUserIdInCallback = recipeUserId; + return undefined; + }, + }); + + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init({ + override: { + functions: (oI) => { + return { + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...primitiveClaim.build(input.userId, input.recipeUserId), + }; + return oI.createNewSession(input); + }, + }; + }, + }, + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + assert(userIdInCallback === epUser.id); + assert(recipeUserIdInCallback.getAsString() === epUser2.loginMethods[0].recipeUserId.getAsString()); + }); + }); + + describe("validateClaimsForSessionHandle tests", function () { + it("validateClaimsForSessionHandle uses the correct recipeUserId and userId", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init(), + EmailVerification.init({ + mode: "REQUIRED", + }), + ], + }); + + let epUser = (await EmailPassword.signUp("public", "test@example.com", "password123")).user; + let token = await EmailVerification.createEmailVerificationToken( + "public", + epUser.loginMethods[0].recipeUserId + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + await AccountLinking.createPrimaryUser(epUser.loginMethods[0].recipeUserId); + + let epUser2 = (await EmailPassword.signUp("public", "test2@example.com", "password123")).user; + + await AccountLinking.linkAccounts(epUser2.loginMethods[0].recipeUserId, epUser.id); + + let session = await Session.createNewSessionWithoutRequestResponse( + "public", + epUser2.loginMethods[0].recipeUserId + ); + + let resp = await Session.validateClaimsForSessionHandle(session.getHandle()); + + assert(resp.status === "OK"); + assert(resp.invalidClaims.length === 1); + assert(resp.invalidClaims[0].id === "st-ev"); + assert.deepStrictEqual(resp.invalidClaims[0].reason, { + message: "wrong value", + expectedValue: true, + actualValue: false, + }); + }); + }); +}); diff --git a/test/accountlinking/thirdparty.test.js b/test/accountlinking/thirdparty.test.js new file mode 100644 index 000000000..f3528a113 --- /dev/null +++ b/test/accountlinking/thirdparty.test.js @@ -0,0 +1,1406 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let EmailVerification = require("../../recipe/emailverification"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/thirdparty.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("sign in up tests", function () { + it("sign up in succeeds and makes primary user if verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + }); + + it("sign up in succeeds and does not make primary user if not verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user.isPrimaryUser === false); + }); + + it("sign up in succeeds and makes primary user if not verified and verification not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user.isPrimaryUser === true); + }); + + it("sign up in succeeds and does not make primary user if account linking is disabled even if verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === false); + }); + + it("sign up in succeeds and does not make primary user if account linking is disabled and not verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: false, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user.isPrimaryUser === false); + }); + + it("sign up in fails cause changed email already associated with another primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user1.isPrimaryUser === true); + + let user2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test2@example.com", true) + ).user; + assert(user2.isPrimaryUser === true); + + let resp = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "github", + "abcd", + "test@example.com", + true + ); + assert.strictEqual(resp.status, "EMAIL_CHANGE_NOT_ALLOWED_ERROR"); + assert.strictEqual(resp.reason, "Email already associated with another primary user."); + }); + + it("sign up in fails cause changed email already associated with another primary user when the user trying to sign in is linked with another user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user1.isPrimaryUser === true); + + let user2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test2@example.com", true) + ).user; + assert(user2.isPrimaryUser === true); + + let user3 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github2", "abcd", "test2@example.com", true) + ).user; + assert(user2.isPrimaryUser === true); + assert(user3.id === user2.id); + assert(user3.loginMethods.length === 2); + + let resp = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "github2", + "abcd", + "test@example.com", + true + ); + assert.strictEqual(resp.status, "EMAIL_CHANGE_NOT_ALLOWED_ERROR"); + assert.strictEqual(resp.reason, "Email already associated with another primary user."); + }); + + it("sign up in succeeds when changed email belongs to a recipe user even though the new email is already associated with another primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user1.isPrimaryUser === true); + + let user2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test2@example.com", false) + ).user; + assert(user2.isPrimaryUser === false); + + let resp = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "github", + "abcd", + "test@example.com", + false + ); + assert(resp.status === "OK"); + }); + + it("sign up in succeeds when changed email belongs to a primary user even though the new email is already associated with another recipe user user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user1.isPrimaryUser === false); + + let user2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test2@example.com", true) + ).user; + assert(user2.isPrimaryUser === true); + + let resp = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "github", + "abcd", + "test@example.com", + false + ); + assert(resp.status === "OK"); + }); + + it("sign up change email succeeds when email is changed to another recipe user's account", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user1.isPrimaryUser === false); + + let user2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test2@example.com", false) + ).user; + assert(user2.isPrimaryUser === false); + + let resp = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "github", + "abcd", + "test@example.com", + false + ); + assert(resp.status === "OK"); + }); + + it("sign up in succeeds to change email of primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user1.isPrimaryUser === true); + + let resp = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test2@example.com", + false + ); + assert(resp.status === "OK"); + + { + let users = await supertokens.listUsersByAccountInfo("public", { + email: "test@example.com", + }); + assert(users.length === 0); + } + + { + let users = await supertokens.listUsersByAccountInfo("public", { + email: "test2@example.com", + }); + assert(users.length === 1); + } + }); + + it("sign up in does not create primary user during sign in", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user1.isPrimaryUser === false); + + user1 = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true)) + .user; + assert(user1.isPrimaryUser === false); + }); + + it("sign up in does not link accounts during sign in", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user1.isPrimaryUser === false); + + user1 = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true)) + .user; + assert(user1.isPrimaryUser === false); + }); + + it("sign up in links accounts during sign up with another third party account", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + + let user1 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user1.isPrimaryUser === true); + assert(user1.id === user.id); + assert(user1.loginMethods.length === 2); + assert(user1.loginMethods[0].thirdParty.id === "github"); + assert(user1.loginMethods[1].thirdParty.id === "google"); + }); + + it("sign up creates primary user only if verified and verification is required and marks email as verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + + let isVerified = await EmailVerification.isEmailVerified(user.loginMethods[0].recipeUserId); + assert(isVerified === true); + }); + + it("sign up does not creates primary user if not verified and verification is required and does not mark email as verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", false) + ).user; + assert(user.isPrimaryUser === false); + + let isVerified = await EmailVerification.isEmailVerified(user.loginMethods[0].recipeUserId); + assert(isVerified === false); + }); + + it("sign up creates primary user if not verified and verification is not required and does not mark email as verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", false) + ).user; + assert(user.isPrimaryUser === true); + + let isVerified = await EmailVerification.isEmailVerified(user.loginMethods[0].recipeUserId); + assert(isVerified === false); + }); + + it("sign up does not crash if is verified boolean is true, but email verification recipe is not initialised, and creates primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser); + }); + + it("sign up does not crash if is verified boolean is true, but email verification recipe is not initialised, and creates primary user if verification not required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + }); + + it("sign in up verifies email based on linked accounts", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + + let user2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user2.isPrimaryUser === false); + + await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); + + // link accuonts above also verifies the account + await EmailVerification.unverifyEmail(user2.loginMethods[0].recipeUserId); + + let pUser = await supertokens.getUser(user2.id); + assert(pUser.isPrimaryUser === true); + assert(pUser.loginMethods[1].verified === false); + assert(pUser.loginMethods[1].thirdParty.id === "google"); + + // now logging in should mark the email as verified + pUser = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false)) + .user; + assert(pUser.isPrimaryUser === true); + assert(pUser.loginMethods[1].verified === true); + assert(pUser.loginMethods[1].thirdParty.id === "google"); + }); + + it("sign in up verifies email if provider says that the email is verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + + // now logging in should mark the email as verified + user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true)) + .user; + assert(user.isPrimaryUser === false); + assert(user.loginMethods.length === 1); + assert(user.loginMethods[0].verified === true); + assert(user.loginMethods[0].thirdParty.id === "google"); + + // during sign up as well + user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", true)) + .user; + assert(user.isPrimaryUser === false); + assert(user.loginMethods.length === 1); + assert(user.loginMethods[0].verified === true); + assert(user.loginMethods[0].thirdParty.id === "github"); + }); + + it("sign in up does not crash if email verification recipe is not used", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + + // now logging in should mark the email as verified + user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true)) + .user; + assert(user.isPrimaryUser === false); + assert(user.loginMethods.length === 1); + assert(user.loginMethods[0].verified === true); + assert(user.loginMethods[0].thirdParty.id === "google"); + + // during sign up as well + user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "github", "abcd", "test@example.com", true)) + .user; + assert(user.isPrimaryUser === false); + assert(user.loginMethods.length === 1); + assert(user.loginMethods[0].verified === true); + assert(user.loginMethods[0].thirdParty.id === "github"); + }); + + it("sign in up does not mark email as unverified even if provider says it's not verified but it was previously verified", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + + // now logging in should mark the email as verified + user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false)) + .user; + assert(user.isPrimaryUser === false); + assert(user.loginMethods.length === 1); + assert(user.loginMethods[0].verified === true); + assert(user.loginMethods[0].thirdParty.id === "google"); + }); + + it("sign up in does not attempt to make primary user / account link during sign in", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", false) + ).user; + assert(user.isPrimaryUser === false); + + user = (await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true)) + .user; + assert(user.isPrimaryUser === false); + }); + }); +}); diff --git a/test/accountlinking/thirdpartyapis.test.js b/test/accountlinking/thirdpartyapis.test.js new file mode 100644 index 000000000..11e9f79ba --- /dev/null +++ b/test/accountlinking/thirdpartyapis.test.js @@ -0,0 +1,1513 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + extractInfoFromResponse, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let AccountLinking = require("../../recipe/accountlinking"); +let EmailVerification = require("../../recipe/emailverification"); +const express = require("express"); +let { middleware, errorHandler } = require("../../framework/express"); +const request = require("supertest"); +let nock = require("nock"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/thirdpartyapis.test.js]")}`, function () { + before(function () { + this.customProviderWithEmailVerified = { + config: { + thirdPartyId: "custom-ev", + authorizationEndpoint: "https://test.com/oauth/auth", + tokenEndpoint: "https://test.com/oauth/token", + clients: [ + { + clientId: "supertokens", + clientSecret: "", + }, + ], + }, + override: (oI) => ({ + ...oI, + exchangeAuthCodeForOAuthTokens: ({ redirectURIInfo }) => redirectURIInfo, + getUserInfo: ({ oAuthTokens }) => { + return { + thirdPartyUserId: oAuthTokens.userId ?? "user", + email: { + id: oAuthTokens.email ?? "email@test.com", + isVerified: true, + }, + rawUserInfoFromProvider: {}, + }; + }, + }), + }; + this.customProviderWithEmailNotVerified = { + config: { + thirdPartyId: "custom-no-ev", + authorizationEndpoint: "https://test.com/oauth/auth", + tokenEndpoint: "https://test.com/oauth/token", + clients: [ + { + clientId: "supertokens", + clientSecret: "", + }, + ], + }, + override: (oI) => ({ + ...oI, + exchangeAuthCodeForOAuthTokens: () => ({}), + getUserInfo: () => { + return { + thirdPartyUserId: "user", + email: { + id: "email@test.com", + isVerified: false, + }, + rawUserInfoFromProvider: {}, + }; + }, + }), + }; + }); + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("signInUpPOST tests", function () { + it("signInUpPOST calls isSignUpAllowed if it's sign up even if user with email already exists with third party", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "email@test.com", true) + ).user; + assert(tpUser.isPrimaryUser); + + assert.strictEqual( + await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED), + undefined + ); + let response = await new Promise((resolve, reject) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "OK"); + assert.notStrictEqual( + await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED), + undefined + ); + }); + + it("signInUpPOST does not call isSignUpAllowed if it's a sign in even if user's email has changed", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-ev", "user", "email2@test.com", true) + ).user; + await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "OK"); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED)) === undefined + ); + }); + + it("signInUpPOST returns SIGN_IN_UP_NOT_ALLOWED if isSignUpAllowed returns false", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailNotVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "email@test.com", true) + ).user; + await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-no-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "SIGN_IN_UP_NOT_ALLOWED"); + assert.strictEqual( + response.body.reason, + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_006)" + ); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED)) !== undefined + ); + }); + + it("signInUpPOST successfully links account and returns the session of the right recipe user if it's a sign up", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "email@test.com", true) + ).user; + await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "OK"); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED)) !== undefined + ); + assert(response.body.createdNewRecipeUser === true); + + let pUser = await supertokens.getUser(tpUser.id); + assert(pUser.isPrimaryUser === true); + assert(pUser.loginMethods.length === 2); + assert(pUser.loginMethods[1].thirdParty.id === "custom-ev"); + assert(pUser.loginMethods[0].thirdParty.id === "google"); + + // checking session + tokens = extractInfoFromResponse(response); + let session = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + assert(session.getUserId() === tpUser.id); + assert(session.getRecipeUserId().getAsString() === pUser.loginMethods[1].recipeUserId.getAsString()); + }); + + it("signInUpPOST successfully does linking of accounts and returns the session of the right recipe user if it's a sign in", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailVerified, + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "email@test.com", true) + ).user; + await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); + + let tpUser2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-ev", "user", "email@test.com", true, { + doNotLink: true, + }) + ).user; + assert(tpUser2.isPrimaryUser === false); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "OK"); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED)) === undefined + ); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED)) !== undefined + ); + assert(response.body.createdNewRecipeUser === false); + + let pUser = await supertokens.getUser(tpUser.id); + assert(pUser.isPrimaryUser === true); + assert(pUser.loginMethods.length === 2); + assert(pUser.loginMethods[0].thirdParty.id === "google"); + assert(pUser.loginMethods[1].thirdParty.id === "custom-ev"); + + // checking session + tokens = extractInfoFromResponse(response); + let session = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + assert(session.getUserId() === tpUser.id); + assert(session.getRecipeUserId().getAsString() === tpUser2.loginMethods[0].recipeUserId.getAsString()); + }); + + it("signInUpPOST gives the right user in the override on successful account linking", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + let userInCallback = undefined; + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + override: { + apis: (oI) => { + return { + ...oI, + signInUpPOST: async function (input) { + let response = await oI.signInUpPOST(input); + if (response.status === "OK") { + userInCallback = response.user; + } + return response; + }, + }; + }, + }, + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "email@test.com", true) + ).user; + await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "OK"); + + let pUser = await supertokens.getUser(tpUser.id); + assert(pUser.isPrimaryUser === true); + assert(pUser.loginMethods.length === 2); + assert(pUser.loginMethods[1].thirdParty.id === "custom-ev"); + assert(pUser.loginMethods[0].thirdParty.id === "google"); + + assert(userInCallback !== undefined); + assert.equal(JSON.stringify(pUser), JSON.stringify(userInCallback)); + }); + + it("signInUpPOST returns SIGN_IN_NOT_ALLOWED if the sign in user's email has changed to another primary user's email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-ev", "user", "email2@test.com", true) + ).user; + await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); + + let tpUser2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "user", "email@test.com", true) + ).user; + assert(tpUser2.isPrimaryUser === true); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "SIGN_IN_UP_NOT_ALLOWED"); + assert.strictEqual( + response.body.reason, + "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)" + ); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED)) === undefined + ); + }); + + it("signInUpPOST returns SIGN_IN_UP_NOT_ALLOWED if it's a sign in and isEmailChangeAllowed returns false", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailNotVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-no-ev", "user", "email2@test.com", false) + ).user; + assert(tpUser.isPrimaryUser === false); + + let tpUser2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "user", "email@test.com", true) + ).user; + assert(tpUser2.isPrimaryUser === true); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-no-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "SIGN_IN_UP_NOT_ALLOWED"); + assert( + response.body.reason === + "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)" + ); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED)) === undefined + ); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED)) === undefined + ); + }); + + it("signInUpPOST checks verification from email verification recipe before calling isEmailChangeAllowed", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailNotVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-no-ev", "user", "email2@test.com", true, { + doNotLink: true, + }) + ).user; + assert(tpUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + tpUser.loginMethods[0].recipeUserId, + "email@test.com" + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let tpUser2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "user", "email@test.com", true) + ).user; + assert(tpUser2.isPrimaryUser === true); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-no-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "OK"); + + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED)) !== undefined + ); + }); + + it("signInUpPOST returns SIGN_IN_UP_NOT_ALLOWED if it's a sign in and isSignInAllowed returns false cause there is no email change", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailNotVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-no-ev", "user", "email@test.com", false) + ).user; + assert(tpUser.isPrimaryUser === false); + + let tpUser2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "user", "email@test.com", true) + ).user; + assert(tpUser2.isPrimaryUser === true); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-no-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert.strictEqual(response.body.status, "SIGN_IN_UP_NOT_ALLOWED"); + assert.strictEqual( + response.body.reason, + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)" + ); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED)) !== undefined + ); + }); + + it("signInUpPOST does account linking during sign in if required", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailNotVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_, __, _tenantId, userContext) => { + if (userContext.doNotLink) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-no-ev", "user", "email@test.com", true, { + doNotLink: true, + }) + ).user; + assert(tpUser.isPrimaryUser === false); + + let tpUser2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "user", "email@test.com", true) + ).user; + assert(tpUser2.isPrimaryUser === true); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-no-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "OK"); + assert(response.body.createdNewRecipeUser === false); + assert(response.body.user.id === tpUser2.id); + assert(response.body.user.loginMethods.length === 2); + + tokens = extractInfoFromResponse(response); + let session = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + assert(session.getUserId() === tpUser2.id); + assert(session.getRecipeUserId().getAsString() === tpUser.id); + }); + + it("signInUpPOST returns SIGN_IN_UP_NOT_ALLOWED even though isEmailChangeAllowed returns true if other recipe exist with unverified, same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailNotVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-no-ev", "user", "email2@test.com", false) + ).user; + assert(tpUser.isPrimaryUser === false); + + let tpUser2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "user", "email@test.com", false) + ).user; + assert(tpUser2.isPrimaryUser === false); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-no-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert.strictEqual(response.body.status, "SIGN_IN_UP_NOT_ALLOWED"); + assert.strictEqual( + response.body.reason, + "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)" + ); + assert( + (await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED)) !== undefined + ); + }); + + it("signInUpPOST returns OK if isEmailChangeAllowed returns true and primary user exists with same email, new email is verified for recipe user, but not for primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailNotVerified, + + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-no-ev", "user", "email2@test.com", false) + ).user; + assert(tpUser.isPrimaryUser === false); + + let token = await EmailVerification.createEmailVerificationToken( + "public", + tpUser.loginMethods[0].recipeUserId, + "email@test.com" + ); + await EmailVerification.verifyEmailUsingToken("public", token.token); + + let tpUser2 = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "user", "email@test.com", false) + ).user; + assert(tpUser2.isPrimaryUser === false); + await AccountLinking.createPrimaryUser(tpUser2.loginMethods[0].recipeUserId); + + let response = await new Promise((resolve, reject) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-no-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + + assert.strictEqual(response.body.status, "OK"); + assert.strictEqual(response.body.createdNewRecipeUser, false); + assert.strictEqual(response.body.user.id, tpUser2.id); + assert.strictEqual(response.body.user.loginMethods.length, 2); + + const tokens = extractInfoFromResponse(response); + const session = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + assert.strictEqual(session.getUserId(), tpUser2.id); + assert.strictEqual(session.getRecipeUserId().getAsString(), tpUser.id); + assert.notEqual( + await ProcessState.getInstance().waitForEvent(PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED), + undefined + ); + }); + + describe("with primary user that has both unverified and verified login methods", () => { + it("signInUpPOST successfully links account and returns the session of the right recipe user if it's a sign up", async function () { + let date = Date.now(); + let email = `john.doe+${date}@supertokens.com`; + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "OPTIONAL", + }), + Session.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + this.customProviderWithEmailVerified, + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + nock("https://test.com").post("/oauth/token").reply(200, {}); + + let tpUser = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd" + date, email, true) + ).user; + await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); + + let tpUserUnverified = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd2" + date, email, false) + ).user; + + const linkRes = await AccountLinking.linkAccounts( + tpUserUnverified.loginMethods[0].recipeUserId, + tpUser.id + ); + + assert.strictEqual(linkRes.status, "OK"); + + await EmailVerification.unverifyEmail(tpUserUnverified.loginMethods[0].recipeUserId); + const primUser = await supertokens.getUser(linkRes.user.id); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: "custom-ev", + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + code: "abcdefghj", + }, + email, + userId: "user" + date, + }, + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + assert(response.body.status === "OK"); + assert(response.body.createdNewRecipeUser === true); + + let updatedPUser = await supertokens.getUser(primUser.id); + assert(updatedPUser.isPrimaryUser === true); + assert(updatedPUser.loginMethods.length === 3); + assert(updatedPUser.loginMethods[2].thirdParty.id === "custom-ev"); + assert(updatedPUser.loginMethods[1].thirdParty.id === "google"); + assert(updatedPUser.loginMethods[0].thirdParty.id === "google"); + + // checking session + tokens = extractInfoFromResponse(response); + let session = await Session.getSessionWithoutRequestResponse(tokens.accessTokenFromAny); + assert.strictEqual(session.getUserId(), primUser.id); + assert.strictEqual( + session.getRecipeUserId().getAsString(), + updatedPUser.loginMethods[2].recipeUserId.getAsString() + ); + }); + }); + }); +}); diff --git a/test/accountlinking/userstructure.test.js b/test/accountlinking/userstructure.test.js new file mode 100644 index 000000000..fa98bc721 --- /dev/null +++ b/test/accountlinking/userstructure.test.js @@ -0,0 +1,212 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + assertJSONEquals, + startSTWithMultitenancyAndAccountLinking, +} = require("../utils"); +let supertokens = require("../../"); +let Session = require("../../recipe/session"); +let assert = require("assert"); +let { ProcessState } = require("../../lib/build/processState"); +let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); +let Passwordless = require("../../recipe/passwordless"); + +describe(`accountlinkingTests: ${printPath("[test/accountlinking/userstructure.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("hasSameEmailAs function in user object work", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + let { user } = await EmailPassword.signUp("public", "test@example.com", "password123"); + + assert(user.loginMethods[0].hasSameEmailAs("test@example.com")); + assert(user.loginMethods[0].hasSameEmailAs(" Test@example.com")); + assert(user.loginMethods[0].hasSameEmailAs("test@examplE.com")); + assert(!user.loginMethods[0].hasSameEmailAs("t2est@examplE.com")); + }); + + it("toJson works as expected", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + let { user } = await EmailPassword.signUp("public", "test@example.com", "password123"); + + let jsonifiedUser = user.toJson(); + + user.loginMethods[0].recipeUserId = user.loginMethods[0].recipeUserId.getAsString(); + assertJSONEquals(jsonifiedUser, user); + }); + + it("hasSameThirdPartyInfoAs function in user object work", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let { user } = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + false + ); + + assert(user.loginMethods[0].hasSameEmailAs("test@example.com")); + assert(user.loginMethods[0].hasSameEmailAs(" Test@example.com")); + assert(user.loginMethods[0].hasSameEmailAs("test@examplE.com")); + assert(!user.loginMethods[0].hasSameEmailAs("t2est@examplE.com")); + + assert( + user.loginMethods[0].hasSameThirdPartyInfoAs({ + id: "google", + userId: "abcd", + }) + ); + assert( + user.loginMethods[0].hasSameThirdPartyInfoAs({ + id: "google ", + userId: " abcd", + }) + ); + assert( + user.loginMethods[0].hasSameThirdPartyInfoAs({ + id: " google ", + userId: "abcd ", + }) + ); + assert( + user.loginMethods[0].hasSameThirdPartyInfoAs({ + id: " google", + userId: " abcd", + }) + ); + assert( + !user.loginMethods[0].hasSameThirdPartyInfoAs({ + id: " gOogle", + userId: "aBcd", + }) + ); + assert( + !user.loginMethods[0].hasSameThirdPartyInfoAs({ + id: "abc", + userId: "abcd", + }) + ); + assert( + !user.loginMethods[0].hasSameThirdPartyInfoAs({ + id: "google", + userId: "aabcd", + }) + ); + }); + + it("hasSamePhoneNumberAs function in user object work", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + supertokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + ], + }); + + const { status, user } = await Passwordless.signInUp({ tenantId: "public", phoneNumber: "+36701234123" }); + + assert.strictEqual(status, "OK"); + + assert(user.loginMethods[0].hasSamePhoneNumberAs("+36701234123")); + assert(user.loginMethods[0].hasSamePhoneNumberAs(" \t+36701234123 \t ")); + assert(user.loginMethods[0].hasSamePhoneNumberAs(" \t+36-70/1234 123 \t ")); + assert(user.loginMethods[0].hasSamePhoneNumberAs(" \t+36-70/1234-123 \t ")); + // TODO: validate these cases should map to false + assert(!user.loginMethods[0].hasSamePhoneNumberAs("36701234123")); + assert(!user.loginMethods[0].hasSamePhoneNumberAs("0036701234123")); + assert(!user.loginMethods[0].hasSamePhoneNumberAs("06701234123")); + assert(!user.loginMethods[0].hasSamePhoneNumberAs("p36701234123")); + }); +}); diff --git a/test/auth-modes.test.js b/test/auth-modes.test.js index e8e72a70a..6e0f80b8b 100644 --- a/test/auth-modes.test.js +++ b/test/auth-modes.test.js @@ -55,10 +55,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { describe("createNewSession", () => { describe("with default getTokenTransferMethod", () => { it("should default to header based session w/ no auth-mode header", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -79,10 +79,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should default to header based session w/ bad auth-mode header", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -103,10 +103,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should use headers if auth-mode specifies it", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -127,10 +127,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should use cookies if auth-mode specifies it", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -157,10 +157,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { describe("with user provided getTokenTransferMethod", () => { it("should use headers if getTokenTransferMethod returns any", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -181,10 +181,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should use headers if getTokenTransferMethod returns header", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -205,10 +205,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should use clear cookies (if present) if getTokenTransferMethod returns header", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -245,10 +245,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should use cookies if getTokenTransferMethod returns cookie", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -273,10 +273,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should clear headers (if present) if getTokenTransferMethod returns cookie", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -316,39 +316,39 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { describe("from behaviour table", () => { // prettier-ignore const behaviourTable = [ - { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, - { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: true, authCookie: false, output: "undefined" }, - { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: false, authCookie: true, output: "undefined" }, - { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: true, authCookie: false, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: false, authCookie: true, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: true, authCookie: true, output: "validateheader" }, - { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: true, authCookie: true, output: "validateheader" }, - { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: true, authCookie: true, output: "validateheader" }, - { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: true, authCookie: true, output: "validateheader" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: true, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: true, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: true, authCookie: false, output: "validateheader" }, - { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: true, authCookie: false, output: "validateheader" }, - { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: true, authCookie: false, output: "validateheader" }, - { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: true, authCookie: false, output: "validateheader" }, - { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: false, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: false, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: false, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: false, authCookie: true, output: "validatecookie" }, + { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, + { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, + { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, + { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: true, authCookie: false, output: "undefined" }, + { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: false, authCookie: true, output: "undefined" }, + { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, + { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, + { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, + { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: true, authCookie: false, output: "UNAUTHORISED" }, + { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: false, authCookie: true, output: "UNAUTHORISED" }, + { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: true, authCookie: true, output: "validateheader" }, + { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: true, authCookie: true, output: "validateheader" }, + { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: true, authCookie: true, output: "validateheader" }, + { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: true, authCookie: true, output: "validateheader" }, + { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: true, authCookie: true, output: "validatecookie" }, + { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: true, authCookie: true, output: "validatecookie" }, + { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: true, authCookie: false, output: "validateheader" }, + { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: true, authCookie: false, output: "validateheader" }, + { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: true, authCookie: false, output: "validateheader" }, + { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: true, authCookie: false, output: "validateheader" }, + { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: false, authCookie: true, output: "validatecookie" }, + { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: false, authCookie: true, output: "validatecookie" }, + { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: false, authCookie: true, output: "validatecookie" }, + { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: false, authCookie: true, output: "validatecookie" }, ]; for (let i = 0; i < behaviourTable.length; ++i) { const conf = behaviourTable[i]; it(`should match line ${i + 1} with a valid token`, async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -397,11 +397,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it(`should match line ${i + 1} with a expired token`, async () => { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 2 } }); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -453,10 +452,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { describe("with access tokens in both headers and cookies", () => { it("should use the value from headers if getTokenTransferMethod returns any", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -500,10 +499,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should use the value from headers if getTokenTransferMethod returns header", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -548,10 +547,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should use the value from cookies if getTokenTransferMethod returns cookie", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -597,10 +596,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should reject requests with sIdRefreshToken", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -640,10 +639,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { describe("with non ST in Authorize header", () => { it("should use the value from cookies if present and getTokenTransferMethod returns any", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -683,10 +682,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should reject with UNAUTHORISED if getTokenTransferMethod returns header", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -727,10 +726,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should reject with UNAUTHORISED if cookies are not present", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -768,10 +767,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { describe("mergeIntoAccessTokenPayload", () => { it("should update cookies if the session was cookie based", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -805,10 +804,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { }); it("should allow headers if the session was header based", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -846,27 +845,27 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { describe("from behaviour table", () => { // prettier-ignore const behaviourTable = [ - { getTokenTransferMethodRes: "any", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, - { getTokenTransferMethodRes: "header", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, - { getTokenTransferMethodRes: "cookie", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, - { getTokenTransferMethodRes: "any", authHeader: false, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "none" }, - { getTokenTransferMethodRes: "header", authHeader: false, authCookie: true, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, // 5 - { getTokenTransferMethodRes: "cookie", authHeader: false, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "none" }, - { getTokenTransferMethodRes: "any", authHeader: true, authCookie: false, output: "validateheader", setTokens: "headers", clearedTokens: "none" }, - { getTokenTransferMethodRes: "header", authHeader: true, authCookie: false, output: "validateheader", setTokens: "headers", clearedTokens: "none" }, - { getTokenTransferMethodRes: "cookie", authHeader: true, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, // 9 - { getTokenTransferMethodRes: "any", authHeader: true, authCookie: true, output: "validateheader", setTokens: "headers", clearedTokens: "cookies" }, - { getTokenTransferMethodRes: "header", authHeader: true, authCookie: true, output: "validateheader", setTokens: "headers", clearedTokens: "cookies" }, - { getTokenTransferMethodRes: "cookie", authHeader: true, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "headers" }, // 12 + { getTokenTransferMethodRes: "any", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, + { getTokenTransferMethodRes: "header", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, + { getTokenTransferMethodRes: "cookie", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, + { getTokenTransferMethodRes: "any", authHeader: false, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "none" }, + { getTokenTransferMethodRes: "header", authHeader: false, authCookie: true, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, // 5 + { getTokenTransferMethodRes: "cookie", authHeader: false, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "none" }, + { getTokenTransferMethodRes: "any", authHeader: true, authCookie: false, output: "validateheader", setTokens: "headers", clearedTokens: "none" }, + { getTokenTransferMethodRes: "header", authHeader: true, authCookie: false, output: "validateheader", setTokens: "headers", clearedTokens: "none" }, + { getTokenTransferMethodRes: "cookie", authHeader: true, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, // 9 + { getTokenTransferMethodRes: "any", authHeader: true, authCookie: true, output: "validateheader", setTokens: "headers", clearedTokens: "cookies" }, + { getTokenTransferMethodRes: "header", authHeader: true, authCookie: true, output: "validateheader", setTokens: "headers", clearedTokens: "cookies" }, + { getTokenTransferMethodRes: "cookie", authHeader: true, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "headers" }, // 12 ]; for (let i = 0; i < behaviourTable.length; ++i) { const conf = behaviourTable[i]; it(`should match line ${i + 1} with a valid token`, async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -955,10 +954,10 @@ describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { const conf = behaviourTable[i]; it(`should match line ${i + 1} with a invalid token`, async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1100,7 +1099,14 @@ function getTestApp(endpoints) { app.use(express.json()); app.post("/create", async (req, res) => { - const session = await Session.createNewSession(req, res, "public", "testing-userId", req.body, {}); + const session = await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + req.body, + {} + ); res.status(200).json({ message: true, sessionHandle: session.getHandle() }); }); diff --git a/test/auth-react-server/index.js b/test/auth-react-server/index.js index b839404fe..da49564e6 100644 --- a/test/auth-react-server/index.js +++ b/test/auth-react-server/index.js @@ -39,23 +39,46 @@ const { default: ThirdPartyEmailPasswordRaw } = require("../../lib/build/recipe/ const { default: DashboardRaw } = require("../../lib/build/recipe/dashboard/recipe"); const { default: MultitenancyRaw } = require("../../lib/build/recipe/multitenancy/recipe"); const Multitenancy = require("../../lib/build/recipe/multitenancy"); +const AccountLinking = require("../../lib/build/recipe/accountlinking"); +const { default: AccountLinkingRaw } = require("../../lib/build/recipe/accountlinking/recipe"); const { default: ThirdPartyPasswordlessRaw } = require("../../lib/build/recipe/thirdpartypasswordless/recipe"); const { default: SessionRaw } = require("../../lib/build/recipe/session/recipe"); -let { startST, killAllST, setupST, cleanST, setKeyValueInConfig, customAuth0Provider, stopST } = require("./utils"); +let { + startST, + killAllST, + setupST, + cleanST, + setKeyValueInConfig, + customAuth0Provider, + stopST, + mockThirdPartyProvider, +} = require("./utils"); let urlencodedParser = bodyParser.urlencoded({ limit: "20mb", extended: true, parameterLimit: 20000 }); let jsonParser = bodyParser.json({ limit: "20mb" }); let app = express(); +// const originalSend = app.response.send; +// app.response.send = function sendOverWrite(body) { +// originalSend.call(this, body); +// this.__custombody__ = body; +// }; + // morgan.token("body", function (req, res) { -// return JSON.stringify(req.body && req.body["formFields"]); +// return JSON.stringify(req.body); +// }); + +// morgan.token("res-body", function (req, res) { +// return typeof res.__custombody__ ? res.__custombody__ : JSON.stringify(res.__custombody__); // }); -// app.use(morgan("[:date[iso]] :url :method :status :response-time ms - :res[content-length] - :body")); app.use(urlencodedParser); app.use(jsonParser); app.use(cookieParser()); +// app.use(morgan("[:date[iso]] :url :method :body", { immediate: true })); +// app.use(morgan("[:date[iso]] :url :method :status :response-time ms - :res[content-length] :res-body")); + const WEB_PORT = process.env.WEB_PORT || 3031; const websiteDomain = `http://localhost:${WEB_PORT}`; let latestURLWithToken = ""; @@ -95,7 +118,7 @@ const formFields = (process.env.MIN_FIELDS && []) || [ }, ]; -const providers = [ +const fullProviderList = [ { config: { thirdPartyId: "google", @@ -130,8 +153,15 @@ const providers = [ }, }, customAuth0Provider(), + mockThirdPartyProvider, ]; +let connectionURI = "http://localhost:9000"; +let passwordlessConfig = {}; +let accountLinkingConfig = {}; +let enabledProviders = undefined; +let enabledRecipes = undefined; + initST(); app.use( @@ -150,30 +180,39 @@ app.get("/ping", async (req, res) => { }); app.post("/startst", async (req, res) => { - if (req.body && req.body.configUpdates) { - for (const update of req.body.configUpdates) { - await setKeyValueInConfig(update.key, update.value); - } + try { + connectionURI = await startST(req.body); + console.log("Connection URI: " + connectionURI); + + const OPAQUE_KEY_WITH_ALL_FEATURES_ENABLED = + "N2yITHflaFS4BPm7n0bnfFCjP4sJoTERmP0J=kXQ5YONtALeGnfOOe2rf2QZ0mfOh0aO3pBqfF-S0jb0ABpat6pySluTpJO6jieD6tzUOR1HrGjJO=50Ob3mHi21tQHJ"; + + await fetch(`${connectionURI}/ee/license`, { + method: "PUT", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + licenseKey: OPAQUE_KEY_WITH_ALL_FEATURES_ENABLED, + }), + }); + + initST(); + res.send(connectionURI + ""); + } catch (err) { + console.log(err); + res.status(500).send(err.toString()); } - let pid = await startST(); - const OPAQUE_KEY_WITH_MULTITENANCY_FEATURE = - "ijaleljUd2kU9XXWLiqFYv5br8nutTxbyBqWypQdv2N-BocoNriPrnYQd0NXPm8rVkeEocN9ayq0B7c3Pv-BTBIhAZSclXMlgyfXtlwAOJk=9BfESEleW6LyTov47dXu"; - - await fetch(`http://localhost:9000/ee/license`, { - method: "PUT", - headers: { - "content-type": "application/json; charset=utf-8", - }, - body: JSON.stringify({ - licenseKey: OPAQUE_KEY_WITH_MULTITENANCY_FEATURE, - }), - }); - res.send(pid + ""); }); app.post("/beforeeach", async (req, res) => { deviceStore = new Map(); + accountLinkingConfig = {}; + passwordlessConfig = {}; + enabledProviders = undefined; + enabledRecipes = undefined; + if (process.env.INSTALL_PATH !== undefined) { await killAllST(); await setupST(); @@ -207,6 +246,7 @@ app.get("/sessioninfo", verifySession(), async (req, res, next) => { res.send({ sessionHandle: session.getHandle(), userId: session.getUserId(), + recipeUserId: session.getRecipeUserId().getAsString(), accessTokenPayload, sessionData, }); @@ -216,16 +256,41 @@ app.get("/sessioninfo", verifySession(), async (req, res, next) => { }); app.post("/deleteUser", async (req, res) => { - if (req.body.rid !== "emailpassword") { - res.status(400).send({ message: "Not implemented" }); + const users = await SuperTokens.listUsersByAccountInfo("public", req.body); + res.send(await SuperTokens.deleteUser(users[0].id)); +}); + +app.post("/changeEmail", async (req, res) => { + let resp; + if (req.body.rid === "emailpassword") { + resp = await EmailPassword.updateEmailOrPassword({ + recipeUserId: SuperTokens.convertToRecipeUserId(req.body.recipeUserId), + email: req.body.email, + tenantIdForPasswordPolicy: req.body.tenantId, + }); + } else if (req.body.rid === "thirdparty") { + const user = await SuperTokens.getUser({ userId: req.body.recipeUserId }); + const loginMethod = user.loginMethod.find((lm) => lm.recipeUserId.getAsString() === req.body.recipeUserId); + resp = await ThirdParty.manuallyCreateOrUpdateUser( + req.body.tenantId, + loginMethod.thirdParty.id, + loginMethod.thirdParty.userId, + req.body.email, + false + ); + } else if (req.body.rid === "passwordless") { + resp = await Passwordless.updateUser({ + recipeUserId: SuperTokens.convertToRecipeUserId(req.body.recipeUserId), + email: req.body.email, + phoneNumber: req.body.phoneNumber, + }); } - const user = await EmailPassword.getUserByEmail("public", req.body.email); - res.send(await SuperTokens.deleteUser(user.id)); + res.json(resp); }); app.get("/unverifyEmail", verifySession(), async (req, res) => { let session = req.session; - await EmailVerification.unverifyEmail(session.getUserId()); + await EmailVerification.unverifyEmail(session.getRecipeUserId()); await session.fetchAndSetClaim(EmailVerification.EmailVerificationClaim); res.send({ status: "OK" }); }); @@ -295,6 +360,85 @@ app.post("/test/setFlow", (req, res) => { res.sendStatus(200); }); +app.post("/setupTenant", async (req, res) => { + const { tenantId, loginMethods, coreConfig } = req.body; + let coreResp = await Multitenancy.createOrUpdateTenant(tenantId, { + emailPasswordEnabled: loginMethods.emailPassword?.enabled === true, + thirdPartyEnabled: loginMethods.thirdParty?.enabled === true, + passwordlessEnabled: loginMethods.passwordless?.enabled === true, + coreConfig, + }); + + if (loginMethods.thirdParty.providers !== undefined) { + for (const provider of loginMethods.thirdParty.providers) { + await Multitenancy.createOrUpdateThirdPartyConfig(tenantId, provider); + } + } + res.send(coreResp); +}); + +app.post("/addUserToTenant", async (req, res) => { + const { tenantId, recipeUserId } = req.body; + let coreResp = await Multitenancy.associateUserToTenant(tenantId, SuperTokens.convertToRecipeUserId(recipeUserId)); + res.send(coreResp); +}); + +app.post("/removeUserFromTenant", async (req, res) => { + const { tenantId, recipeUserId } = req.body; + let coreResp = await Multitenancy.disassociateUserFromTenant( + tenantId, + SuperTokens.convertToRecipeUserId(recipeUserId) + ); + res.send(coreResp); +}); + +app.post("/removeTenant", async (req, res) => { + const { tenantId } = req.body; + let coreResp = await Multitenancy.deleteTenant(tenantId); + res.send(coreResp); +}); + +app.post("/test/setFlow", (req, res) => { + passwordlessConfig = { + contactMethod: req.body.contactMethod, + flowType: req.body.flowType, + + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: saveCode, + }; + }, + }, + smsDelivery: { + override: (oI) => { + return { + ...oI, + sendSms: saveCode, + }; + }, + }, + }; + initST(); + res.sendStatus(200); +}); + +app.post("/test/setAccountLinkingConfig", (req, res) => { + accountLinkingConfig = { + ...req.body, + }; + initST(); + res.sendStatus(200); +}); + +app.post("/test/setEnabledRecipes", (req, res) => { + enabledRecipes = req.body.enabledRecipes; + enabledProviders = req.body.enabledProviders; + initST(); + res.sendStatus(200); +}); + app.get("/test/getDevice", (req, res) => { res.send(deviceStore.get(req.query.preAuthSessionId)); }); @@ -307,6 +451,9 @@ app.get("/test/featureFlags", (req, res) => { available.push("generalerror"); available.push("userroles"); available.push("multitenancy"); + available.push("multitenancyManagementEndpoints"); + available.push("accountlinking"); + available.push("recipeConfig"); res.send({ available, @@ -347,6 +494,7 @@ server.listen(process.env.NODE_PORT === undefined ? 8083 : process.env.NODE_PORT })(process.env.START === "true"); function initST({ passwordlessConfig } = {}) { + AccountLinkingRaw.reset(); UserRolesRaw.reset(); ThirdPartyPasswordlessRaw.reset(); PasswordlessRaw.reset(); @@ -356,6 +504,7 @@ function initST({ passwordlessConfig } = {}) { ThirdPartyEmailPasswordRaw.reset(); SessionRaw.reset(); MultitenancyRaw.reset(); + AccountLinkingRaw.reset(); SuperTokensRaw.reset(); @@ -368,283 +517,296 @@ function initST({ passwordlessConfig } = {}) { }; const recipeList = [ - EmailVerification.init({ - mode: "OPTIONAL", - emailDelivery: { - override: (oI) => { - return { - ...oI, - sendEmail: async (input) => { - console.log(input.emailVerifyLink); - latestURLWithToken = input.emailVerifyLink; - }, - }; + [ + "emailverification", + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + latestURLWithToken = input.emailVerifyLink; + }, + }; + }, }, - }, - override: { - apis: (oI) => { - return { - ...oI, - generateEmailVerifyTokenPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API email verification code", - }; - } - return oI.generateEmailVerifyTokenPOST(input); - }, - verifyEmailPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API email verify", - }; - } - return oI.verifyEmailPOST(input); - }, - }; + override: { + apis: (oI) => { + return { + ...oI, + generateEmailVerifyTokenPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API email verification code", + }; + } + return oI.generateEmailVerifyTokenPOST(input); + }, + verifyEmailPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API email verify", + }; + } + return oI.verifyEmailPOST(input); + }, + }; + }, }, - }, - }), - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - passwordResetPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API reset password consume", - }; - } - return oI.passwordResetPOST(input); - }, - generatePasswordResetTokenPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API reset password", - }; - } - return oI.generatePasswordResetTokenPOST(input); - }, - emailExistsGET: async function (input) { - let generalError = input.options.req.getKeyValueFromQuery("generalError"); - if (generalError === "true") { - return { - status: "GENERAL_ERROR", - message: "general error from API email exists", - }; - } - return oI.emailExistsGET(input); - }, - signUpPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API sign up", - }; - } - return oI.signUpPOST(input); - }, - signInPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - let message = "general error from API sign in"; - - if (body.generalErrorMessage !== undefined) { - message = body.generalErrorMessage; + }), + ], + [ + "emailpassword", + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API reset password consume", + }; } - - return { - status: "GENERAL_ERROR", - message, - }; - } - return oI.signInPOST(input); - }, - }; + return oI.passwordResetPOST(input); + }, + generatePasswordResetTokenPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API reset password", + }; + } + return oI.generatePasswordResetTokenPOST(input); + }, + emailExistsGET: async function (input) { + let generalError = input.options.req.getKeyValueFromQuery("generalError"); + if (generalError === "true") { + return { + status: "GENERAL_ERROR", + message: "general error from API email exists", + }; + } + return oI.emailExistsGET(input); + }, + signUpPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API sign up", + }; + } + return oI.signUpPOST(input); + }, + signInPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + let message = "general error from API sign in"; + + if (body.generalErrorMessage !== undefined) { + message = body.generalErrorMessage; + } + + return { + status: "GENERAL_ERROR", + message, + }; + } + return oI.signInPOST(input); + }, + }; + }, }, - }, - signUpFeature: { - formFields, - }, - emailDelivery: { - override: (oI) => { - return { - ...oI, - sendEmail: async (input) => { - console.log(input.passwordResetLink); - latestURLWithToken = input.passwordResetLink; - }, - }; + signUpFeature: { + formFields, }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers, - }, - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - authorisationUrlGET: async function (input) { - let generalErrorFromQuery = input.options.req.getKeyValueFromQuery("generalError"); - if (generalErrorFromQuery === "true") { - return { - status: "GENERAL_ERROR", - message: "general error from API authorisation url get", - }; - } + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + console.log(input.passwordResetLink); + latestURLWithToken = input.passwordResetLink; + }, + }; + }, + }, + }), + ], + [ + "thirdparty", + ThirdParty.init({ + signInAndUpFeature: { + providers: + enabledProviders !== undefined + ? fullProviderList.filter(({ config }) => enabledProviders.includes(config.thirdPartyId)) + : fullProviderList, + }, + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + authorisationUrlGET: async function (input) { + let generalErrorFromQuery = input.options.req.getKeyValueFromQuery("generalError"); + if (generalErrorFromQuery === "true") { + return { + status: "GENERAL_ERROR", + message: "general error from API authorisation url get", + }; + } - return originalImplementation.authorisationUrlGET(input); - }, - signInUpPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API sign in up", - }; - } + return originalImplementation.authorisationUrlGET(input); + }, + signInUpPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API sign in up", + }; + } - return originalImplementation.signInUpPOST(input); - }, - }; + return originalImplementation.signInUpPOST(input); + }, + }; + }, }, - }, - }), - ThirdPartyEmailPassword.init({ - signUpFeature: { - formFields, - }, - emailDelivery: { - override: (oI) => { - return { - ...oI, - sendEmail: async (input) => { - console.log(input.passwordResetLink); - latestURLWithToken = input.passwordResetLink; - }, - }; + }), + ], + [ + "thirdpartyemailpassword", + ThirdPartyEmailPassword.init({ + signUpFeature: { + formFields, }, - }, - providers, - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - emailPasswordSignUpPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API sign up", - }; - } + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + console.log(input.passwordResetLink); + latestURLWithToken = input.passwordResetLink; + }, + }; + }, + }, + providers: + enabledProviders !== undefined + ? fullProviderList.filter((config) => enabledProviders.includes(config.config)) + : fullProviderList, + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + emailPasswordSignUpPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API sign up", + }; + } - return originalImplementation.emailPasswordSignUpPOST(input); - }, - passwordResetPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API reset password consume", - }; - } - return originalImplementation.passwordResetPOST(input); - }, - generatePasswordResetTokenPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API reset password", - }; - } - return originalImplementation.generatePasswordResetTokenPOST(input); - }, - emailPasswordEmailExistsGET: async function (input) { - let generalError = input.options.req.getKeyValueFromQuery("generalError"); - if (generalError === "true") { - return { - status: "GENERAL_ERROR", - message: "general error from API email exists", - }; - } - return originalImplementation.emailPasswordEmailExistsGET(input); - }, - emailPasswordSignInPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API sign in", - }; - } - return originalImplementation.emailPasswordSignInPOST(input); - }, - authorisationUrlGET: async function (input) { - let generalErrorFromQuery = input.options.req.getKeyValueFromQuery("generalError"); - if (generalErrorFromQuery === "true") { - return { - status: "GENERAL_ERROR", - message: "general error from API authorisation url get", - }; - } + return originalImplementation.emailPasswordSignUpPOST(input); + }, + passwordResetPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API reset password consume", + }; + } + return originalImplementation.passwordResetPOST(input); + }, + generatePasswordResetTokenPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API reset password", + }; + } + return originalImplementation.generatePasswordResetTokenPOST(input); + }, + emailPasswordEmailExistsGET: async function (input) { + let generalError = input.options.req.getKeyValueFromQuery("generalError"); + if (generalError === "true") { + return { + status: "GENERAL_ERROR", + message: "general error from API email exists", + }; + } + return originalImplementation.emailPasswordEmailExistsGET(input); + }, + emailPasswordSignInPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API sign in", + }; + } + return originalImplementation.emailPasswordSignInPOST(input); + }, + authorisationUrlGET: async function (input) { + let generalErrorFromQuery = input.options.req.getKeyValueFromQuery("generalError"); + if (generalErrorFromQuery === "true") { + return { + status: "GENERAL_ERROR", + message: "general error from API authorisation url get", + }; + } - return originalImplementation.authorisationUrlGET(input); - }, - thirdPartySignInUpPOST: async function (input) { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from API sign in up", - }; - } + return originalImplementation.authorisationUrlGET(input); + }, + thirdPartySignInUpPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API sign in up", + }; + } - return originalImplementation.thirdPartySignInUpPOST(input); - }, - }; + return originalImplementation.thirdPartySignInUpPOST(input); + }, + }; + }, }, - }, - }), - Session.init({ - override: { - apis: function (originalImplementation) { - return { - ...originalImplementation, - signOutPOST: async (input) => { - let body = await input.options.req.getJSONBody(); - if (body.generalError === true) { - return { - status: "GENERAL_ERROR", - message: "general error from signout API", - }; - } - return originalImplementation.signOutPOST(input); - }, - }; + }), + ], + [ + "session", + Session.init({ + override: { + apis: function (originalImplementation) { + return { + ...originalImplementation, + signOutPOST: async (input) => { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from signout API", + }; + } + return originalImplementation.signOutPOST(input); + }, + }; + }, }, - }, - }), - - Multitenancy.init({ - getAllowedDomainsForTenantId: (tenantId) => [ - `${tenantId}.example.com`, - websiteDomain.replace(/https?:\/\/([^:\/]*).*/, "$1"), - ], - }), + }), + ], ]; passwordlessConfig = { @@ -669,7 +831,8 @@ function initST({ passwordlessConfig } = {}) { ...passwordlessConfig, }; - recipeList.push( + recipeList.push([ + "passwordless", Passwordless.init({ ...passwordlessConfig, override: { @@ -709,13 +872,17 @@ function initST({ passwordlessConfig } = {}) { }; }, }, - }) - ); + }), + ]); - recipeList.push( + recipeList.push([ + "thirdpartypasswordless", ThirdPartyPasswordless.init({ ...passwordlessConfig, - providers, + providers: + enabledProviders !== undefined + ? fullProviderList.filter((config) => enabledProviders.includes(config.config)) + : fullProviderList, override: { apis: (originalImplementation) => { return { @@ -775,10 +942,41 @@ function initST({ passwordlessConfig } = {}) { }; }, }, - }) - ); + }), + ]); + + recipeList.push(["userroles", UserRoles.init()]); + + recipeList.push([ + "multitenancy", + Multitenancy.init({ + getAllowedDomainsForTenantId: (tenantId) => [ + `${tenantId}.example.com`, + websiteDomain.replace(/https?:\/\/([^:\/]*).*/, "$1"), + ], + }), + ]); + + accountLinkingConfig = { + enabled: false, + shouldAutoLink: { + ...accountLinkingConfig?.shouldAutoLink, + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }, + ...accountLinkingConfig, + }; - recipeList.push(UserRoles.init()); + if (accountLinkingConfig.enabled) { + recipeList.push([ + "accountlinking", + AccountLinking.init({ + shouldDoAutomaticAccountLinking: () => ({ + ...accountLinkingConfig.shouldAutoLink, + }), + }), + ]); + } SuperTokens.init({ appInfo: { @@ -787,8 +985,11 @@ function initST({ passwordlessConfig } = {}) { websiteDomain, }, supertokens: { - connectionURI: "http://localhost:9000", + connectionURI, }, - recipeList, + recipeList: + enabledRecipes !== undefined + ? recipeList.filter(([key]) => enabledRecipes.includes(key)).map(([_key, recipeFunc]) => recipeFunc) + : recipeList.map(([_key, recipeFunc]) => recipeFunc), }); } diff --git a/test/auth-react-server/package-lock.json b/test/auth-react-server/package-lock.json index 0f7711620..6e95c90bd 100644 --- a/test/auth-react-server/package-lock.json +++ b/test/auth-react-server/package-lock.json @@ -1,36 +1,51 @@ { "name": "server", "version": "0.0.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "accepts": { + "packages": { + "": { + "name": "server", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "axios": "^0.24.0", + "cookie-parser": "1.4.4", + "cors": "^2.8.5", + "dotenv": "^8.2.0", + "express": "4.17.1" + } + }, + "node_modules/accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { + "dependencies": { "mime-types": "~2.1.24", "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" } }, - "array-flatten": { + "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, - "axios": { + "node_modules/axios": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", - "requires": { + "dependencies": { "follow-redirects": "^1.14.4" } }, - "body-parser": { + "node_modules/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { + "dependencies": { "bytes": "3.1.0", "content-type": "~1.0.4", "debug": "2.6.9", @@ -41,102 +56,135 @@ "qs": "6.7.0", "raw-body": "2.4.0", "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" } }, - "bytes": { + "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } }, - "content-disposition": { + "node_modules/content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { + "dependencies": { "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" } }, - "content-type": { + "node_modules/content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } }, - "cookie": { + "node_modules/cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "engines": { + "node": ">= 0.6" + } }, - "cookie-parser": { + "node_modules/cookie-parser": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", - "requires": { + "dependencies": { "cookie": "0.3.1", "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" } }, - "cookie-signature": { + "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "cors": { + "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { + "dependencies": { "object-assign": "^4", "vary": "^1" + }, + "engines": { + "node": ">= 0.10" } }, - "debug": { + "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { + "dependencies": { "ms": "2.0.0" } }, - "depd": { + "node_modules/depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } }, - "destroy": { + "node_modules/destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, - "dotenv": { + "node_modules/dotenv": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "engines": { + "node": ">=10" + } }, - "ee-first": { + "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, - "encodeurl": { + "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } }, - "escape-html": { + "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, - "etag": { + "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } }, - "express": { + "node_modules/express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { + "dependencies": { "accepts": "~1.3.7", "array-flatten": "1.1.1", "body-parser": "1.19.0", @@ -168,19 +216,23 @@ "utils-merge": "1.0.1", "vary": "~1.1.2" }, - "dependencies": { - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - } + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" } }, - "finalhandler": { + "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { + "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -188,164 +240,238 @@ "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "follow-redirects": { + "node_modules/follow-redirects": { "version": "1.14.7", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } }, - "forwarded": { + "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } }, - "fresh": { + "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } }, - "http-errors": { + "node_modules/http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { + "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.1", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" } }, - "iconv-lite": { + "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { + "dependencies": { "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "ipaddr.js": { + "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } }, - "media-typer": { + "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } }, - "merge-descriptors": { + "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, - "methods": { + "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } }, - "mime": { + "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } }, - "mime-db": { + "node_modules/mime-db": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", + "engines": { + "node": ">= 0.6" + } }, - "mime-types": { + "node_modules/mime-types": { "version": "2.1.33", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", - "requires": { + "dependencies": { "mime-db": "1.50.0" + }, + "engines": { + "node": ">= 0.6" } }, - "ms": { + "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, - "negotiator": { + "node_modules/negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } }, - "object-assign": { + "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } }, - "on-finished": { + "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { + "dependencies": { "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "parseurl": { + "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } }, - "path-to-regexp": { + "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "proxy-addr": { + "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { + "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" } }, - "qs": { + "node_modules/qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } }, - "range-parser": { + "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } }, - "raw-body": { + "node_modules/raw-body": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { + "dependencies": { "bytes": "3.1.0", "http-errors": "1.7.2", "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "safe-buffer": { + "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "safer-buffer": { + "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "send": { + "node_modules/send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { + "dependencies": { "debug": "2.6.9", "depd": "~1.1.2", "destroy": "~1.0.4", @@ -360,63 +486,85 @@ "range-parser": "~1.2.1", "statuses": "~1.5.0" }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } + "engines": { + "node": ">= 0.8.0" } }, - "serve-static": { + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { + "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "setprototypeof": { + "node_modules/setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, - "statuses": { + "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } }, - "toidentifier": { + "node_modules/toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } }, - "type-is": { + "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { + "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" } }, - "unpipe": { + "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } }, - "utils-merge": { + "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } }, - "vary": { + "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } } } } diff --git a/test/auth-react-server/utils.js b/test/auth-react-server/utils.js index 3ad60d4ce..b9fb159b1 100644 --- a/test/auth-react-server/utils.js +++ b/test/auth-react-server/utils.js @@ -14,7 +14,7 @@ */ const { exec } = require("child_process"); let fs = require("fs"); -const { default: fetch } = require("cross-fetch"); +let assert = require("assert"); module.exports.executeCommand = async function (cmd) { return new Promise((resolve, reject) => { @@ -94,7 +94,18 @@ module.exports.killAllST = async function () { } }; -module.exports.startST = async function (host = "localhost", port = 9000) { +module.exports.startST = async function (config = {}) { + const host = config.host ?? "localhost"; + const port = config.port ?? 9000; + + const notUsingTestApp = + process.env.REAL_DB_TEST !== "true" || host !== "localhost" || port !== 9000 || config.noApp === true; + if (config.coreConfig && notUsingTestApp) { + for (const [k, v] of Object.entries(config.coreConfig)) { + await module.exports.setKeyValueInConfig(k, v); + } + } + return new Promise(async (resolve, reject) => { let installationPath = process.env.INSTALL_PATH; let pidsBefore = await getListOfPids(); @@ -106,7 +117,8 @@ module.exports.startST = async function (host = "localhost", port = 9000) { ` && java -Djava.security.egd=file:/dev/urandom -classpath "./core/*:./plugin-interface/*" io.supertokens.Main ./ DEV host=` + host + " port=" + - port + port + + " test_mode" ) .catch((err) => { if (!returned) { @@ -130,7 +142,53 @@ module.exports.startST = async function (host = "localhost", port = 9000) { } else { if (!returned) { returned = true; - resolve(nonIntersection[0]); + console.log(`Application started on http://localhost:${process.env.NODE_PORT | 8080}`); + console.log(`processId: ${nonIntersection[0]}`); + + if (notUsingTestApp) { + return resolve(`http://${host}:${port}`); + } + + try { + // Math.random is an unsafe random but it doesn't actually matter here + // const appId = configs.appId ?? `testapp-${Date.now()}-${Math.floor(Math.random() * 1000)}`; + const appId = config.appId ?? `testapp`; + + await module.exports.removeAppAndTenants(host, port, appId); + + const OPAQUE_KEY_WITH_MULTITENANCY_FEATURE = + "ijaleljUd2kU9XXWLiqFYv5br8nutTxbyBqWypQdv2N-BocoNriPrnYQd0NXPm8rVkeEocN9ayq0B7c3Pv-BTBIhAZSclXMlgyfXtlwAOJk=9BfESEleW6LyTov47dXu"; + + await fetch(`http://${host}:${port}/ee/license`, { + method: "PUT", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + licenseKey: OPAQUE_KEY_WITH_MULTITENANCY_FEATURE, + }), + }); + + // Create app + const createAppResp = await fetch(`http://${host}:${port}/recipe/multitenancy/app`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + appId, + emailPasswordEnabled: true, + thirdPartyEnabled: true, + passwordlessEnabled: true, + coreConfig: config.coreConfig, + }), + }); + const respBody = await createAppResp.json(); + assert.strictEqual(respBody.status, "OK"); + resolve(`http://${host}:${port}/appid-${appId}`); + } catch (err) { + reject(err); + } } } } @@ -141,6 +199,85 @@ module.exports.startST = async function (host = "localhost", port = 9000) { }); }; +module.exports.removeAppAndTenants = async function (host, port, appId) { + const tenantsResp = await fetch(`http://${host}:${port}/appid-${appId}/recipe/multitenancy/tenant/list`); + if (tenantsResp.status === 401) { + const updateAppResp = await fetch(`http://${host}:${port}/recipe/multitenancy/app`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + appId, + coreConfig: { api_keys: null }, + }), + }); + assert.strictEqual(updateAppResp.status, 200); + await module.exports.removeAppAndTenants(host, port, appId); + } else if (tenantsResp.status === 200) { + const tenants = (await tenantsResp.json()).tenants; + for (const t of tenants) { + if (t.tenantId !== "public") { + await fetch(`http://${host}:${port}/appid-${appId}/recipe/multitenancy/tenant/remove`, { + method: "POST", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + tenantId: t.tenantId, + }), + }); + } + } + + const removeResp = await fetch(`http://${host}:${port}/recipe/multitenancy/app/remove`, { + method: "POST", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + appId, + }), + }); + const removeRespBody = await removeResp.json(); + assert.strictEqual(removeRespBody.status, "OK"); + } +}; + +const WEB_PORT = process.env.WEB_PORT || 3031; +const websiteDomain = `http://localhost:${WEB_PORT}`; +module.exports.mockThirdPartyProvider = { + config: { + name: "Mock Provider", + thirdPartyId: "mock-provider", + authorizationEndpoint: `${websiteDomain}/mockProvider/auth`, + tokenEndpoint: `${websiteDomain}/mockProvider/token`, + clients: [ + { + clientId: "supertokens", + clientSecret: "", + }, + ], + }, + override: (oI) => ({ + ...oI, + exchangeAuthCodeForOAuthTokens: ({ redirectURIInfo }) => redirectURIInfo.redirectURIQueryParams, + getUserInfo: ({ oAuthTokens }) => { + return { + thirdPartyUserId: oAuthTokens.userId ?? "user", + email: { + id: oAuthTokens.email ?? "email@test.com", + isVerified: oAuthTokens.isVerified !== "false", + }, + rawUserInfoFromProvider: {}, + }; + }, + }), +}; +/** + * + * @returns {import("supertokens-node/lib/build/recipe/thirdparty/types").ProviderConfig} + */ module.exports.customAuth0Provider = () => { return { config: { @@ -210,3 +347,22 @@ async function getListOfPids() { } return result; } + +module.exports.maxVersion = function (version1, version2) { + let splittedv1 = version1.split("."); + let splittedv2 = version2.split("."); + let minLength = Math.min(splittedv1.length, splittedv2.length); + for (let i = 0; i < minLength; i++) { + let v1 = Number(splittedv1[i]); + let v2 = Number(splittedv2[i]); + if (v1 > v2) { + return version1; + } else if (v2 > v1) { + return version2; + } + } + if (splittedv1.length >= splittedv2.length) { + return version1; + } + return version2; +}; diff --git a/test/config.test.js b/test/config.test.js index 00220df3a..e3802f6cd 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -57,12 +57,12 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { // test various inputs for appInfo // Failure condition: passing data of invalid type/ syntax to appInfo it("test values for optional inputs for appInfo", async function () { - await startST(); + const connectionURI = await startST(); { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -80,7 +80,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -100,13 +100,13 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("test values for compulsory inputs for appInfo", async function () { - await startST(); + const connectionURI = await startST(); { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { appName: "SuperTokens", @@ -131,7 +131,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -156,7 +156,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -181,13 +181,13 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { // test using zero, one and two recipe modules // Failure condition: initial supertokens with the incorrect number of modules as specified in the checks it("test using zero, one and two recipe modules", async function () { - await startST(); + const connectionURI = await startST(); { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -202,14 +202,13 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { throw err; } } - resetAll(); } { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -226,7 +225,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -245,10 +244,10 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { // test config for session module // Failure condition: passing data of invalid type/ syntax to the modules config it("test config for session module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -270,12 +269,12 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("various sameSite values", async function () { - await startST(); + const connectionURI = await startST(); { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -293,7 +292,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -311,7 +310,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -330,7 +329,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -353,7 +352,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -375,7 +374,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -393,7 +392,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -411,7 +410,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -429,7 +428,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -446,6 +445,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("sameSite none invalid domain values", async function () { + const connectionURI = await startST(); const domainCombinations = [ ["http://localhost:3000", "http://supertokensapi.io"], ["http://127.0.0.1:3000", "http://supertokensapi.io"], @@ -458,7 +458,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { let err; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { appName: "SuperTokens", @@ -468,7 +468,12 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "none" })], }); try { - await Session.createNewSession(mockRequest(), mockResponse(), "public", "asdf"); + await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("asdf") + ); } catch (e) { err = e; } @@ -679,12 +684,12 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("various config values", async function () { - await startST(); + const connectionURI = await startST(); { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -700,7 +705,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -716,7 +721,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -736,7 +741,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -756,7 +761,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -775,7 +780,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, apiKey: "haha", }, appInfo: { @@ -792,7 +797,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -809,7 +814,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080;try.supertokens.io;try.supertokens.io:8080;localhost:90", + connectionURI: `${connectionURI};try.supertokens.io;try.supertokens.io:8080;localhost:90`, }, appInfo: { apiDomain: "api.supertokens.io", @@ -831,7 +836,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -850,7 +855,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "https://api.supertokens.io", @@ -872,7 +877,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "https://api.supertokens.io", @@ -893,7 +898,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -914,7 +919,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.com", @@ -935,7 +940,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.co.uk", @@ -956,7 +961,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "127.0.0.1:3000", @@ -977,7 +982,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "127.0.0.1:3000", @@ -997,7 +1002,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1017,7 +1022,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "127.0.0.1:3000", @@ -1037,7 +1042,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "https://127.0.0.1:3000", @@ -1056,7 +1061,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "127.0.0.1:3000", @@ -1080,7 +1085,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.test.com:3000", @@ -1091,7 +1096,12 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }, recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], }); - await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); assert(false); } catch (err) { assert.strictEqual( @@ -1106,7 +1116,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "https://api.test.com:3000", @@ -1117,7 +1127,12 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }, recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSecure: false })], }); - await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); assert(false); } catch (err) { assert.strictEqual( @@ -1131,7 +1146,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "https://api.test.com:3000", @@ -1148,14 +1163,19 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }), ], }); - await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); resetAll(); } { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "https://localhost", @@ -1173,10 +1193,10 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("checking for default cookie config", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1196,10 +1216,10 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("Test that the JWKS and OpenId endpoints are exposed by Session", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1234,11 +1254,11 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("apiGatewayPath test", async function () { - await startST(); + const connectionURI = await startST(); { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1254,7 +1274,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -1297,7 +1317,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1314,7 +1334,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -1358,7 +1378,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1374,7 +1394,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -1415,12 +1435,12 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("checking for empty item in recipeList config", async function () { - await startST(); + const connectionURI = await startST(); let errorCaught = true; try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1437,10 +1457,10 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("Check that telemetry is set to true properly", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1455,10 +1475,10 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("Check that telemetry is set to false by default", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1472,10 +1492,10 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); it("Check that telemetry is set to false properly", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/dashboard.test.js b/test/dashboard.test.js index a5b1148e7..8445c178f 100644 --- a/test/dashboard.test.js +++ b/test/dashboard.test.js @@ -1,9 +1,28 @@ -const { printPath, setupST, startST, killAllST, cleanST, resetAll } = require("./utils"); +const { + printPath, + setupST, + startSTWithMultitenancyAndAccountLinki, + startSTWithMultitenancyAndAccountLinkingng, + killAllST, + cleanST, + resetAll, + startSTWithMultitenancyAndAccountLinking, +} = require("./utils"); let STExpress = require("../"); +let Session = require("../recipe/session"); +let Passwordless = require("../recipe/passwordless"); +let ThirdParty = require("../recipe/thirdparty"); +let EmailPassword = require("../recipe/emailpassword"); +let AccountLinking = require("../recipe/accountlinking"); +let EmailVerification = require("../recipe/emailverification"); +let UserMetadata = require("../recipe/usermetadata"); let Dashboard = require("../recipe/dashboard"); let DashboardRecipe = require("../lib/build/recipe/dashboard/recipe").default; let assert = require("assert"); let { ProcessState } = require("../lib/build/processState"); +const express = require("express"); +const request = require("supertest"); +let { middleware, errorHandler } = require("../framework/express"); describe(`dashboard: ${printPath("[test/dashboard.test.js]")}`, function () { beforeEach(async function () { @@ -18,12 +37,12 @@ describe(`dashboard: ${printPath("[test/dashboard.test.js]")}`, function () { }); it("Test that normalised config is generated correctly", async function () { - await startST(); + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -44,7 +63,7 @@ describe(`dashboard: ${printPath("[test/dashboard.test.js]")}`, function () { { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -60,4 +79,758 @@ describe(`dashboard: ${printPath("[test/dashboard.test.js]")}`, function () { assert.equal(config.apiKey, "test"); } }); + + describe("with account linking", () => { + it("should get user info with first&last names", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + const linkRes = await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, user.id); + assert(linkRes.status, "OK"); + + let res = await request(app).get("/auth/dashboard/api/users?limit=100").expect(200); + assert.strictEqual(res.body.status, "OK"); + assert.strictEqual(res.body.users.length, 1); + const expectedLinkedUser = { + id: user.id, + tenantIds: user.tenantIds, + phoneNumbers: [], + isPrimaryUser: true, + emails: ["test@example.com"], + thirdParty: [ + { + id: "google", + userId: "abcd", + }, + ], + timeJoined: user.timeJoined, + loginMethods: [ + { + email: "test@example.com", + recipeId: "thirdparty", + recipeUserId: user.id, + tenantIds: user.tenantIds, + thirdParty: { + id: "google", + userId: "abcd", + }, + timeJoined: user.timeJoined, + verified: true, + }, + { + email: "test@example.com", + recipeId: "emailpassword", + recipeUserId: epUser.user.id, + tenantIds: user.tenantIds, + timeJoined: epUser.user.timeJoined, + verified: true, + }, + ], + }; + assert.deepStrictEqual(res.body.users, [expectedLinkedUser]); + for (const u of res.body.users) { + let metadataRes = await request(app) + .get(`/auth/dashboard/api/user/metadata?userId=${u.id}`) + .expect(200); + assert.deepStrictEqual(metadataRes.body, { + status: "OK", + data: {}, + }); + await request(app) + .put(`/auth/dashboard/api/user/metadata`) + .set("Content-Type", "application/json") + .send( + JSON.stringify({ + userId: u.id, + data: JSON.stringify({ + first_name: "test", + last_name: "user", + }), + }) + ) + .expect(200); + metadataRes = await request(app).get(`/auth/dashboard/api/user/metadata?userId=${u.id}`).expect(200); + assert.deepStrictEqual(metadataRes.body, { + status: "OK", + data: { + first_name: "test", + last_name: "user", + }, + }); + const getUserRes = await request(app).get(`/auth/dashboard/api/user?userId=${u.id}`).expect(200); + assert.deepStrictEqual(getUserRes.body.user, { + ...expectedLinkedUser, + firstName: "test", + lastName: "user", + }); + } + }); + + it("should reset password of linked user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + const linkRes = await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, user.id); + assert(linkRes.status, "OK"); + + // TODO: validate that this should not be a 500 + // const updatePwWrongId = await request(app) + // .put(`/auth/dashboard/api/user/password`) + // .set("Content-Type", "application/json") + // .send( + // JSON.stringify({ + // recipeUserId: user.id, + // newPassword: "newPassword123", + // }) + // ) + // .expect(200); + // assert.strictEqual(updatePwWrongId.body.status, "OK"); + // const signInResWrongPW = await EmailPassword.signIn("public", "test@example.com", "newPassword123"); + // assert.strictEqual(signInResWrongPW.status, "WRONG_CREDENTIALS_ERROR"); + + const updatePw = await request(app) + .put(`/auth/dashboard/api/user/password`) + .set("Content-Type", "application/json") + .send( + JSON.stringify({ + recipeUserId: epUser.user.id, + newPassword: "newPassword123", + }) + ) + .expect(200); + assert.strictEqual(updatePw.body.status, "OK"); + + const signInRes = await EmailPassword.signIn("public", "test@example.com", "newPassword123"); + assert.strictEqual(signInRes.status, "OK"); + }); + + it("should link accounts after verification", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + + const verify = await request(app) + .put(`/auth/dashboard/api/user/email/verify`) + .set("Content-Type", "application/json") + .send( + JSON.stringify({ + recipeUserId: epUser.user.id, + verified: true, + }) + ) + .expect(200); + assert.strictEqual(verify.body.status, "OK"); + + const signInRes = await EmailPassword.signIn("public", "test@example.com", "password123"); + assert.strictEqual(signInRes.status, "OK"); + assert.deepStrictEqual(signInRes.user.toJson(), { + ...user, + loginMethods: [ + user.loginMethods[0].toJson(), + { + ...epUser.user.loginMethods[0].toJson(), + verified: true, + }, + ], + }); + }); + + it("should delete all linked users if removeAllLinkedAccounts is true", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + const linkRes = await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, user.id); + assert(linkRes.status, "OK"); + + const deleteRes = await request(app) + .delete(`/auth/dashboard/api/user?userId=${user.id}&removeAllLinkedAccounts=true`) + .expect(200); + assert.deepStrictEqual(deleteRes.body, { + status: "OK", + }); + + let res = await request(app).get("/auth/dashboard/api/users?limit=100").expect(200); + assert.strictEqual(res.body.status, "OK"); + assert.strictEqual(res.body.users.length, 0); + }); + + it("should not delete all linked users if removeAllLinkedAccounts is false when deleting the primary user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + const linkRes = await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, user.id); + assert(linkRes.status, "OK"); + + const deleteRes = await request(app) + .delete(`/auth/dashboard/api/user?userId=${user.id}&removeAllLinkedAccounts=false`) + .expect(200); + assert.deepStrictEqual(deleteRes.body, { + status: "OK", + }); + + let res = await request(app).get("/auth/dashboard/api/users?limit=100").expect(200); + assert.strictEqual(res.body.status, "OK"); + assert.strictEqual(res.body.users.length, 1); + + const epLoginMethod = epUser.user.loginMethods[0].toJson(); + epLoginMethod.verified = true; + delete epLoginMethod.phoneNumber; + delete epLoginMethod.thirdParty; + + assert.deepStrictEqual(res.body.users, [ + { + ...user.toJson(), + thirdParty: [], + timeJoined: epUser.user.timeJoined, + loginMethods: [epLoginMethod], + }, + ]); + }); + + it("should not delete all linked users if removeAllLinkedAccounts is false when deleting the recipe user", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + let user = ( + await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abcd", "test@example.com", true) + ).user; + assert(user.isPrimaryUser === true); + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123"); + const linkRes = await AccountLinking.linkAccounts(epUser.user.loginMethods[0].recipeUserId, user.id); + assert(linkRes.status, "OK"); + + const deleteRes = await request(app) + .delete(`/auth/dashboard/api/user?userId=${epUser.user.id}&removeAllLinkedAccounts=false`) + .expect(200); + assert.deepStrictEqual(deleteRes.body, { + status: "OK", + }); + + let res = await request(app).get("/auth/dashboard/api/users?limit=100").expect(200); + assert.strictEqual(res.body.status, "OK"); + assert.strictEqual(res.body.users.length, 1); + + const expectedUser = user.toJson(); + delete expectedUser.loginMethods[0].phoneNumber; + assert.deepStrictEqual(res.body.users, [expectedUser]); + }); + }); + + describe("deleteUser", () => { + it("should respond with error if userId is missing", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const deleteRes = await request(app).delete(`/auth/dashboard/api/user`).expect(400); + assert.deepStrictEqual(deleteRes.body, { + message: "Missing required parameter 'userId'", + }); + }); + + it("should respond with error if userId is empty", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const deleteRes = await request(app).delete(`/auth/dashboard/api/user?userId`).expect(400); + assert.deepStrictEqual(deleteRes.body, { + message: "Missing required parameter 'userId'", + }); + }); + }); + + describe("userPut", () => { + it("should respond with error if another user exists with the same email", async function () { + const connectionURI = await startSTWithMultitenancyAndAccountLinking(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + override: { + functions: (oI) => ({ + ...oI, + shouldAllowAccess: async () => true, + }), + }, + }), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailVerification.init({ mode: "REQUIRED" }), + UserMetadata.init(), + Session.init(), + ], + }); + + const app = express(); + app.use(middleware()); + app.use(errorHandler()); + + const signUp1 = await Passwordless.signInUp({ + tenantId: "public", + phoneNumber: `+3630${Date.now().toString().substr(-7)}`, + }); + assert.strictEqual(signUp1.status, "OK"); + const signUp2 = await Passwordless.signInUp({ + tenantId: "public", + phoneNumber: `+3670${Date.now().toString().substr(-7)}`, + }); + assert.strictEqual(signUp2.status, "OK"); + + const resp = await request(app) + .put(`/auth/dashboard/api/user`) + .set("Content-Type", "application/json") + .send( + JSON.stringify({ + recipeId: "passwordless", + recipeUserId: signUp2.user.loginMethods[0].recipeUserId.getAsString(), + phone: signUp1.user.phoneNumbers[0], + email: "", + firstName: "", + lastName: "", + }) + ) + .expect(200); + + assert.strictEqual(resp.body.status, "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"); + }); + }); }); diff --git a/test/dashboard/userEmailVerifyGet.test.js b/test/dashboard/userEmailVerifyGet.test.js index 74a37c4e2..a1a43722c 100644 --- a/test/dashboard/userEmailVerifyGet.test.js +++ b/test/dashboard/userEmailVerifyGet.test.js @@ -23,10 +23,10 @@ describe(`User Dashboard userEmailVerifyGet: ${printPath("[test/dashboard/userEm }); it("Test that api returns correct value for email verification", async () => { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -60,7 +60,7 @@ describe(`User Dashboard userEmailVerifyGet: ${printPath("[test/dashboard/userEm let emailVerifyResponse = await new Promise((res) => { request(app) - .get(emailVerificationUrl + "?email=test@supertokens.com&userId=" + user.id) + .get(emailVerificationUrl + "?email=test@supertokens.com&recipeUserId=" + user.id) .set("Authorization", "Bearer testapikey") .end((err, response) => { if (err) { @@ -71,10 +71,13 @@ describe(`User Dashboard userEmailVerifyGet: ${printPath("[test/dashboard/userEm }); }); - assert(emailVerifyResponse.status === "OK"); - assert(emailVerifyResponse.isVerified === false); + assert.strictEqual(emailVerifyResponse.status, "OK"); + assert.strictEqual(emailVerifyResponse.isVerified, false); - const tokenResponse = await EmailVerification.createEmailVerificationToken("public", user.id); + const tokenResponse = await EmailVerification.createEmailVerificationToken( + "public", + user.loginMethods[0].recipeUserId + ); assert(tokenResponse.status === "OK"); const verificationResponse = await EmailVerification.verifyEmailUsingToken("public", tokenResponse.token); @@ -83,7 +86,7 @@ describe(`User Dashboard userEmailVerifyGet: ${printPath("[test/dashboard/userEm emailVerifyResponse = await new Promise((res) => { request(app) - .get(emailVerificationUrl + "?email=test@supertokens.com&userId=" + user.id) + .get(emailVerificationUrl + "?email=test@supertokens.com&recipeUserId=" + user.id) .set("Authorization", "Bearer testapikey") .end((err, response) => { if (err) { @@ -94,7 +97,7 @@ describe(`User Dashboard userEmailVerifyGet: ${printPath("[test/dashboard/userEm }); }); - assert(emailVerifyResponse.status === "OK"); - assert(emailVerifyResponse.isVerified === true); + assert.strictEqual(emailVerifyResponse.status, "OK"); + assert.strictEqual(emailVerifyResponse.isVerified, true); }); }); diff --git a/test/emailpassword/config.test.js b/test/emailpassword/config.test.js index 82dffd798..5ba442e44 100644 --- a/test/emailpassword/config.test.js +++ b/test/emailpassword/config.test.js @@ -42,10 +42,10 @@ describe(`configTest: ${printPath("[test/emailpassword/config.test.js]")}`, func // test config for emailpassword module // Failure condition: passing custom data or data of invalid type/ syntax to the module it("test default config for emailpassword module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -83,10 +83,10 @@ describe(`configTest: ${printPath("[test/emailpassword/config.test.js]")}`, func // Failure condition: passing data of invalid type/ syntax to the module it("test config for emailpassword module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -128,10 +128,10 @@ describe(`configTest: ${printPath("[test/emailpassword/config.test.js]")}`, func * - Check that the default password and email validators work fine */ it("test that no email/password validators given should add them", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -160,10 +160,10 @@ describe(`configTest: ${printPath("[test/emailpassword/config.test.js]")}`, func }); it("test that giving optional true in email / password field should be ignored", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -195,10 +195,10 @@ describe(`configTest: ${printPath("[test/emailpassword/config.test.js]")}`, func //Check that the default password and email validators work fine it("test that default password and email validators work fine", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/emailpassword/deleteUser.test.js b/test/emailpassword/deleteUser.test.js index 7a1171c24..2aff22bb3 100644 --- a/test/emailpassword/deleteUser.test.js +++ b/test/emailpassword/deleteUser.test.js @@ -54,10 +54,11 @@ describe(`deleteUser: ${printPath("[test/emailpassword/deleteUser.test.js]")}`, }); it("test deleteUser", async function () { - await startST(); + const connectionURI = await startST(); + STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/emailpassword/emailDelivery.test.js b/test/emailpassword/emailDelivery.test.js index 7686c7c89..99ebd9621 100644 --- a/test/emailpassword/emailDelivery.test.js +++ b/test/emailpassword/emailDelivery.test.js @@ -39,10 +39,10 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test default backward compatibility api being called: reset password", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -95,10 +95,10 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test default backward compatibility api being called, error message not sent back to user: reset password", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -152,13 +152,12 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test backward compatibility: reset password", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let passwordResetURL = undefined; - let timeJoined = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -168,12 +167,15 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] recipeList: [ EmailPassword.init({ emailDelivery: { - service: { - sendEmail: async (input) => { - email = input.user.email; - passwordResetURL = input.passwordResetLink; - timeJoined = input.user.timeJoined; - }, + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail: async function (input) { + email = input.user.email; + passwordResetURL = input.passwordResetLink; + timeJoined = input.user.timeJoined; + }, + }; }, }, }), @@ -204,15 +206,14 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] await delay(2); assert.strictEqual(email, "test@example.com"); assert.notStrictEqual(passwordResetURL, undefined); - assert.notStrictEqual(timeJoined, undefined); }); it("test backward compatibility: reset password (non existent user)", async function () { - await startST(); + const connectionURI = await startST(); let functionCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -222,10 +223,13 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] recipeList: [ EmailPassword.init({ emailDelivery: { - service: { - sendEmail: async (input) => { - functionCalled = true; - }, + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail: async function (input) { + functionCalled = true; + }, + }; }, }, }), @@ -274,14 +278,14 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test custom override: reset password", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let passwordResetURL = undefined; let type = undefined; let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -346,7 +350,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test smtp service: reset password", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let passwordResetURL = undefined; let outerOverrideCalled = false; @@ -354,7 +358,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -440,10 +444,10 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test default backward compatibility api being called: email verify", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -462,7 +466,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -499,10 +503,10 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test default backward compatibility api being called, error message not sent back to user: email verify", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -521,7 +525,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -559,13 +563,13 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test backward compatibility: email verify", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let emailVerifyURL = undefined; let userIdInCb = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -575,12 +579,15 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] recipeList: [ EmailVerification.init({ emailDelivery: { - service: { - sendEmail: async (input) => { - email = input.user.email; - userIdInCb = input.user.id; - emailVerifyURL = input.emailVerifyLink; - }, + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + email = input.user.email; + userIdInCb = input.user.recipeUserId.getAsString(); + emailVerifyURL = input.emailVerifyLink; + }, + }; }, }, }), @@ -594,7 +601,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -614,14 +621,14 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test custom override: email verify", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let emailVerifyURL = undefined; let type = undefined; let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -654,7 +661,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -687,7 +694,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] }); it("test smtp service: email verify", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let emailVerifyURL = undefined; let outerOverrideCalled = false; @@ -695,7 +702,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -759,7 +766,7 @@ describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js] app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); diff --git a/test/emailpassword/emailExists.test.js b/test/emailpassword/emailExists.test.js index 642fa3f31..f9d390d35 100644 --- a/test/emailpassword/emailExists.test.js +++ b/test/emailpassword/emailExists.test.js @@ -57,10 +57,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` // disable the email exists API, and check that calling it returns a 404. it("test that if disableing api, the default email exists API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -108,10 +108,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` // email exists it("test good input, email exists", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -154,10 +154,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` //email does not exist it("test good input, email does not exists", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -196,10 +196,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` //pass an invalid (syntactically) email and check that you get exists: false it("test email exists a syntactically invalid email", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -242,10 +242,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` //pass an unnormalised email, and check that you get exists true it("test sending an unnormalised email and you get exists is true", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -288,10 +288,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` //do not pass email it("test bad input, do not pass email", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -325,10 +325,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` // pass an array instead of string in the email it("test passing an array instead of a string in the email", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -368,10 +368,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` // email exists it("test good input, email exists, with bodyParser applied before", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -417,10 +417,10 @@ describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}` // email exists it("test good input, email exists, with bodyParser applied after", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/emailpassword/emailverify.test.js b/test/emailpassword/emailverify.test.js index 12e881a70..3b940acd2 100644 --- a/test/emailpassword/emailverify.test.js +++ b/test/emailpassword/emailverify.test.js @@ -61,10 +61,10 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // Call the API with valid input, email not verified it("test the generate token api with valid input, email not verified", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -99,10 +99,10 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` //Call the API with valid input, email verified and test error it("test the generate token api with valid input, email verified and test error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -127,9 +127,14 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert(response.status === 200); let userId = JSON.parse(response.text).user.id; + let emailId = JSON.parse(response.text).user.emails[0]; let infoFromResponse = extractInfoFromResponse(response); - let verifyToken = await EmailVerification.createEmailVerificationToken("public", userId); + let verifyToken = await EmailVerification.createEmailVerificationToken( + "public", + STExpress.convertToRecipeUserId(userId), + emailId + ); await EmailVerification.verifyEmailUsingToken("public", verifyToken.token); response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); @@ -141,10 +146,10 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // Call the API with no session and see the output it("test the generate token api with valid input, no session and check output", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -183,12 +188,11 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // Call the API with an expired access token and see that try refresh token is returned it("test the generate token api with an expired access token and see that try refresh token is returned", async function () { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 2 } }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -260,14 +264,14 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // Provide your own email callback and make sure that is called it("test that providing your own email callback and make sure it is called", async function () { - await startST(); + const connectionURI = await startST(); let userInfo = null; let emailToken = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -316,7 +320,7 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert(JSON.parse(response2.text).status === "OK"); assert(Object.keys(JSON.parse(response2.text)).length === 1); - assert(userInfo.id === userId); + assert(userInfo.recipeUserId.getAsString() === userId); assert(userInfo.email === "test@gmail.com"); assert(emailToken !== null); }); @@ -334,12 +338,12 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` - Call the API with an expired access token and see that try refresh token is returned */ it("test the email verify API with valid input", async function () { - await startST(); + const connectionURI = await startST(); let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -403,11 +407,11 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // Call the API with an invalid token and see the error it("test the email verify API with invalid token and check error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -451,11 +455,11 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // token is not of type string from input it("test the email verify API with token of not type string", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -499,14 +503,14 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // provide a handlePostEmailVerification callback and make sure it's called on success verification it("test that the handlePostEmailVerification callback is called on successfull verification, if given", async function () { - await startST(); + const connectionURI = await startST(); let userInfoFromCallback = null; let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -583,19 +587,19 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // wait for the callback to be called... await new Promise((res) => setTimeout(res, 500)); - assert(userInfoFromCallback.id === userId); + assert(userInfoFromCallback.recipeUserId.getAsString() === userId); assert(userInfoFromCallback.email === "test@gmail.com"); }); // Call the API with valid input it("test the email verify with valid input, using the get method", async function () { - await startST(); + const connectionURI = await startST(); let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -675,11 +679,11 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // Call the API with no session and see the error it("test the email verify with no session, using the get method", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -720,14 +724,13 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` // Call the API with an expired access token and see that try refresh token is returned it("test the email verify with an expired access token, using the get method", async function () { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 2 } }); let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -843,13 +846,13 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` }); it("test the email verify API with valid input, overriding apis", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -921,18 +924,18 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert(response2.status === "OK"); assert(Object.keys(response2).length === 1); - assert.strictEqual(user.id, userId); + assert.strictEqual(user.recipeUserId.getAsString(), userId); assert.strictEqual(user.email, "test@gmail.com"); }); it("test the email verify API with valid input, overriding functions", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1004,18 +1007,18 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert(response2.status === "OK"); assert(Object.keys(response2).length === 1); - assert.strictEqual(user.id, userId); + assert.strictEqual(user.recipeUserId.getAsString(), userId); assert.strictEqual(user.email, "test@gmail.com"); }); it("test the email verify API with valid input, overriding apis throws error", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1095,18 +1098,18 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` ); assert.deepStrictEqual(response2, { customError: true, error: "verify email error" }); - assert.strictEqual(user.id, userId); + assert.strictEqual(user.recipeUserId.getAsString(), userId); assert.strictEqual(user.email, "test@gmail.com"); }); it("test the email verify API with valid input, overriding functions throws error", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1186,15 +1189,15 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` ); assert.deepStrictEqual(response2, { customError: true, error: "verify email error" }); - assert.strictEqual(user.id, userId); + assert.strictEqual(user.recipeUserId.getAsString(), userId); assert.strictEqual(user.email, "test@gmail.com"); }); it("test the generate token api with valid input, and then remove token", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1223,12 +1226,12 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert(JSON.parse(response.text).status === "OK"); assert(response.status === 200); - let userId = JSON.parse(response.text).user.id; + let userId = STExpress.convertToRecipeUserId(JSON.parse(response.text).user.id); let infoFromResponse = extractInfoFromResponse(response); let verifyToken = await EmailVerification.createEmailVerificationToken("public", userId, "test@gmail.com"); - await EmailVerification.revokeEmailVerificationTokens("public", userId); + await EmailVerification.revokeEmailVerificationTokens("public", userId, "test@gmail.com"); { let response = await EmailVerification.verifyEmailUsingToken("public", verifyToken.token); @@ -1237,10 +1240,10 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` }); it("test the generate token api with valid input, verify and then unverify email", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1269,27 +1272,28 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert(JSON.parse(response.text).status === "OK"); assert(response.status === 200); - let userId = JSON.parse(response.text).user.id; + let userId = STExpress.convertToRecipeUserId(JSON.parse(response.text).user.id); + let emailId = JSON.parse(response.text).user.emails[0]; let infoFromResponse = extractInfoFromResponse(response); - const verifyToken = await EmailVerification.createEmailVerificationToken("public", userId); + const verifyToken = await EmailVerification.createEmailVerificationToken("public", userId, emailId); await EmailVerification.verifyEmailUsingToken("public", verifyToken.token); - assert(await EmailVerification.isEmailVerified(userId)); + assert(await EmailVerification.isEmailVerified(userId, emailId)); - await EmailVerification.unverifyEmail(userId); + await EmailVerification.unverifyEmail(userId, emailId); - assert(!(await EmailVerification.isEmailVerified(userId))); + assert(!(await EmailVerification.isEmailVerified(userId, emailId))); }); it("test the email verify API with deleted user", async function () { - await startST(); + const connectionURI = await startST(); let token = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1339,49 +1343,12 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert.deepStrictEqual(response.body, { message: "unauthorised" }); }); - it("should work with getEmailForUserId returning errors", async () => { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - getEmailForUserId: (userId) => - userId === "testuserid" - ? { status: "EMAIL_DOES_NOT_EXIST_ERROR" } - : { status: "UNKNOWN_USER_ID_ERROR" }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - assert.deepStrictEqual(await EmailVerification.revokeEmailVerificationTokens("public", "testuserid"), { - status: "OK", - }); - - let caughtError; - try { - await EmailVerification.revokeEmailVerificationTokens("public", "nouserid"); - } catch (err) { - caughtError = err; - } - - assert.ok(caughtError); - assert.strictEqual(caughtError.message, "Unknown User ID provided without email"); - }); - it("test that generate email verification token API updates session claims", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1423,10 +1390,11 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert.strictEqual(response.body.status, "OK"); assert.strictEqual(response.status, 200); - let userId = response.body.user.id; + let userId = STExpress.convertToRecipeUserId(response.body.user.id); + let emailId = response.body.user.emails[0]; let infoFromResponse = extractInfoFromResponse(response); let antiCsrfToken = infoFromResponse.antiCsrf; - let token = await EmailVerification.createEmailVerificationToken("public", userId); + let token = await EmailVerification.createEmailVerificationToken("public", userId, emailId); await EmailVerification.verifyEmailUsingToken("public", token.token); response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId); infoFromResponse = extractInfoFromResponse(response); @@ -1443,7 +1411,7 @@ describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}` assert.strictEqual(infoFromResponse2.frontToken, undefined); // now we mark the email as unverified and try again - await EmailVerification.unverifyEmail(userId); + await EmailVerification.unverifyEmail(userId, emailId); response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId); infoFromResponse = extractInfoFromResponse(response); assert.strictEqual(response.statusCode, 200); diff --git a/test/emailpassword/multitenancy.test.js b/test/emailpassword/multitenancy.test.js index d3b272184..4146ab3e5 100644 --- a/test/emailpassword/multitenancy.test.js +++ b/test/emailpassword/multitenancy.test.js @@ -13,7 +13,7 @@ * under the License. */ const { printPath, setupST, startSTWithMultitenancy, stopST, killAllST, cleanST, resetAll } = require("../utils"); -let STExpress = require("../../"); +let SuperTokens = require("../../"); let Session = require("../../recipe/session"); let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; let assert = require("assert"); @@ -42,10 +42,10 @@ describe(`multitenancy: ${printPath("[test/emailpassword/multitenancy.test.js]") // test config for emailpassword module // Failure condition: passing custom data or data of invalid type/ syntax to the module it("test recipe functions", async function () { - await startSTWithMultitenancy(); - STExpress.init({ + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -68,9 +68,9 @@ describe(`multitenancy: ${printPath("[test/emailpassword/multitenancy.test.js]") assert(user1.user.id !== user3.user.id); assert(user2.user.id !== user3.user.id); - assert.deepEqual(user1.user.tenantIds, ["t1"]); - assert.deepEqual(user2.user.tenantIds, ["t2"]); - assert.deepEqual(user3.user.tenantIds, ["t3"]); + assert.deepEqual(user1.user.loginMethods[0].tenantIds, ["t1"]); + assert.deepEqual(user2.user.loginMethods[0].tenantIds, ["t2"]); + assert.deepEqual(user3.user.loginMethods[0].tenantIds, ["t3"]); // Sign in let sUser1 = await EmailPassword.signIn("t1", "test@example.com", "password1"); @@ -82,44 +82,62 @@ describe(`multitenancy: ${printPath("[test/emailpassword/multitenancy.test.js]") assert(sUser3.user.id === user3.user.id); // get user by id - let gUser1 = await EmailPassword.getUserById(user1.user.id); - let gUser2 = await EmailPassword.getUserById(user2.user.id); - let gUser3 = await EmailPassword.getUserById(user3.user.id); + let gUser1 = await SuperTokens.getUser(user1.user.id); + let gUser2 = await SuperTokens.getUser(user2.user.id); + let gUser3 = await SuperTokens.getUser(user3.user.id); - assert.deepEqual(gUser1, user1.user); - assert.deepEqual(gUser2, user2.user); - assert.deepEqual(gUser3, user3.user); - - // get user by email - let gUserByEmail1 = await EmailPassword.getUserByEmail("t1", "test@example.com"); - let gUserByEmail2 = await EmailPassword.getUserByEmail("t2", "test@example.com"); - let gUserByEmail3 = await EmailPassword.getUserByEmail("t3", "test@example.com"); - - assert.deepEqual(gUserByEmail1, user1.user); - assert.deepEqual(gUserByEmail2, user2.user); - assert.deepEqual(gUserByEmail3, user3.user); + assert.deepEqual(gUser1.toJson(), user1.user.toJson()); + assert.deepEqual(gUser2.toJson(), user2.user.toJson()); + assert.deepEqual(gUser3.toJson(), user3.user.toJson()); // create password reset token - let passwordResetLink1 = await EmailPassword.createResetPasswordToken("t1", user1.user.id); - let passwordResetLink2 = await EmailPassword.createResetPasswordToken("t2", user2.user.id); - let passwordResetLink3 = await EmailPassword.createResetPasswordToken("t3", user3.user.id); + let passwordResetLink1 = await EmailPassword.createResetPasswordToken("t1", user1.user.id, "test@example.com"); + let passwordResetLink2 = await EmailPassword.createResetPasswordToken("t2", user2.user.id, "test@example.com"); + let passwordResetLink3 = await EmailPassword.createResetPasswordToken("t3", user3.user.id, "test@example.com"); assert(passwordResetLink1.token !== undefined); assert(passwordResetLink2.token !== undefined); assert(passwordResetLink3.token !== undefined); // reset password using token - await EmailPassword.resetPasswordUsingToken("t1", passwordResetLink1.token, "newpassword1"); - await EmailPassword.resetPasswordUsingToken("t2", passwordResetLink2.token, "newpassword2"); - await EmailPassword.resetPasswordUsingToken("t3", passwordResetLink3.token, "newpassword3"); + const consumeRes1 = await EmailPassword.consumePasswordResetToken("t1", passwordResetLink1.token); + const consumeRes2 = await EmailPassword.consumePasswordResetToken("t2", passwordResetLink2.token); + const consumeRes3 = await EmailPassword.consumePasswordResetToken("t3", passwordResetLink3.token); + + assert.strictEqual(consumeRes1.status, "OK"); + assert.strictEqual(consumeRes2.status, "OK"); + assert.strictEqual(consumeRes3.status, "OK"); + + await EmailPassword.updateEmailOrPassword({ + recipeUserId: user1.user.loginMethods[0].recipeUserId, + email: "test@example.com", + password: "newpassword1", + tenantIdForPasswordPolicy: "t1", + }); + + await EmailPassword.updateEmailOrPassword({ + recipeUserId: user2.user.loginMethods[0].recipeUserId, + email: "test@example.com", + password: "newpassword2", + tenantIdForPasswordPolicy: "t2", + }); + await EmailPassword.updateEmailOrPassword({ + recipeUserId: user3.user.loginMethods[0].recipeUserId, + email: "test@example.com", + password: "newpassword3", + }); // new password should work sUser1 = await EmailPassword.signIn("t1", "test@example.com", "newpassword1"); sUser2 = await EmailPassword.signIn("t2", "test@example.com", "newpassword2"); sUser3 = await EmailPassword.signIn("t3", "test@example.com", "newpassword3"); - assert.deepEqual(sUser1.user, user1.user); - assert.deepEqual(sUser2.user, user2.user); - assert.deepEqual(sUser3.user, user3.user); + assert.strictEqual(sUser1.status, "OK"); + assert.strictEqual(sUser2.status, "OK"); + assert.strictEqual(sUser3.status, "OK"); + + assert.deepEqual(sUser1.user.toJson(), user1.user.toJson()); + assert.deepEqual(sUser2.user.toJson(), user2.user.toJson()); + assert.deepEqual(sUser3.user.toJson(), user3.user.toJson()); }); }); diff --git a/test/emailpassword/override.test.js b/test/emailpassword/override.test.js index 191d314e7..44823dd60 100644 --- a/test/emailpassword/override.test.js +++ b/test/emailpassword/override.test.js @@ -12,7 +12,17 @@ * License for the specific language governing permissions and limitations * under the License. */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll, signUPRequest } = require("../utils"); +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + signUPRequest, + assertJSONEquals, +} = require("../utils"); let STExpress = require("../../"); let Session = require("../../recipe/session"); let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; @@ -23,6 +33,7 @@ let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLD let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); const { Querier } = require("../../lib/build/querier"); let EmailPassword = require("../../recipe/emailpassword"); +let AccountLinking = require("../../recipe/accountlinking"); let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; let utils = require("../../lib/build/recipe/emailpassword/utils"); const express = require("express"); @@ -42,11 +53,11 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, }); it("overriding functions tests", async () => { - await startST(); + const connectionURI = await startST(); let user = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -54,6 +65,34 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, websiteDomain: "supertokens.io", }, recipeList: [ + AccountLinking.init({ + override: { + functions: (oI) => { + return { + ...oI, + getUser: async (input) => { + let response = await oI.getUser(input); + if (response !== undefined) { + user = { + ...response, + loginMethods: [ + { + ...response.loginMethods[0], + recipeUserId: response.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; + } + return response; + }, + }; + }, + }, + }), EmailPassword.init({ override: { functions: (oI) => { @@ -62,22 +101,41 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, signUp: async (input) => { let response = await oI.signUp(input); if (response.status === "OK") { - user = response.user; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; } return response; }, signIn: async (input) => { let response = await oI.signIn(input); if (response.status === "OK") { - user = response.user; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; } return response; }, - getUserById: async (input) => { - let response = await oI.getUserById(input); - user = response; - return response; - }, }; }, }, @@ -94,13 +152,15 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, app.get("/user", async (req, res) => { let userId = req.query.userId; - res.json(await EmailPassword.getUserById(userId)); + let user = await STExpress.getUser(userId); + user.loginMethods[0].recipeUserId = user.loginMethods[0].recipeUserId.getAsString(); + res.json(user); }); let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body.user, user); + assertJSONEquals(signUpResponse.body.user, user); user = undefined; assert.strictEqual(user, undefined); @@ -131,7 +191,7 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, ); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signInResponse.user, user); + assertJSONEquals(signInResponse.user, user); user = undefined; assert.strictEqual(user, undefined); @@ -153,16 +213,16 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, ); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(userByIdResponse, user); + assertJSONEquals(userByIdResponse, user); }); it("overriding api tests", async () => { - await startST(); + const connectionURI = await startST(); let user = undefined; let emailExists = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -178,14 +238,38 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, signUpPOST: async (input) => { let response = await oI.signUpPOST(input); if (response.status === "OK") { - user = response.user; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; } return response; }, signInPOST: async (input) => { let response = await oI.signInPOST(input); if (response.status === "OK") { - user = response.user; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; } return response; }, @@ -208,11 +292,6 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, app.use(errorHandler()); - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await EmailPassword.getUserById(userId)); - }); - let emailExistsResponse = await new Promise((resolve) => request(app) .get("/auth/signup/email/exists") @@ -233,7 +312,7 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body.user, user); + assertJSONEquals(signUpResponse.body.user, user); emailExistsResponse = await new Promise((resolve) => request(app) @@ -282,15 +361,15 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, ); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signInResponse.user, user); + assertJSONEquals(signInResponse.user, user); }); it("overriding functions tests, throws error", async () => { - await startST(); + const connectionURI = await startST(); let user = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -298,6 +377,24 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, websiteDomain: "supertokens.io", }, recipeList: [ + AccountLinking.init({ + override: { + functions: (oI) => { + return { + ...oI, + getUser: async (input) => { + let response = await oI.getUser(input); + if (input.userContext.shouldError === undefined) { + return response; + } + throw { + error: "get user error", + }; + }, + }; + }, + }, + }), EmailPassword.init({ override: { functions: (oI) => { @@ -316,12 +413,6 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, error: "signin error", }; }, - getUserById: async (input) => { - await oI.getUserById(input); - throw { - error: "get user error", - }; - }, }; }, }, @@ -339,7 +430,7 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, app.get("/user", async (req, res, next) => { try { let userId = req.query.userId; - res.json(await EmailPassword.getUserById(userId)); + res.json(await STExpress.getUser(userId, { shouldError: true })); } catch (err) { next(err); } @@ -404,12 +495,12 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, }); it("overriding api tests, throws error", async () => { - await startST(); + const connectionURI = await startST(); let user = undefined; let emailExists = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -458,7 +549,7 @@ describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, app.get("/user", async (req, res) => { let userId = req.query.userId; - res.json(await EmailPassword.getUserById(userId)); + res.json(await SuperTokens.getUser(userId)); }); app.use((err, req, res, next) => { diff --git a/test/emailpassword/passwordreset.test.js b/test/emailpassword/passwordreset.test.js index bae833efb..7cf0336da 100644 --- a/test/emailpassword/passwordreset.test.js +++ b/test/emailpassword/passwordreset.test.js @@ -24,6 +24,7 @@ let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLD let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); const { Querier } = require("../../lib/build/querier"); let EmailPassword = require("../../recipe/emailpassword"); +let ThirdParty = require("../../recipe/thirdparty"); let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; let generatePasswordResetToken = require("../../lib/build/recipe/emailpassword/api/generatePasswordResetToken").default; let passwordReset = require("../../lib/build/recipe/emailpassword/api/passwordReset").default; @@ -61,10 +62,10 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] * - check that the generated password reset link is correct */ it("test email validation checks in generate token API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -106,14 +107,14 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] }); it("test that generated password link is correct", async function () { - await startST(); + const connectionURI = await startST(); let resetURL = ""; let tokenInfo = ""; let ridInfo = ""; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -180,10 +181,10 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] * - input is valid, check that password has changed (call sign in) */ it("test password validation", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -248,10 +249,10 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] }); it("test token missing from input", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -290,10 +291,10 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] }); it("test invalid token input", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -333,13 +334,13 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] }); it("test valid token input and passoword has changed", async function () { - await startST(); + const connectionURI = await startST(); let passwordResetUserId = undefined; let token = ""; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -348,15 +349,23 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] }, recipeList: [ EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + token = input.passwordResetLink.split("?")[1].split("&")[0].split("=")[1]; + }, + }; + }, + }, override: { apis: (oI) => { return { ...oI, passwordResetPOST: async function (input) { let resp = await oI.passwordResetPOST(input); - if (resp.userId !== undefined) { - passwordResetUserId = resp.userId; - } + passwordResetUserId = resp.user.id; return resp; }, }; @@ -430,12 +439,7 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] }) ); - let currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(currCDIVersion, "2.12") === currCDIVersion) { - assert(passwordResetUserId !== undefined && passwordResetUserId === userInfo.id); - } else { - assert(passwordResetUserId === undefined); - } + assert(passwordResetUserId !== undefined && passwordResetUserId === userInfo.id); let failureResponse = await new Promise((resolve) => request(app) @@ -491,4 +495,171 @@ describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js] assert(successResponse.user.id === userInfo.id); assert(successResponse.user.email === userInfo.email); }); + + describe("createPasswordResetToken tests", function () { + it("createPasswordResetToken with random user ID fails", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + let resetPassword = await EmailPassword.createResetPasswordToken("public", "random", "test@example.com"); + + assert(resetPassword.status === "UNKNOWN_USER_ID_ERROR"); + }); + + it("createPasswordResetToken with primary user, non email password succeeds", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + false + ); + + let tokenInfo = await EmailPassword.createResetPasswordToken("public", user.user.id, "test@example.com"); + + assert.strictEqual(tokenInfo.status, "OK"); + }); + }); + + describe("consumePasswordResetToken tests", function () { + it("consumePasswordResetToken works when token is valid", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + let user = await EmailPassword.signUp("public", "test@example.com", "password1234"); + + let resetPassword = await EmailPassword.createResetPasswordToken( + "public", + user.user.id, + "test@example.com" + ); + + let info = await EmailPassword.consumePasswordResetToken("public", resetPassword.token); + + assert(info.status === "OK"); + assert(info.userId === user.user.id); + assert(info.email === "test@example.com"); + }); + + it("consumePasswordResetToken returns RESET_PASSWORD_INVALID_TOKEN_ERROR error if the token does not exist", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init()], + }); + + let info = await EmailPassword.consumePasswordResetToken("public", "random"); + + assert(info.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR"); + }); + + it("consumePasswordResetToken with primary user, non email password succeeds", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), + ], + }); + + let user = await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abcd", + "test@example.com", + false + ); + + let tokenInfo = await EmailPassword.createResetPasswordToken("public", user.user.id, "test@example.com"); + + let info = await EmailPassword.consumePasswordResetToken("public", tokenInfo.token); + + assert(info.status === "OK"); + assert(info.userId === user.user.id); + assert(info.email === "test@example.com"); + }); + }); }); diff --git a/test/emailpassword/signinFeature.test.js b/test/emailpassword/signinFeature.test.js index 559e299b1..75d6c95ea 100644 --- a/test/emailpassword/signinFeature.test.js +++ b/test/emailpassword/signinFeature.test.js @@ -23,6 +23,7 @@ const { resetAll, signUPRequest, extractInfoFromResponse, + assertJSONEquals, } = require("../utils"); let STExpress = require("../../"); let Session = require("../../recipe/session"); @@ -57,10 +58,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] /* */ it("test that disabling api, the default signin API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -125,10 +126,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] Setting invalid email or password values in the request body when sending a request to /signin */ it("test singinAPI works when input is fine", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -179,10 +180,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] }); it("test password must be of type string in input", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -232,10 +233,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] }); it("test email must be of type string in input", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -288,10 +289,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] Setting the email value in form field as random@gmail.com causes the test to fail */ it("test singinAPI throws an error when email does not match", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -343,10 +344,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] passing the correct password "validpass123" causes the test to fail */ it("test singinAPI throws an error if password is incorrect", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -404,10 +405,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] setting valid JSON body to /singin API */ it("test bad input, not a JSON to /signin API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -451,10 +452,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] setting valid formFields JSON body to /singin API */ it("test bad input, no POST body to /signin API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -494,10 +495,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] setting valid JSON body to /singin API */ it("test bad input, input is Json but incorrect structure to /signin API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -539,10 +540,10 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] Passing invalid credentials to the /signin API fails the test */ it("test that a successfull signin yields a session", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -615,11 +616,11 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] having the email start with "test" (requierment of the custom validator) will cause the test to fail */ it("test custom email validators to sign up and make sure they are applied to sign in", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -690,13 +691,13 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] sending the correct password "valid" will cause the test to fail */ it("test custom password validators to sign up and make sure they are applied to sign in", async function () { - await startST(); + const connectionURI = await startST(); let failsValidatorCtr = 0; let passesValidatorCtr = 0; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -772,11 +773,11 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] sending the correct password to the /signin API will cause the test to fail */ it("test password field validation error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -826,11 +827,11 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] //sending the correct email to the /signin API will cause the test to fail it("test email field validation error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -881,11 +882,11 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] // Input formFields has no email field //passing the email field in formFields will cause the test to fail it("test formFields has no email field", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -930,11 +931,11 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] // Input formFields has no password field //passing the password field in formFields will cause the test to fail it("test formFields has no password field", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -981,11 +982,11 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] passing email with valid syntax and correct password will cause the test to fail */ it("test invalid email and wrong password", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1040,11 +1041,11 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] * - User exists */ it("test getUserByEmail when user does not exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1056,48 +1057,13 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] let emailpassword = EmailPasswordRecipe.getInstanceOrThrowError(); - assert((await EmailPassword.getUserByEmail("public", "random@gmail.com")) === undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let signUpUserInfo = JSON.parse(signUpResponse.text).user; - let userInfo = await EmailPassword.getUserByEmail("public", "random@gmail.com"); - - assert(userInfo.email === signUpUserInfo.email); - assert(userInfo.id === signUpUserInfo.id); - }); - - /* - * Test getUserById - * - User does not exist - * - User exists - */ - it("test getUserById when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - let emailpassword = EmailPasswordRecipe.getInstanceOrThrowError(); - - assert((await EmailPassword.getUserById("randomID")) === undefined); + assert( + ( + await STExpress.listUsersByAccountInfo("public", { + email: "random@gmail.com", + }) + ).length === 0 + ); const app = express(); @@ -1110,19 +1076,23 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] assert(signUpResponse.status === 200); let signUpUserInfo = JSON.parse(signUpResponse.text).user; - let userInfo = await EmailPassword.getUserById(signUpUserInfo.id); + let userInfo = ( + await STExpress.listUsersByAccountInfo("public", { + email: "random@gmail.com", + }) + )[0]; - assert(userInfo.email === signUpUserInfo.email); + assert(userInfo.emails[0] === signUpUserInfo.emails[0]); assert(userInfo.id === signUpUserInfo.id); }); it("test the handlePostSignIn function", async function () { - await startST(); + const connectionURI = await startST(); let customUser = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1138,7 +1108,19 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] signInPOST: async (formFields, options) => { let response = await oI.signInPOST(formFields, options); if (response.status === "OK") { - customUser = response.user; + customUser = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete customUser.loginMethods[0].hasSameEmailAs; + delete customUser.loginMethods[0].hasSamePhoneNumberAs; + delete customUser.loginMethods[0].hasSameThirdPartyInfoAs; + delete customUser.toJson; } return response; }, @@ -1205,6 +1187,6 @@ describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js] }) ); assert(customUser !== undefined); - assert.deepStrictEqual(response.user, customUser); + assertJSONEquals(response.user, customUser); }); }); diff --git a/test/emailpassword/signoutFeature.test.js b/test/emailpassword/signoutFeature.test.js index b7ab1f39e..0e063669a 100644 --- a/test/emailpassword/signoutFeature.test.js +++ b/test/emailpassword/signoutFeature.test.js @@ -56,11 +56,11 @@ describe(`signoutFeature: ${printPath("[test/emailpassword/signoutFeature.test.j // Test the default route and it should revoke the session (with clearing the cookies) it("test the default route and it should revoke the session", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -113,11 +113,11 @@ describe(`signoutFeature: ${printPath("[test/emailpassword/signoutFeature.test.j // Disable default route and test that that API returns 404 it("test that disabling default route and calling the API returns 404", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -162,11 +162,11 @@ describe(`signoutFeature: ${printPath("[test/emailpassword/signoutFeature.test.j // Call the API without a session and it should return "OK" it("test that calling the API without a session should return OK", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -201,13 +201,11 @@ describe(`signoutFeature: ${printPath("[test/emailpassword/signoutFeature.test.j //Call the API with an expired access token, refresh, and call the API again to get OK and clear cookies it("test that signout API reutrns try refresh token, refresh session and signout should return OK", async function () { - await setKeyValueInConfig("access_token_validity", 2); - - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 2 } }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/emailpassword/signupFeature.test.js b/test/emailpassword/signupFeature.test.js index b9b583a49..511ff37c6 100644 --- a/test/emailpassword/signupFeature.test.js +++ b/test/emailpassword/signupFeature.test.js @@ -54,11 +54,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] // * check if disable api, the default signup API does not work - you get a 404 it("test that if disable api, the default signup API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -97,11 +97,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] */ it("test signUpAPI works when input is fine", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -123,15 +123,15 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] let userInfo = JSON.parse(response.text).user; assert(userInfo.id !== undefined); - assert(userInfo.email === "random@gmail.com"); + assert(userInfo.emails[0] === "random@gmail.com"); }); it("test signUpAPI throws an error in case of a duplicate email", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -153,7 +153,7 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] let userInfo = JSON.parse(response.text).user; assert(userInfo.id !== undefined); - assert(userInfo.email === "random@gmail.com"); + assert(userInfo.emails[0] === "random@gmail.com"); response = await signUPRequest(app, "random@gmail.com", "validpass123"); assert(response.status === 200); @@ -166,11 +166,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] }); it("test signUpAPI throws an error for email and password with invalid syntax", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -208,11 +208,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] * - formField elements have no id or no value field * */ it("test bad input, not a JSON to /signup API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -248,11 +248,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] }); it("test bad input, no POST body to /signup API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -284,11 +284,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] }); it("test bad input, Input is JSON, but wrong structure to /signup API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -323,11 +323,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] }); it("test bad input, formFields is not an array in /signup API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -364,11 +364,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] }); it("test bad input, formField elements have no id or no value field in /signup API", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -411,10 +411,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] //* Make sure that a successful sign up yields a session it("test that a successful signup yields a session", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -458,11 +458,11 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] //If not provided by the user, it should not result in an error it("test that if not provided by the user, it should not result in an error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -519,17 +519,17 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] assert(response.status === "OK"); assert(response.user.id !== undefined); - assert(response.user.email === "random@gmail.com"); + assert(response.user.emails[0] === "random@gmail.com"); }); //- If provided by the user, and custom fields are there, only those should be sent it("test that if provided by the user, and custom fields are there, only those are sent, using handlePostSignUp", async function () { - await startST(); + const connectionURI = await startST(); let customFormFields = ""; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -610,12 +610,12 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] //If provided by the user, and no custom fields are there, then the formFields param must sbe empty it("test that if provided by the user, and no custom fields are there, then formFields must be empty, using handlePostSignUp", async function () { - await startST(); + const connectionURI = await startST(); let customFormFields = ""; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -693,10 +693,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] * - Pass a non string value in the formFields array and make sure it passes through the signUp API and is sent in the handlePostSignup as that type */ it("test formFields added in config but not in inout to signup, check error about it being missing", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -730,10 +730,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] //- Good test case without optional it("test valid formFields without optional", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -790,15 +790,15 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] assert(response.status === "OK"); assert(response.user.id !== undefined); - assert(response.user.email === "random@gmail.com"); + assert(response.user.emails[0] === "random@gmail.com"); }); //- Bad test case without optional (something is missing, and it's not optional) it("test bad case input to signup without optional", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -860,10 +860,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] //- Good test case with optionals it("test good case input to signup with optional", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -921,15 +921,15 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] assert(response.status === "OK"); assert(response.user.id !== undefined); - assert(response.user.email === "random@gmail.com"); + assert(response.user.emails[0] === "random@gmail.com"); }); //- Input formFields has no email field (and not in config) it("test input formFields has no email field", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -969,10 +969,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] // Input formFields has no password field (and not in config it("test inut formFields has no password field", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1012,10 +1012,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] // Input form field has different number of custom fields than in config form fields) it("test input form field has a different number of custom fields than in config form fields", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1078,10 +1078,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] // Input form field has same number of custom fields as in config form field, but some ids mismatch it("test input form field has the same number of custom fields than in config form fields, but ids mismatch", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1152,10 +1152,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] // Test custom field validation error (one and two custom fields) it("test custom field validation error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1234,10 +1234,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] //Test password field validation error it("test signup password field validation error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1284,10 +1284,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] //Test email field validation error it("test signup email field validation error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1334,10 +1334,10 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] //Make sure that the input email is trimmed it("test that input email is trimmed", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1378,17 +1378,17 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] ); assert(response.status === "OK"); assert(response.user.id !== undefined); - assert(response.user.email === "random@gmail.com"); + assert(response.user.emails[0] === "random@gmail.com"); }); // Pass a non string value in the formFields array and make sure it passes through the signUp API and is sent in the handlePostSignUp as that type it("test that non string value in formFields array and it passes through the signup API and it is sent to the handlePostSignUp", async function () { - await startST(); + const connectionURI = await startST(); let customFormFields = ""; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/emailpassword/updateEmailPass.test.js b/test/emailpassword/updateEmailPass.test.js index dcc6315ed..55c0ac765 100644 --- a/test/emailpassword/updateEmailPass.test.js +++ b/test/emailpassword/updateEmailPass.test.js @@ -37,10 +37,10 @@ describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass. }); it("test updateEmailPass", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -67,7 +67,7 @@ describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass. let res = await signIn("public", "test@gmail.com", "testPass123"); await updateEmailOrPassword({ - userId: res.user.id, + recipeUserId: STExpress.convertToRecipeUserId(res.user.id), email: "test2@gmail.com", password: "testPass", applyPasswordPolicy: false, @@ -79,10 +79,10 @@ describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass. }); it("test updateEmailPass with failing password validation", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -127,7 +127,7 @@ describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass. let res = await signIn("public", "test@gmail.com", "testPass123"); const res2 = await updateEmailOrPassword({ - userId: res.user.id, + userId: STExpress.convertToRecipeUserId(res.user.id), email: "test2@gmail.com", password: "test", }); @@ -137,10 +137,10 @@ describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass. }); it("test updateEmailPass with passing password validation", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -185,7 +185,7 @@ describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass. let res = await signIn("public", "test@gmail.com", "testPass123"); const res2 = await updateEmailOrPassword({ - userId: res.user.id, + recipeUserId: STExpress.convertToRecipeUserId(res.user.id), email: "test2@gmail.com", password: "testPass2", }); @@ -194,10 +194,10 @@ describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass. }); it("test updateEmailPass with failing default password validation", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -224,7 +224,7 @@ describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass. let res = await signIn("public", "test@gmail.com", "testPass123"); const res2 = await updateEmailOrPassword({ - userId: res.user.id, + userId: STExpress.convertToRecipeUserId(res.user.id), email: "test2@gmail.com", password: "1", }); diff --git a/test/emailpassword/users.test.js b/test/emailpassword/users.test.js index 5a3919ff3..af53534c2 100644 --- a/test/emailpassword/users.test.js +++ b/test/emailpassword/users.test.js @@ -37,10 +37,10 @@ describe(`usersTest: ${printPath("[test/emailpassword/users.test.js]")}`, functi }); it("test getUsersOldestFirst", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -69,12 +69,12 @@ describe(`usersTest: ${printPath("[test/emailpassword/users.test.js]")}`, functi users = await getUsersOldestFirst({ tenantId: "public", limit: 1 }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersOldestFirst({ tenantId: "public", limit: 1, paginationToken: users.nextPaginationToken }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test1@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test1@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersOldestFirst({ tenantId: "public", limit: 5, paginationToken: users.nextPaginationToken }); @@ -101,10 +101,10 @@ describe(`usersTest: ${printPath("[test/emailpassword/users.test.js]")}`, functi }); it("test getUsersOldestFirst with search queries", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -137,13 +137,19 @@ describe(`usersTest: ${printPath("[test/emailpassword/users.test.js]")}`, functi users = await getUsersOldestFirst({ tenantId: "public", query: { email: "john" } }); assert.strictEqual(users.users.length, 1); + assert.strictEqual(users.users[0].emails[0], "john@gmail.com"); + assert.strictEqual(users.users[0].phoneNumbers[0], undefined); + assert.strictEqual(users.users[0].thirdParty[0], undefined); + assert.strictEqual(users.users[0].loginMethods[0].email, "john@gmail.com"); + assert.strictEqual(users.users[0].loginMethods[0].phoneNumber, undefined); + assert.strictEqual(users.users[0].loginMethods[0].thirdParty, undefined); }); it("test getUsersNewestFirst", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -172,12 +178,12 @@ describe(`usersTest: ${printPath("[test/emailpassword/users.test.js]")}`, functi users = await getUsersNewestFirst({ tenantId: "public", limit: 1 }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test4@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test4@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersNewestFirst({ tenantId: "public", limit: 1, paginationToken: users.nextPaginationToken }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test3@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test3@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersNewestFirst({ tenantId: "public", limit: 5, paginationToken: users.nextPaginationToken }); @@ -204,10 +210,10 @@ describe(`usersTest: ${printPath("[test/emailpassword/users.test.js]")}`, functi }); it("test getUsersNewestFirst with search queries", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -243,10 +249,10 @@ describe(`usersTest: ${printPath("[test/emailpassword/users.test.js]")}`, functi }); it("test getUserCount", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/framework/awsLambda.test.js b/test/framework/awsLambda.test.js index e186b04bf..4b2293959 100644 --- a/test/framework/awsLambda.test.js +++ b/test/framework/awsLambda.test.js @@ -50,11 +50,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct //check basic usage of session it("test basic usage of sessions for lambda proxy event v1", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -66,7 +66,14 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "public", "userId", {}, {}); + await Session.createNewSession( + awsEvent, + awsEvent, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); return { body: JSON.stringify(""), statusCode: 200, @@ -218,11 +225,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct //check basic usage of session it("test basic usage of sessions for lambda proxy event v2", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -234,7 +241,14 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "public", "userId", {}, {}); + await Session.createNewSession( + awsEvent, + awsEvent, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); return { body: JSON.stringify(""), statusCode: 200, @@ -373,11 +387,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("sending custom response awslambda", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -418,11 +432,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct for (const tokenTransferMethod of ["header", "cookie"]) { describe(`Throwing UNATHORISED w/ auth-mode=${tokenTransferMethod}`, () => { it("should clear all response cookies during refresh", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -452,7 +466,14 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "public", "userId", {}, {}); + await Session.createNewSession( + awsEvent, + awsEvent, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); return { body: JSON.stringify(""), statusCode: 200, @@ -523,11 +544,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("test revoking a session after createNewSession with throwing unauthorised error", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -543,7 +564,14 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "public", "userId", {}, {}); + await Session.createNewSession( + awsEvent, + awsEvent, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); throw new Session.Error({ message: "unauthorised", type: Session.Error.UNAUTHORISED, @@ -586,11 +614,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct } it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -639,11 +667,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -697,11 +725,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("test that search results correct output for 'email: t", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -754,17 +782,17 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct ); let result = await middleware()(event, undefined); - assert(result.statusCode === 200); + assert.strictEqual(result.statusCode, 200); const body = JSON.parse(result.body); - assert(body.users.length === 5); + assert.strictEqual(body.users.length, 5); }); it("test that search results correct output for multiple search items", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -823,11 +851,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("test that search results correct output for 'email: iresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -885,11 +913,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("test that search results correct output for 'phone: +1", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -950,11 +978,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("test that search results correct output for 'phone: 1(", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -1015,11 +1043,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -1121,11 +1149,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, funct }); it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", diff --git a/test/framework/awsLambda.withTenantId.test.js b/test/framework/awsLambda.withTenantId.test.js index 28fb2a697..c38d174ff 100644 --- a/test/framework/awsLambda.withTenantId.test.js +++ b/test/framework/awsLambda.withTenantId.test.js @@ -50,11 +50,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j //check basic usage of session it("test basic usage of sessions for lambda proxy event v1", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -66,7 +66,14 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "public", "userId", {}, {}); + await Session.createNewSession( + awsEvent, + awsEvent, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); return { body: JSON.stringify(""), statusCode: 200, @@ -218,11 +225,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j //check basic usage of session it("test basic usage of sessions for lambda proxy event v2", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -234,7 +241,14 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "public", "userId", {}, {}); + await Session.createNewSession( + awsEvent, + awsEvent, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); return { body: JSON.stringify(""), statusCode: 200, @@ -380,11 +394,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("sending custom response awslambda", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -425,11 +439,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j for (const tokenTransferMethod of ["header", "cookie"]) { describe(`Throwing UNATHORISED w/ auth-mode=${tokenTransferMethod}`, () => { it("should clear all response cookies during refresh", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -459,7 +473,14 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "public", "userId", {}, {}); + await Session.createNewSession( + awsEvent, + awsEvent, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); return { body: JSON.stringify(""), statusCode: 200, @@ -530,11 +551,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test revoking a session after createNewSession with throwing unauthorised error", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -550,7 +571,14 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "public", "userId", {}, {}); + await Session.createNewSession( + awsEvent, + awsEvent, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); throw new Session.Error({ message: "unauthorised", type: Session.Error.UNAUTHORISED, @@ -593,11 +621,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j } it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -646,11 +674,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -704,11 +732,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test that search results correct output for 'email: t", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -767,11 +795,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test that search results correct output for multiple search items", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -830,11 +858,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test that search results correct output for 'email: iresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -892,11 +920,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test that search results correct output for 'phone: +1", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -957,11 +985,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test that search results correct output for 'phone: 1(", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -1022,11 +1050,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -1128,11 +1156,11 @@ describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.withTenantId.test.j }); it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "awsLambda", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", diff --git a/test/framework/crossFramework.testgen.js b/test/framework/crossFramework.testgen.js index 0a6bb3899..451476b04 100644 --- a/test/framework/crossFramework.testgen.js +++ b/test/framework/crossFramework.testgen.js @@ -74,9 +74,14 @@ module.exports.addCrossFrameworkTests = (getTestCases, { allTokenTransferMethods getTestCases( async ({ stConfig, routes }) => { - await startST(); + const connectionURI = await startST(); - SuperTokens.init(stConfig); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + ...stConfig, + }); app = express(); @@ -153,10 +158,13 @@ module.exports.addCrossFrameworkTests = (getTestCases, { allTokenTransferMethods getTestCases( async ({ stConfig, routes }) => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", + supertokens: { + connectionURI, + }, ...stConfig, }); @@ -229,10 +237,13 @@ module.exports.addCrossFrameworkTests = (getTestCases, { allTokenTransferMethods getTestCases( async ({ stConfig, routes }) => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", + supertokens: { + connectionURI, + }, ...stConfig, }); @@ -303,10 +314,13 @@ module.exports.addCrossFrameworkTests = (getTestCases, { allTokenTransferMethods getTestCases( async ({ stConfig, routes }) => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", + supertokens: { + connectionURI, + }, ...stConfig, }); @@ -387,10 +401,13 @@ module.exports.addCrossFrameworkTests = (getTestCases, { allTokenTransferMethods } app = require("./loopback-server/index.js"); - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", + supertokens: { + connectionURI, + }, ...stConfig, }); diff --git a/test/framework/crossframework/unauthorised.test.js b/test/framework/crossframework/unauthorised.test.js index 0a815eaf6..6710a56d2 100644 --- a/test/framework/crossframework/unauthorised.test.js +++ b/test/framework/crossframework/unauthorised.test.js @@ -2,6 +2,7 @@ const { addCrossFrameworkTests } = require("../crossFramework.testgen"); let Session = require("../../../recipe/session"); const { extractInfoFromResponse } = require("../../utils"); let assert = require("assert"); +const SuperTokens = require("../../.."); addCrossFrameworkTests( (setup, callServer, tokenTransferMethod) => { @@ -9,9 +10,6 @@ addCrossFrameworkTests( it("should clear all response cookies during refresh", async () => { await setup({ stConfig: { - supertokens: { - connectionURI: "http://localhost:8080", - }, appInfo: { apiDomain: "http://api.supertokens.io", appName: "SuperTokens", @@ -44,7 +42,14 @@ addCrossFrameworkTests( path: "/create", method: "post", handler: async (req, res, next) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("id1"), + {}, + {} + ); res.setStatusCode(200); res.sendJSONResponse(""); return res.response; @@ -105,9 +110,6 @@ addCrossFrameworkTests( it("test revoking a session after createNewSession with throwing unauthorised error", async function () { await setup({ stConfig: { - supertokens: { - connectionURI: "http://localhost:8080", - }, appInfo: { apiDomain: "http://api.supertokens.io", appName: "SuperTokens", @@ -125,7 +127,14 @@ addCrossFrameworkTests( path: "/create-throw", method: "post", handler: async (req, res, next) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("id1"), + {}, + {} + ); next( new Session.Error({ message: "unauthorised", @@ -173,9 +182,6 @@ addCrossFrameworkTests( it("should return a 401 for invalid tokens", async function () { await setup({ stConfig: { - supertokens: { - connectionURI: "http://localhost:8080", - }, appInfo: { apiDomain: "http://api.supertokens.io", appName: "SuperTokens", diff --git a/test/framework/fastify.test.js b/test/framework/fastify.test.js index 0d03858ea..cddf71be9 100644 --- a/test/framework/fastify.test.js +++ b/test/framework/fastify.test.js @@ -57,11 +57,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( // check if disabling api, the default refresh API does not work - you get a 404 it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -85,7 +85,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -111,11 +111,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -139,7 +139,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -160,11 +160,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( //- check for token theft detection it("token theft detection", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -174,7 +174,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( recipeList: [ Session.init({ errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { response.sendJSONResponse({ success: true, }); @@ -197,7 +197,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( this.server.setErrorHandler(FastifyFramework.errorHandler()); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -262,11 +262,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( // - check for token theft detection it("token theft detection with auto refresh middleware", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -279,7 +279,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( this.server.setErrorHandler(FastifyFramework.errorHandler()); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -338,11 +338,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( // - check for token theft detection without error handler it("token theft detection with auto refresh middleware without error handler", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -353,7 +353,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -412,11 +412,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( // - check if session verify middleware responds with a nice error even without the global error handler it("test session verify middleware without error handler added", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -449,11 +449,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( // check basic usage of session it("test basic usage of sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -464,7 +464,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -562,11 +562,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test signout API works", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -577,7 +577,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -608,11 +608,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( // check basic usage of session it("test basic usage of sessions with auto refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -624,7 +624,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -723,11 +723,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( // check session verify for with / without anti-csrf present it("test session verify with anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -738,7 +738,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.send("").code(200); }); @@ -784,11 +784,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( // check session verify for with / without anti-csrf present it("test session verify without anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -801,7 +801,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( await this.server.register(FastifyFramework.plugin); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.send("").code(200); }); @@ -850,11 +850,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( //check revoking session(s)** it("test revoking sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -865,11 +865,18 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); this.server.post("/usercreate", async (req, res) => { - await Session.createNewSession(req, res, "public", "someUniqueUserId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("someUniqueUserId"), + {}, + {} + ); return res.send("").code(200); }); this.server.post("/session/revoke", async (req, res) => { @@ -939,11 +946,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( //check manipulating session data it("test manipulating session data", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -954,7 +961,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -1066,11 +1073,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( //check manipulating jwt payload it("test manipulating jwt payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1081,7 +1088,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "user1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("user1"), {}, {}); return res.send("").code(200); }); @@ -1142,7 +1149,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //call the updateAccessTokenPayload api to add jwt payload let updatedResponse = extractInfoFromResponse( @@ -1161,7 +1168,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); //call the getAccessTokenPayload api to get jwt payload let response2 = await this.server.inject({ @@ -1192,7 +1199,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); // change the value of the inserted jwt payload let updatedResponse2 = extractInfoFromResponse( @@ -1210,7 +1217,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //retrieve the changed jwt payload let response3 = await this.server.inject({ @@ -1235,6 +1242,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( "sub", "iss", "tId", + "rsub", ]) ); //invalid session handle when updating the jwt payload @@ -1250,11 +1258,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("sending custom response fastify", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1296,11 +1304,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("generating email verification token without payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1351,11 +1359,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test same cookie is not getting set multiple times", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1368,7 +1376,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( await this.server.register(FastifyFramework.plugin); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.send("").code(200); }); @@ -1383,11 +1391,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1430,11 +1438,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1481,11 +1489,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that search results correct output for 'email: t'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1534,11 +1542,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that search results correct output for multiple search terms", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1587,11 +1595,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that search results correct output for 'email: iresh'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1640,11 +1648,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that search results correct output for 'phone: +1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1696,11 +1704,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that search results correct output for 'phone: 1('", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1752,11 +1760,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1849,11 +1857,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function ( }); it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/framework/fastify.withTenantId.test.js b/test/framework/fastify.withTenantId.test.js index 3f6c23de1..3315829f0 100644 --- a/test/framework/fastify.withTenantId.test.js +++ b/test/framework/fastify.withTenantId.test.js @@ -57,11 +57,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} // check if disabling api, the default refresh API does not work - you get a 404 it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -85,7 +85,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -111,11 +111,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -139,7 +139,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -160,11 +160,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} //- check for token theft detection it("token theft detection", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -174,7 +174,8 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} recipeList: [ Session.init({ errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { + debugger; response.sendJSONResponse({ success: true, }); @@ -197,7 +198,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} this.server.setErrorHandler(FastifyFramework.errorHandler()); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -262,11 +263,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} // - check for token theft detection it("token theft detection with auto refresh middleware", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -279,7 +280,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} this.server.setErrorHandler(FastifyFramework.errorHandler()); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -338,11 +339,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} // - check for token theft detection without error handler it("token theft detection with auto refresh middleware without error handler", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -353,7 +354,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -412,11 +413,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} // - check if session verify middleware responds with a nice error even without the global error handler it("test session verify middleware without error handler added", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -449,11 +450,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} // check basic usage of session it("test basic usage of sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -464,7 +465,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -562,11 +563,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test signout API works", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -577,7 +578,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -608,11 +609,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} // check basic usage of session it("test basic usage of sessions with auto refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -624,7 +625,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -723,11 +724,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} // check session verify for with / without anti-csrf present it("test session verify with anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -738,7 +739,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.send("").code(200); }); @@ -784,11 +785,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} // check session verify for with / without anti-csrf present it("test session verify without anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -801,7 +802,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} await this.server.register(FastifyFramework.plugin); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.send("").code(200); }); @@ -850,11 +851,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} //check revoking session(s)** it("test revoking sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -865,11 +866,18 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); this.server.post("/usercreate", async (req, res) => { - await Session.createNewSession(req, res, "public", "someUniqueUserId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("someUniqueUserId"), + {}, + {} + ); return res.send("").code(200); }); this.server.post("/session/revoke", async (req, res) => { @@ -939,11 +947,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} //check manipulating session data it("test manipulating session data", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -954,7 +962,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.send("").code(200); }); @@ -1066,11 +1074,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} //check manipulating jwt payload it("test manipulating jwt payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1081,7 +1089,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "user1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("user1"), {}, {}); return res.send("").code(200); }); @@ -1142,7 +1150,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //call the updateAccessTokenPayload api to add jwt payload let updatedResponse = extractInfoFromResponse( @@ -1161,7 +1169,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); //call the getAccessTokenPayload api to get jwt payload let response2 = await this.server.inject({ @@ -1192,7 +1200,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); // change the value of the inserted jwt payload let updatedResponse2 = extractInfoFromResponse( @@ -1210,7 +1218,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //retrieve the changed jwt payload let response3 = await this.server.inject({ @@ -1235,6 +1243,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} "sub", "iss", "tId", + "rsub", ]) ); //invalid session handle when updating the jwt payload @@ -1250,11 +1259,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("sending custom response fastify", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1296,11 +1305,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("generating email verification token without payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1351,11 +1360,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test same cookie is not getting set multiple times", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1368,7 +1377,7 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} await this.server.register(FastifyFramework.plugin); this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.send("").code(200); }); @@ -1383,11 +1392,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1430,11 +1439,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1481,11 +1490,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that search results correct output for 'email: t'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1534,11 +1543,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that search results correct output for multiple search terms", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1587,11 +1596,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that search results correct output for 'email: iresh'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1640,11 +1649,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that search results correct output for 'phone: +1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1696,11 +1705,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that search results correct output for 'phone: 1('", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1752,11 +1761,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1849,11 +1858,11 @@ describe(`Fastify: ${printPath("[test/framework/fastify.withTenantId.test.js]")} }); it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "fastify", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/framework/hapi.test.js b/test/framework/hapi.test.js index f23c54ab1..8a9bcb5b0 100644 --- a/test/framework/hapi.test.js +++ b/test/framework/hapi.test.js @@ -52,11 +52,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { // check if disabling api, the default refresh API does not work - you get a 404 it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -83,7 +83,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { method: "post", path: "/create", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -112,11 +112,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -143,7 +143,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -167,11 +167,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { //- check for token theft detection it("token theft detection", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -181,7 +181,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { recipeList: [ Session.init({ errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { response.sendJSONResponse({ success: true, }); @@ -205,7 +205,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -281,11 +281,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { //- check for token theft detection it("token theft detection with auto refresh middleware", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -299,7 +299,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -366,11 +366,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { //check basic usage of session it("test basic usage of sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -384,7 +384,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -493,11 +493,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test signout API works", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -510,7 +510,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -544,11 +544,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { //check basic usage of session it("test basic usage of sessions with auto refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -563,7 +563,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -672,11 +672,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { // check session verify for with / without anti-csrf present it("test session verify with anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -690,7 +690,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.response("").code(200); }, }); @@ -747,11 +747,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { // check session verify for with / without anti-csrf present it("test session verify without anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -767,7 +767,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.response("").code(200); }, }); @@ -828,11 +828,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { //check revoking session(s)** it("test revoking sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -845,7 +845,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -853,7 +853,14 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/usercreate", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "someUniqueUserId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("someUniqueUserId"), + {}, + {} + ); return res.response("").code(200); }, }); @@ -938,11 +945,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { //check manipulating session data it("test manipulating session data", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -956,7 +963,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -1080,11 +1087,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { //check manipulating jwt payload it("test manipulating jwt payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1098,7 +1105,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "user1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("user1"), {}, {}); return res.response("").code(200); }, }); @@ -1177,7 +1184,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //call the updateAccessTokenPayload api to add jwt payload let updatedResponse = extractInfoFromResponse( @@ -1196,7 +1203,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); //call the getAccessTokenPayload api to get jwt payload let response2 = await this.server.inject({ @@ -1227,7 +1234,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); // change the value of the inserted jwt payload let updatedResponse2 = extractInfoFromResponse( @@ -1245,7 +1252,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //retrieve the changed jwt payload let response3 = await this.server.inject({ @@ -1270,6 +1277,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { "sub", "iss", "tId", + "rsub", ]) ); @@ -1286,11 +1294,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("sending custom response hapi", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1332,11 +1340,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1378,11 +1386,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test verifySession/getSession without accessToken", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1432,11 +1440,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1487,11 +1495,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that search results correct output for 'email: t'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1542,11 +1550,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that search results correct output for 'email: iresh'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1596,11 +1604,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { assert(resp.result.users.length === 0); }); it("test that search results correct output for multiple search items", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1651,11 +1659,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that search results correct output for 'phone: +1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1709,11 +1717,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that search results correct output for 'phone: 1('", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1767,11 +1775,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1870,11 +1878,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/framework/hapi.withTenantId.test.js b/test/framework/hapi.withTenantId.test.js index a09553c6f..de398888e 100644 --- a/test/framework/hapi.withTenantId.test.js +++ b/test/framework/hapi.withTenantId.test.js @@ -52,11 +52,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun // check if disabling api, the default refresh API does not work - you get a 404 it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -83,7 +83,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun method: "post", path: "/create", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -112,11 +112,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -143,7 +143,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -167,11 +167,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun //- check for token theft detection it("token theft detection", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -181,7 +181,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun recipeList: [ Session.init({ errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { response.sendJSONResponse({ success: true, }); @@ -205,7 +205,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -281,11 +281,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun //- check for token theft detection it("token theft detection with auto refresh middleware", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -299,7 +299,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -366,11 +366,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun //check basic usage of session it("test basic usage of sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -384,7 +384,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -493,11 +493,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test signout API works", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -510,7 +510,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -544,11 +544,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun //check basic usage of session it("test basic usage of sessions with auto refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -563,7 +563,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -672,11 +672,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun // check session verify for with / without anti-csrf present it("test session verify with anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -690,7 +690,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.response("").code(200); }, }); @@ -747,11 +747,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun // check session verify for with / without anti-csrf present it("test session verify without anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -767,7 +767,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); return res.response("").code(200); }, }); @@ -828,11 +828,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun //check revoking session(s)** it("test revoking sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -845,7 +845,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -853,7 +853,14 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/usercreate", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "someUniqueUserId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("someUniqueUserId"), + {}, + {} + ); return res.response("").code(200); }, }); @@ -938,11 +945,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun //check manipulating session data it("test manipulating session data", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -956,7 +963,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); return res.response("").code(200); }, }); @@ -1080,11 +1087,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun //check manipulating jwt payload it("test manipulating jwt payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1098,7 +1105,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun path: "/create", method: "post", handler: async (req, res) => { - await Session.createNewSession(req, res, "public", "user1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("user1"), {}, {}); return res.response("").code(200); }, }); @@ -1177,7 +1184,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //call the updateAccessTokenPayload api to add jwt payload let updatedResponse = extractInfoFromResponse( @@ -1196,7 +1203,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); //call the getAccessTokenPayload api to get jwt payload let response2 = await this.server.inject({ @@ -1227,7 +1234,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); // change the value of the inserted jwt payload let updatedResponse2 = extractInfoFromResponse( @@ -1245,7 +1252,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //retrieve the changed jwt payload let response3 = await this.server.inject({ @@ -1270,6 +1277,7 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun "sub", "iss", "tId", + "rsub", ]) ); @@ -1286,11 +1294,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("sending custom response hapi", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1332,11 +1340,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1378,11 +1386,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test verifySession/getSession without accessToken", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1432,11 +1440,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1487,11 +1495,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that search results correct output for 'email: t'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1542,11 +1550,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that search results correct output for 'email: iresh'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1596,11 +1604,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun assert(resp.result.users.length === 0); }); it("test that search results correct output for multiple search items", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1651,11 +1659,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that search results correct output for 'phone: +1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1709,11 +1717,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that search results correct output for 'phone: 1('", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1767,11 +1775,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1870,11 +1878,11 @@ describe(`Hapi: ${printPath("[test/framework/hapi.withTenantId.test.js]")}`, fun }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "hapi", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/framework/koa.test.js b/test/framework/koa.test.js index 9045c0b27..c457c4815 100644 --- a/test/framework/koa.test.js +++ b/test/framework/koa.test.js @@ -51,11 +51,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { // check if disabling api, the default refresh API does not work - you get a 404 it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -83,7 +83,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -123,12 +123,12 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -156,7 +156,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -193,11 +193,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { //- check for token theft detection it("koa token theft detection", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -207,7 +207,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { recipeList: [ Session.init({ errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { response.sendJSONResponse({ success: true, }); @@ -231,7 +231,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -316,11 +316,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { //- check for token theft detection it("koa token theft detection with auto refresh middleware", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -335,7 +335,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { app.use(KoaFramework.middleware()); router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -413,11 +413,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { //check basic usage of session it("test basic usage of express sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -432,7 +432,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -568,11 +568,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test signout API works", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -586,7 +586,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -631,12 +631,12 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { //check basic usage of session it("test basic usage of express sessions with auto refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -652,7 +652,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -785,11 +785,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { //check session verify for with / without anti-csrf present it("test express session verify with anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -803,7 +803,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "id1", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); ctx.body = ""; }); @@ -868,11 +868,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { // check session verify for with / without anti-csrf present it("test session verify without anti-csrf present express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -887,7 +887,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "id1", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); ctx.body = ""; }); @@ -955,11 +955,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { //check revoking session(s)** it("test revoking express sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -972,11 +972,18 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); router.post("/usercreate", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "someUniqueUserId", {}, {}); + await Session.createNewSession( + ctx, + ctx, + "public", + SuperTokens.convertToRecipeUserId("someUniqueUserId"), + {}, + {} + ); ctx.body = ""; }); router.post("/session/revoke", async (ctx, _) => { @@ -1091,11 +1098,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { //check manipulating session data it("test manipulating session data with express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -1109,7 +1116,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); router.post("/updateSessionData", async (ctx, _) => { @@ -1241,11 +1248,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { //check manipulating jwt payload it("test manipulating jwt payload with express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -1258,7 +1265,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "user1", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId("user1"), {}, {}); ctx.body = ""; }); router.post("/updateAccessTokenPayload", async (ctx, _) => { @@ -1311,7 +1318,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //call the updateAccessTokenPayload api to add jwt payload let updatedResponse = extractInfoFromResponse( @@ -1336,7 +1343,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); //call the getAccessTokenPayload api to get jwt payload let response2 = await new Promise((resolve) => @@ -1379,7 +1386,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); // change the value of the inserted jwt payload let updatedResponse2 = extractInfoFromResponse( @@ -1403,7 +1410,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //retrieve the changed jwt payload response2 = await new Promise((resolve) => @@ -1434,6 +1441,7 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { "sub", "iss", "tId", + "rsub", ]) ); //invalid session handle when updating the jwt payload @@ -1455,11 +1463,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("sending custom response koa", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1507,11 +1515,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1562,11 +1570,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1620,11 +1628,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that search results correct output for 'email: t'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1684,11 +1692,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that search results correct output for multiple search items", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1748,11 +1756,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that search results correct output for 'email: iresh'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1812,11 +1820,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that search results correct output for 'phone: +1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1879,11 +1887,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that search results correct output for 'phone: 1('", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1946,11 +1954,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2054,11 +2062,11 @@ describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { }); it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/framework/koa.withTenantId.test.js b/test/framework/koa.withTenantId.test.js index 0090a9917..bcddd77d6 100644 --- a/test/framework/koa.withTenantId.test.js +++ b/test/framework/koa.withTenantId.test.js @@ -51,11 +51,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct // check if disabling api, the default refresh API does not work - you get a 404 it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -83,7 +83,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -123,12 +123,12 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -156,7 +156,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -193,11 +193,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct //- check for token theft detection it("koa token theft detection", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -207,7 +207,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct recipeList: [ Session.init({ errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { response.sendJSONResponse({ success: true, }); @@ -231,7 +231,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -316,11 +316,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct //- check for token theft detection it("koa token theft detection with auto refresh middleware", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -335,7 +335,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct app.use(KoaFramework.middleware()); router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -413,11 +413,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct //check basic usage of session it("test basic usage of express sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -432,7 +432,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -568,11 +568,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test signout API works", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -586,7 +586,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -631,12 +631,12 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct //check basic usage of session it("test basic usage of express sessions with auto refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -652,7 +652,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); @@ -785,11 +785,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct //check session verify for with / without anti-csrf present it("test express session verify with anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -803,7 +803,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "id1", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); ctx.body = ""; }); @@ -868,11 +868,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct // check session verify for with / without anti-csrf present it("test session verify without anti-csrf present express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -887,7 +887,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct app.use(KoaFramework.middleware()); router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "public", "id1", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); ctx.body = ""; }); @@ -955,11 +955,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct //check revoking session(s)** it("test revoking express sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -972,11 +972,18 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); router.post("/usercreate", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "someUniqueUserId", {}, {}); + await Session.createNewSession( + ctx, + ctx, + "public", + SuperTokens.convertToRecipeUserId("someUniqueUserId"), + {}, + {} + ); ctx.body = ""; }); router.post("/session/revoke", async (ctx, _) => { @@ -1091,11 +1098,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct //check manipulating session data it("test manipulating session data with express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -1109,7 +1116,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); ctx.body = ""; }); router.post("/updateSessionData", async (ctx, _) => { @@ -1241,11 +1248,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct //check manipulating jwt payload it("test manipulating jwt payload with express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "http://api.supertokens.io", @@ -1258,7 +1265,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct const router = new Router(); app.use(KoaFramework.middleware()); router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "public", "user1", {}, {}); + await Session.createNewSession(ctx, ctx, "public", SuperTokens.convertToRecipeUserId("user1"), {}, {}); ctx.body = ""; }); router.post("/updateAccessTokenPayload", async (ctx, _) => { @@ -1311,7 +1318,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //call the updateAccessTokenPayload api to add jwt payload let updatedResponse = extractInfoFromResponse( @@ -1336,7 +1343,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); //call the getAccessTokenPayload api to get jwt payload let response2 = await new Promise((resolve) => @@ -1379,7 +1386,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); // change the value of the inserted jwt payload let updatedResponse2 = extractInfoFromResponse( @@ -1403,7 +1410,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //retrieve the changed jwt payload response2 = await new Promise((resolve) => @@ -1434,6 +1441,7 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct "sub", "iss", "tId", + "rsub", ]) ); //invalid session handle when updating the jwt payload @@ -1455,11 +1463,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("sending custom response koa", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1507,11 +1515,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1562,11 +1570,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1620,11 +1628,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that search results correct output for 'email: t'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1684,11 +1692,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that search results correct output for multiple search items", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1748,11 +1756,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that search results correct output for 'email: iresh'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1812,11 +1820,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that search results correct output for 'phone: +1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1879,11 +1887,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that search results correct output for 'phone: 1('", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1946,11 +1954,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2054,11 +2062,11 @@ describe(`Koa: ${printPath("[test/framework/koa.withTenantId.test.js]")}`, funct }); it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "koa", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/framework/loopback-server/index.js b/test/framework/loopback-server/index.js index e2b2e4ad2..6b0761b28 100644 --- a/test/framework/loopback-server/index.js +++ b/test/framework/loopback-server/index.js @@ -30,12 +30,20 @@ const rest_1 = require("@loopback/rest"); const loopback_1 = require("../../../framework/loopback"); const loopback_2 = require("../../../recipe/session/framework/loopback"); const session_1 = __importDefault(require("../../../recipe/session")); +const supertokens_1 = __importDefault(require("../../..")); let Create = class Create { constructor(ctx) { this.ctx = ctx; } async handler() { - await session_1.default.createNewSession(this.ctx, this.ctx, "public", "userId", {}, {}); + await session_1.default.createNewSession( + this.ctx, + this.ctx, + "public", + supertokens_1.convertToRecipeUserId("userId"), + {}, + {} + ); return {}; } }; @@ -46,7 +54,14 @@ let CreateThrowing = class CreateThrowing { this.ctx = ctx; } async handler() { - await session_1.default.createNewSession(this.ctx, this.ctx, "public", "userId", {}, {}); + await session_1.default.createNewSession( + this.ctx, + this.ctx, + "public", + supertokens_1.convertToRecipeUserId("userId"), + {}, + {} + ); throw new session_1.default.Error({ message: "unauthorised", type: session_1.default.Error.UNAUTHORISED, diff --git a/test/framework/loopback-server/index.ts b/test/framework/loopback-server/index.ts index e8c327667..01464483d 100644 --- a/test/framework/loopback-server/index.ts +++ b/test/framework/loopback-server/index.ts @@ -3,13 +3,21 @@ import { post, response, RestApplication, RestBindings, MiddlewareContext } from import { middleware } from "../../../framework/loopback"; import { verifySession } from "../../../recipe/session/framework/loopback"; import Session from "../../../recipe/session"; +import SuperTokens from "../../.."; class Create { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/create") @response(200) async handler() { - await Session.createNewSession(this.ctx, this.ctx, "public", "userId", {}, {}); + await Session.createNewSession( + this.ctx, + this.ctx, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); return {}; } } @@ -19,7 +27,14 @@ class CreateThrowing { @post("/create-throw") @response(200) async handler() { - await Session.createNewSession(this.ctx, this.ctx, "public", "userId", {}, {}); + await Session.createNewSession( + this.ctx, + this.ctx, + "public", + SuperTokens.convertToRecipeUserId("userId"), + {}, + {} + ); throw new Session.Error({ message: "unauthorised", type: Session.Error.UNAUTHORISED, diff --git a/test/framework/loopback.test.js b/test/framework/loopback.test.js index 940011028..1da3e2c04 100644 --- a/test/framework/loopback.test.js +++ b/test/framework/loopback.test.js @@ -49,11 +49,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function //check basic usage of session it("test basic usage of sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -192,11 +192,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("sending custom response", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -237,11 +237,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -286,11 +286,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -338,11 +338,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that search results correct output for 'email: t'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -396,11 +396,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that search results correct output for multiple search items", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -455,11 +455,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that search results correct output for 'email: iresh'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -514,11 +514,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that search results correct output for 'phone: +1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -576,11 +576,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that search results correct output for 'phone: 1('", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -638,11 +638,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that search results correct output for 'provider: google', phone: 1", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -746,11 +746,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/framework/loopback.withTenantId.test.js b/test/framework/loopback.withTenantId.test.js index 92b8ba7fb..997fc66c0 100644 --- a/test/framework/loopback.withTenantId.test.js +++ b/test/framework/loopback.withTenantId.test.js @@ -49,11 +49,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" //check basic usage of session it("test basic usage of sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -192,11 +192,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("sending custom response", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -237,11 +237,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -286,11 +286,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that tags request respond with correct tags", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -338,11 +338,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that search results correct output for 'email: t'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -396,11 +396,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that search results correct output for multiple search items", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -455,11 +455,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that search results correct output for 'email: iresh'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -514,11 +514,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that search results correct output for 'phone: +1'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -576,11 +576,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that search results correct output for 'phone: 1('", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -638,11 +638,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that search results correct output for 'provider: google', phone: 1", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -746,11 +746,11 @@ describe(`Loopback: ${printPath("[test/framework/loopback.withTenantId.test.js]" }); it("test that search results correct output for 'provider: google'", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ framework: "loopback", supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/frontendIntegration/index.js b/test/frontendIntegration/index.js index d9362a7b0..e10de3969 100644 --- a/test/frontendIntegration/index.js +++ b/test/frontendIntegration/index.js @@ -261,7 +261,7 @@ app.post("/reinitialiseBackendConfig", async (req, res) => { app.post("/login", async (req, res) => { let userId = req.body.userId; - let session = await Session.createNewSession(req, res, "public", userId); + let session = await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(userId)); res.send(session.getUserId()); }); diff --git a/test/import.test.js b/test/import.test.js index d423be472..f1af19df0 100644 --- a/test/import.test.js +++ b/test/import.test.js @@ -36,11 +36,13 @@ describe(`importTests: ${printPath("[test/import.test.js]")}`, function () { const relativeFilePath = fileName.replace(process.cwd(), ""); writeFileSync(testFilePath, `require(".${relativeFilePath}")`); + // This will throw an error if the command fails try { - // This will throw an error if the command fails execSync(`node ${resolve(process.cwd(), `./${testFileName}`)}`); } catch (err) { - console.log(`Testing file: ${relativeFilePath}`); + console.log(); + console.log("failed for ", relativeFilePath); + console.log(); throw err; } }); diff --git a/test/jwt/config.test.js b/test/jwt/config.test.js index c07ffdbdc..8d3194221 100644 --- a/test/jwt/config.test.js +++ b/test/jwt/config.test.js @@ -20,10 +20,10 @@ describe(`configTest: ${printPath("[test/jwt/config.test.js]")}`, function () { }); it("Test that the default config sets values correctly for JWT recipe", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -45,10 +45,10 @@ describe(`configTest: ${printPath("[test/jwt/config.test.js]")}`, function () { }); it("Test that the config sets values correctly for JWT recipe when jwt validity is set", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/jwt/createJWTFeature.test.js b/test/jwt/createJWTFeature.test.js index 318013626..96024c5d1 100644 --- a/test/jwt/createJWTFeature.test.js +++ b/test/jwt/createJWTFeature.test.js @@ -21,10 +21,10 @@ describe(`createJWTFeature: ${printPath("[test/jwt/createJWTFeature.test.js]")}` }); it("Test that sending 0 validity throws an error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -50,10 +50,10 @@ describe(`createJWTFeature: ${printPath("[test/jwt/createJWTFeature.test.js]")}` }); it("Test that sending a invalid json throws an error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -82,10 +82,10 @@ describe(`createJWTFeature: ${printPath("[test/jwt/createJWTFeature.test.js]")}` }); it("Test that returned JWT uses 100 years for expiry for default config", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -117,10 +117,10 @@ describe(`createJWTFeature: ${printPath("[test/jwt/createJWTFeature.test.js]")}` }); it("Test that jwt validity is same as validity set in config", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -156,10 +156,10 @@ describe(`createJWTFeature: ${printPath("[test/jwt/createJWTFeature.test.js]")}` }); it("Test that jwt validity is same as validity passed in createJWT function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/jwt/getJWKS.test.js b/test/jwt/getJWKS.test.js index 97d74dd27..f46b207a2 100644 --- a/test/jwt/getJWKS.test.js +++ b/test/jwt/getJWKS.test.js @@ -23,10 +23,10 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { }); it("Test that default getJWKS api does not work when disabled", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -76,10 +76,10 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { }); it("Test that default getJWKS works fine", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -121,10 +121,10 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { }); it("Test that we can override the Cache-Control header through the function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -181,10 +181,10 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { }); it("Test that we can remove the Cache-Control header through the function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -241,10 +241,10 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { }); it("Test that we can override the Cache-Control header through the api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/jwt/override.test.js b/test/jwt/override.test.js index 1c00f3c31..d9176d604 100644 --- a/test/jwt/override.test.js +++ b/test/jwt/override.test.js @@ -23,14 +23,14 @@ describe(`overrideTest: ${printPath("[test/jwt/override.test.js]")}`, function ( }); it("Test overriding functions", async function () { - await startST(); + const connectionURI = await startST(); let jwtCreated = undefined; let jwksKeys = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -120,13 +120,13 @@ describe(`overrideTest: ${printPath("[test/jwt/override.test.js]")}`, function ( }); it("Test overriding APIs", async function () { - await startST(); + const connectionURI = await startST(); let jwksKeys = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/middleware.test.js b/test/middleware.test.js index 98774c868..04f297eb7 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -51,10 +51,10 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { // check that disabling default API actually disables it (for session) it("test disabling default API actually disables it", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -97,10 +97,10 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { }); it("test session verify middleware", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -111,7 +111,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { Session.init({ getTokenTransferMethod: () => "cookie", errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { + onTokenTheftDetected: (sessionHandle, userId, recipeUserId, req, res) => { res.setStatusCode(403); return res.sendJSONResponse({ message: "token theft detected", @@ -124,7 +124,14 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { }); const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "testing-userId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + {}, + {} + ); res.status(200).json({ message: true }); }); @@ -272,7 +279,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { assert(r2Optional === false); let res2 = extractInfoFromResponse( - await new Promise((resolve) => + await new Promise((resolve, reject) => request(app) .post("/auth/session/refresh") .expect(200) @@ -280,7 +287,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { .set("anti-csrf", res1.antiCsrf) .end((err, res) => { if (err) { - resolve(undefined); + reject(err); } else { resolve(res); } @@ -374,11 +381,10 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { }); it("test session verify middleware with auto refresh", async function () { - await setKeyValueInConfig(2); - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -390,7 +396,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN", errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { + onTokenTheftDetected: (sessionHandle, userId, recipeUserId, req, res) => { res.setStatusCode(403); return res.sendJSONResponse({ message: "token theft detected", @@ -406,7 +412,14 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "testing-userId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + {}, + {} + ); res.status(200).json({ message: true }); }); @@ -657,11 +670,10 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { }); it("test session verify middleware with driver config", async function () { - await setKeyValueInConfig(2); - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -676,7 +688,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { cookieSecure: true, cookieSameSite: "strict", errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { + onTokenTheftDetected: (sessionHandle, userId, recipeUserId, req, res) => { res.setStatusCode(403); return res.sendJSONResponse({ message: "token theft detected", @@ -690,7 +702,14 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "testing-userId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + {}, + {} + ); res.status(200).json({ message: true }); }); @@ -945,11 +964,11 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { }); it("test session verify middleware with driver config with auto refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -964,7 +983,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { cookieSecure: true, cookieSameSite: "strict", errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { + onTokenTheftDetected: (sessionHandle, userId, recipeUserId, req, res) => { res.setStatusCode(403); return res.sendJSONResponse({ message: "token theft detected", @@ -981,7 +1000,14 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "testing-userId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + {}, + {} + ); res.status(200).json({ message: true }); }); @@ -1238,12 +1264,11 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { // https://github.com/supertokens/supertokens-node/pull/108 // An expired access token is used and we see that try refresh token error is thrown it("test session verify middleware with expired access token and session required false", async function () { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 2 } }); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1258,7 +1283,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { cookieSecure: true, cookieSameSite: "strict", errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { + onTokenTheftDetected: (sessionHandle, userId, recipeUserId, req, res, next) => { res.statusCode = 403; return res.json({ message: "token theft detected", @@ -1275,7 +1300,14 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "testing-userId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + {}, + {} + ); res.status(200).json({ message: true }); }); @@ -1372,11 +1404,11 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { // https://github.com/supertokens/supertokens-node/pull/108 // A session exists, is refreshed, then is revoked, and then we try and use the access token (after first refresh), and we see that unauthorised error is called. it("test session verify middleware with old access token and session required false", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1391,7 +1423,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { cookieSecure: true, cookieSameSite: "strict", errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { + onTokenTheftDetected: (sessionHandle, userId, recipeUserId, req, res, next) => { res.statusCode = 403; return res.json({ message: "token theft detected", @@ -1408,7 +1440,14 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "testing-userId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + {}, + {} + ); res.status(200).json({ message: true }); }); @@ -1586,11 +1625,11 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { // https://github.com/supertokens/supertokens-node/pull/108 // A session doesn't exist, and we call verifySession, and it let's go through it("test session verify middleware with no session and session required false", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1605,7 +1644,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { cookieSecure: true, cookieSameSite: "strict", errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { + onTokenTheftDetected: (sessionHandle, userId, recipeUserId, req, res, next) => { res.statusCode = 403; return res.json({ message: "token theft detected", @@ -1649,11 +1688,10 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { }); it("test session verify middleware without error handler added", async function () { - await setKeyValueInConfig("access_token_validity", 5); - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 5 } }); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1664,7 +1702,7 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { Session.init({ getTokenTransferMethod: () => "cookie", errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { + onTokenTheftDetected: (sessionHandle, userId, recipeUserId, req, res) => { res.setStatusCode(403); return res.sendJSONResponse({ message: "token theft detected", @@ -1681,7 +1719,14 @@ describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "testing-userId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + {}, + {} + ); res.status(200).json({ message: true }); }); diff --git a/test/middleware2.test.js b/test/middleware2.test.js index ef704c795..b6ab541a6 100644 --- a/test/middleware2.test.js +++ b/test/middleware2.test.js @@ -46,10 +46,10 @@ describe(`middleware2: ${printPath("[test/middleware2.test.js]")}`, function () }); it("test rid with session and non existant API in session recipe gives 404", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -80,10 +80,10 @@ describe(`middleware2: ${printPath("[test/middleware2.test.js]")}`, function () }); it("test no rid with existent API does not give 404", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -113,10 +113,10 @@ describe(`middleware2: ${printPath("[test/middleware2.test.js]")}`, function () }); it("test rid as anti-csrf with existent API does not give 404", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -147,10 +147,10 @@ describe(`middleware2: ${printPath("[test/middleware2.test.js]")}`, function () }); it("test random rid with existent API gives 404", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -181,10 +181,10 @@ describe(`middleware2: ${printPath("[test/middleware2.test.js]")}`, function () }); it("custom response express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -199,11 +199,12 @@ describe(`middleware2: ${printPath("[test/middleware2.test.js]")}`, function () return { ...oI, emailExistsGET: async function (input) { + const res = await oI.emailExistsGET(input); input.options.res.setStatusCode(201); input.options.res.sendJSONResponse({ custom: true, }); - return oI.emailExistsGET(input); + return res; }, }; }, diff --git a/test/middleware3.test.js b/test/middleware3.test.js index 04f06af44..e34c25404 100644 --- a/test/middleware3.test.js +++ b/test/middleware3.test.js @@ -47,10 +47,10 @@ describe(`middleware3: ${printPath("[test/middleware3.test.js]")}`, function () }); it("test APIs work with tenantId in the request", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -94,10 +94,10 @@ describe(`middleware3: ${printPath("[test/middleware3.test.js]")}`, function () }); it("test Dashboard APIs match with tenantId", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/multitenancy/tenants-crud.test.js b/test/multitenancy/tenants-crud.test.js index 0071a7f93..676477c84 100644 --- a/test/multitenancy/tenants-crud.test.js +++ b/test/multitenancy/tenants-crud.test.js @@ -36,10 +36,10 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} }); it("test creation of tenants", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -63,10 +63,10 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} }); it("test get tenant", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -105,10 +105,10 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} }); it("test update tenant", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -142,10 +142,10 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} }); it("test delete tenant", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -178,10 +178,10 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} }); it("test creation of thirdParty config", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -212,10 +212,10 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} }); it("test deletion of thirdparty id", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -248,10 +248,10 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} }); it("test updation of thirdparty provider", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -292,10 +292,10 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} }); it("test user association and disassociation with tenants", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -315,22 +315,22 @@ describe(`tenants-crud: ${printPath("[test/multitenancy/tenants-crud.test.js]")} await Multitenancy.createOrUpdateTenant("t3", { emailPasswordEnabled: true }); const user = await EmailPassword.signUp("public", "test@example.com", "password1"); - const userId = user.user.id; + const userId = user.user.loginMethods[0].recipeUserId; await Multitenancy.associateUserToTenant("t1", userId); await Multitenancy.associateUserToTenant("t2", userId); await Multitenancy.associateUserToTenant("t3", userId); - let newUser = await EmailPassword.getUserById(userId); + let newUser = await SuperTokens.getUser(userId.getAsString()); - assert(newUser.tenantIds.length === 4); // public + 3 tenants created above + assert.strictEqual(newUser.loginMethods[0].tenantIds.length, 4); // public + 3 tenants created above await Multitenancy.disassociateUserFromTenant("t1", userId); await Multitenancy.disassociateUserFromTenant("t2", userId); await Multitenancy.disassociateUserFromTenant("t3", userId); - newUser = await EmailPassword.getUserById(userId); + newUser = await SuperTokens.getUser(userId.getAsString()); - assert(newUser.tenantIds.length === 1); // only public + assert(newUser.loginMethods[0].tenantIds.length === 1); // only public }); }); diff --git a/test/nextjs.test.js b/test/nextjs.test.js index ee68f8d1e..4e7c01e89 100644 --- a/test/nextjs.test.js +++ b/test/nextjs.test.js @@ -71,11 +71,11 @@ describe(`NextJS Middleware Test: ${printPath("[test/nextjs.test.js]")}`, functi process.env.user = undefined; await killAllST(); await setupST(); - await startST(); + const connectionURI = await startST(); ProcessState.getInstance().reset(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -133,7 +133,7 @@ describe(`NextJS Middleware Test: ${printPath("[test/nextjs.test.js]")}`, functi }); const respJson = await res.json(); assert.deepStrictEqual(respJson.status, "OK"); - assert.deepStrictEqual(respJson.user.email, "john.doe@supertokens.io"); + assert.deepStrictEqual(respJson.user.emails[0], "john.doe@supertokens.io"); assert.strictEqual(respJson.user.id, process.env.user); assert.notStrictEqual(res.headers.get("front-token"), undefined); const tokens = getSessionTokensFromResponse(res); @@ -170,7 +170,7 @@ describe(`NextJS Middleware Test: ${printPath("[test/nextjs.test.js]")}`, functi const respJson = await res.json(); assert.deepStrictEqual(respJson.status, "OK"); - assert.deepStrictEqual(respJson.user.email, "john.doe@supertokens.io"); + assert.deepStrictEqual(respJson.user.emails[0], "john.doe@supertokens.io"); assert(res.headers.get("front-token") !== undefined); tokens = getSessionTokensFromResponse(res); assert.notEqual(tokens.access, undefined); @@ -375,7 +375,14 @@ describe(`NextJS Middleware Test: ${printPath("[test/nextjs.test.js]")}`, functi handler: async (request, response) => { const session = await superTokensNextWrapper( async () => { - return await Session.createNewSession(request, response, "public", "1", {}, {}); + return await Session.createNewSession( + request, + response, + "public", + SuperTokens.convertToRecipeUserId("1"), + {}, + {} + ); }, request, response @@ -405,11 +412,11 @@ describe(`NextJS Middleware Test: ${printPath("[test/nextjs.test.js]")}`, functi process.env.user = undefined; await killAllST(); await setupST(); - await startST(); + const connectionURI = await startST(); ProcessState.getInstance().reset(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -526,11 +533,11 @@ describe(`NextJS Middleware Test: ${printPath("[test/nextjs.test.js]")}`, functi process.env.user = undefined; await killAllST(); await setupST(); - await startST(); + const connectionURI = await startST(); ProcessState.getInstance().reset(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/openid/api.test.js b/test/openid/api.test.js index 62715c4c0..724c633df 100644 --- a/test/openid/api.test.js +++ b/test/openid/api.test.js @@ -24,10 +24,10 @@ describe(`apiTest: ${printPath("[test/openid/api.test.js]")}`, function () { }); it("Test that with default config calling discovery configuration endpoint works as expected", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -68,10 +68,10 @@ describe(`apiTest: ${printPath("[test/openid/api.test.js]")}`, function () { }); it("Test that with apiBasePath calling discovery configuration endpoint works as expected", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -113,10 +113,10 @@ describe(`apiTest: ${printPath("[test/openid/api.test.js]")}`, function () { }); it("Test that discovery endpoint does not work when disabled", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/openid/config.test.js b/test/openid/config.test.js index 0cf936ae1..2a73550a0 100644 --- a/test/openid/config.test.js +++ b/test/openid/config.test.js @@ -20,10 +20,10 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () }); it("Test that the default config sets values correctly for OpenID recipe", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -47,10 +47,10 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () }); it("Test that the default config sets values correctly for OpenID recipe with apiBasePath", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -75,10 +75,10 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () }); it("Test that the config sets values correctly for OpenID recipe with issuer", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -107,12 +107,12 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () }); it("Test that issuer without apiBasePath throws error", async function () { - await startST(); + const connectionURI = await startST(); try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -135,10 +135,10 @@ describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () }); it("Test that issuer with gateway path works fine", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/openid/openid.test.js b/test/openid/openid.test.js index e171f398b..657f4958d 100644 --- a/test/openid/openid.test.js +++ b/test/openid/openid.test.js @@ -21,10 +21,10 @@ describe(`openIdTest: ${printPath("[test/openid/openid.test.js]")}`, function () }); it("Test that with default config discovery configuration is as expected", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -48,10 +48,10 @@ describe(`openIdTest: ${printPath("[test/openid/openid.test.js]")}`, function () }); it("Test that with default config discovery configuration is as expected with api base path", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -76,10 +76,10 @@ describe(`openIdTest: ${printPath("[test/openid/openid.test.js]")}`, function () }); it("Test that with default config discovery configuration is as expected with custom issuer", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/openid/override.test.js b/test/openid/override.test.js index 977ada3b7..d414d906d 100644 --- a/test/openid/override.test.js +++ b/test/openid/override.test.js @@ -24,10 +24,10 @@ describe(`overrideTest: ${printPath("[test/openid/override.test.js]")}`, functio }); it("Test overriding open id functions", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -85,10 +85,10 @@ describe(`overrideTest: ${printPath("[test/openid/override.test.js]")}`, functio }); it("Test overriding open id apis", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/passwordless/apis.test.js b/test/passwordless/apis.test.js index a4d81cf24..de9a3e8e7 100644 --- a/test/passwordless/apis.test.js +++ b/test/passwordless/apis.test.js @@ -33,6 +33,7 @@ const request = require("supertest"); const express = require("express"); let { middleware, errorHandler } = require("../../framework/express"); let { isCDIVersionCompatible } = require("../utils"); +const { default: RecipeUserId } = require("../../lib/build/recipeUserId"); describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, function () { beforeEach(async function () { @@ -52,12 +53,12 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func */ it("test the sign up /in flow with email using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -99,12 +100,13 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func app.use(errorHandler()); + const email = "test@example.com"; // createCodeAPI with email let validCreateCodeResponse = await new Promise((resolve) => request(app) .post("/auth/signinup/code") .send({ - email: "test@example.com", + email: email, }) .expect(200) .end((err, res) => { @@ -122,7 +124,7 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func assert(Object.keys(validCreateCodeResponse).length === 4); // consumeCode API - let validUserInputCodeResponse = await new Promise((resolve) => + let validUserInputCodeResponse = await new Promise((resolve, reject) => request(app) .post("/auth/signinup/code/consume") .send({ @@ -133,20 +135,19 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func .expect(200) .end((err, res) => { if (err) { - resolve(undefined); + reject(err); } else { resolve(JSON.parse(res.text)); } }) ); - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.email === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 4); - assert(Object.keys(validUserInputCodeResponse).length === 3); + checkConsumeResponse(validUserInputCodeResponse, { + email, + phoneNumber: undefined, + isNew: true, + isPrimary: false, + }); }); /** @@ -155,12 +156,12 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func */ it("test the sign up /in flow with phoneNumber using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -202,12 +203,13 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func app.use(errorHandler()); + const phoneNumber = "+12345678901"; // createCodeAPI with phoneNumber let validCreateCodeResponse = await new Promise((resolve) => request(app) .post("/auth/signinup/code") .send({ - phoneNumber: "+12345678901", + phoneNumber, }) .expect(200) .end((err, res) => { @@ -243,13 +245,12 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }) ); - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.phoneNumber === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 4); - assert(Object.keys(validUserInputCodeResponse).length === 3); + checkConsumeResponse(validUserInputCodeResponse, { + email: undefined, + phoneNumber, + isNew: true, + isPrimary: false, + }); }); /** @@ -257,13 +258,13 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func * - create code with email and then resend code and make sure that sending email function is called while resending code */ it("test creating a code with email and then resending the code and check that the sending custom email function is called while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -352,13 +353,13 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func * - create code with phone and then resend code and make sure that sending SMS function is called while resending code */ it("test creating a code with phone and then resending the code and check that the sending custom SMS function is called while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomTextMessageCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -448,11 +449,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func * - sending neither email and phone in createCode API throws bad request */ it("test invalid input to createCodeAPI while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -557,13 +558,13 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func */ it("test adding phoneNumber to a users info and signing in will sign in the same user, using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -647,7 +648,7 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func // add users phoneNumber to userInfo await Passwordless.updateUser({ - userId: emailUserInputCodeResponse.user.id, + recipeUserId: STExpress.convertToRecipeUserId(emailUserInputCodeResponse.user.loginMethods[0].recipeUserId), phoneNumber: "+12345678901", }); @@ -696,11 +697,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func // check that if user has not given linkCode nor (deviceId+userInputCode), it throws a bad request error. it("test not passing any fields to consumeCodeAPI", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -760,11 +761,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); it("test consumeCodeAPI with magic link", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -798,9 +799,10 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func app.use(errorHandler()); + const email = "test@example.com"; let codeInfo = await Passwordless.createCode({ tenantId: "public", - email: "test@example.com", + email, }); { @@ -844,22 +846,21 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }) ); - assert(validLinkCodeResponse.status === "OK"); - assert(validLinkCodeResponse.createdNewUser === true); - assert(typeof validLinkCodeResponse.user.id === "string"); - assert(typeof validLinkCodeResponse.user.email === "string"); - assert(typeof validLinkCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validLinkCodeResponse.user).length === 4); - assert(Object.keys(validLinkCodeResponse).length === 3); + checkConsumeResponse(validLinkCodeResponse, { + email, + phoneNumber: undefined, + isNew: true, + isPrimary: false, + }); } }); it("test consumeCodeAPI with code", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -893,9 +894,10 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func app.use(errorHandler()); + const email = "test@example.com"; let codeInfo = await Passwordless.createCode({ tenantId: "public", - email: "test@example.com", + email, }); { @@ -945,13 +947,12 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }) ); - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.email === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 4); - assert(Object.keys(validUserInputCodeResponse).length === 3); + checkConsumeResponse(validUserInputCodeResponse, { + email, + phoneNumber: undefined, + isNew: true, + isPrimary: false, + }); } { @@ -978,12 +979,15 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); it("test consumeCodeAPI with expired code", async function () { - await setKeyValueInConfig("passwordless_code_lifetime", 1000); // one second lifetime - await startST(); + const connectionURI = await startST({ + coreConfig: { + passwordless_code_lifetime: 1000, // one second lifetime + }, + }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1058,11 +1062,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); it("test createCodeAPI with email", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1150,11 +1154,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); it("test createCodeAPI with phoneNumber", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1242,12 +1246,12 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); it("test magicLink format in createCodeAPI", async function () { - await startST(); + const connectionURI = await startST(); let magicLinkURL = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1319,11 +1323,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); it("test emailExistsAPI", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1414,11 +1418,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); it("test phoneNumberExistsAPI", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1511,11 +1515,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func //resendCode API it("test resendCodeAPI", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1600,11 +1604,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func // test that you create a code with PHONE in config, you then change the config to use EMAIL, you call resendCode API, it should return RESTART_FLOW_ERROR it("test resendCodeAPI when changing contact method", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1645,11 +1649,11 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); await killAllST(); - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1697,13 +1701,13 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func }); it("test resend code from different tenant throws error", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); let isCreateAndSendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1787,7 +1791,53 @@ describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, func } }) ); - assert(response.status === "RESTART_FLOW_ERROR"); - assert(isCreateAndSendCustomEmailCalled === false); + assert.strictEqual(response.status, "RESTART_FLOW_ERROR"); + assert.strictEqual(isCreateAndSendCustomEmailCalled, false); }); }); + +function checkConsumeResponse(validUserInputCodeResponse, { email, phoneNumber, isNew, isPrimary }) { + assert.strictEqual(validUserInputCodeResponse.status, "OK"); + assert.strictEqual(validUserInputCodeResponse.createdNewRecipeUser, isNew); + + assert.strictEqual(typeof validUserInputCodeResponse.user.id, "string"); + assert.strictEqual(typeof validUserInputCodeResponse.user.timeJoined, "number"); + assert.strictEqual(validUserInputCodeResponse.user.isPrimaryUser, isPrimary); + + assert(validUserInputCodeResponse.user.emails instanceof Array); + if (email !== undefined) { + assert.strictEqual(validUserInputCodeResponse.user.emails.length, 1); + assert.strictEqual(validUserInputCodeResponse.user.emails[0], email); + } else { + assert.strictEqual(validUserInputCodeResponse.user.emails.length, 0); + } + + assert(validUserInputCodeResponse.user.phoneNumbers instanceof Array); + if (phoneNumber !== undefined) { + assert.strictEqual(validUserInputCodeResponse.user.phoneNumbers.length, 1); + assert.strictEqual(validUserInputCodeResponse.user.phoneNumbers[0], phoneNumber); + } else { + assert.strictEqual(validUserInputCodeResponse.user.phoneNumbers.length, 0); + } + + assert.strictEqual(validUserInputCodeResponse.user.thirdParty.length, 0); + + assert.strictEqual(validUserInputCodeResponse.user.loginMethods.length, 1); + const loginMethod = { + recipeId: "passwordless", + recipeUserId: validUserInputCodeResponse.user.id, + timeJoined: validUserInputCodeResponse.user.timeJoined, + verified: true, + tenantIds: ["public"], + }; + if (email) { + loginMethod.email = email; + } + if (phoneNumber) { + loginMethod.phoneNumber = phoneNumber; + } + assert.deepStrictEqual(validUserInputCodeResponse.user.loginMethods, [loginMethod]); + + assert.strictEqual(Object.keys(validUserInputCodeResponse.user).length, 8); + assert.strictEqual(Object.keys(validUserInputCodeResponse).length, 3); +} diff --git a/test/passwordless/config.test.js b/test/passwordless/config.test.js index 9b227b610..5e8540983 100644 --- a/test/passwordless/config.test.js +++ b/test/passwordless/config.test.js @@ -12,7 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig, stopST } = require("../utils"); +const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); let STExpress = require("../../"); let Session = require("../../recipe/session"); let Passwordless = require("../../recipe/passwordless"); @@ -42,11 +42,11 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun - minimal config */ it("test minimum config with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -91,14 +91,14 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun - adding custom validators for phone and email and making sure that they are called */ it("test adding custom validators for phone and email with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isValidateEmailAddressCalled = false; let isValidatePhoneNumberCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -198,13 +198,13 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test custom function to send email with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -276,13 +276,13 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test custom function to send text SMS with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomTextMessageCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -352,11 +352,11 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun - minimal input works */ it("test minimum config with phone contactMethod", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -396,12 +396,12 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test if validatePhoneNumber is called with phone contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isValidatePhoneNumberCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -465,13 +465,13 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun // If you return a string from the function, the API throws a GENERIC ERROR await killAllST(); - await startST(); + const connectionURI = await startST(); isValidatePhoneNumberCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -529,13 +529,13 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE and phone contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; let isOtherInputValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -606,12 +606,12 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test createAndSendCustomTextMessage with flowType: MAGIC_LINK and phone contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -672,12 +672,12 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined */ it("test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and phone contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -739,12 +739,12 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test createAndSendCustomTextMessage, if error is thrown, it should contain a general error in the response", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomTextMessageCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -809,11 +809,11 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test minimum config with email contactMethod", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -854,12 +854,12 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test if validateEmailAddress is called with email contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isValidateEmailAddressCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -923,13 +923,13 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun // If you return a string from the function, the API throws a GENERIC ERROR await killAllST(); - await startST(); + const connectionURI = await startST(); isValidateEmailAddressCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -986,13 +986,13 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test createAndSendCustomEmail with flowType: USER_INPUT_CODE and email contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; let isOtherInputValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1063,12 +1063,12 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test createAndSendCustomEmail with flowType: MAGIC_LINK and email contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1129,12 +1129,12 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined */ it("test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and email contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1196,12 +1196,12 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test createAndSendCustomEmail, if error is thrown, the status in the response should be a general error", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1268,14 +1268,14 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test missing compulsory configs throws an error", async function () { - await startST(); + const connectionURI = await startST(); { // missing flowType try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1299,13 +1299,13 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun { await killAllST(); - await startST(); + const connectionURI = await startST(); // missing contactMethod try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1336,14 +1336,14 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun */ it("test passing getCustomUserInputCode using different codes", async function () { - await startST(); + const connectionURI = await startST(); let customCode = undefined; let userCodeSent = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1425,7 +1425,7 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun }); it("test passing getCustomUserInputCode using the same code", async function () { - await startST(); + const connectionURI = await startST(); // using the same customCode let customCode = "customCode"; @@ -1433,7 +1433,7 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1520,13 +1520,13 @@ describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, fun // Check basic override usage it("test basic override usage in passwordless", async function () { - await startST(); + const connectionURI = await startST(); let customDeviceId = "customDeviceId"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/passwordless/emailDelivery.test.js b/test/passwordless/emailDelivery.test.js index 3dee4170e..1554a12da 100644 --- a/test/passwordless/emailDelivery.test.js +++ b/test/passwordless/emailDelivery.test.js @@ -39,10 +39,10 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test default backward compatibility api being called: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -107,14 +107,14 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test backward compatibility: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -167,7 +167,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test custom override: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -176,7 +176,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -247,7 +247,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test smtp service: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -257,7 +257,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -347,10 +347,10 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test default backward compatibility api being called, error message sent back to user: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -422,10 +422,10 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test default backward compatibility api being called: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -508,7 +508,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test backward compatibility: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -516,7 +516,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" let sendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -587,7 +587,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test custom override: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -598,7 +598,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" let loginCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -694,7 +694,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test smtp service: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -704,7 +704,7 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -822,10 +822,10 @@ describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]" }); it("test default backward compatibility api being called, error message sent back to user: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/passwordless/multitenancy.test.js b/test/passwordless/multitenancy.test.js index 6eeb82b2c..774f916dd 100644 --- a/test/passwordless/multitenancy.test.js +++ b/test/passwordless/multitenancy.test.js @@ -13,7 +13,7 @@ * under the License. */ const { printPath, setupST, startSTWithMultitenancy, stopST, killAllST, cleanST, resetAll } = require("../utils"); -let STExpress = require("../../"); +let SuperTokens = require("../../"); let Session = require("../../recipe/session"); let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; let assert = require("assert"); @@ -40,10 +40,10 @@ describe(`multitenancy: ${printPath("[test/passwordless/multitenancy.test.js]")} }); it("test recipe functions", async function () { - await startSTWithMultitenancy(); - STExpress.init({ + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -79,21 +79,18 @@ describe(`multitenancy: ${printPath("[test/passwordless/multitenancy.test.js]")} }); let user1 = await Passwordless.consumeCode({ - tenantId: "public", preAuthSessionId: code1.preAuthSessionId, deviceId: code1.deviceId, userInputCode: "123456", tenantId: "t1", }); let user2 = await Passwordless.consumeCode({ - tenantId: "public", preAuthSessionId: code2.preAuthSessionId, deviceId: code2.deviceId, userInputCode: "456789", tenantId: "t2", }); let user3 = await Passwordless.consumeCode({ - tenantId: "public", preAuthSessionId: code3.preAuthSessionId, deviceId: code3.deviceId, userInputCode: "789123", @@ -104,26 +101,17 @@ describe(`multitenancy: ${printPath("[test/passwordless/multitenancy.test.js]")} assert(user1.user.id !== user3.user.id); assert(user2.user.id !== user3.user.id); - assert.deepEqual(user1.user.tenantIds, ["t1"]); - assert.deepEqual(user2.user.tenantIds, ["t2"]); - assert.deepEqual(user3.user.tenantIds, ["t3"]); + assert.deepEqual(user1.user.loginMethods[0].tenantIds, ["t1"]); + assert.deepEqual(user2.user.loginMethods[0].tenantIds, ["t2"]); + assert.deepEqual(user3.user.loginMethods[0].tenantIds, ["t3"]); // get user by id - let gUser1 = await Passwordless.getUserById({ userId: user1.user.id }); - let gUser2 = await Passwordless.getUserById({ userId: user2.user.id }); - let gUser3 = await Passwordless.getUserById({ userId: user3.user.id }); + let gUser1 = await SuperTokens.getUser(user1.user.id); + let gUser2 = await SuperTokens.getUser(user2.user.id); + let gUser3 = await SuperTokens.getUser(user3.user.id); - assert.deepEqual(gUser1, user1.user); - assert.deepEqual(gUser2, user2.user); - assert.deepEqual(gUser3, user3.user); - - // get user by email - let gUserByEmail1 = await Passwordless.getUserByEmail({ email: "test@example.com", tenantId: "t1" }); - let gUserByEmail2 = await Passwordless.getUserByEmail({ email: "test@example.com", tenantId: "t2" }); - let gUserByEmail3 = await Passwordless.getUserByEmail({ email: "test@example.com", tenantId: "t3" }); - - assert.deepEqual(gUserByEmail1, user1.user); - assert.deepEqual(gUserByEmail2, user2.user); - assert.deepEqual(gUserByEmail3, user3.user); + assert.deepEqual(gUser1.toJson(), user1.user.toJson()); + assert.deepEqual(gUser2.toJson(), user2.user.toJson()); + assert.deepEqual(gUser3.toJson(), user3.user.toJson()); }); }); diff --git a/test/passwordless/recipeFunctions.test.js b/test/passwordless/recipeFunctions.test.js index 44bf06301..81fbd7e4d 100644 --- a/test/passwordless/recipeFunctions.test.js +++ b/test/passwordless/recipeFunctions.test.js @@ -16,10 +16,12 @@ const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig } = let STExpress = require("../../"); let Session = require("../../recipe/session"); let Passwordless = require("../../recipe/passwordless"); +let EmailVerification = require("../../recipe/emailverification"); let assert = require("assert"); let { ProcessState } = require("../../lib/build/processState"); let SuperTokens = require("../../lib/build/supertokens").default; let { isCDIVersionCompatible } = require("../utils"); +const { default: RecipeUserId } = require("../../lib/build/recipeUserId"); describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test.js]")}`, function () { beforeEach(async function () { @@ -34,11 +36,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); it("getUser test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -63,39 +65,37 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. if (!(await isCDIVersionCompatible("2.11"))) { return; } + const email = "test@example.com"; { - let user = await Passwordless.getUserById({ - userId: "random", - }); + let user = await STExpress.getUser("random"); assert(user === undefined); user = ( await Passwordless.signInUp({ tenantId: "public", - email: "test@example.com", + email, }) ).user; - let result = await Passwordless.getUserById({ - userId: user.id, - }); + let result = await STExpress.getUser(user.id); - assert(result.id === user.id); - assert(result.email !== undefined && user.email === result.email); - assert(result.phoneNumber === undefined); - assert(typeof result.timeJoined === "number"); - assert(Object.keys(result).length === 4); + assert.strictEqual(result.id, user.id); + assert.strictEqual(result.emails[0], email); + assert.strictEqual(result.phoneNumbers.length, 0); + assert.strictEqual(result.thirdParty.length, 0); + assert.strictEqual(result.loginMethods.length, 1); + assert.strictEqual(typeof result.loginMethods[0].timeJoined, "number"); + assert.strictEqual(Object.keys(result).length, 8); } { - let user = await Passwordless.getUserByEmail({ - tenantId: "public", + let user = await STExpress.listUsersByAccountInfo("public", { email: "random", }); - assert(user === undefined); + assert(user.length === 0); user = ( await Passwordless.signInUp({ @@ -104,52 +104,58 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }) ).user; - let result = await Passwordless.getUserByEmail({ - tenantId: "public", - email: user.email, + let result = await STExpress.listUsersByAccountInfo("public", { + email: email, }); + assert(result.length === 1); + result = result[0]; assert(result.id === user.id); - assert(result.email !== undefined && user.email === result.email); - assert(result.phoneNumber === undefined); - assert(typeof result.timeJoined === "number"); - assert(Object.keys(result).length === 4); + assert(result.emails[0] === email); + assert(result.phoneNumbers.length === 0); + assert(result.thirdParty.length === 0); + assert(result.loginMethods.length === 1); + assert(typeof result.loginMethods[0].timeJoined === "number"); + assert(Object.keys(result).length === 8); } { - let user = await Passwordless.getUserByPhoneNumber({ - tenantId: "public", + let user = await STExpress.listUsersByAccountInfo("public", { phoneNumber: "random", }); - assert(user === undefined); + assert(user.length === 0); + const phoneNumber = "+1234567890"; user = ( await Passwordless.signInUp({ tenantId: "public", - phoneNumber: "+1234567890", + phoneNumber, }) ).user; - let result = await Passwordless.getUserByPhoneNumber({ - tenantId: "public", - phoneNumber: user.phoneNumber, + let result = await STExpress.listUsersByAccountInfo("public", { + phoneNumber: phoneNumber, }); + assert(result.length === 1); + result = result[0]; assert(result.id === user.id); - assert(result.phoneNumber !== undefined && user.phoneNumber === result.phoneNumber); - assert(result.email === undefined); - assert(typeof result.timeJoined === "number"); - assert(Object.keys(result).length === 4); + assert(result.emails.length === 0); + assert(result.phoneNumbers[0] === phoneNumber); + assert(result.thirdParty.length === 0); + assert(result.loginMethods.length === 1); + assert(typeof result.loginMethods[0].timeJoined === "number"); + assert(Object.keys(result).length === 8); } }); it("createCode test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -212,11 +218,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); it("createNewCodeForDevice test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -321,11 +327,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); it("consumeCode test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -334,6 +340,9 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }, recipeList: [ Session.init({ getTokenTransferMethod: () => "cookie" }), + EmailVerification.init({ + mode: "REQUIRED", + }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", @@ -365,13 +374,18 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); assert(resp.status === "OK"); - assert(resp.createdNewUser); - assert(typeof resp.user.id === "string"); - assert(resp.user.email === "test@example.com"); - assert(resp.user.phoneNumber === undefined); - assert(typeof resp.user.timeJoined === "number"); - assert(Object.keys(resp).length === 3); - assert(Object.keys(resp.user).length === 4); + assert(resp.createdNewRecipeUser); + assert.strictEqual(typeof resp.user.id, "string"); + assert.strictEqual(resp.user.emails[0], "test@example.com"); + assert.strictEqual(resp.user.phoneNumbers[0], undefined); + assert.strictEqual(typeof resp.user.timeJoined, "number"); + assert.strictEqual(Object.keys(resp).length, 4); + assert.strictEqual(Object.keys(resp.user).length, 8); + + const emailVerified = await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(resp.user.id) + ); + assert(emailVerified); } { @@ -414,12 +428,15 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); it("consumeCode test with EXPIRED_USER_INPUT_CODE_ERROR", async function () { - await setKeyValueInConfig("passwordless_code_lifetime", 1000); // one second lifetime - await startST(); + const connectionURI = await startST({ + coreConfig: { + passwordless_code_lifetime: 1000, // one second lifetime + }, + }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -469,11 +486,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. // updateUser it("updateUser contactMethod email test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -507,21 +524,19 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. { // update users email let response = await Passwordless.updateUser({ - userId: userInfo.user.id, + recipeUserId: userInfo.user.loginMethods[0].recipeUserId, email: "test2@example.com", }); assert(response.status === "OK"); - let result = await Passwordless.getUserById({ - userId: userInfo.user.id, - }); + let result = await STExpress.getUser(userInfo.user.id); - assert(result.email === "test2@example.com"); + assert(result.emails[0] === "test2@example.com"); } { // update user with invalid userId let response = await Passwordless.updateUser({ - userId: "invalidUserId", + recipeUserId: new RecipeUserId("invalidUserId"), email: "test2@example.com", }); assert(response.status === "UNKNOWN_USER_ID_ERROR"); @@ -534,7 +549,7 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); let result = await Passwordless.updateUser({ - userId: userInfo2.user.id, + recipeUserId: userInfo2.user.loginMethods[0].recipeUserId, email: "test2@example.com", }); @@ -543,11 +558,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); it("updateUser contactMethod phone test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -585,16 +600,14 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. { // update users email let response = await Passwordless.updateUser({ - userId: userInfo.user.id, + recipeUserId: userInfo.user.loginMethods[0].recipeUserId, phoneNumber: phoneNumber_2, }); assert(response.status === "OK"); - let result = await Passwordless.getUserById({ - userId: userInfo.user.id, - }); + let result = await STExpress.getUser(userInfo.user.id); - assert(result.phoneNumber === phoneNumber_2); + assert(result.phoneNumbers[0] === phoneNumber_2); } { // update user with a phoneNumber that already exists @@ -604,7 +617,7 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); let result = await Passwordless.updateUser({ - userId: userInfo2.user.id, + recipeUserId: userInfo2.user.loginMethods[0].recipeUserId, phoneNumber: phoneNumber_2, }); @@ -614,11 +627,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. // revokeAllCodes it("revokeAllCodes test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -685,11 +698,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); it("revokeCode test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -757,11 +770,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. // listCodesByEmail it("listCodesByEmail test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -813,11 +826,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. //listCodesByPhoneNumber it("listCodesByPhoneNumber test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -869,11 +882,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. // listCodesByDeviceId and listCodesByPreAuthSessionId it("listCodesByDeviceId and listCodesByPreAuthSessionId test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -927,11 +940,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. */ it("createMagicLink test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -973,11 +986,11 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. // signInUp test it("signInUp test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1009,12 +1022,12 @@ describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test. }); assert(result.status === "OK"); - assert(result.createdNewUser === true); - assert(Object.keys(result).length === 3); + assert(result.createdNewRecipeUser === true); + assert(Object.keys(result).length === 4); - assert(result.user.phoneNumber === "+12345678901"); + assert(result.user.phoneNumbers[0] === "+12345678901"); assert(typeof result.user.id === "string"); assert(typeof result.user.timeJoined === "number"); - assert(Object.keys(result.user).length === 4); + assert(Object.keys(result.user).length === 8); }); }); diff --git a/test/passwordless/smsDelivery.test.js b/test/passwordless/smsDelivery.test.js index 55ad21cbe..5cf37d5d2 100644 --- a/test/passwordless/smsDelivery.test.js +++ b/test/passwordless/smsDelivery.test.js @@ -40,10 +40,10 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test default backward compatibility api being called: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -111,14 +111,14 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test backward compatibility: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -171,7 +171,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test custom override: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -180,7 +180,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -251,7 +251,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test twilio service: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -261,7 +261,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, let twilioAPICalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -352,10 +352,10 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test default backward compatibility api being called, error message sent back to user: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -427,7 +427,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test supertokens service: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -437,7 +437,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, let type = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -507,10 +507,10 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test default backward compatibility api being called, error message not sent back to user if response code is 429: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -576,10 +576,10 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test default backward compatibility api being called: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -665,7 +665,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test backward compatibility: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -673,7 +673,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, let sendCustomSMSCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -744,7 +744,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test custom override: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -755,7 +755,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, let loginCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -851,7 +851,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test twilio service: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -863,7 +863,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, let twilioAPICalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -992,10 +992,10 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test default backward compatibility api being called, error message sent back to user: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1091,7 +1091,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test supertokens service: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -1102,7 +1102,7 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, let loginCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1192,10 +1192,10 @@ describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, }); it("test default backward compatibility api being called, error message not sent back to user if response code is 429: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/querier.test.js b/test/querier.test.js index fd3cbde7d..eda9b9369 100644 --- a/test/querier.test.js +++ b/test/querier.test.js @@ -40,10 +40,10 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { // Check that once the API version is there, it doesn't need to query again it("test that if that once API version is there, it doesn't need to query again", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -73,10 +73,10 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { // Check that rid is added to the header iff it's a "/recipe" || "/recipe/*" request. it("test that rid is added to the header if it's a recipe request", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -88,7 +88,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { let querier = Querier.getNewInstanceOrThrowError(SessionRecipe.getInstanceOrThrowError().getRecipeId()); - nock("http://localhost:8080", { + nock(connectionURI, { allowUnmocked: true, }) .get("/recipe") @@ -99,7 +99,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe"), {}); assert.deepStrictEqual(response.rid, ["session"]); - nock("http://localhost:8080", { + nock(connectionURI, { allowUnmocked: true, }) .get("/recipe/random") @@ -110,7 +110,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { let response2 = await querier.sendGetRequest(new NormalisedURLPath("/recipe/random"), {}); assert.deepStrictEqual(response2.rid, ["session"]); - nock("http://localhost:8080", { + nock(connectionURI, { allowUnmocked: true, }) .get("/test") @@ -146,12 +146,13 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { }); it("three cores and round robin", async function () { - await startST(); - await startST("localhost", 8081); - await startST("localhost", 8082); + const connectionURI = await startST(); + await startST({ host: "localhost", port: 8081 }); + await startST({ host: "localhost", port: 8082 }); + ST.init({ supertokens: { - connectionURI: "http://localhost:8080;http://localhost:8081/;http://localhost:8082", + connectionURI: `${connectionURI};http://localhost:8081/;http://localhost:8082`, }, appInfo: { apiDomain: "api.supertokens.io", @@ -168,17 +169,17 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); // this will be the 4th API call hostsAlive = q.getHostsAliveForTesting(); assert.equal(hostsAlive.size, 3); - assert.equal(hostsAlive.has("http://localhost:8080"), true); + assert.equal(hostsAlive.has(connectionURI), true); assert.equal(hostsAlive.has("http://localhost:8081"), true); assert.equal(hostsAlive.has("http://localhost:8082"), true); }); it("three cores, one dead and round robin", async function () { - await startST(); - await startST("localhost", 8082); + const connectionURI = await startST(); + await startST({ host: "localhost", port: 8082 }); ST.init({ supertokens: { - connectionURI: "http://localhost:8080;http://localhost:8081/;http://localhost:8082", + connectionURI: `${connectionURI};http://localhost:8081/;http://localhost:8082`, }, appInfo: { apiDomain: "api.supertokens.io", @@ -195,13 +196,13 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { assert.equal(await q.sendPutRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); // this will be the 4th API call hostsAlive = q.getHostsAliveForTesting(); assert.equal(hostsAlive.size, 2); - assert.equal(hostsAlive.has("http://localhost:8080"), true); + assert.equal(hostsAlive.has(connectionURI), true); assert.equal(hostsAlive.has("http://localhost:8081"), false); assert.equal(hostsAlive.has("http://localhost:8082"), true); }); it("test that no connectionURI given, but recipe used throws an error", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ appInfo: { apiDomain: "api.supertokens.io", @@ -223,7 +224,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { }); it("test that no connectionURI given, recipe override and used doesn't thrown an error", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ appInfo: { apiDomain: "api.supertokens.io", @@ -253,11 +254,10 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { it("test with core base path", async function () { // first we need to know if the core used supports base_path config - await setKeyValueInConfig("base_path", "/test"); - await startST(); + const connectionURI = await startST({ port: 8081, coreConfig: { base_path: "/test" } }); try { - const res = await fetch("http://localhost:8080/test/hello"); + const res = await fetch(`${connectionURI}/test/hello`); if (res.status === 404) { return; } @@ -267,7 +267,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { ST.init({ supertokens: { - connectionURI: "http://localhost:8080/test", + connectionURI: `${connectionURI}/test`, }, appInfo: { apiDomain: "api.supertokens.io", @@ -284,11 +284,10 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { it("test with incorrect core base path should fail", async function () { // first we need to know if the core used supports base_path config - await setKeyValueInConfig("base_path", "/some/path"); - await startST(); + const connectionURI = await startST({ port: 8081, coreConfig: { base_path: "/some/path" } }); try { - const res = await fetch("http://localhost:8080/some/path/hello"); + const res = await fetch(`${connectionURI}/some/path/hello`); if (res.status === 404) { return; } @@ -302,7 +301,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { ST.init({ supertokens: { - connectionURI: "http://localhost:8080/test", + connectionURI: `${connectionURI}/test`, }, appInfo: { apiDomain: "api.supertokens.io", @@ -314,7 +313,9 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { try { // we query the core now - await Session.getAllSessionHandlesForUser("user1"); + await Session.getAllSessionHandlesForUser("user1", true, undefined, { + doNotMock: true, + }); fail(); } catch (err) { assert(err.message.startsWith("SuperTokens core threw an error")); @@ -323,11 +324,10 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { it("test with multiple core base path", async function () { // first we need to know if the core used supports base_path config - await setKeyValueInConfig("base_path", "/some/path"); - await startST(); + const connectionURI = await startST({ port: 8081, coreConfig: { base_path: "/some/path" } }); try { - const res = await fetch("http://localhost:8080/some/path/hello"); + const res = await fetch(`${connectionURI}/some/path/hello`); if (res.status === 404) { return; } @@ -339,12 +339,17 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { throw error; } - await setKeyValueInConfig("base_path", "/test"); - await startST("localhost", 8082); + await startST({ + host: "localhost", + port: 8082, + coreConfig: { + base_path: "/test", + }, + }); ST.init({ supertokens: { - connectionURI: "http://localhost:8080/some/path;http://localhost:8082/test", + connectionURI: `${connectionURI}/some/path;http://localhost:8082/test`, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/ratelimiting.test.js b/test/ratelimiting.test.js index 7caa5b904..c909944d5 100644 --- a/test/ratelimiting.test.js +++ b/test/ratelimiting.test.js @@ -26,7 +26,7 @@ describe(`Querier rate limiting: ${printPath("[test/ratelimiting.test.js]")}`, ( }); it("Test that network call is retried properly", async () => { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { // Using 8083 because we need querier to call the test express server instead of the core @@ -105,7 +105,7 @@ describe(`Querier rate limiting: ${printPath("[test/ratelimiting.test.js]")}`, ( }); it("Test that rate limiting errors are thrown back to the user", async () => { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { // Using 8083 because we need querier to call the test express server instead of the core @@ -159,7 +159,7 @@ describe(`Querier rate limiting: ${printPath("[test/ratelimiting.test.js]")}`, ( }); it("Test that parallel calls have independent retry counters", async () => { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { // Using 8083 because we need querier to call the test express server instead of the core diff --git a/test/recipeModuleManager.test.js b/test/recipeModuleManager.test.js index 20d551bca..b067cdf45 100644 --- a/test/recipeModuleManager.test.js +++ b/test/recipeModuleManager.test.js @@ -50,11 +50,11 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j }); it("calling init multiple times", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -69,7 +69,7 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -86,7 +86,7 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j // Check that querier has been inited when we call supertokens.init // Failure condition: initalizing supertoknes before the the first try catch will fail the test it("test that querier has been initiated when we call supertokens.init", async function () { - await startST(); + const connectionURI = await startST(); try { await Querier.getNewInstanceOrThrowError(undefined); @@ -99,7 +99,7 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -115,19 +115,22 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j // Check that modules have been inited when we call supertokens.init // Failure condition: initalizing supertoknes before the the first try catch will fail the test it("test that modules have been initiated when we call supertokens.init", async function () { - await startST(); + const connectionURI = await startST(); try { - await SessionRecipe.getInstanceOrThrowError(); + SessionRecipe.getInstanceOrThrowError(); assert(false); } catch (err) { - if (err.message !== "Initialisation not done. Did you forget to call the SuperTokens.init function?") { + if ( + err.message !== + "Initialisation not done. Did you forget to call the SuperTokens.init or Session.init function?" + ) { throw err; } } try { - await EmailPasswordRecipe.getInstanceOrThrowError(); + EmailPasswordRecipe.getInstanceOrThrowError(); assert(false); } catch (err) { if (err.message !== "Initialisation not done. Did you forget to call the SuperTokens.init function?") { @@ -137,7 +140,7 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -149,8 +152,8 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j EmailPassword.init(), ], }); - await SessionRecipe.getInstanceOrThrowError(); - await EmailPasswordRecipe.getInstanceOrThrowError(); + SessionRecipe.getInstanceOrThrowError(); + EmailPasswordRecipe.getInstanceOrThrowError(); }); /* @@ -162,10 +165,10 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j //Failure condition: Tests will fail is using the incorrect base path it("test various inputs to routing with default base path", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -243,11 +246,11 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j //Failure condition: Tests will fail is using the wrong base path it("test various inputs to routing when base path is /", async function () { - await startST(); + const connectionURI = await startST(); { ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -330,11 +333,11 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j //Failure condition: Tests will fail if the incorrect rid header value is set when sending a request the path it("test routing with multiple recipes", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -395,11 +398,11 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j // Test various inputs to errorHandler (if it accepts or not) it("test various inputs to errorHandler", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -453,11 +456,11 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j // Error thrown from APIs implemented by recipes must not go unhandled it("test that error thrown from APIs implemented by recipes must not go unhandled", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -511,11 +514,11 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j // Disable a default route, and then implement your own API and check that that gets called // Failure condition: in testRecipe1 if the disabled value for the /default-route-disabled is set to false, the test will fail it("test if you diable a default route, and then implement your own API, your own api is called", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -552,11 +555,11 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j // If an error handler in a recipe throws an error, that error next to go to the user's error handler it("test if the error handler in a recipe throws an error, it goes to the user's error handler", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -595,11 +598,11 @@ describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.j // Test getAllCORSHeaders it("test the getAllCORSHeaders function", async function () { - await startST(); + const connectionURI = await startST(); ST.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/session.test.js b/test/session.test.js index 7df564f79..f19ca2e13 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -38,6 +38,7 @@ let SessionRecipe = require("../lib/build/recipe/session/recipe").default; const { maxVersion } = require("../lib/build/utils"); const { fail } = require("assert"); let { middleware, errorHandler } = require("../framework/express"); +const { default: RecipeUserId } = require("../lib/build/recipeUserId"); /* TODO: - the opposite of the above (check that if signing key changes, things are still fine) condition @@ -61,10 +62,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { // check if output headers and set cookies for create session is fine it("test that output headers and set cookie for create session is fine", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -79,7 +80,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("testuserid"), {}, {}); res.status(200).send(""); }); @@ -109,14 +110,17 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { assert(cookies.accessTokenDomain === undefined); assert(cookies.refreshTokenDomain === undefined); assert(cookies.frontToken !== undefined); + + let frontendInfo = JSON.parse(new Buffer.from(cookies.frontToken, "base64").toString()); + assert.strictEqual(frontendInfo.up["rsub"], "testuserid"); }); // check if output headers and set cookies for refresh session is fine it("test that output headers and set cookie for refresh session is fine", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -130,7 +134,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("testuserid"), {}, {}); res.status(200).send(""); }); @@ -176,15 +180,18 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { assert(cookies.accessTokenDomain === undefined); assert(cookies.refreshTokenDomain === undefined); assert(cookies.frontToken !== undefined); + + let frontendInfo = JSON.parse(new Buffer.from(cookies.frontToken, "base64").toString()); + assert.strictEqual(frontendInfo.up["rsub"], "testuserid"); }); // check if input cookies are missing, an appropriate error is thrown // Failure condition: if valid cookies are set in the refresh call the test will fail it("test that if input cookies are missing, an appropriate error is thrown", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -198,7 +205,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -235,10 +242,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { // check if input cookies are there, no error is thrown // Failure condition: if cookies are no set in the refresh call the test will fail it("test that if input cookies are there, no error is thrown", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -252,7 +259,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -289,10 +296,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { //- check for token theft detection it("token theft detection", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -305,7 +312,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let response = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -342,11 +349,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("token theft detection with API key", async function () { - await setKeyValueInConfig("api_keys", "shfo3h98308hOIHoei309saiho"); - await startST(); + const connectionURI = await startST({ coreConfig: { api_keys: "shfo3h98308hOIHoei309saiho" } }); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, apiKey: "shfo3h98308hOIHoei309saiho", }, appInfo: { @@ -360,7 +366,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let response = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -397,11 +403,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("query without API key", async function () { - await setKeyValueInConfig("api_keys", "shfo3h98308hOIHoei309saiho"); - await startST(); + const connectionURI = await startST({ coreConfig: { api_keys: "shfo3h98308hOIHoei309saiho" } }); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -426,10 +431,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { //check basic usage of session it("test basic usage of sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -444,7 +449,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let response = await SessionFunctions.createNewSession( s.recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -511,10 +516,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { //check session verify for with / without anti-csrf present it("test session verify with anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -529,7 +534,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let response = await SessionFunctions.createNewSession( s.recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -543,7 +548,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { true ); assert(response2.session != undefined); - assert(Object.keys(response2.session).length === 5); + assert.strictEqual(Object.keys(response2.session).length, 6); let response3 = await SessionFunctions.getSession( s.recipeInterfaceImpl.helpers, @@ -553,15 +558,15 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { true ); assert(response3.session != undefined); - assert(Object.keys(response3.session).length === 5); + assert.strictEqual(Object.keys(response3.session).length, 6); }); //check session verify for with / without anti-csrf present** it("test session verify without anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -576,7 +581,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let response = await SessionFunctions.createNewSession( s.recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -592,7 +597,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { ); assert.notStrictEqual(response2.session, undefined); - assert.strictEqual(Object.keys(response2.session).length, 5); + assert.strictEqual(Object.keys(response2.session).length, 6); //passing anti-csrf token as undefined and anti-csrf check as true try { @@ -613,10 +618,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { //check revoking session(s) it("test revoking of sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -628,16 +633,37 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //create a single session and revoke using the session handle - let res = await SessionFunctions.createNewSession(s.helpers, "public", "someUniqueUserId", false, {}, {}); + let res = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId("someUniqueUserId"), + false, + {}, + {} + ); let res2 = await SessionFunctions.revokeSession(s.helpers, res.session.handle); assert(res2 === true); - let res3 = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId"); + let res3 = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId", true); assert(res3.length === 0); //create multiple sessions with the same userID and use revokeAllSessionsForUser to revoke sessions - await SessionFunctions.createNewSession(s.helpers, "public", "someUniqueUserId", false, {}, {}); - await SessionFunctions.createNewSession(s.helpers, "public", "someUniqueUserId", false, {}, {}); + await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId("someUniqueUserId"), + false, + {}, + {} + ); + await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId("someUniqueUserId"), + false, + {}, + {} + ); let sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId"); assert(sessionIdResponse.length === 2); @@ -659,10 +685,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { //check manipulating session data it("test manipulating session data", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -674,7 +700,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, {}); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }); let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; @@ -691,10 +717,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test manipulating session data with new get session function", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -714,7 +740,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, {}); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }); let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); @@ -731,10 +757,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test null and undefined values passed for session data", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -746,7 +772,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, null); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, null); let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; assert.deepStrictEqual(res2, {}); @@ -773,10 +799,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test null and undefined values passed for session data with new get session method", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -796,7 +822,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, null); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, null); let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); assert.deepStrictEqual(res2.sessionDataInDatabase, {}); @@ -824,10 +850,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { //check manipulating jwt payload it("test manipulating jwt payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -839,7 +865,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, {}); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); @@ -859,10 +885,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test manipulating jwt payload with new get session method", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -882,7 +908,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, {}); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); @@ -900,10 +926,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test null and undefined values passed for jwt payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -915,7 +941,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, null, {}); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, null, {}); let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)) .customClaimsInAccessTokenPayload; @@ -947,10 +973,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test null and undefined values passed for jwt payload with new get session method", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -970,7 +996,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, null, {}); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, null, {}); let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); assert.deepStrictEqual(res2.customClaimsInAccessTokenPayload, {}); @@ -998,10 +1024,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { //if anti-csrf is disabled from ST core, check that not having that in input to verify session is fine** it("test that when anti-csrf is disabled from ST core not having that in input to verify session is fine", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1013,7 +1039,14 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; s.helpers.config = { antiCsrf: "NONE", useDynamicAccessTokenSigningKey: true }; - let response = await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, {}); + let response = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId(""), + false, + {}, + {} + ); //passing anti-csrf token as undefined and anti-csrf check as false let response2 = await SessionFunctions.getSession( @@ -1024,7 +1057,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { true ); assert(response2.session != undefined); - assert(Object.keys(response2.session).length === 5); + assert.strictEqual(Object.keys(response2.session).length, 6); //passing anti-csrf token as undefined and anti-csrf check as true let response3 = await SessionFunctions.getSession( @@ -1035,15 +1068,15 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { true ); assert(response3.session != undefined); - assert(Object.keys(response3.session).length === 5); + assert.strictEqual(Object.keys(response3.session).length, 6); }); it("test that anti-csrf disabled and sameSite none does not throw an error", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1057,10 +1090,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test that anti-csrf disabled and sameSite lax does now throw an error", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1073,14 +1106,14 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, {}); + await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); }); it("test that anti-csrf disabled and sameSite strict does now throw an error", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1093,14 +1126,14 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, {}); + await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); }); it("test that custom user id is returned correctly", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1120,7 +1153,14 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", "customuserid", false, {}, null); + let res = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId("customuserid"), + false, + {}, + null + ); let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); @@ -1128,10 +1168,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test that get session by session handle payload is correct", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1151,7 +1191,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", "", false, {}, null); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, null); let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); assert(typeof res2.userId === "string"); @@ -1163,10 +1203,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("test that revoked session throws error when calling get session by session handle", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1186,7 +1226,14 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", "someid", false, {}, null); + let res = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId("someid"), + false, + {}, + null + ); let response = await SessionFunctions.revokeAllSessionsForUser(s.helpers, "someid"); assert(response.length === 1); @@ -1195,10 +1242,10 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); it("should use override functions in sessioncontainer methods", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1223,7 +1270,12 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { ], }); - const session = await Session.createNewSession(mockRequest(), mockResponse(), "public", "testId"); + const session = await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + SuperTokens.convertToRecipeUserId("testId") + ); const data = await session.getSessionDataFromDatabase(); diff --git a/test/session/accessTokenVersions.test.js b/test/session/accessTokenVersions.test.js index a2394a476..db46a7945 100644 --- a/test/session/accessTokenVersions.test.js +++ b/test/session/accessTokenVersions.test.js @@ -41,11 +41,11 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t }); describe("createNewSession", () => { - it("should create a V4 token", async function () { - await startST(); + it("should create a V5 token", async function () { + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -76,18 +76,18 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t assert(cookies.frontToken !== undefined); const parsedToken = parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny); - assert.strictEqual(parsedToken.version, 4); + assert.strictEqual(parsedToken.version, 5); const parsedHeader = JSON.parse(Buffer.from(parsedToken.header, "base64").toString()); assert.strictEqual(typeof parsedHeader.kid, "string"); assert(parsedHeader.kid.startsWith("d-")); }); - it("should create a V4 token signed by a static key if set in session recipe config", async function () { - await startST(); + it("should create a V5 token signed by a static key if set in session recipe config", async function () { + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -122,18 +122,18 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t assert(cookies.frontToken !== undefined); const parsedToken = parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny); - assert.strictEqual(parsedToken.version, 4); + assert.strictEqual(parsedToken.version, 5); const parsedHeader = JSON.parse(Buffer.from(parsedToken.header, "base64").toString()); assert.strictEqual(typeof parsedHeader.kid, "string"); assert(parsedHeader.kid.startsWith("s-")); }); - it("should throw an error when adding protected props", async function () { - await startST(); + it("should ignore protected props", async function () { + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -156,7 +156,7 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t }, }) ) - .expect(400) + .expect(200) .end((err, resp) => { if (err) { rej(err); @@ -167,16 +167,99 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t ); let cookies = extractInfoFromResponse(res); - assert.strictEqual(cookies.accessTokenFromAny, undefined); - assert.strictEqual(cookies.refreshTokenFromAny, undefined); - assert.strictEqual(cookies.frontToken, undefined); + assert.notEqual(cookies.accessTokenFromAny, undefined); + assert.notEqual(cookies.refreshTokenFromAny, undefined); + assert.notEqual(cookies.frontToken, undefined); + + const parsedToken = parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny); + assert.notEqual(parsedToken.payload.sub, "asdf"); + }); + + it("should ignore protected props when creating from prev payload", async function () { + const connectionURI = await startST(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init()], + }); + + const app = getTestExpressApp(); + + const testPropValue = Date.now(); + let res = await new Promise((res, rej) => + request(app) + .post("/create") + .type("application/json") + .send( + JSON.stringify({ + payload: { + custom: testPropValue, + }, + userId: "user1", + }) + ) + .expect(200) + .end((err, resp) => { + if (err) { + rej(err); + } else { + res(resp); + } + }) + ); + + let cookies = extractInfoFromResponse(res); + assert.notEqual(cookies.accessTokenFromAny, undefined); + assert.notEqual(cookies.refreshTokenFromAny, undefined); + assert.notEqual(cookies.frontToken, undefined); + + const parsedToken = parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny); + assert.equal(parsedToken.payload.sub, "user1"); + + let res2 = await new Promise((res, rej) => + request(app) + .post("/create") + .type("application/json") + .send( + JSON.stringify({ + payload: { + ...parsedToken.payload, + }, + userId: "user2", + }) + ) + .expect(200) + .end((err, resp) => { + if (err) { + rej(err); + } else { + res(resp); + } + }) + ); + + let cookies2 = extractInfoFromResponse(res2); + assert.notEqual(cookies2.accessTokenFromAny, undefined); + assert.notEqual(cookies2.refreshTokenFromAny, undefined); + assert.notEqual(cookies2.frontToken, undefined); + + const parsedToken2 = parseJWTWithoutSignatureVerification(cookies2.accessTokenFromAny); + assert.notEqual(parsedToken2.payload.sessionHandle, parsedToken.payload.sessionHandle); + assert.equal(parsedToken2.payload.sub, "user2"); + assert.equal(parsedToken2.payload.custom, testPropValue); }); it("should make sign in/up return a 500 when adding protected props", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -237,10 +320,10 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t describe("mergeIntoAccessTokenPayload", () => { it("should help migrating a v2 token using protected props", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -270,7 +353,6 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t const legacyRefreshToken = legacySessionResp.refreshToken.token; const app = getTestExpressApp(); - let mergeRes = await new Promise((res, rej) => request(app) .post("/merge-into-payload") @@ -321,12 +403,12 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t assert(cookiesAfterRefresh.refreshTokenFromAny !== undefined); assert(cookiesAfterRefresh.frontToken !== undefined); - assert.strictEqual(parseJWTWithoutSignatureVerification(cookiesAfterRefresh.accessTokenFromAny).version, 4); + assert.strictEqual(parseJWTWithoutSignatureVerification(cookiesAfterRefresh.accessTokenFromAny).version, 5); const parsedTokenAfterRefresh = parseJWTWithoutSignatureVerification( cookiesAfterRefresh.accessTokenFromAny ); - assert.strictEqual(parsedTokenAfterRefresh.version, 4); + assert.strictEqual(parsedTokenAfterRefresh.version, 5); assert.strictEqual(parsedTokenAfterRefresh.payload.sub, "test-user-id"); assert.strictEqual(parsedTokenAfterRefresh.payload.appSub, "asdf"); @@ -338,10 +420,10 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t }); it("should help migrating a v2 token using protected props when called using session handle", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -391,10 +473,10 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t assert(cookies.refreshTokenFromAny !== undefined); assert(cookies.frontToken !== undefined); - assert.strictEqual(parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny).version, 4); + assert.strictEqual(parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny).version, 5); const parsedToken = parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny); - assert.strictEqual(parsedToken.version, 4); + assert.strictEqual(parsedToken.version, 5); assert.strictEqual(parsedToken.payload.sub, "test-user-id"); assert.strictEqual(parsedToken.payload.appSub, "asdf"); @@ -406,10 +488,10 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t describe("verifySession", () => { it("should validate v2 tokens", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -464,10 +546,10 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t }); it("should validate v2 tokens with check database enabled", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -551,11 +633,176 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t }); }); + it("should validate v4 tokens", async function () { + const connectionURI = await startST(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init()], + }); + + // This CDI version is no longer supported by this SDK, but we want to ensure that sessions keep working after the upgrade + // We can hard-code the structure of the request&response, since this is a fixed CDI version and it's not going to change + Querier.apiVersion = "3.0"; + const legacySessionResp = await Querier.getNewInstanceOrThrowError().sendPostRequest( + new NormalisedURLPath("/recipe/session"), + { + userId: "test-user-id", + enableAntiCsrf: false, + userDataInJWT: {}, + userDataInDatabase: {}, + } + ); + Querier.apiVersion = undefined; + + const legacyToken = legacySessionResp.accessToken.token; + + const app = getTestExpressApp(); + + let res = await new Promise((resolve, reject) => + request(app) + .get("/verify") + .set("Authorization", `Bearer ${legacyToken}`) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + + assert.deepStrictEqual( + new Set(Object.keys(res.body.payload)), + new Set([ + "antiCsrfToken", + "exp", + "iat", + "parentRefreshTokenHash1", + "refreshTokenHash1", + "sessionHandle", + "sub", + "tId", + ]) + ); + delete res.body.payload; + assert.deepStrictEqual(res.body, { + message: true, + sessionExists: true, + sessionHandle: legacySessionResp.session.handle, + }); + }); + it("should validate v4 tokens with check database enabled", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init()], + }); + + // This CDI version is no longer supported by this SDK, but we want to ensure that sessions keep working after the upgrade + // We can hard-code the structure of the request&response, since this is a fixed CDI version and it's not going to change + Querier.apiVersion = "3.0"; + const legacySessionResp = await Querier.getNewInstanceOrThrowError().sendPostRequest( + new NormalisedURLPath("/recipe/session"), + { + userId: "test-user-id", + enableAntiCsrf: false, + userDataInJWT: {}, + userDataInDatabase: {}, + } + ); + Querier.apiVersion = undefined; + + const legacyToken = legacySessionResp.accessToken.token; + + const app = getTestExpressApp(); + + await new Promise((resolve, reject) => + request(app) + .get("/revoke-session") + .set("Authorization", `Bearer ${legacyToken}`) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + + let resDBCheck = await new Promise((resolve, reject) => + request(app) + .get("/verify-checkdb") + .set("Authorization", `Bearer ${legacyToken}`) + .expect(401) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + + assert.deepStrictEqual(resDBCheck.body, { message: "unauthorised" }); + + let resNoDBCheck = await new Promise((resolve, reject) => + request(app) + .get("/verify") + .set("Authorization", `Bearer ${legacyToken}`) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + + assert.deepStrictEqual( + new Set(Object.keys(resNoDBCheck.body.payload)), + new Set([ + "antiCsrfToken", + "exp", + "iat", + "parentRefreshTokenHash1", + "refreshTokenHash1", + "sessionHandle", + "sub", + "tId", + ]) + ); + delete resNoDBCheck.body.payload; + + assert.deepStrictEqual(resNoDBCheck.body, { + message: true, + sessionExists: true, + sessionHandle: legacySessionResp.session.handle, + }); + }); + + it("should validate v5 tokens with check database enabled", async function () { + const connectionURI = await startST(); + SuperTokens.init({ + supertokens: { + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -634,10 +881,10 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t }); it("should not validate token signed by a static key if not set in session recipe config", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -674,7 +921,7 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t resetAll(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -708,10 +955,10 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t describe("refresh session", () => { it("should refresh legacy sessions to new version", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -758,14 +1005,14 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t assert(cookies.refreshTokenFromAny !== undefined); assert(cookies.frontToken !== undefined); - assert.strictEqual(parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny).version, 4); + assert.strictEqual(parseJWTWithoutSignatureVerification(cookies.accessTokenFromAny).version, 5); }); it("should throw when refreshing legacy session with protected prop in payload", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -875,8 +1122,16 @@ function getTestExpressApp() { app.use(json()); app.post("/create", async (req, res) => { + const userId = req.body.userId || ""; try { - await Session.createNewSession(req, res, "public", "", req.body.payload, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId(userId), + req.body.payload, + {} + ); res.status(200).send(""); } catch (ex) { res.status(400).json({ message: ex.message }); diff --git a/test/session/claims/createNewSession.test.js b/test/session/claims/createNewSession.test.js index 238bb254e..b4b0f2604 100644 --- a/test/session/claims/createNewSession.test.js +++ b/test/session/claims/createNewSession.test.js @@ -33,11 +33,11 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea describe("createNewSession", () => { it("should create access token payload w/ session claims", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -53,7 +53,7 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea createNewSession: async (input) => { input.accessTokenPayload = { ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), + ...(await TrueClaim.build(input.userId, input.recipeUserId, input.userContext)), }; return oI.createNewSession(input); }, @@ -64,10 +64,15 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea }); const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const res = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 10); + assert.equal(Object.keys(payload).length, 11); assert.ok(payload["iss"], "http://api.supertokens.io/auth"); assert.ok(payload["st-true"]); assert.equal(payload["st-true"].v, true); @@ -75,10 +80,10 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea }); it("should create access token payload wo/ session claims with an undefined value", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -96,7 +101,7 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea ...input.accessTokenPayload, ...(await UndefinedClaim.build( input.userId, - input.accessTokenPayload, + input.recipeUserId, input.userContext )), }; @@ -109,13 +114,18 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea }); const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const res = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 9); + assert.equal(Object.keys(payload).length, 10); }); it("should merge claims and the passed access token payload obj", async function () { - await startST(); + const connectionURI = await startST(); const payloadParam = { initial: true }; const custom2 = { undef: undefined, nullProp: null, inner: "asdf" }; const customClaims = { @@ -125,7 +135,7 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea }; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -141,7 +151,7 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea createNewSession: async (input) => { input.accessTokenPayload = { ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), + ...(await TrueClaim.build(input.userId, input.recipeUserId, input.userContext)), ...customClaims, }; return oI.createNewSession(input); @@ -153,13 +163,19 @@ describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/crea }); const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "public", "someId", payloadParam); + const res = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId"), + payloadParam + ); // The passed object should be unchanged assert.strictEqual(Object.keys(payloadParam).length, 1); const payload = res.getAccessTokenPayload(); - assert.strictEqual(Object.keys(payload).length, 14); // 5 + 9 standard + assert.strictEqual(Object.keys(payload).length, 15); // 5 + 10 standard // We have the prop from the payload param assert.strictEqual(payload["initial"], true); // We have the boolean claim diff --git a/test/session/claims/fetchAndSetClaim.test.js b/test/session/claims/fetchAndSetClaim.test.js index a9cc3717f..9ac87867d 100644 --- a/test/session/claims/fetchAndSetClaim.test.js +++ b/test/session/claims/fetchAndSetClaim.test.js @@ -64,11 +64,11 @@ describe(`sessionClaims/fetchAndSetClaim: ${printPath("[test/session/claims/fetc }); it("should update using a handle if claim fetchValue returns a value", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -79,7 +79,12 @@ describe(`sessionClaims/fetchAndSetClaim: ${printPath("[test/session/claims/fetc }); const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const res = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); await Session.fetchAndSetClaim(res.getHandle(), TrueClaim); diff --git a/test/session/claims/getClaimValue.test.js b/test/session/claims/getClaimValue.test.js index 7892417db..ac097f2ea 100644 --- a/test/session/claims/getClaimValue.test.js +++ b/test/session/claims/getClaimValue.test.js @@ -38,11 +38,11 @@ describe(`sessionClaims/getClaimValue: ${printPath("[test/session/claims/getClai }); it("should get the right value", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -69,18 +69,23 @@ describe(`sessionClaims/getClaimValue: ${printPath("[test/session/claims/getClai }); const response = mockResponse(); - const session = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const session = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const res = await session.getClaimValue(TrueClaim); assert.equal(res, true); }); it("should get the right value using session handle", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -107,7 +112,12 @@ describe(`sessionClaims/getClaimValue: ${printPath("[test/session/claims/getClai }); const response = mockResponse(); - const session = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const session = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const res = await Session.getClaimValue(session.getHandle(), TrueClaim); assert.deepStrictEqual(res, { @@ -117,11 +127,11 @@ describe(`sessionClaims/getClaimValue: ${printPath("[test/session/claims/getClai }); it("should work for not existing handle", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/session/claims/removeClaim.test.js b/test/session/claims/removeClaim.test.js index 05f2c6695..583d27c50 100644 --- a/test/session/claims/removeClaim.test.js +++ b/test/session/claims/removeClaim.test.js @@ -49,11 +49,11 @@ describe(`sessionClaims/removeClaim: ${printPath("[test/session/claims/removeCla }); it("should clear previously set claim", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -80,10 +80,15 @@ describe(`sessionClaims/removeClaim: ${printPath("[test/session/claims/removeCla }); const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const res = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 10); + assert.equal(Object.keys(payload).length, 11); assert.ok(payload["st-true"]); assert.equal(payload["st-true"].v, true); assert(payload["st-true"].t > Date.now() - 10000); @@ -91,15 +96,15 @@ describe(`sessionClaims/removeClaim: ${printPath("[test/session/claims/removeCla await res.removeClaim(TrueClaim); const payloadAfter = res.getAccessTokenPayload(); - assert.equal(Object.keys(payloadAfter).length, 9); + assert.equal(Object.keys(payloadAfter).length, 10); }); it("should clear previously set claim using a handle", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -126,10 +131,15 @@ describe(`sessionClaims/removeClaim: ${printPath("[test/session/claims/removeCla }); const response = mockResponse(); - const session = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const session = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const payload = session.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 10); + assert.equal(Object.keys(payload).length, 11); assert.ok(payload["st-true"]); assert.equal(payload["st-true"].v, true); assert(payload["st-true"].t > Date.now() - 10000); @@ -143,11 +153,11 @@ describe(`sessionClaims/removeClaim: ${printPath("[test/session/claims/removeCla }); it("should work ok for not existing handle", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/session/claims/setClaimValue.test.js b/test/session/claims/setClaimValue.test.js index f877afdde..01e96d29d 100644 --- a/test/session/claims/setClaimValue.test.js +++ b/test/session/claims/setClaimValue.test.js @@ -56,11 +56,11 @@ describe(`sessionClaims/setClaimValue: ${printPath("[test/session/claims/setClai }); it("should overwrite claim value", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -87,10 +87,15 @@ describe(`sessionClaims/setClaimValue: ${printPath("[test/session/claims/setClai }); const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const res = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 10); + assert.equal(Object.keys(payload).length, 11); assert.ok(payload["st-true"]); assert.equal(payload["st-true"].v, true); assert(payload["st-true"].t > Date.now() - 2000); @@ -98,18 +103,18 @@ describe(`sessionClaims/setClaimValue: ${printPath("[test/session/claims/setClai await res.setClaimValue(TrueClaim, false); const payloadAfter = res.getAccessTokenPayload(); - assert.equal(Object.keys(payloadAfter).length, 10); + assert.equal(Object.keys(payloadAfter).length, 11); assert.ok(payloadAfter["st-true"]); assert.equal(payloadAfter["st-true"].v, false); assert(payloadAfter["st-true"].t > payload["st-true"].t); }); it("should overwrite claim value using session handle", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -136,10 +141,15 @@ describe(`sessionClaims/setClaimValue: ${printPath("[test/session/claims/setClai }); const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const res = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 10); + assert.equal(Object.keys(payload).length, 11); assert.ok(payload["st-true"]); assert.equal(payload["st-true"].v, true); assert(payload["st-true"].t > Date.now() - 10000); @@ -155,11 +165,11 @@ describe(`sessionClaims/setClaimValue: ${printPath("[test/session/claims/setClai }); it("should work ok for not existing handle", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/session/claims/validateClaimsForSessionHandle.test.js b/test/session/claims/validateClaimsForSessionHandle.test.js index 9a671c086..c9d8d343a 100644 --- a/test/session/claims/validateClaimsForSessionHandle.test.js +++ b/test/session/claims/validateClaimsForSessionHandle.test.js @@ -40,11 +40,11 @@ describe(`sessionClaims/validateClaimsForSessionHandle: ${printPath( }); it("should return the right validation errors", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -71,7 +71,12 @@ describe(`sessionClaims/validateClaimsForSessionHandle: ${printPath( }); const response = mockResponse(); - const session = await Session.createNewSession(mockRequest(), response, "public", "someId"); + const session = await Session.createNewSession( + mockRequest(), + response, + "public", + SuperTokens.convertToRecipeUserId("someId") + ); const failingValidator = UndefinedClaim.validators.hasValue(true); assert.deepStrictEqual( @@ -96,11 +101,11 @@ describe(`sessionClaims/validateClaimsForSessionHandle: ${printPath( }); it("should work for not existing handle", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/session/claims/verifySession.test.js b/test/session/claims/verifySession.test.js index 59ea8f09d..49d0fb7a1 100644 --- a/test/session/claims/verifySession.test.js +++ b/test/session/claims/verifySession.test.js @@ -26,6 +26,7 @@ const request = require("supertest"); const { TrueClaim, UndefinedClaim } = require("./testClaims"); const sinon = require("sinon"); const { default: SessionError } = require("../../../lib/build/recipe/session/error"); +const { default: RecipeUserId } = require("../../../lib/build/recipeUserId"); describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifySession.test.js]")}`, function () { beforeEach(async function () { @@ -46,10 +47,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS describe("verifySession", () => { describe("with getGlobalClaimValidators override", () => { it("should allow without claims required or present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -66,10 +67,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS }); it("should allow with claim valid after refetching", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -100,10 +101,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS }); it("should reject with claim required but not added", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -144,14 +145,14 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS }); it("should allow with custom validator returning true", async function () { - await startST(); + const connectionURI = await startST(); const customValidator = { id: "testid", validate: () => ({ isValid: true }), }; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -182,14 +183,14 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS }); it("should reject with custom validator returning false", async function () { - await startST(); + const connectionURI = await startST(); const customValidator = { id: "testid", validate: () => ({ isValid: false }), }; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -222,14 +223,14 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS }); it("should reject with validator returning false with reason", async function () { - await startST(); + const connectionURI = await startST(); const customValidator = { id: "testid", validate: () => ({ isValid: false, reason: "testReason" }), }; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -269,8 +270,11 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS .create("validateClaims") .once() .withArgs({ - accessTokenPayload: sinon.match.object, userId: "testing-userId", + recipeUserId: sinon.match + .has("recipeUserId", "testing-userId") + .and(sinon.match.instanceOf(RecipeUserId)), + accessTokenPayload: sinon.match.object, claimValidators: testValidatorArr, userContext: { _default: { @@ -286,10 +290,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS }, ], }); - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -330,6 +334,9 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS .withArgs({ accessTokenPayload: sinon.match.object, userId: "testing-userId", + recipeUserId: sinon.match + .has("recipeUserId", "testing-userId") + .and(sinon.match.instanceOf(RecipeUserId)), claimValidators: testValidatorArr, userContext: { _default: { @@ -340,10 +347,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS .resolves({ invalidClaims: [], }); - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -376,10 +383,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS describe("with overrideGlobalClaimValidators", () => { it("should allow with empty list as override", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -415,10 +422,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS }); it("should allow with refetched claim", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -454,10 +461,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS }); it("should reject with invalid refetched claim", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -501,10 +508,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS validate: () => ({ isValid: false, reason: "testReason" }), }; - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -550,10 +557,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS validate: () => ({ isValid: true }), }; - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -598,10 +605,10 @@ describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifyS validate: () => ({ isValid: false, reason: "testReason" }), }; - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -693,7 +700,15 @@ function getTestApp(endpoints) { app.use(express.json()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "testing-userId", req.body, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("testing-userId"), + undefined, + req.body, + {} + ); res.status(200).json({ message: true }); }); diff --git a/test/session/exposeAccessTokenToFrontendInCookieBasedAuth.test.js b/test/session/exposeAccessTokenToFrontendInCookieBasedAuth.test.js index 436a8ee21..ab2ad0ebb 100644 --- a/test/session/exposeAccessTokenToFrontendInCookieBasedAuth.test.js +++ b/test/session/exposeAccessTokenToFrontendInCookieBasedAuth.test.js @@ -65,10 +65,10 @@ function getTestCases(exposeAccessTokenToFrontendInCookieBasedAuth) { const sessionConfig = { exposeAccessTokenToFrontendInCookieBasedAuth, getTokenTransferMethod: () => "cookie" }; describe("createNewSession", () => { it("should attach the appropriate tokens", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -98,10 +98,10 @@ function getTestCases(exposeAccessTokenToFrontendInCookieBasedAuth) { describe("mergeIntoAccessTokenPayload", () => { it("should attach the appropriate tokens", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -156,10 +156,10 @@ function getTestCases(exposeAccessTokenToFrontendInCookieBasedAuth) { describe("verifySession", () => { it("should attach the appropriate tokens after refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -218,10 +218,10 @@ function getTestCases(exposeAccessTokenToFrontendInCookieBasedAuth) { describe("refresh session", () => { it("should attach the appropriate tokens", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -286,7 +286,14 @@ function getTestExpressApp() { app.post("/create", async (req, res) => { try { - await Session.createNewSession(req, res, "public", "", req.body.payload, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId(""), + req.body.payload, + {} + ); res.status(200).send(""); } catch (ex) { res.status(400).json({ message: ex.message }); diff --git a/test/session/jwksCache.test.js b/test/session/jwksCache.test.js index 9ecd96280..ce2999783 100644 --- a/test/session/jwksCache.test.js +++ b/test/session/jwksCache.test.js @@ -46,10 +46,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct it("should fetch the keys as expected", async () => { clock = sinon.useFakeTimers({ shouldAdvanceTime: true, now: Date.now() }); - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -59,7 +59,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct recipeList: [Session.init()], }); - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); assert.strictEqual(requestMock.callCount, 0); @@ -74,10 +77,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct it("should not re-fetch if the cache is available", async () => { clock = sinon.useFakeTimers({ shouldAdvanceTime: true, now: Date.now() }); - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -87,7 +90,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct recipeList: [Session.init()], }); - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); assert.strictEqual(requestMock.callCount, 0); @@ -100,11 +106,12 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct }); it("should re-fetch keys for unknown kid", async () => { - await setKeyValueInConfig("access_token_dynamic_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); + const connectionURI = await startST({ + coreConfig: { access_token_dynamic_signing_key_update_interval: "0.001" }, + }); // 5 seconds is the update interval SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -115,7 +122,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct }); // We create a new token - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); assert.strictEqual(requestMock.callCount, 0); @@ -125,7 +135,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct // We wait for signing key to expire await new Promise((r) => setTimeout(r, 6000)); - const createResWithNewKey = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createResWithNewKey = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokensWithNewKey = createResWithNewKey.getAllSessionTokensDangerously(); assert.ok( await Session.getSessionWithoutRequestResponse(tokensWithNewKey.accessToken, tokensWithNewKey.antiCsrfToken) @@ -139,10 +152,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct }); it("should throw if jwks endpoint errors", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -153,7 +166,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct }); // We create a new token - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); // Check that the keys have not been loaded @@ -173,10 +189,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct it("should re-fetch after the cache expires", async () => { clock = sinon.useFakeTimers({ shouldAdvanceTime: true, now: Date.now() }); - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -186,7 +202,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct recipeList: [Session.init()], }); - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); assert.strictEqual(requestMock.callCount, 0); @@ -208,10 +227,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct }); it("jwks multiple cores work ok", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080;http://localhost:8081", + connectionURI: `${connectionURI};http://localhost:8081`, }, appInfo: { apiDomain: "api.supertokens.io", @@ -222,7 +241,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct }); // We create a new token - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); // Check that the keys have not been loaded @@ -233,11 +255,11 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct }); it("jwks endpoint throwing should fall back to next core if fetching throws", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { // We are using two valid core values here, but make it throw below - connectionURI: "http://localhost:8080;http://localhost:8080", + connectionURI: `${connectionURI};${connectionURI}`, }, appInfo: { apiDomain: "api.supertokens.io", @@ -249,7 +271,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct await new Promise((r) => setTimeout(r, 300)); // We create a new token - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); // Check that the keys have not been loaded @@ -262,10 +287,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct }); it("jwks endpoint throwing should fall back to next core if fetching returns the wrong status/shape", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -275,7 +300,10 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct recipeList: [Session.init()], }); - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); resetAll(); @@ -284,7 +312,7 @@ describe(`JWKs caching: ${printPath("[test/session/jwksCache.test.js]")}`, funct // meaning we have to use a separate config SuperTokens.init({ supertokens: { - connectionURI: "http://example.com;http://localhost:8080", + connectionURI: `http://example.com;${connectionURI}`, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/session/sessionHandlingFuncsWithoutReq.test.js b/test/session/sessionHandlingFuncsWithoutReq.test.js index b5d25eca3..b09d0f223 100644 --- a/test/session/sessionHandlingFuncsWithoutReq.test.js +++ b/test/session/sessionHandlingFuncsWithoutReq.test.js @@ -35,10 +35,10 @@ describe(`Session handling functions without modifying response: ${printPath( describe("createNewSessionWithoutRequestResponse", () => { it("should create a new session", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -50,7 +50,7 @@ describe(`Session handling functions without modifying response: ${printPath( const res = await Session.createNewSessionWithoutRequestResponse( "public", - "test-user-id", + SuperTokens.convertToRecipeUserId("test-user-id"), { tokenProp: true }, { dbProp: true } ); @@ -68,10 +68,10 @@ describe(`Session handling functions without modifying response: ${printPath( }); it("should create a new session w/ anti-csrf", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -87,7 +87,7 @@ describe(`Session handling functions without modifying response: ${printPath( const session = await Session.createNewSessionWithoutRequestResponse( "public", - "test-user-id", + SuperTokens.convertToRecipeUserId("test-user-id"), { tokenProp: true }, { dbProp: true } ); @@ -106,10 +106,10 @@ describe(`Session handling functions without modifying response: ${printPath( describe("getSessionWithoutRequestResponse", () => { it("should validate basic access token", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -119,7 +119,10 @@ describe(`Session handling functions without modifying response: ${printPath( recipeList: [Session.init()], }); - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); const session = await Session.getSessionWithoutRequestResponse(tokens.accessToken, tokens.antiCsrfToken); assert.ok(session); @@ -135,10 +138,10 @@ describe(`Session handling functions without modifying response: ${printPath( }); it("should validate basic access token with anti-csrf", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -152,7 +155,10 @@ describe(`Session handling functions without modifying response: ${printPath( ], }); - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); const session = await Session.getSessionWithoutRequestResponse(tokens.accessToken, tokens.antiCsrfToken); @@ -186,10 +192,10 @@ describe(`Session handling functions without modifying response: ${printPath( }); it("should validate access tokens created by createJWT", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -199,7 +205,10 @@ describe(`Session handling functions without modifying response: ${printPath( recipeList: [Session.init(), JWT.init()], }); - const session = await Session.createNewSessionWithoutRequestResponse("public", "testId"); + const session = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("testId") + ); const originalPayload = session.getAccessTokenPayload(); const customAccessToken = await JWT.createJWT( @@ -214,15 +223,14 @@ describe(`Session handling functions without modifying response: ${printPath( const customSession = await Session.getSessionWithoutRequestResponse(customAccessToken.jwt); const customPayload = customSession.getAccessTokenPayload(); - - assert.notEqual(customPayload.exp, originalPayload.exp); + assert.strictEqual(customPayload.exp - customPayload.iat, 1234); }); it("should validate access tokens created by createJWT w/ checkDatabase", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -240,7 +248,10 @@ describe(`Session handling functions without modifying response: ${printPath( return; } - const session = await Session.createNewSessionWithoutRequestResponse("public", "testId"); + const session = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("testId") + ); const originalPayload = session.getAccessTokenPayload(); const customAccessToken = await JWT.createJWT( @@ -258,8 +269,7 @@ describe(`Session handling functions without modifying response: ${printPath( checkDatabase: true, }); const customPayload = customSession.getAccessTokenPayload(); - - assert.notEqual(customPayload.exp, originalPayload.exp); + assert.strictEqual(customPayload.exp - customPayload.iat, 1234); }); it("should return error for non-tokens", async () => { @@ -312,10 +322,10 @@ describe(`Session handling functions without modifying response: ${printPath( }); it("should return error for claim validation failures", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -325,7 +335,10 @@ describe(`Session handling functions without modifying response: ${printPath( recipeList: [Session.init()], }); - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); let caught; try { @@ -345,10 +358,10 @@ describe(`Session handling functions without modifying response: ${printPath( describe("refreshSessionWithoutRequestResponse", () => { it("should refresh session", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -360,7 +373,7 @@ describe(`Session handling functions without modifying response: ${printPath( const createRes = await Session.createNewSessionWithoutRequestResponse( "public", - "test-user-id", + SuperTokens.convertToRecipeUserId("test-user-id"), { tokenProp: true }, { dbProp: true } ); @@ -384,10 +397,10 @@ describe(`Session handling functions without modifying response: ${printPath( }); it("should work with anti-csrf", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -401,7 +414,10 @@ describe(`Session handling functions without modifying response: ${printPath( ], }); - const createRes = await Session.createNewSessionWithoutRequestResponse("public", "test-user-id"); + const createRes = await Session.createNewSessionWithoutRequestResponse( + "public", + SuperTokens.convertToRecipeUserId("test-user-id") + ); const tokens = createRes.getAllSessionTokensDangerously(); const session = await Session.refreshSessionWithoutRequestResponse( @@ -433,10 +449,10 @@ describe(`Session handling functions without modifying response: ${printPath( }); it("should return error for non-tokens", async () => { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/sessionAccessTokenSigningKeyUpdate.test.js b/test/sessionAccessTokenSigningKeyUpdate.test.js index 76af0b4be..ef38b07b0 100644 --- a/test/sessionAccessTokenSigningKeyUpdate.test.js +++ b/test/sessionAccessTokenSigningKeyUpdate.test.js @@ -12,7 +12,16 @@ * License for the specific language governing permissions and limitations * under the License. */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig, killAllSTCoresOnly } = require("./utils"); +const { + printPath, + setupST, + startST, + killAllST, + cleanST, + setKeyValueInConfig, + killAllSTCoresOnly, + mockRequest, +} = require("./utils"); let assert = require("assert"); let { Querier } = require("../lib/build/querier"); let { ProcessState, PROCESS_STATE } = require("../lib/build/processState"); @@ -25,6 +34,7 @@ const { maxVersion } = require("../lib/build/utils"); const { fail } = require("assert"); const sinon = require("sinon"); const request = require("http"); +const { default: RecipeUserId } = require("../lib/build/recipeUserId"); /* TODO: - the opposite of the above (check that if signing key changes, things are still fine) condition @@ -57,11 +67,12 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( }); it("check that if signing key changes, things are still fine", async function () { - await setKeyValueInConfig("access_token_dynamic_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); + const connectionURI = await startST({ + coreConfig: { access_token_dynamic_signing_key_update_interval: "0.001" }, + }); // 5 seconds is the update interval SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -75,7 +86,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( let response = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -192,11 +203,12 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( }); it("check that if signing key changes, after new key is fetched - via token query, old tokens don't query the core", async function () { - await setKeyValueInConfig("access_token_dynamic_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); + const connectionURI = await startST({ + coreConfig: { access_token_dynamic_signing_key_update_interval: "0.001" }, + }); // 5 seconds is the update interval SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -209,7 +221,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( const oldSession = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -218,7 +230,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -229,7 +241,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( const newSession = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -275,11 +287,12 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( }); it("check that if signing key changes, after new key is fetched - via creation of new token, old tokens don't query the core", async function () { - await setKeyValueInConfig("access_token_dynamic_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); + const connectionURI = await startST({ + coreConfig: { access_token_dynamic_signing_key_update_interval: "0.001" }, + }); // 5 seconds is the update interval SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -292,7 +305,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( let response2 = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -303,7 +316,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( let response = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -356,11 +369,12 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( }); it("check that if signing key changes, after new key is fetched - via verification of old token, old tokens don't query the core", async function () { - await setKeyValueInConfig("access_token_dynamic_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); + const connectionURI = await startST({ + coreConfig: { access_token_dynamic_signing_key_update_interval: "0.001" }, + }); // 5 seconds is the update interval SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -375,7 +389,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( let response2 = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, @@ -387,7 +401,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( let response = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -441,11 +455,16 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( }); it("test reducing access token signing key update interval time", async function () { - await setKeyValueInConfig("access_token_dynamic_signing_key_update_interval", "0.0041"); // 10 seconds - await startST(); + const appId = "testapp-" + Date.now(); + const connectionURI = await startST({ + appId, + coreConfig: { + access_token_dynamic_signing_key_update_interval: "0.0041", // 10 seconds + }, + }); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -458,7 +477,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( let session = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -485,7 +504,12 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( await setupST(); // start server again - await startST(); + await startST({ + appId, + coreConfig: { + access_token_dynamic_signing_key_update_interval: "0.0041", // 10 seconds + }, + }); { await SessionFunctions.getSession( @@ -506,7 +530,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( let session2 = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} @@ -583,12 +607,15 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( }); it("no access token signing key update", async function () { - await setKeyValueInConfig("access_token_dynamic_signing_key_update_interval", "0.0011"); // 4 seconds - await setKeyValueInConfig("access_token_signing_key_dynamic", "false"); - await startST(); + const connectionURI = await startST({ + coreConfig: { + access_token_signing_key_dynamic: "false", + access_token_dynamic_signing_key_update_interval: "0.0011", // 4 seconds + }, + }); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -609,7 +636,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( let session = await SessionFunctions.createNewSession( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, "public", - "", + new RecipeUserId(""), false, {}, {} diff --git a/test/sessionExpress.test.js b/test/sessionExpress.test.js index 4fdf3f09d..51122336c 100644 --- a/test/sessionExpress.test.js +++ b/test/sessionExpress.test.js @@ -48,10 +48,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi // check if disabling api, the default refresh API does not work - you get a 404 it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -77,7 +77,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -115,10 +115,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -144,7 +144,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -181,10 +181,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //- check for token theft detection it("express token theft detection", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -195,7 +195,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi Session.init({ getTokenTransferMethod: () => "cookie", errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { response.sendJSONResponse({ success: true, }); @@ -208,7 +208,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -296,10 +296,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //- check for token theft detection it("express token theft detection with auto refresh middleware", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -314,7 +314,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -391,10 +391,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //check basic usage of session it("test basic usage of express sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -407,7 +407,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -540,10 +540,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test basic usage of express sessions with headers", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -561,7 +561,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -698,10 +698,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test that if accessTokenPath is set to custom /access, then path of accessToken from session is equal to this", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -720,7 +720,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); const res = await new Promise((resolve) => @@ -746,10 +746,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test that if default accessTokenPath is used, then path of accessToken from session is equal to slash", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -767,7 +767,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); const res = await new Promise((resolve) => @@ -793,10 +793,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test signout API works", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -809,7 +809,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -850,10 +850,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test signout API works if even session is deleted on the backend after creation", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -868,7 +868,14 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi let sessionHandle = ""; app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "public", "", {}, {}); + let session = await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId(""), + {}, + {} + ); sessionHandle = session.getHandle(); res.status(200).send(""); }); @@ -913,11 +920,11 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //check basic usage of session it("test basic usage of express sessions with auto refresh", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -933,7 +940,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -1065,10 +1072,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //check session verify for with / without anti-csrf present it("test express session verify with anti-csrf present", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1080,7 +1087,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); res.status(200).send(""); }); @@ -1142,10 +1149,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi // check session verify for with / without anti-csrf present it("test session verify without anti-csrf present express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1158,7 +1165,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); res.status(200).send(""); }); @@ -1224,10 +1231,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //check revoking session(s)** it("test revoking express sessions", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1238,11 +1245,18 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); app.post("/usercreate", async (req, res) => { - await Session.createNewSession(req, res, "public", "someUniqueUserId", {}, {}); + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId("someUniqueUserId"), + {}, + {} + ); res.status(200).send(""); }); app.post("/session/revoke", async (req, res) => { @@ -1355,10 +1369,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //check manipulating session data it("test manipulating session data with express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1370,7 +1384,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); app.post("/updateSessionData", async (req, res) => { @@ -1501,10 +1515,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //check manipulating jwt payload it("test manipulating jwt payload with express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1515,7 +1529,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "user1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("user1"), {}, {}); res.status(200).send(""); }); app.post("/updateAccessTokenPayload", async (req, res) => { @@ -1568,7 +1582,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //call the updateAccessTokenPayload api to add jwt payload let updatedResponse = extractInfoFromResponse( @@ -1593,7 +1607,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); //call the getAccessTokenPayload api to get jwt payload let response2 = await new Promise((resolve) => @@ -1636,7 +1650,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.key, "value"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 10); + assert.strictEqual(Object.keys(frontendInfo.up).length, 11); // change the value of the inserted jwt payload let updatedResponse2 = extractInfoFromResponse( @@ -1660,7 +1674,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi assert(frontendInfo.uid === "user1"); assert.strictEqual(frontendInfo.up.sub, "user1"); assert.strictEqual(frontendInfo.up.exp, Math.floor(frontendInfo.ate / 1000)); - assert.strictEqual(Object.keys(frontendInfo.up).length, 9); + assert.strictEqual(Object.keys(frontendInfo.up).length, 10); //retrieve the changed jwt payload response2 = await new Promise((resolve) => @@ -1691,6 +1705,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi "sessionHandle", "sub", "tId", + "rsub", ]) ); assert.strictEqual(response2.body.iss, "https://api.supertokens.io/auth"); @@ -1714,10 +1729,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi // test with existing header params being there and that the lib appends to those and not overrides those it("test that express appends to existing header params and does not override", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1730,7 +1745,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.post("/create", async (req, res) => { res.header("testHeader", "testValue"); res.header("Access-Control-Expose-Headers", "customValue"); - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -1760,10 +1775,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi //if anti-csrf is disabled from ST core, check that not having that in input to verify session is fine** it("test that when anti-csrf is disabled from from ST core, not having to input in verify session is fine in express", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1775,7 +1790,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); res.status(200).send(""); }); @@ -1833,10 +1848,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test that getSession does not clear cookies if a session does not exist in the first place", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1883,10 +1898,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test that refreshSession does not clear cookies if a session does not exist in the first place", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1933,10 +1948,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test that when anti-csrf is enabled with custom header, and we don't provide that in verifySession, we get try refresh token", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1948,7 +1963,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi const app = express(); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); res.status(200).send(""); }); @@ -2077,10 +2092,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test resfresh API when using CUSTOM HEADER anti-csrf", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2093,7 +2108,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -2152,14 +2167,14 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test that init can be called post route and middleware declaration", async function () { - await startST(); + const connectionURI = await startST(); const app = express(); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "id1", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId("id1"), {}, {}); res.status(200).send(""); }); @@ -2176,7 +2191,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2235,7 +2250,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test overriding of sessions functions", async function () { - await startST(); + const connectionURI = await startST(); let createNewSessionCalled = false; let getSessionCalled = false; @@ -2243,7 +2258,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi let session = undefined; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2289,7 +2304,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -2431,12 +2446,12 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test overriding of sessions apis", async function () { - await startST(); + const connectionURI = await startST(); let signoutCalled = false; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2469,7 +2484,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -2521,13 +2536,13 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test overriding of sessions functions, error thrown", async function () { - await startST(); + const connectionURI = await startST(); let createNewSessionCalled = false; let session = undefined; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2564,7 +2579,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.post("/create", async (req, res, next) => { try { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); } catch (err) { next(err); @@ -2599,12 +2614,12 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test overriding of sessions apis, error thrown", async function () { - await startST(); + const connectionURI = await startST(); let signoutCalled = false; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2639,7 +2654,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -2694,10 +2709,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("check that refresh doesn't clear cookies if missing anti csrf via custom header", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2710,7 +2725,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -2752,10 +2767,10 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("check that refresh doesn't clear cookies if missing anti csrf via token", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2768,7 +2783,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -2809,11 +2824,11 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test session error handler overriding", async function () { - await startST(); + const connectionURI = await startST(); let testpass = false; SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2876,11 +2891,11 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test revoking a session during refresh with revokeSession function", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -2911,7 +2926,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -2966,11 +2981,11 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test revoking a session during refresh with revokeSession function and sending 401", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -3003,7 +3018,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -3058,11 +3073,11 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test revoking a session during refresh with throwing unauthorised error", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -3097,7 +3112,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); @@ -3152,11 +3167,11 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi }); it("test revoking a session during refresh fails if just sending 401", async function () { - await startST(); + const connectionURI = await startST(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -3188,7 +3203,7 @@ describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, functi app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", "", {}, {}); + await Session.createNewSession(req, res, "public", SuperTokens.convertToRecipeUserId(""), {}, {}); res.status(200).send(""); }); diff --git a/test/thirdparty/authorisationUrlFeature.test.js b/test/thirdparty/authorisationUrlFeature.test.js index e5a8f0acd..96ec415ff 100644 --- a/test/thirdparty/authorisationUrlFeature.test.js +++ b/test/thirdparty/authorisationUrlFeature.test.js @@ -71,12 +71,12 @@ describe(`authorisationTest: ${printPath("[test/thirdparty/authorisationFeature. }); it("test that using development OAuth keys will use the development authorisation url", async function () { - await startST(); + const connectionURI = await startST(); // testing with the google OAuth development key STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -133,12 +133,12 @@ describe(`authorisationTest: ${printPath("[test/thirdparty/authorisationFeature. }); it("test calling authorisation url API with empty init", async function () { - await startST(); + const connectionURI = await startST(); // testing with the google OAuth development key STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -179,12 +179,12 @@ describe(`authorisationTest: ${printPath("[test/thirdparty/authorisationFeature. }); it("test calling authorisation url API with empty init with dynamic third party provider", async function () { - await startST(); + const connectionURI = await startST(); // testing with the google OAuth development key STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -239,10 +239,10 @@ describe(`authorisationTest: ${printPath("[test/thirdparty/authorisationFeature. }); it("test minimum config for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -287,10 +287,10 @@ describe(`authorisationTest: ${printPath("[test/thirdparty/authorisationFeature. }); it("test thirdparty provider doesn't exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -329,10 +329,10 @@ describe(`authorisationTest: ${printPath("[test/thirdparty/authorisationFeature. }); it("test invalid GET params for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdparty/config.test.js b/test/thirdparty/config.test.js index c8ad2671f..6ff3dc805 100644 --- a/test/thirdparty/config.test.js +++ b/test/thirdparty/config.test.js @@ -37,11 +37,11 @@ describe(`configTest: ${printPath("[test/thirdparty/config.test.js]")}`, functio }); it("test config for thirdparty module, no provider passed", async function () { - await startST(); + const connectionURI = await startST(); try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -63,10 +63,10 @@ describe(`configTest: ${printPath("[test/thirdparty/config.test.js]")}`, functio }); it("test minimum config for thirdparty module, custom provider", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdparty/getUsersByEmailFeature.test.js b/test/thirdparty/getUsersByEmailFeature.test.js deleted file mode 100644 index 791fbac6f..000000000 --- a/test/thirdparty/getUsersByEmailFeature.test.js +++ /dev/null @@ -1,104 +0,0 @@ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyRecipe = require("../../lib/build/recipe/thirdparty/recipe").default; -const { manuallyCreateOrUpdateUser } = require("../../lib/build/recipe/thirdparty"); -const { getUsersByEmail } = require("../../lib/build/recipe/thirdparty"); -const { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`getUsersByEmail: ${printPath("[test/thirdparty/getUsersByEmailFeature.test.js]")}`, function () { - const MockThirdPartyProvider = { - config: { - thirdPartyId: "mock", - }, - }; - - const MockThirdPartyProvider2 = { - config: { - thirdPartyId: "mock2", - }, - }; - - const testSTConfig = { - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [MockThirdPartyProvider], - }, - }), - ], - }; - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("invalid email yields empty users array", async function () { - await startST(); - STExpress.init(testSTConfig); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - // given there are no users - - // when - const thirdPartyUsers = await getUsersByEmail("public", "john.doe@example.com"); - - // then - assert.strictEqual(thirdPartyUsers.length, 0); - }); - - it("valid email yields third party users", async function () { - await startST(); - STExpress.init({ - ...testSTConfig, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [MockThirdPartyProvider, MockThirdPartyProvider2], - }, - }), - ], - }); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - await manuallyCreateOrUpdateUser("public", "mock", "thirdPartyJohnDoe", "john.doe@example.com"); - await manuallyCreateOrUpdateUser("public", "mock2", "thirdPartyDaveDoe", "john.doe@example.com"); - - const thirdPartyUsers = await getUsersByEmail("public", "john.doe@example.com"); - - assert.strictEqual(thirdPartyUsers.length, 2); - - thirdPartyUsers.forEach((user) => { - assert.notStrictEqual(user.thirdParty.id, undefined); - assert.notStrictEqual(user.id, undefined); - assert.notStrictEqual(user.timeJoined, undefined); - assert.strictEqual(user.email, "john.doe@example.com"); - }); - }); -}); diff --git a/test/thirdparty/multitenancy.test.js b/test/thirdparty/multitenancy.test.js index 867de50c3..14a75d761 100644 --- a/test/thirdparty/multitenancy.test.js +++ b/test/thirdparty/multitenancy.test.js @@ -12,18 +12,11 @@ * License for the specific language governing permissions and limitations * under the License. */ -const { printPath, setupST, startSTWithMultitenancy, stopST, killAllST, cleanST, resetAll } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; +const { printPath, setupST, startSTWithMultitenancy, killAllST, cleanST } = require("../utils"); +let SuperTokens = require("../../"); let assert = require("assert"); let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); let ThirdParty = require("../../recipe/thirdparty"); -let { middleware, errorHandler } = require("../../framework/express"); let Multitenancy = require("../../recipe/multitenancy"); describe(`multitenancy: ${printPath("[test/thirdparty/multitenancy.test.js]")}`, function () { @@ -41,10 +34,10 @@ describe(`multitenancy: ${printPath("[test/thirdparty/multitenancy.test.js]")}`, // test config for emailpassword module // Failure condition: passing custom data or data of invalid type/ syntax to the module it("test recipe functions", async function () { - await startSTWithMultitenancy(); - STExpress.init({ + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -59,71 +52,59 @@ describe(`multitenancy: ${printPath("[test/thirdparty/multitenancy.test.js]")}`, await Multitenancy.createOrUpdateTenant("t3", { thirdPartyEnabled: true }); // Sign up - let user1a = await ThirdParty.manuallyCreateOrUpdateUser("t1", "google", "googleid1", "test@example.com"); - let user1b = await ThirdParty.manuallyCreateOrUpdateUser("t1", "facebook", "fbid1", "test@example.com"); - let user2a = await ThirdParty.manuallyCreateOrUpdateUser("t2", "google", "googleid1", "test@example.com"); - let user2b = await ThirdParty.manuallyCreateOrUpdateUser("t2", "facebook", "fbid1", "test@example.com"); - let user3a = await ThirdParty.manuallyCreateOrUpdateUser("t3", "google", "googleid1", "test@example.com"); - let user3b = await ThirdParty.manuallyCreateOrUpdateUser("t3", "facebook", "fbid1", "test@example.com"); + let user1a = await ThirdParty.manuallyCreateOrUpdateUser( + "t1", + "google", + "googleid1", + "test@example.com", + false + ); + let user1b = await ThirdParty.manuallyCreateOrUpdateUser("t1", "facebook", "fbid1", "test@example.com", false); + let user2a = await ThirdParty.manuallyCreateOrUpdateUser( + "t2", + "google", + "googleid1", + "test@example.com", + false + ); + let user2b = await ThirdParty.manuallyCreateOrUpdateUser("t2", "facebook", "fbid1", "test@example.com", false); + let user3a = await ThirdParty.manuallyCreateOrUpdateUser( + "t3", + "google", + "googleid1", + "test@example.com", + false + ); + let user3b = await ThirdParty.manuallyCreateOrUpdateUser("t3", "facebook", "fbid1", "test@example.com", false); - assert.deepEqual(user1a.user.tenantIds, ["t1"]); - assert.deepEqual(user1b.user.tenantIds, ["t1"]); - assert.deepEqual(user2a.user.tenantIds, ["t2"]); - assert.deepEqual(user2b.user.tenantIds, ["t2"]); - assert.deepEqual(user3a.user.tenantIds, ["t3"]); - assert.deepEqual(user3b.user.tenantIds, ["t3"]); + assert.deepEqual(user1a.user.loginMethods[0].tenantIds, ["t1"]); + assert.deepEqual(user1b.user.loginMethods[0].tenantIds, ["t1"]); + assert.deepEqual(user2a.user.loginMethods[0].tenantIds, ["t2"]); + assert.deepEqual(user2b.user.loginMethods[0].tenantIds, ["t2"]); + assert.deepEqual(user3a.user.loginMethods[0].tenantIds, ["t3"]); + assert.deepEqual(user3b.user.loginMethods[0].tenantIds, ["t3"]); // get user by id - let gUser1a = await ThirdParty.getUserById(user1a.user.id); - let gUser1b = await ThirdParty.getUserById(user1b.user.id); - let gUser2a = await ThirdParty.getUserById(user2a.user.id); - let gUser2b = await ThirdParty.getUserById(user2b.user.id); - let gUser3a = await ThirdParty.getUserById(user3a.user.id); - let gUser3b = await ThirdParty.getUserById(user3b.user.id); - - assert.deepEqual(gUser1a, user1a.user); - assert.deepEqual(gUser1b, user1b.user); - assert.deepEqual(gUser2a, user2a.user); - assert.deepEqual(gUser2b, user2b.user); - assert.deepEqual(gUser3a, user3a.user); - assert.deepEqual(gUser3b, user3b.user); - - // get user by email - let gUserByEmail1 = await ThirdParty.getUsersByEmail("t1", "test@example.com"); - let gUserByEmail2 = await ThirdParty.getUsersByEmail("t2", "test@example.com"); - let gUserByEmail3 = await ThirdParty.getUsersByEmail("t3", "test@example.com"); - - assert(gUserByEmail1.length === 2); - assert.deepEqual(gUserByEmail1[0], user1a.user); - assert.deepEqual(gUserByEmail1[1], user1b.user); - assert(gUserByEmail2.length === 2); - assert.deepEqual(gUserByEmail2[0], user2a.user); - assert.deepEqual(gUserByEmail2[1], user2b.user); - assert(gUserByEmail3.length === 2); - assert.deepEqual(gUserByEmail3[0], user3a.user); - assert.deepEqual(gUserByEmail3[1], user3b.user); - - // get user by thirdparty id - let gUserByThirdPartyId1 = await ThirdParty.getUserByThirdPartyInfo("t1", "google", "googleid1"); - let gUserByThirdPartyId2 = await ThirdParty.getUserByThirdPartyInfo("t1", "facebook", "fbid1"); - let gUserByThirdPartyId3 = await ThirdParty.getUserByThirdPartyInfo("t2", "google", "googleid1"); - let gUserByThirdPartyId4 = await ThirdParty.getUserByThirdPartyInfo("t2", "facebook", "fbid1"); - let gUserByThirdPartyId5 = await ThirdParty.getUserByThirdPartyInfo("t3", "google", "googleid1"); - let gUserByThirdPartyId6 = await ThirdParty.getUserByThirdPartyInfo("t3", "facebook", "fbid1"); - - assert.deepEqual(gUserByThirdPartyId1, user1a.user); - assert.deepEqual(gUserByThirdPartyId2, user1b.user); - assert.deepEqual(gUserByThirdPartyId3, user2a.user); - assert.deepEqual(gUserByThirdPartyId4, user2b.user); - assert.deepEqual(gUserByThirdPartyId5, user3a.user); - assert.deepEqual(gUserByThirdPartyId6, user3b.user); + let gUser1a = await SuperTokens.getUser(user1a.user.id); + let gUser1b = await SuperTokens.getUser(user1b.user.id); + let gUser2a = await SuperTokens.getUser(user2a.user.id); + let gUser2b = await SuperTokens.getUser(user2b.user.id); + let gUser3a = await SuperTokens.getUser(user3a.user.id); + let gUser3b = await SuperTokens.getUser(user3b.user.id); + + assert.deepEqual(gUser1a.toJson(), user1a.user.toJson()); + assert.deepEqual(gUser1b.toJson(), user1b.user.toJson()); + assert.deepEqual(gUser2a.toJson(), user2a.user.toJson()); + assert.deepEqual(gUser2b.toJson(), user2b.user.toJson()); + assert.deepEqual(gUser3a.toJson(), user3a.user.toJson()); + assert.deepEqual(gUser3b.toJson(), user3b.user.toJson()); }); it("test getProvider", async function () { - await startSTWithMultitenancy(); - STExpress.init({ + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -184,10 +165,10 @@ describe(`multitenancy: ${printPath("[test/thirdparty/multitenancy.test.js]")}`, }); it("test getProvider merges the config from static and core 1", async function () { - await startSTWithMultitenancy(); - STExpress.init({ + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -226,10 +207,10 @@ describe(`multitenancy: ${printPath("[test/thirdparty/multitenancy.test.js]")}`, }); it("test getProvider returns correct config from core", async function () { - await startSTWithMultitenancy(); - STExpress.init({ + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdparty/override.test.js b/test/thirdparty/override.test.js index 6d5f71d94..28746ff15 100644 --- a/test/thirdparty/override.test.js +++ b/test/thirdparty/override.test.js @@ -12,7 +12,17 @@ * License for the specific language governing permissions and limitations * under the License. */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll, signUPRequest } = require("../utils"); +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + signUPRequest, + assertJSONEquals, +} = require("../utils"); let STExpress = require("../../"); let Session = require("../../recipe/session"); let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; @@ -23,6 +33,7 @@ let ThirdParty = require("../../recipe/thirdparty"); const express = require("express"); const request = require("supertest"); let nock = require("nock"); +let AccountLinking = require("../../recipe/accountlinking"); let { middleware, errorHandler } = require("../../framework/express"); describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, function () { @@ -71,12 +82,12 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun }); it("overriding functions tests", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let newUser = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -84,6 +95,34 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun websiteDomain: "supertokens.io", }, recipeList: [ + AccountLinking.init({ + override: { + functions: (oI) => { + return { + ...oI, + getUser: async (input) => { + let response = await oI.getUser(input); + if (response !== undefined) { + user = { + ...response, + loginMethods: [ + { + ...response.loginMethods[0], + recipeUserId: response.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; + } + return response; + }, + }; + }, + }, + }), ThirdParty.init({ signInAndUpFeature: { providers: [this.customProvider1], @@ -94,13 +133,20 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun ...oI, signInUp: async (input) => { let response = await oI.signInUp(input); - user = response.user; - newUser = response.createdNewUser; - return response; - }, - getUserById: async (input) => { - let response = await oI.getUserById(input); - user = response; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; + newUser = response.createdNewRecipeUser; return response; }, }; @@ -121,7 +167,9 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun app.get("/user", async (req, res) => { let userId = req.query.userId; - res.json(await ThirdParty.getUserById(userId)); + let user = await STExpress.getUser(userId); + user.loginMethods[0].recipeUserId = user.loginMethods[0].recipeUserId.getAsString(); + res.json(user); }); let signUpResponse = await new Promise((resolve) => @@ -147,7 +195,7 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse.user, user); + assertJSONEquals(signUpResponse.user, user); user = undefined; assert.strictEqual(user, undefined); @@ -175,7 +223,7 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse.user, user); + assertJSONEquals(signInResponse.user, user); user = undefined; assert.strictEqual(user, undefined); @@ -197,16 +245,16 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun ); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(userByIdResponse, user); + assertJSONEquals(userByIdResponse, user); }); it("overriding api tests", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let newUser = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -225,8 +273,20 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun signInUpPOST: async (input) => { let response = await oI.signInUpPOST(input); if (response.status === "OK") { - user = response.user; - newUser = response.createdNewUser; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; + newUser = response.createdNewRecipeUser; } return response; }, @@ -244,11 +304,6 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun app.use(errorHandler()); - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdParty.getUserById(userId)); - }); - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); let signUpResponse = await new Promise((resolve) => @@ -274,7 +329,7 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse.user, user); + assertJSONEquals(signUpResponse.user, user); user = undefined; assert.strictEqual(user, undefined); @@ -302,15 +357,15 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse.user, user); + assertJSONEquals(signInResponse.user, user); }); it("overriding functions tests, throws error", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -318,6 +373,24 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun websiteDomain: "supertokens.io", }, recipeList: [ + AccountLinking.init({ + override: { + functions: (oI) => { + return { + ...oI, + getUser: async (input) => { + let response = await oI.getUser(input); + if (input.userContext.shouldError === undefined) { + return response; + } + throw { + error: "get user error", + }; + }, + }; + }, + }, + }), ThirdParty.init({ signInAndUpFeature: { providers: [this.customProvider1], @@ -329,7 +402,7 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun signInUp: async (input) => { let response = await oI.signInUp(input); user = response.user; - newUser = response.createdNewUser; + newUser = response.createdNewRecipeUser; if (newUser) { throw { error: "signup error", @@ -339,12 +412,6 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun error: "signin error", }; }, - getUserById: async (input) => { - await oI.getUserById(input); - throw { - error: "get user error", - }; - }, }; }, }, @@ -362,7 +429,7 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun app.get("/user", async (req, res, next) => { try { let userId = req.query.userId; - res.json(await ThirdParty.getUserById(userId)); + res.json(await STExpress.getUser(userId, { shouldError: true })); } catch (err) { next(err); } @@ -444,13 +511,13 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun }); it("overriding api tests, throws error", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let newUser = undefined; let emailExists = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -469,7 +536,7 @@ describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, fun signInUpPOST: async (input) => { let response = await oI.signInUpPOST(input); user = response.user; - newUser = response.createdNewUser; + newUser = response.createdNewRecipeUser; if (newUser) { throw { error: "signup error", diff --git a/test/thirdparty/provider.config.test.js b/test/thirdparty/provider.config.test.js index a41c0fb76..8746f851b 100644 --- a/test/thirdparty/provider.config.test.js +++ b/test/thirdparty/provider.config.test.js @@ -12,7 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -const { printPath, setupST, startSTWithMultitenancy, killAllST, cleanST } = require("../utils"); +const { printPath, setupST, startSTWithMultitenancy, killAllST, cleanST, removeAppAndTenants } = require("../utils"); let STExpress = require("../../"); let assert = require("assert"); let { ProcessState } = require("../../lib/build/processState"); @@ -217,11 +217,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test built-in provider computed config from static config", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -313,11 +313,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test it(`should work for ${provider.config.thirdPartyId} with override ${JSON.stringify( overrideVal.input )}`, async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -437,11 +437,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test it(`should work for ${provider.config.thirdPartyId} with override ${JSON.stringify( overrideVal.input )}`, async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -488,11 +488,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test built-in provider computed config from core config", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -517,11 +517,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test clientType matching when only one clientType is provided from static", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -550,11 +550,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test clientType matching when there are more than one clientType from static", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -590,11 +590,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test clientType matching when there is one clientType from core", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -615,11 +615,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test clientType matching when there are more than one clientType from core", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -647,11 +647,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test clientType matching when there are same clientTypes from static and core", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -695,11 +695,11 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test clientType matching when there are different clientTypes from static and core", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -737,10 +737,12 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test }); it("test getProvider and signInUp on an app and tenant", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy({ noApp: true }); + + await removeAppAndTenants("a1"); // Create app - await fetch("http://localhost:8080/recipe/multitenancy/app", { + await fetch(`http://localhost:8080/recipe/multitenancy/app`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -822,10 +824,10 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "google"); - assert.strictEqual(response1.body.user.thirdParty.userId, "googleuser"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.thirdParty[0].id, "google"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "googleuser"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); await Multitenancy.createOrUpdateTenant("t1", { emailPasswordEnabled: true, @@ -856,10 +858,10 @@ describe(`providerConfigTest: ${printPath("[test/thirdparty/provider.config.test ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "google"); - assert.strictEqual(response1.body.user.thirdParty.userId, "googleuser"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.thirdParty[0].id, "google"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "googleuser"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); assert.deepEqual(response1.body.user.tenantIds, ["t1"]); }); }); diff --git a/test/thirdparty/provider.test.js b/test/thirdparty/provider.test.js index e2c6e364d..e23e8a231 100644 --- a/test/thirdparty/provider.test.js +++ b/test/thirdparty/provider.test.js @@ -58,10 +58,10 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test minimum config for third party provider google", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -151,10 +151,10 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test passing additional params, check they are present in authorisation url for thirdparty provider google", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -224,10 +224,10 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test passing scopes in config for thirdparty provider google", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -292,14 +292,14 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test minimum config for third party provider facebook", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -387,14 +387,14 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test passing scopes in config for third party provider facebook", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -457,14 +457,14 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test minimum config for third party provider github", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -552,14 +552,14 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test additional params, check they are present in authorisation url for third party provider github", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -627,14 +627,14 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test passing scopes in config for third party provider github", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -697,7 +697,7 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test minimum config for third party provider apple", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let additionalConfig = { @@ -708,7 +708,7 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -799,7 +799,7 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test passing additional params, check they are present in authorisation url for third party provider apple", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let additionalConfig = { @@ -810,7 +810,7 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -879,7 +879,7 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test passing scopes in config for third party provider apple", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let additionalConfig = { @@ -890,7 +890,7 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -954,7 +954,7 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it.skip("test passing invalid privateKey in config for third party provider apple", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let additionalConfig = { @@ -964,7 +964,7 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1002,11 +1002,11 @@ describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, fun }); it("test duplicate provider", async function () { - await startST(); + const connectionURI = await startST(); try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdparty/signinupFeature.test.js b/test/thirdparty/signinupFeature.test.js index 0d88083ac..f7c4fd45a 100644 --- a/test/thirdparty/signinupFeature.test.js +++ b/test/thirdparty/signinupFeature.test.js @@ -148,10 +148,10 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") }); it("test that disable api, the default signinup API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -217,10 +217,10 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") }); it("test minimum config without code for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -262,10 +262,12 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.loginMethods[0].thirdParty.id, "custom"); + assert.strictEqual(response1.body.user.loginMethods[0].thirdParty.userId, "user"); + assert.strictEqual(response1.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); let cookies1 = extractInfoFromResponse(response1); assert.notStrictEqual(cookies1.accessToken, undefined); @@ -298,10 +300,12 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") assert.notStrictEqual(response2, undefined); assert.strictEqual(response2.body.status, "OK"); - assert.strictEqual(response2.body.createdNewUser, false); - assert.strictEqual(response2.body.user.thirdParty.id, "custom"); - assert.strictEqual(response2.body.user.thirdParty.userId, "user"); - assert.strictEqual(response2.body.user.email, "email@test.com"); + assert.strictEqual(response2.body.createdNewRecipeUser, false); + assert.strictEqual(response2.body.user.loginMethods[0].thirdParty.id, "custom"); + assert.strictEqual(response2.body.user.loginMethods[0].thirdParty.userId, "user"); + assert.strictEqual(response2.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response2.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response2.body.user.emails[0], "email@test.com"); let cookies2 = extractInfoFromResponse(response2); assert.notStrictEqual(cookies2.accessToken, undefined); @@ -316,10 +320,10 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") }); it("test missing redirectURIInfo and oAuthTokens", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -360,10 +364,10 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") }); it("test minimum config for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -411,10 +415,12 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.loginMethods[0].thirdParty.id, "custom"); + assert.strictEqual(response1.body.user.loginMethods[0].thirdParty.userId, "user"); + assert.strictEqual(response1.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); let cookies1 = extractInfoFromResponse(response1); assert.notStrictEqual(cookies1.accessToken, undefined); @@ -426,8 +432,13 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") assert.strictEqual(cookies1.accessTokenDomain, undefined); assert.strictEqual(cookies1.refreshTokenDomain, undefined); assert.notStrictEqual(cookies1.frontToken, "remove"); - - assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), true); + assert.strictEqual( + await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(response1.body.user.id), + response1.body.user.email + ), + true + ); nock("https://test.com").post("/oauth/token").reply(200, {}); @@ -454,10 +465,12 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") assert.notStrictEqual(response2, undefined); assert.strictEqual(response2.body.status, "OK"); - assert.strictEqual(response2.body.createdNewUser, false); - assert.strictEqual(response2.body.user.thirdParty.id, "custom"); - assert.strictEqual(response2.body.user.thirdParty.userId, "user"); - assert.strictEqual(response2.body.user.email, "email@test.com"); + assert.strictEqual(response2.body.createdNewRecipeUser, false); + assert.strictEqual(response2.body.user.loginMethods[0].thirdParty.id, "custom"); + assert.strictEqual(response2.body.user.loginMethods[0].thirdParty.userId, "user"); + assert.strictEqual(response2.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response2.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response2.body.user.emails[0], "email@test.com"); let cookies2 = extractInfoFromResponse(response2); assert.notStrictEqual(cookies2.accessToken, undefined); @@ -472,10 +485,10 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") }); it("test minimum config for thirdparty module, email unverified", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -523,10 +536,12 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.loginMethods[0].thirdParty.id, "custom"); + assert.strictEqual(response1.body.user.loginMethods[0].thirdParty.userId, "user"); + assert.strictEqual(response1.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); let cookies1 = extractInfoFromResponse(response1); assert.notStrictEqual(cookies1.accessToken, undefined); @@ -539,14 +554,20 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") assert.strictEqual(cookies1.refreshTokenDomain, undefined); assert.notStrictEqual(cookies1.frontToken, "remove"); - assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), false); + assert.strictEqual( + await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(response1.body.user.id), + response1.body.user.email + ), + false + ); }); it("test thirdparty provider doesn't exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -594,10 +615,10 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") }); it("test email not returned in getProfileInfo function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -647,10 +668,10 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") }); it("test error thrown from getProfileInfo function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -706,10 +727,10 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") }); it("test invalid POST params for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -825,130 +846,4 @@ describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]") ); assert.strictEqual(response8.statusCode, 200); }); - - it("test getUserById when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyRecipe.getInstanceOrThrowError(); - - assert.strictEqual(await ThirdParty.getUserById("randomID"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - redirectURIInfo: { - redirectURIOnProviderDashboard: "http://localhost.org", - redirectURIQueryParams: { - code: "32432432", - }, - }, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdParty.getUserById(signUpUserInfo.id); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); - - it("test getUserByThirdPartyInfo when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyRecipe.getInstanceOrThrowError(); - - assert.strictEqual(await ThirdParty.getUserByThirdPartyInfo("public", "custom", "user"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - redirectURIInfo: { - redirectURIOnProviderDashboard: "http://localhost.org", - redirectURIQueryParams: { - code: "32432432", - }, - }, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdParty.getUserByThirdPartyInfo("public", "custom", "user"); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); }); diff --git a/test/thirdparty/signoutFeature.test.js b/test/thirdparty/signoutFeature.test.js index f2f2995a8..846a062f5 100644 --- a/test/thirdparty/signoutFeature.test.js +++ b/test/thirdparty/signoutFeature.test.js @@ -68,11 +68,11 @@ describe(`signoutTest: ${printPath("[test/thirdparty/signoutFeature.test.js]")}` }); it("test the default route and it should revoke the session", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -149,11 +149,11 @@ describe(`signoutTest: ${printPath("[test/thirdparty/signoutFeature.test.js]")}` }); it("test that disabling default route and calling the API returns 404", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -200,11 +200,11 @@ describe(`signoutTest: ${printPath("[test/thirdparty/signoutFeature.test.js]")}` }); it("test that calling the API without a session should return OK", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -245,13 +245,11 @@ describe(`signoutTest: ${printPath("[test/thirdparty/signoutFeature.test.js]")}` }); it("test that signout API reutrns try refresh token, refresh session and signout should return OK", async function () { - await setKeyValueInConfig("access_token_validity", 2); - - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 2 } }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdparty/users.test.js b/test/thirdparty/users.test.js index a5ee711a2..3735d6902 100644 --- a/test/thirdparty/users.test.js +++ b/test/thirdparty/users.test.js @@ -59,10 +59,10 @@ describe(`usersTest: ${printPath("[test/thirdparty/users.test.js]")}`, function }); it("test getUsersOldestFirst", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -98,12 +98,12 @@ describe(`usersTest: ${printPath("[test/thirdparty/users.test.js]")}`, function users = await getUsersOldestFirst({ tenantId: "public", limit: 1 }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersOldestFirst({ tenantId: "public", limit: 1, paginationToken: users.nextPaginationToken }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test1@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test1@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersOldestFirst({ tenantId: "public", limit: 5, paginationToken: users.nextPaginationToken }); @@ -130,10 +130,10 @@ describe(`usersTest: ${printPath("[test/thirdparty/users.test.js]")}`, function }); it("test getUsersNewestFirst", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -169,12 +169,12 @@ describe(`usersTest: ${printPath("[test/thirdparty/users.test.js]")}`, function users = await getUsersNewestFirst({ tenantId: "public", limit: 1 }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test4@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test4@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersNewestFirst({ tenantId: "public", limit: 1, paginationToken: users.nextPaginationToken }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test3@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test3@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersNewestFirst({ tenantId: "public", limit: 5, paginationToken: users.nextPaginationToken }); @@ -201,10 +201,10 @@ describe(`usersTest: ${printPath("[test/thirdparty/users.test.js]")}`, function }); it("test getUserCount", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartyemailpassword/authorisationUrlFeature.test.js b/test/thirdpartyemailpassword/authorisationUrlFeature.test.js index af82046bd..67f3e3249 100644 --- a/test/thirdpartyemailpassword/authorisationUrlFeature.test.js +++ b/test/thirdpartyemailpassword/authorisationUrlFeature.test.js @@ -61,10 +61,10 @@ describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authoris }); it("test minimum config for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -105,12 +105,12 @@ describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authoris }); it("test calling authorisation url API with empty init", async function () { - await startST(); + const connectionURI = await startST(); // testing with the google OAuth development key STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -151,12 +151,12 @@ describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authoris }); it("test calling authorisation url API with empty init with dynamic third party provider", async function () { - await startST(); + const connectionURI = await startST(); // testing with the google OAuth development key STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -212,10 +212,10 @@ describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authoris // testing 4xx error correctly thrown from sub-recipe it("test thirdparty provider doesn't exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartyemailpassword/config.test.js b/test/thirdpartyemailpassword/config.test.js index fc7a6c154..e6ddf4dd7 100644 --- a/test/thirdpartyemailpassword/config.test.js +++ b/test/thirdpartyemailpassword/config.test.js @@ -57,10 +57,10 @@ describe(`configTest: ${printPath("[test/thirdpartyemailpassword/config.test.js] }); it("test default config for thirdpartyemailpassword module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -99,10 +99,10 @@ describe(`configTest: ${printPath("[test/thirdpartyemailpassword/config.test.js] }); it("test config for thirdpartyemailpassword module, with provider", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartyemailpassword/emailDelivery.test.js b/test/thirdpartyemailpassword/emailDelivery.test.js index 1fcb85876..c069f57cc 100644 --- a/test/thirdpartyemailpassword/emailDelivery.test.js +++ b/test/thirdpartyemailpassword/emailDelivery.test.js @@ -39,10 +39,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test default backward compatibility api being called: reset password", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -95,10 +95,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test default backward compatibility api being called, error message not sent back to user: reset password", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -152,13 +152,12 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test backward compatibility: reset password (emailpassword user)", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let passwordResetURL = undefined; - let timeJoined = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -204,17 +203,16 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver await delay(2); assert.strictEqual(email, "test@example.com"); assert.notStrictEqual(passwordResetURL, undefined); - assert.notStrictEqual(timeJoined, undefined); }); it("test backward compatibility: reset password (non-existent user)", async function () { - await startST(); + const connectionURI = await startST(); let functionCalled = false; let email = undefined; let passwordResetURL = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -281,11 +279,11 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test backward compatibility: reset password (thirdparty user)", async function () { - await startST(); + const connectionURI = await startST(); let functionCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -315,7 +313,8 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver "public", "custom-provider", "test-user-id", - "test@example.com" + "test@example.com", + false ); await supertest(app) @@ -336,14 +335,14 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test custom override: reset password", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let passwordResetURL = undefined; let type = undefined; let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -408,7 +407,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test smtp service: reset password", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let passwordResetURL = undefined; let outerOverrideCalled = false; @@ -416,7 +415,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -502,10 +501,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test default backward compatibility api being called: email verify", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -524,7 +523,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -561,10 +560,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test default backward compatibility api being called, error message not sent back to user: email verify", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -583,7 +582,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -621,14 +620,14 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test custom override: email verify", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let emailVerifyURL = undefined; let type = undefined; let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -661,7 +660,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -694,7 +693,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver }); it("test smtp service: email verify", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let emailVerifyURL = undefined; let outerOverrideCalled = false; @@ -702,7 +701,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -766,7 +765,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDeliver app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); diff --git a/test/thirdpartyemailpassword/emailExists.test.js b/test/thirdpartyemailpassword/emailExists.test.js index 780cb25bc..9183eff90 100644 --- a/test/thirdpartyemailpassword/emailExists.test.js +++ b/test/thirdpartyemailpassword/emailExists.test.js @@ -38,10 +38,10 @@ describe(`emailExists: ${printPath("[test/thirdpartyemailpassword/emailExists.te // disable the email exists API, and check that calling it returns a 404. it("test that if disable api, the default email exists API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -89,10 +89,10 @@ describe(`emailExists: ${printPath("[test/thirdpartyemailpassword/emailExists.te // email exists it("test good input, email exists", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -135,10 +135,10 @@ describe(`emailExists: ${printPath("[test/thirdpartyemailpassword/emailExists.te //email does not exist it("test good input, email does not exists", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -177,10 +177,10 @@ describe(`emailExists: ${printPath("[test/thirdpartyemailpassword/emailExists.te // testing error is correctly handled by the sub-recipe it("test bad input, do not pass email", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartyemailpassword/emailverify.test.js b/test/thirdpartyemailpassword/emailverify.test.js index 2523346ca..da9831806 100644 --- a/test/thirdpartyemailpassword/emailverify.test.js +++ b/test/thirdpartyemailpassword/emailverify.test.js @@ -72,10 +72,10 @@ describe(`emailverify: ${printPath("[test/thirdpartyemailpassword/emailverify.te }); it("test the generate token api with valid input, email not verified", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -109,10 +109,10 @@ describe(`emailverify: ${printPath("[test/thirdpartyemailpassword/emailverify.te }); it("test the generate token api with valid input, email verified and test error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -137,9 +137,14 @@ describe(`emailverify: ${printPath("[test/thirdpartyemailpassword/emailverify.te assert(response.status === 200); let userId = JSON.parse(response.text).user.id; + let email = JSON.parse(response.text).user.emails[0]; let infoFromResponse = extractInfoFromResponse(response); - let verifyToken = await EmailVerification.createEmailVerificationToken("public", userId); + let verifyToken = await EmailVerification.createEmailVerificationToken( + "public", + STExpress.convertToRecipeUserId(userId), + email + ); await EmailVerification.verifyEmailUsingToken("public", verifyToken.token); response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); @@ -150,10 +155,10 @@ describe(`emailverify: ${printPath("[test/thirdpartyemailpassword/emailverify.te }); it("test the generate token api with valid input, no session and check output", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -191,14 +196,14 @@ describe(`emailverify: ${printPath("[test/thirdpartyemailpassword/emailverify.te }); it("test that providing your own email callback and make sure it is called", async function () { - await startST(); + const connectionURI = await startST(); let userInfo = null; let emailToken = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -253,14 +258,14 @@ describe(`emailverify: ${printPath("[test/thirdpartyemailpassword/emailverify.te }); it("test that providing your own email callback and make sure it is called (thirdparty user)", async function () { - await startST(); + const connectionURI = await startST(); let userInfo = null; let emailToken = null; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -317,11 +322,11 @@ describe(`emailverify: ${printPath("[test/thirdpartyemailpassword/emailverify.te }); it("test the email verify API with invalid token and check error", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartyemailpassword/getUsersByEmailFeature.test.js b/test/thirdpartyemailpassword/getUsersByEmailFeature.test.js deleted file mode 100644 index 617916240..000000000 --- a/test/thirdpartyemailpassword/getUsersByEmailFeature.test.js +++ /dev/null @@ -1,102 +0,0 @@ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -const { - thirdPartySignInUp, - getUsersByEmail, - emailPasswordSignUp, - thirdPartyManuallyCreateOrUpdateUser, -} = require("../../lib/build/recipe/thirdpartyemailpassword"); -const { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`getUsersByEmail: ${printPath("[test/thirdpartyemailpassword/getUsersByEmailFeature.test.js]")}`, function () { - const MockThirdPartyProvider = { - id: "mock", - }; - - const MockThirdPartyProvider2 = { - id: "mock2", - }; - - const testSTConfig = { - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - signInAndUpFeature: { - providers: [MockThirdPartyProvider], - }, - }), - ], - }; - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("invalid email yields empty users array", async function () { - await startST(); - STExpress.init(testSTConfig); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - // given there are no users - - // when - const thirdPartyUsers = await getUsersByEmail("public", "john.doe@example.com"); - - // then - assert.strictEqual(thirdPartyUsers.length, 0); - }); - - it("valid email yields third party users", async function () { - await startST(); - STExpress.init({ - ...testSTConfig, - recipeList: [ - ThirdPartyEmailPassword.init({ - signInAndUpFeature: { - providers: [MockThirdPartyProvider, MockThirdPartyProvider2], - }, - }), - ], - }); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - await emailPasswordSignUp("public", "john.doe@example.com", "somePass"); - await thirdPartyManuallyCreateOrUpdateUser("public", "mock", "thirdPartyJohnDoe", "john.doe@example.com"); - await thirdPartyManuallyCreateOrUpdateUser("public", "mock2", "thirdPartyDaveDoe", "john.doe@example.com"); - - const thirdPartyUsers = await getUsersByEmail("public", "john.doe@example.com"); - - assert.strictEqual(thirdPartyUsers.length, 3); - - thirdPartyUsers.forEach((user) => { - assert.strictEqual(user.email, "john.doe@example.com"); - }); - }); -}); diff --git a/test/thirdpartyemailpassword/override.test.js b/test/thirdpartyemailpassword/override.test.js index 9d83581b4..93dc86c78 100644 --- a/test/thirdpartyemailpassword/override.test.js +++ b/test/thirdpartyemailpassword/override.test.js @@ -12,7 +12,17 @@ * License for the specific language governing permissions and limitations * under the License. */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll, signUPRequest } = require("../utils"); +const { + printPath, + setupST, + startST, + stopST, + killAllST, + cleanST, + resetAll, + signUPRequest, + assertJSONEquals, +} = require("../utils"); let STExpress = require("../../"); let Session = require("../../recipe/session"); let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; @@ -20,6 +30,7 @@ let assert = require("assert"); let { ProcessState } = require("../../lib/build/processState"); const { Querier } = require("../../lib/build/querier"); let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); +let AccountLinking = require("../../recipe/accountlinking"); const express = require("express"); const request = require("supertest"); let { middleware, errorHandler } = require("../../framework/express"); @@ -37,11 +48,11 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test }); it("overriding functions tests", async () => { - await startST(); + const connectionURI = await startST(); let user = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -49,6 +60,34 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test websiteDomain: "supertokens.io", }, recipeList: [ + AccountLinking.init({ + override: { + functions: (oI) => { + return { + ...oI, + getUser: async (input) => { + let response = await oI.getUser(input); + if (response !== undefined) { + user = { + ...response, + loginMethods: [ + { + ...response.loginMethods[0], + recipeUserId: response.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; + } + return response; + }, + }; + }, + }, + }), ThirdPartyEmailPassword.init({ override: { functions: (oI) => { @@ -57,22 +96,41 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test emailPasswordSignUp: async (input) => { let response = await oI.emailPasswordSignUp(input); if (response.status === "OK") { - user = response.user; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; } return response; }, emailPasswordSignIn: async (input) => { let response = await oI.emailPasswordSignIn(input); if (response.status === "OK") { - user = response.user; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; } return response; }, - getUserById: async (input) => { - let response = await oI.getUserById(input); - user = response; - return response; - }, }; }, }, @@ -89,16 +147,17 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test app.get("/user", async (req, res) => { let userId = req.query.userId; - res.json(await ThirdPartyEmailPassword.getUserById(userId)); + let user = await STExpress.getUser(userId); + user.loginMethods[0].recipeUserId = user.loginMethods[0].recipeUserId.getAsString(); + res.json(user); }); let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body.user, user); + assertJSONEquals(signUpResponse.body.user, user); user = undefined; - assert.strictEqual(user, undefined); let signInResponse = await new Promise((resolve) => request(app) @@ -126,10 +185,9 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test ); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signInResponse.user, user); + assertJSONEquals(signInResponse.user, user); user = undefined; - assert.strictEqual(user, undefined); let userByIdResponse = await new Promise((resolve) => request(app) @@ -148,18 +206,18 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test ); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(userByIdResponse, user); + assertJSONEquals(userByIdResponse, user); }); it("overriding api tests", async () => { - await startST(); + const connectionURI = await startST(); let user = undefined; let newUser = undefined; let emailExists = undefined; let type = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -175,7 +233,19 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test emailPasswordSignInPOST: async (input) => { let response = await oI.emailPasswordSignInPOST(input); if (response.status === "OK") { - user = response.user; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; newUser = false; type = "emailpassword"; } @@ -184,7 +254,19 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test emailPasswordSignUpPOST: async (input) => { let response = await oI.emailPasswordSignUpPOST(input); if (response.status === "OK") { - user = response.user; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; newUser = true; type = "emailpassword"; } @@ -211,7 +293,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test app.get("/user", async (req, res) => { let userId = req.query.userId; - res.json(await ThirdPartyEmailPassword.getUserById(userId)); + res.json(await STExpress.getUser(userId)); }); let emailExistsResponse = await new Promise((resolve) => @@ -236,7 +318,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, true); assert.strictEqual(type, "emailpassword"); - assert.deepStrictEqual(signUpResponse.body.user, user); + assertJSONEquals(signUpResponse.body.user, user); emailExistsResponse = await new Promise((resolve) => request(app) @@ -287,15 +369,15 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, false); assert.strictEqual(type, "emailpassword"); - assert.deepStrictEqual(signInResponse.user, user); + assertJSONEquals(signInResponse.user, user); }); it("overriding functions tests, throws error", async () => { - await startST(); + const connectionURI = await startST(); let user = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -303,6 +385,24 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test websiteDomain: "supertokens.io", }, recipeList: [ + AccountLinking.init({ + override: { + functions: (oI) => { + return { + ...oI, + getUser: async (input) => { + let response = await oI.getUser(input); + if (input.userContext.shouldError === undefined) { + return response; + } + throw { + error: "get user error", + }; + }, + }; + }, + }, + }), ThirdPartyEmailPassword.init({ override: { functions: (oI) => { @@ -321,12 +421,6 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test error: "signin error", }; }, - getUserById: async (input) => { - await oI.getUserById(input); - throw { - error: "get user error", - }; - }, }; }, }, @@ -344,7 +438,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test app.get("/user", async (req, res, next) => { try { let userId = req.query.userId; - res.json(await ThirdPartyEmailPassword.getUserById(userId)); + res.json(await STExpress.getUser(userId, { shouldError: true })); } catch (err) { next(err); } @@ -409,14 +503,14 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test }); it("overriding api tests, throws error", async () => { - await startST(); + const connectionURI = await startST(); let user = undefined; let newUser = undefined; let emailExists = undefined; let type = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -480,7 +574,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test app.get("/user", async (req, res) => { let userId = req.query.userId; - res.json(await ThirdPartyEmailPassword.getUserById(userId)); + res.json(await SuperTokens.getUser(userId)); }); app.use((err, req, res, next) => { diff --git a/test/thirdpartyemailpassword/signinFeature.test.js b/test/thirdpartyemailpassword/signinFeature.test.js index 39b18625e..c42969c20 100644 --- a/test/thirdpartyemailpassword/signinFeature.test.js +++ b/test/thirdpartyemailpassword/signinFeature.test.js @@ -73,10 +73,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test that disable api, the default signinup API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -140,10 +140,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test that disable api, the default signin API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -197,7 +197,7 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test handlePostSignUpIn gets set correctly", async function () { - await startST(); + const connectionURI = await startST(); process.env.userId = ""; process.env.loginType = ""; @@ -207,7 +207,7 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -271,10 +271,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI works when input is fine", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -325,10 +325,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI works when input is fine and user has added JSON middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -380,10 +380,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI works when input is fine and user has added urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -435,10 +435,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI works when input is fine and user has added both JSON and urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -491,10 +491,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI works when input is fine and user has added bodyParser JSON middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -546,10 +546,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI works when input is fine and user has added bodyParser urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -601,10 +601,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI works when input is fine and user has added both bodyParser JSON and bodyParser urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -657,10 +657,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI with empty JSON and user has added JSON middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -683,10 +683,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI with empty JSON and user has added urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -709,10 +709,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI with empty JSON and user has added both JSON and urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -736,10 +736,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI with empty JSON and user has added both bodyParser JSON and bodyParser urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -763,10 +763,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI with empty request body and user has added JSON middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -789,10 +789,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI with empty request body and user has added urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -815,10 +815,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur }); it("test singinAPI with empty request body and user has added both JSON and urlencoded middleware", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -846,10 +846,10 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur */ // testing error gets corectly routed to sub-recipe it("test singinAPI throws an error when email does not match", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -900,11 +900,11 @@ describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeatur having the email start with "test" (requierment of the custom validator) will cause the test to fail */ it("test custom email validators to sign up and make sure they are applied to sign in", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartyemailpassword/signoutFeature.test.js b/test/thirdpartyemailpassword/signoutFeature.test.js index b416fa254..29840dace 100644 --- a/test/thirdpartyemailpassword/signoutFeature.test.js +++ b/test/thirdpartyemailpassword/signoutFeature.test.js @@ -69,11 +69,11 @@ describe(`signoutTest: ${printPath("[test/thirdpartyemailpassword/signoutFeature }); it("test the default route and it should revoke the session", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -179,11 +179,11 @@ describe(`signoutTest: ${printPath("[test/thirdpartyemailpassword/signoutFeature }); it("test that disabling default route and calling the API returns 404", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -228,11 +228,11 @@ describe(`signoutTest: ${printPath("[test/thirdpartyemailpassword/signoutFeature }); it("test that calling the API without a session should return OK", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -271,13 +271,11 @@ describe(`signoutTest: ${printPath("[test/thirdpartyemailpassword/signoutFeature }); it("test that signout API reutrns try refresh token, refresh session and signout should return OK", async function () { - await setKeyValueInConfig("access_token_validity", 2); - - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 2 } }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartyemailpassword/signupFeature.test.js b/test/thirdpartyemailpassword/signupFeature.test.js index 05dd0f2bd..5920895cc 100644 --- a/test/thirdpartyemailpassword/signupFeature.test.js +++ b/test/thirdpartyemailpassword/signupFeature.test.js @@ -124,10 +124,10 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t }); it("test that disable api, the default signinup API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -191,11 +191,11 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t }); it("test that if disable api, the default signup API does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -228,10 +228,10 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t }); it("test minimum config with one provider", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -277,20 +277,26 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); - assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), true); + assert.strictEqual( + await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(response1.body.user.id), + response1.body.user.email + ), + true + ); }); it("test signUpAPI works when input is fine", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -312,11 +318,11 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t let userInfo = JSON.parse(response.text).user; assert(userInfo.id !== undefined); - assert(userInfo.email === "random@gmail.com"); + assert(userInfo.emails[0] === "random@gmail.com"); }); it("test handlePostSignUpIn gets set correctly", async function () { - await startST(); + const connectionURI = await startST(); process.env.userId = ""; process.env.loginType = ""; @@ -326,7 +332,7 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -390,7 +396,7 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t }); it("test handlePostSignUp gets set correctly", async function () { - await startST(); + const connectionURI = await startST(); process.env.userId = ""; process.env.loginType = ""; @@ -400,7 +406,7 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -447,11 +453,11 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t // will test that the error is correctly propagated to the required sub-recipe it("test signUpAPI throws an error in case of a duplicate email (emailpassword)", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -473,7 +479,7 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t let userInfo = JSON.parse(response.text).user; assert(userInfo.id !== undefined); - assert(userInfo.email === "random@gmail.com"); + assert(userInfo.emails[0] === "random@gmail.com"); response = await signUPRequest(app, "random@gmail.com", "validpass123"); assert(response.status === 200); @@ -487,10 +493,10 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t // NO_EMAIL_GIVEN_BY_PROVIDER thrown from sub recipe it("test email not returned in getProfileInfo function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -538,10 +544,10 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t }); it("test error thrown from getProfileInfo function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -594,137 +600,12 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t assert.deepStrictEqual(response1.body, { message: "error from getProfileInfo" }); }); - it("test getUserById when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - - assert.strictEqual(await ThirdPartyEmailPassword.getUserById("randomID"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - redirectURIInfo: { - redirectURIOnProviderDashboard: "http://127.0.0.1/callback", - redirectURIQueryParams: { - code: "abcdefghj", - }, - }, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyEmailPassword.getUserById(signUpUserInfo.id); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); - - it("test getUserByThirdPartyInfo when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - - assert.strictEqual( - await ThirdPartyEmailPassword.getUserByThirdPartyInfo("public", "custom", "user"), - undefined - ); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - redirectURIInfo: { - redirectURIOnProviderDashboard: "http://127.0.0.1/callback", - redirectURIQueryParams: { - code: "abcdefghj", - }, - }, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyEmailPassword.getUserByThirdPartyInfo("public", "custom", "user"); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); - it("test getUserCount and pagination works fine", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -759,7 +640,8 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t "public", "google", "randomUserId", - "test@example.com" + "test@example.com", + false ); assert((await STExpress.getUserCount()) === 2); @@ -772,8 +654,8 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t let usersOldest = await STExpress.getUsersOldestFirst({ tenantId: "public" }); assert(usersOldest.nextPaginationToken === undefined); assert(usersOldest.users.length === 3); - assert(usersOldest.users[0].recipeId === "emailpassword"); - assert(usersOldest.users[0].user.email === "random@gmail.com"); + assert(usersOldest.users[0].loginMethods[0].recipeId === "emailpassword"); + assert(usersOldest.users[0].emails[0] === "random@gmail.com"); let usersNewest = await STExpress.getUsersNewestFirst({ tenantId: "public", @@ -781,8 +663,8 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t }); assert(usersNewest.nextPaginationToken !== undefined); assert(usersNewest.users.length === 2); - assert(usersNewest.users[0].recipeId === "emailpassword"); - assert(usersNewest.users[0].user.email === "random1@gmail.com"); + assert(usersNewest.users[0].loginMethods[0].recipeId === "emailpassword"); + assert(usersNewest.users[0].emails[0] === "random1@gmail.com"); let usersNewest2 = await STExpress.getUsersNewestFirst({ tenantId: "public", @@ -790,16 +672,16 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t }); assert(usersNewest2.nextPaginationToken === undefined); assert(usersNewest2.users.length === 1); - assert(usersNewest2.users[0].recipeId === "emailpassword"); - assert(usersNewest2.users[0].user.email === "random@gmail.com"); + assert(usersNewest2.users[0].loginMethods[0].recipeId === "emailpassword"); + assert(usersNewest2.users[0].emails[0] === "random@gmail.com"); }); it("updateEmailOrPassword function test for third party login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -831,8 +713,12 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t let thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); assert.strictEqual( - await ThirdPartyEmailPassword.getUserByThirdPartyInfo("public", "custom", "user"), - undefined + ( + await STExpress.listUsersByAccountInfo("public", { + thirdParty: { id: "custom", userId: "user" }, + }) + ).length, + 0 ); const app = express(); @@ -867,14 +753,16 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t assert.strictEqual(response.statusCode, 200); let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyEmailPassword.getUserByThirdPartyInfo("public", "custom", "user"); + let userInfo = await STExpress.listUsersByAccountInfo("public", { + thirdParty: { id: "custom", userId: "user" }, + }); - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); + assert.strictEqual(userInfo[0].emails[0], signUpUserInfo.emails[0]); + assert.strictEqual(userInfo[0].id, signUpUserInfo.id); try { await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: userInfo.id, + recipeUserId: STExpress.convertToRecipeUserId(userInfo[0].id), email: "test2@example.com", }); throw new Error("test failed"); @@ -914,24 +802,21 @@ describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.t assert.strictEqual(response.statusCode, 200); let signUpUserInfo = response.body.user; - let r = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: signUpUserInfo.id, + recipeUserId: STExpress.convertToRecipeUserId(signUpUserInfo.id), email: "test2@example.com", password: "haha@1234", }); assert(r.status === "OK"); - let r2 = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: signUpUserInfo.id + "123", + recipeUserId: STExpress.convertToRecipeUserId(signUpUserInfo.id + "123"), email: "test2@example.com", }); assert(r2.status === "UNKNOWN_USER_ID_ERROR"); - let r3 = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: signUpUserInfo.id, + recipeUserId: STExpress.convertToRecipeUserId(signUpUserInfo.id), email: "test2@example.com", password: "test", }); diff --git a/test/thirdpartypasswordless/api.test.js b/test/thirdpartypasswordless/api.test.js index 39d29391a..63c886bcf 100644 --- a/test/thirdpartypasswordless/api.test.js +++ b/test/thirdpartypasswordless/api.test.js @@ -42,12 +42,12 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") */ it("test for thirdPartyPasswordless, the sign up /in flow with email using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -89,12 +89,13 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") app.use(errorHandler()); + const email = "test@example.com"; // createCodeAPI with email let validCreateCodeResponse = await new Promise((resolve) => request(app) .post("/auth/signinup/code") .send({ - email: "test@example.com", + email, }) .expect(200) .end((err, res) => { @@ -130,13 +131,12 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }) ); - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.email === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 4); - assert(Object.keys(validUserInputCodeResponse).length === 3); + checkConsumeResponse(validUserInputCodeResponse, { + email, + phoneNumber: undefined, + isNew: true, + isPrimary: false, + }); }); /** @@ -145,12 +145,12 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") */ it("test for thirdPartyPasswordless, the sign up /in flow with phoneNumber using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -192,12 +192,13 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") app.use(errorHandler()); + const phoneNumber = "+12345678901"; // createCodeAPI with phoneNumber let validCreateCodeResponse = await new Promise((resolve) => request(app) .post("/auth/signinup/code") .send({ - phoneNumber: "+12345678901", + phoneNumber, }) .expect(200) .end((err, res) => { @@ -233,14 +234,12 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }) ); - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.phoneNumber === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(validUserInputCodeResponse.user.tenantIds.length === 1); - assert(Object.keys(validUserInputCodeResponse.user).length === 4); - assert(Object.keys(validUserInputCodeResponse).length === 3); + checkConsumeResponse(validUserInputCodeResponse, { + email: undefined, + phoneNumber, + isNew: true, + isPrimary: false, + }); }); /** @@ -248,13 +247,13 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") * - create code with email and then resend code and make sure that sending email function is called while resending code */ it("test for thirdPartyPasswordless creating a code with email and then resending the code and check that the sending custom email function is called while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -343,13 +342,13 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") * - create code with phone and then resend code and make sure that sending SMS function is called while resending code */ it("test with thirdPartyPasswordless, creating a code with phone and then resending the code and check that the sending custom SMS function is called while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomTextMessageCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -439,11 +438,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") * - sending neither email and phone in createCode API throws bad request */ it("test with thirdPartyPasswordless, invalid input to createCodeAPI while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -530,13 +529,13 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") */ it("test with thirdPartyPasswordless, adding phoneNumber to a users info and signing in will sign in the same user, using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -620,7 +619,7 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") // add users phoneNumber to userInfo await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: emailUserInputCodeResponse.user.id, + recipeUserId: STExpress.convertToRecipeUserId(emailUserInputCodeResponse.user.loginMethods[0].recipeUserId), phoneNumber: "+12345678901", }); @@ -669,11 +668,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") // check that if user has not given linkCode nor (deviceId+userInputCode), it throws a bad request error. it("test for thirdPartyPasswordless, not passing any fields to consumeCodeAPI", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -731,11 +730,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }); it("test with thirdPartyPasswordless consumeCodeAPI with magic link", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -767,9 +766,10 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") app.use(errorHandler()); + const email = "test@example.com"; let codeInfo = await ThirdPartyPasswordless.createCode({ tenantId: "public", - email: "test@example.com", + email, }); { @@ -813,22 +813,21 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }) ); - assert(validLinkCodeResponse.status === "OK"); - assert(validLinkCodeResponse.createdNewUser === true); - assert(typeof validLinkCodeResponse.user.id === "string"); - assert(typeof validLinkCodeResponse.user.email === "string"); - assert(typeof validLinkCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validLinkCodeResponse.user).length === 4); - assert(Object.keys(validLinkCodeResponse).length === 3); + checkConsumeResponse(validLinkCodeResponse, { + email, + phoneNumber: undefined, + isNew: true, + isPrimary: false, + }); } }); it("test with thirdPartyPasswordless, consumeCodeAPI with code", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -860,9 +859,10 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") app.use(errorHandler()); + const email = "test@example.com"; let codeInfo = await ThirdPartyPasswordless.createCode({ tenantId: "public", - email: "test@example.com", + email, }); { @@ -912,13 +912,12 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }) ); - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.email === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 4); - assert(Object.keys(validUserInputCodeResponse).length === 3); + checkConsumeResponse(validUserInputCodeResponse, { + email, + phoneNumber: undefined, + isNew: true, + isPrimary: false, + }); } { @@ -945,12 +944,15 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }); it("test with thirdPartyPasswordless, consumeCodeAPI with expired code", async function () { - await setKeyValueInConfig("passwordless_code_lifetime", 1000); // one second lifetime - await startST(); + const connectionURI = await startST({ + coreConfig: { + passwordless_code_lifetime: 1000, // one second lifetime + }, + }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1016,11 +1018,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }); it("test with thirdPartyPasswordless, createCodeAPI with email", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1099,11 +1101,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }); it("test with thirdPartyPasswordless, createCodeAPI with phoneNumber", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1182,12 +1184,12 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }); it("test with thirdPartyPasswordless, magicLink format in createCodeAPI", async function () { - await startST(); + const connectionURI = await startST(); let magicLinkURL = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1251,11 +1253,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }); it("test with ThirdPartyPasswordless, emailExistsAPI", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1344,11 +1346,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }); it("test with thirdPartyPasswordless, phoneNumberExistsAPI", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1439,11 +1441,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") //resendCode API it("test with thirdPartyPasswordless, resendCodeAPI", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1528,11 +1530,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") // test that you create a code with PHONE in config, you then change the config to use EMAIL, you call resendCode API, it should return RESTART_FLOW_ERROR it("test with thirdPartyPasswordless, resendCodeAPI when changing contact method", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1571,11 +1573,11 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") }); await killAllST(); - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1620,3 +1622,49 @@ describe(`apiFunctions: ${printPath("[test/thirdpartypasswordless/api.test.js]") } }); }); + +function checkConsumeResponse(validUserInputCodeResponse, { email, phoneNumber, isNew, isPrimary }) { + assert.strictEqual(validUserInputCodeResponse.status, "OK"); + assert.strictEqual(validUserInputCodeResponse.createdNewRecipeUser, isNew); + + assert.strictEqual(typeof validUserInputCodeResponse.user.id, "string"); + assert.strictEqual(typeof validUserInputCodeResponse.user.timeJoined, "number"); + assert.strictEqual(validUserInputCodeResponse.user.isPrimaryUser, isPrimary); + + assert(validUserInputCodeResponse.user.emails instanceof Array); + if (email !== undefined) { + assert.strictEqual(validUserInputCodeResponse.user.emails.length, 1); + assert.strictEqual(validUserInputCodeResponse.user.emails[0], email); + } else { + assert.strictEqual(validUserInputCodeResponse.user.emails.length, 0); + } + + assert(validUserInputCodeResponse.user.phoneNumbers instanceof Array); + if (phoneNumber !== undefined) { + assert.strictEqual(validUserInputCodeResponse.user.phoneNumbers.length, 1); + assert.strictEqual(validUserInputCodeResponse.user.phoneNumbers[0], phoneNumber); + } else { + assert.strictEqual(validUserInputCodeResponse.user.phoneNumbers.length, 0); + } + + assert.strictEqual(validUserInputCodeResponse.user.thirdParty.length, 0); + + assert.strictEqual(validUserInputCodeResponse.user.loginMethods.length, 1); + const loginMethod = { + recipeId: "passwordless", + recipeUserId: validUserInputCodeResponse.user.id, + timeJoined: validUserInputCodeResponse.user.timeJoined, + verified: true, + tenantIds: ["public"], + }; + if (email) { + loginMethod.email = email; + } + if (phoneNumber) { + loginMethod.phoneNumber = phoneNumber; + } + assert.deepStrictEqual(validUserInputCodeResponse.user.loginMethods, [loginMethod]); + + assert.strictEqual(Object.keys(validUserInputCodeResponse.user).length, 8); + assert.strictEqual(Object.keys(validUserInputCodeResponse).length, 3); +} diff --git a/test/thirdpartypasswordless/authorisationUrlFeature.test.js b/test/thirdpartypasswordless/authorisationUrlFeature.test.js index ebd58d128..66cf83c17 100644 --- a/test/thirdpartypasswordless/authorisationUrlFeature.test.js +++ b/test/thirdpartypasswordless/authorisationUrlFeature.test.js @@ -61,10 +61,10 @@ describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authoris }); it("test with thirdPartyPasswordless, minimum config for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -117,12 +117,12 @@ describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authoris }); it("test calling authorisation url API with empty init", async function () { - await startST(); + const connectionURI = await startST(); // testing with the google OAuth development key STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -166,12 +166,12 @@ describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authoris }); it("test calling authorisation url API with empty init with dynamic third party provider", async function () { - await startST(); + const connectionURI = await startST(); // testing with the google OAuth development key STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -230,10 +230,10 @@ describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authoris // testing 4xx error correctly thrown from sub-recipe it("test with thirdPartyPasswordless, thirdparty provider doesn't exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartypasswordless/config.test.js b/test/thirdpartypasswordless/config.test.js index a45f0db52..3c016fdbe 100644 --- a/test/thirdpartypasswordless/config.test.js +++ b/test/thirdpartypasswordless/config.test.js @@ -42,11 +42,11 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js - minimal config */ it("test minimum config for thirdpartypasswordless with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -87,14 +87,14 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js - adding custom validators for phone and email and making sure that they are called */ it("test for thirdPartyPasswordless, adding custom validators for phone and email with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isValidateEmailAddressCalled = false; let isValidatePhoneNumberCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -194,13 +194,13 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, use custom function to send email with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -271,13 +271,13 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, use custom function to send text SMS with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomTextMessageCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -347,11 +347,11 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js - minimal input works */ it("test for thirdPartyPasswordless minimum config with phone contactMethod", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -389,12 +389,12 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, if validatePhoneNumber is called with phone contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isValidatePhoneNumberCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -458,13 +458,13 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js // If you return a string from the function, the API throws a GENERIC ERROR await killAllST(); - await startST(); + const connectionURI = await startST(); isValidatePhoneNumberCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -520,13 +520,13 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE and phone contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; let isOtherInputValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -597,12 +597,12 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: MAGIC_LINK and phone contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -663,12 +663,12 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined */ it("test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and phone contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -730,12 +730,12 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test with thirdPartyPasswordless, createAndSendCustomTextMessage, if error is thrown, it should contain a general error in the response", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomTextMessageCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -801,11 +801,11 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test with thirdPartyPasswordless, minimum config with email contactMethod", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -844,12 +844,12 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, if validateEmailAddress is called with email contactMethod", async function () { - await startST(); + const connectionURI = await startST(); let isValidateEmailAddressCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -911,13 +911,13 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js // If you return a string from the function, the API throws a GENERIC ERROR await killAllST(); - await startST(); + const connectionURI = await startST(); isValidateEmailAddressCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -974,13 +974,13 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, createAndSendCustomEmail with flowType: USER_INPUT_CODE and email contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; let isOtherInputValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1051,12 +1051,12 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test with thirdPartyPasswordless, createAndSendCustomEmail with flowType: MAGIC_LINK and email contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1117,12 +1117,12 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined */ it("test with ThirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and email contact method", async function () { - await startST(); + const connectionURI = await startST(); let isUserInputCodeAndUrlWithLinkCodeValid = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1184,12 +1184,12 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, that for createAndSendCustomEmail, if error is thrown, the status in the response should be a general error", async function () { - await startST(); + const connectionURI = await startST(); let isCreateAndSendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1256,14 +1256,14 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test thirdPartyPasswordless, missing compulsory configs throws an error", async function () { - await startST(); + const connectionURI = await startST(); { // missing flowType try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1287,13 +1287,13 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js { await killAllST(); - await startST(); + const connectionURI = await startST(); // missing contactMethod try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1324,14 +1324,14 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js */ it("test for thirdPartyPasswordless, passing getCustomUserInputCode using different codes", async function () { - await startST(); + const connectionURI = await startST(); let customCode = undefined; let userCodeSent = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1413,7 +1413,7 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js }); it("test for thirdPartyPasswordless, passing getCustomUserInputCode using the same code", async function () { - await startST(); + const connectionURI = await startST(); // using the same customCode let customCode = "customCode"; @@ -1421,7 +1421,7 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1508,13 +1508,13 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js // Check basic override usage it("test basic override usage in thirdPartyPasswordless", async function () { - await startST(); + const connectionURI = await startST(); let customDeviceId = "customDeviceId"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1578,11 +1578,11 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js }); it("test for thirdPartyPasswordless, default config for thirdparty", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1629,10 +1629,10 @@ describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js }); it("test for thirdPartyPasswordless, minimum config for thirdparty module, custom provider", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartypasswordless/emailDelivery.test.js b/test/thirdpartypasswordless/emailDelivery.test.js index 042ab1a35..c121d9b2d 100644 --- a/test/thirdpartypasswordless/emailDelivery.test.js +++ b/test/thirdpartypasswordless/emailDelivery.test.js @@ -65,10 +65,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test default backward compatibility api being called: email verify", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -96,7 +96,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -105,7 +105,8 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery "public", "supertokens", "test-user-id", - "test@example.com" + "test@example.com", + false ); let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); @@ -138,10 +139,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test default backward compatibility api being called, error message not sent back to user: email verify", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -169,7 +170,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -178,7 +179,8 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery "public", "supertokens", "test-user-id", - "test@example.com" + "test@example.com", + false ); let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); @@ -212,14 +214,14 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test custom override: email verify", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let emailVerifyURL = undefined; let type = undefined; let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -261,7 +263,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -270,7 +272,8 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery "public", "supertokens", "test-user-id", - "test@example.com" + "test@example.com", + false ); let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); @@ -299,7 +302,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test smtp service: email verify", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let emailVerifyURL = undefined; let outerOverrideCalled = false; @@ -307,7 +310,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -380,7 +383,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery app.use(express.json()); app.use(middleware()); app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "public", req.body.id, {}, {}); + await Session.createNewSession(req, res, "public", STExpress.convertToRecipeUserId(req.body.id), {}, {}); res.status(200).send(""); }); app.use(errorHandler()); @@ -389,7 +392,8 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery "public", "supertokens", "test-user-id", - "test@example.com" + "test@example.com", + false ); let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); @@ -408,10 +412,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test default backward compatibility api being called: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -476,14 +480,14 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test backward compatibility: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -536,7 +540,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test custom override: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -545,7 +549,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -616,7 +620,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test smtp service: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -626,7 +630,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -717,10 +721,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test default backward compatibility api being called, error message sent back to user: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -792,10 +796,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test default backward compatibility api being called: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -878,7 +882,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test backward compatibility: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -886,7 +890,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery let sendCustomEmailCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -957,7 +961,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test custom override: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -968,7 +972,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery let loginCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1064,7 +1068,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test smtp service: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let email = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -1074,7 +1078,7 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery let getContentCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1192,10 +1196,10 @@ describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery }); it("test default backward compatibility api being called, error message sent back to user: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartypasswordless/getUsersByEmailFeature.test.js b/test/thirdpartypasswordless/getUsersByEmailFeature.test.js deleted file mode 100644 index 3ef44eb6e..000000000 --- a/test/thirdpartypasswordless/getUsersByEmailFeature.test.js +++ /dev/null @@ -1,128 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, isCDIVersionCompatible } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyPasswordlessRecipe = require("../../lib/build/recipe/thirdpartypasswordless/recipe").default; -let ThirdPartyPasswordless = require("../../lib/build/recipe/thirdpartypasswordless"); -const { thirdPartyManuallyCreateOrUpdateUser } = require("../../lib/build/recipe/thirdpartypasswordless"); -const { getUsersByEmail } = require("../../lib/build/recipe/thirdpartypasswordless"); -const { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`getUsersByEmail: ${printPath("[test/thirdpartypasswordless/getUsersByEmailFeature.test.js]")}`, function () { - const MockThirdPartyProvider = { - config: { - thirdPartyId: "mock", - }, - }; - - const MockThirdPartyProvider2 = { - config: { - thirdPartyId: "mock2", - }, - }; - - const testSTConfig = { - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - emailDelivery: { - sendEmail: async (input) => { - return; - }, - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [MockThirdPartyProvider], - }), - ], - }; - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("invalid email yields empty users array", async function () { - await startST(); - STExpress.init(testSTConfig); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - // given there are no users - - // when - const thirdPartyUsers = await getUsersByEmail("public", "john.doe@example.com"); - - // then - assert.strictEqual(thirdPartyUsers.length, 0); - }); - - it("valid email yields third party users", async function () { - await startST(); - STExpress.init({ - ...testSTConfig, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - emailDelivery: { - sendEmail: async (input) => { - return; - }, - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [MockThirdPartyProvider, MockThirdPartyProvider2], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - await thirdPartyManuallyCreateOrUpdateUser("public", "mock", "thirdPartyJohnDoe", "john.doe@example.com"); - await thirdPartyManuallyCreateOrUpdateUser("public", "mock2", "thirdPartyDaveDoe", "john.doe@example.com"); - - const thirdPartyUsers = await getUsersByEmail("public", "john.doe@example.com"); - - assert.strictEqual(thirdPartyUsers.length, 2); - - thirdPartyUsers.forEach((user) => { - assert.notStrictEqual(user.thirdParty.id, undefined); - assert.notStrictEqual(user.id, undefined); - assert.notStrictEqual(user.timeJoined, undefined); - assert.strictEqual(user.email, "john.doe@example.com"); - }); - }); -}); diff --git a/test/thirdpartypasswordless/override.test.js b/test/thirdpartypasswordless/override.test.js index d805985bb..8c53950a8 100644 --- a/test/thirdpartypasswordless/override.test.js +++ b/test/thirdpartypasswordless/override.test.js @@ -22,6 +22,7 @@ const { resetAll, signUPRequest, isCDIVersionCompatible, + assertJSONEquals, } = require("../utils"); let STExpress = require("../../"); let Session = require("../../recipe/session"); @@ -34,6 +35,7 @@ const express = require("express"); const request = require("supertest"); let nock = require("nock"); let { middleware, errorHandler } = require("../../framework/express"); +let AccountLinking = require("../../recipe/accountlinking"); describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test.js]")}`, function () { before(function () { @@ -72,12 +74,12 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. }); it("overriding functions tests", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let newUser = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -85,6 +87,34 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. websiteDomain: "supertokens.io", }, recipeList: [ + AccountLinking.init({ + override: { + functions: (oI) => { + return { + ...oI, + getUser: async (input) => { + let response = await oI.getUser(input); + if (response !== undefined) { + user = { + ...response, + loginMethods: [ + { + ...response.loginMethods[0], + recipeUserId: response.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; + } + return response; + }, + }; + }, + }, + }), ThirdPartyPasswordless.init({ contactMethod: "EMAIL", emailDelivery: { @@ -100,13 +130,20 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. ...oI, thirdPartySignInUp: async (input) => { let response = await oI.thirdPartySignInUp(input); - user = response.user; - newUser = response.createdNewUser; - return response; - }, - getUserById: async (input) => { - let response = await oI.getUserById(input); - user = response; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; + newUser = response.createdNewRecipeUser; return response; }, }; @@ -132,7 +169,9 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. app.get("/user", async (req, res) => { let userId = req.query.userId; - res.json(await ThirdPartyPasswordless.getUserById(userId)); + let user = await STExpress.getUser(userId); + user.loginMethods[0].recipeUserId = user.loginMethods[0].recipeUserId.getAsString(); + res.json(user); }); let signUpResponse = await new Promise((resolve) => @@ -158,7 +197,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse.user, user); + assertJSONEquals(signUpResponse.user, user); user = undefined; assert.strictEqual(user, undefined); @@ -186,7 +225,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse.user, user); + assertJSONEquals(signInResponse.user, user); user = undefined; assert.strictEqual(user, undefined); @@ -208,16 +247,16 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. ); assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(userByIdResponse, user); + assertJSONEquals(userByIdResponse, user); }); it("overriding api tests", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let newUser = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -241,8 +280,20 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. thirdPartySignInUpPOST: async (input) => { let response = await oI.thirdPartySignInUpPOST(input); if (response.status === "OK") { - user = response.user; - newUser = response.createdNewUser; + user = { + ...response.user, + loginMethods: [ + { + ...response.user.loginMethods[0], + recipeUserId: response.user.loginMethods[0].recipeUserId.getAsString(), + }, + ], + }; + delete user.loginMethods[0].hasSameEmailAs; + delete user.loginMethods[0].hasSamePhoneNumberAs; + delete user.loginMethods[0].hasSameThirdPartyInfoAs; + delete user.toJson; + newUser = response.createdNewRecipeUser; } return response; }, @@ -265,11 +316,6 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. app.use(errorHandler()); - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdPartyPasswordless.getUserById(userId)); - }); - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); let signUpResponse = await new Promise((resolve) => @@ -295,7 +341,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse.user, user); + assertJSONEquals(signUpResponse.user, user); user = undefined; assert.strictEqual(user, undefined); @@ -323,15 +369,15 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. assert.notStrictEqual(user, undefined); assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse.user, user); + assertJSONEquals(signInResponse.user, user); }); it("overriding functions tests, throws error", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -339,6 +385,24 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. websiteDomain: "supertokens.io", }, recipeList: [ + AccountLinking.init({ + override: { + functions: (oI) => { + return { + ...oI, + getUser: async (input) => { + let response = await oI.getUser(input); + if (input.userContext.shouldError === undefined) { + return response; + } + throw { + error: "get user error", + }; + }, + }; + }, + }, + }), ThirdPartyPasswordless.init({ contactMethod: "EMAIL", emailDelivery: { @@ -355,7 +419,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. thirdPartySignInUp: async (input) => { let response = await oI.thirdPartySignInUp(input); user = response.user; - newUser = response.createdNewUser; + newUser = response.createdNewRecipeUser; if (newUser) { throw { error: "signup error", @@ -393,7 +457,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. app.get("/user", async (req, res, next) => { try { let userId = req.query.userId; - res.json(await ThirdPartyPasswordless.getUserById(userId)); + res.json(await STExpress.getUser(userId, { shouldError: true })); } catch (err) { next(err); } @@ -475,13 +539,13 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. }); it("overriding api tests, throws error", async function () { - await startST(); + const connectionURI = await startST(); let user = undefined; let newUser = undefined; let emailExists = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -505,7 +569,7 @@ describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test. thirdPartySignInUpPOST: async (input) => { let response = await oI.thirdPartySignInUpPOST(input); user = response.user; - newUser = response.createdNewUser; + newUser = response.createdNewRecipeUser; if (newUser) { throw { error: "signup error", diff --git a/test/thirdpartypasswordless/provider.test.js b/test/thirdpartypasswordless/provider.test.js index d7dded7f3..03ab264f0 100644 --- a/test/thirdpartypasswordless/provider.test.js +++ b/test/thirdpartypasswordless/provider.test.js @@ -58,10 +58,10 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdpartypasswordless, the minimum config for third party provider google", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -161,10 +161,10 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless, passing additional params, check they are present in authorisation url for thirdparty provider google", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -244,10 +244,10 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test for thirdPartyPasswordless, passing scopes in config for thirdparty provider google", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -322,14 +322,14 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test for thirdPartyPasswordless, minimum config for third party provider facebook", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -427,14 +427,14 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless, passing scopes in config for third party provider facebook", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -507,14 +507,14 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless, minimum config for third party provider github", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -612,14 +612,14 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless, additional params, check they are present in authorisation url for third party provider github", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -697,14 +697,14 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless, passing scopes in config for third party provider github", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let clientSecret = "test-secret"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -777,7 +777,7 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless, minimum config for third party provider apple", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let additionalConfig = { @@ -788,7 +788,7 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -889,7 +889,7 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless, passing additional params, check they are present in authorisation url for third party provider apple", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let additionalConfig = { @@ -900,7 +900,7 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -979,7 +979,7 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless, passing scopes in config for third party provider apple", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let additionalConfig = { @@ -990,7 +990,7 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1064,7 +1064,7 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it.skip("test with thirdPartyPasswordless, passing invalid privateKey in config for third party provider apple", async function () { - await startST(); + const connectionURI = await startST(); let clientId = "test"; let additionalConfig = { @@ -1075,7 +1075,7 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1118,11 +1118,11 @@ describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test. }); it("test with thirdPartyPasswordless duplicate provider without any default", async function () { - await startST(); + const connectionURI = await startST(); try { STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartypasswordless/recipeFunctions.test.js b/test/thirdpartypasswordless/recipeFunctions.test.js index aaa112341..d9c87fc5b 100644 --- a/test/thirdpartypasswordless/recipeFunctions.test.js +++ b/test/thirdpartypasswordless/recipeFunctions.test.js @@ -20,6 +20,7 @@ let assert = require("assert"); let { ProcessState } = require("../../lib/build/processState"); const EmailVerification = require("../../recipe/emailverification"); let { isCDIVersionCompatible } = require("../utils"); +const { default: RecipeUserId } = require("../../lib/build/recipeUserId"); describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunctions.test.js]")}`, function () { beforeEach(async function () { @@ -35,11 +36,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct // test that creating a user with ThirdParty, and they have a verified email that, isEmailVerified returns true and the opposite case it("test with thirdPartyPasswordless, for ThirdParty user that isEmailVerified returns the correct email verification status", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -77,33 +78,44 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct "public", "customProvider", "verifiedUser", - "test@example.com" + "test@example.com", + false ); // verify the user's email - let emailVerificationToken = await EmailVerification.createEmailVerificationToken("public", response.user.id); + let emailVerificationToken = await EmailVerification.createEmailVerificationToken( + "public", + STExpress.convertToRecipeUserId(response.user.id), + response.user.email + ); await EmailVerification.verifyEmailUsingToken("public", emailVerificationToken.token); // check that the ThirdParty user's email is verified - assert(await EmailVerification.isEmailVerified(response.user.id)); + assert(await EmailVerification.isEmailVerified(STExpress.convertToRecipeUserId(response.user.id))); // create a ThirdParty user with an unverfied email and check that it is not verified let response2 = await ThirdPartyPasswordless.thirdPartyManuallyCreateOrUpdateUser( "public", "customProvider2", "NotVerifiedUser", - "test@example.com" + "test@example.com", + false ); - assert(!(await EmailVerification.isEmailVerified(response2.user.id))); + assert( + !(await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(response2.user.id), + response2.user.email + )) + ); }); it("test with thirdPartyPasswordless, for Passwordless user that isEmailVerified returns true for both email and phone", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -141,17 +153,23 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct email: "test@example.com", }); - // verify the user's email - let emailVerificationToken = await EmailVerification.createEmailVerificationToken("public", response.user.id); - await EmailVerification.verifyEmailUsingToken("public", emailVerificationToken.token); - // check that the Passwordless user's email is verified - assert(await EmailVerification.isEmailVerified(response.user.id)); + assert( + await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(response.user.id), + response.user.email + ) + ); // check that creating an email verification with a verified passwordless user should return EMAIL_ALREADY_VERIFIED_ERROR assert( - (await EmailVerification.createEmailVerificationToken("public", response.user.id)).status === - "EMAIL_ALREADY_VERIFIED_ERROR" + ( + await EmailVerification.createEmailVerificationToken( + "public", + STExpress.convertToRecipeUserId(response.user.id), + response.user.email + ) + ).status === "EMAIL_ALREADY_VERIFIED_ERROR" ); // create a Passwordless user with phone and check that it is verified @@ -161,21 +179,25 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); // check that the Passwordless phone number user's is automatically verified - assert(await EmailVerification.isEmailVerified(response2.user.id)); - + assert(await EmailVerification.isEmailVerified(STExpress.convertToRecipeUserId(response2.user.id))); // check that creating an email verification with a phone-based passwordless user should return EMAIL_ALREADY_VERIFIED_ERROR assert.equal( - (await EmailVerification.createEmailVerificationToken("public", response2.user.id)).status, + ( + await EmailVerification.createEmailVerificationToken( + "public", + STExpress.convertToRecipeUserId(response2.user.id) + ) + ).status, "EMAIL_ALREADY_VERIFIED_ERROR" ); }); it("test with thirdPartyPasswordless, getUser functionality", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -203,31 +225,34 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct return; } + const email = "test@example.com"; { - let user = await ThirdPartyPasswordless.getUserById("random"); + let user = await STExpress.getUser("random"); assert(user === undefined); user = ( await ThirdPartyPasswordless.passwordlessSignInUp({ tenantId: "public", - email: "test@example.com", + email, }) ).user; let userId = user.id; - let result = await ThirdPartyPasswordless.getUserById(userId); - - assert(result.id === user.id); - assert(result.email !== undefined && user.email === result.email); - assert(result.phoneNumber === undefined); - assert(typeof result.timeJoined === "number"); - assert(result.tenantIds.length === 1); - assert(Object.keys(result).length === 4); + let result = await STExpress.getUser(userId); + + assert.strictEqual(result.id, user.id); + assert.strictEqual(result.emails[0], email); + assert.strictEqual(result.phoneNumbers.length, 0); + assert.strictEqual(typeof result.timeJoined, "number"); + assert.strictEqual(result.loginMethods[0].tenantIds.length, 1); + assert.strictEqual(Object.keys(result).length, 8); } { - let users = await ThirdPartyPasswordless.getUsersByEmail("public", "random"); + let users = await STExpress.listUsersByAccountInfo("public", { + email: "random", + }); assert(users.length === 0); @@ -238,54 +263,55 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }) ).user; - let result = await ThirdPartyPasswordless.getUsersByEmail("public", user.email); + let result = await STExpress.listUsersByAccountInfo("public", { + email, + }); assert(result.length === 1); let userInfo = result[0]; - assert(userInfo.id === user.id); - assert(userInfo.email === user.email); - assert(userInfo.phoneNumber === undefined); - assert(typeof userInfo.timeJoined === "number"); - assert(userInfo.tenantIds.length === 1); - assert(Object.keys(userInfo).length === 4); + assert.strictEqual(userInfo.id, user.id); + assert.strictEqual(userInfo.emails[0], email); + assert.strictEqual(userInfo.phoneNumbers.length, 0); + assert.strictEqual(typeof userInfo.timeJoined, "number"); + assert.strictEqual(userInfo.loginMethods[0].tenantIds.length, 1); + assert.strictEqual(Object.keys(userInfo).length, 8); } { - let user = await ThirdPartyPasswordless.getUserByPhoneNumber({ - tenantId: "public", + let user = await STExpress.listUsersByAccountInfo("public", { phoneNumber: "random", }); - assert(user === undefined); + assert(user.length === 0); + const phoneNumber = "+1234567890"; user = ( await ThirdPartyPasswordless.passwordlessSignInUp({ tenantId: "public", - phoneNumber: "+1234567890", + phoneNumber, }) ).user; - let result = await ThirdPartyPasswordless.getUserByPhoneNumber({ - tenantId: "public", - phoneNumber: user.phoneNumber, + let result = await STExpress.listUsersByAccountInfo("public", { + phoneNumber, }); - assert(result.id === user.id); - assert(result.phoneNumber !== undefined && user.phoneNumber === result.phoneNumber); - assert(result.email === undefined); - assert(typeof result.timeJoined === "number"); - assert(result.tenantIds.length === 1); - assert(Object.keys(result).length === 4); + assert.strictEqual(result[0].id, user.id); + assert.strictEqual(result[0].phoneNumbers[0], phoneNumber); + assert.strictEqual(result[0].emails.length, 0); + assert.strictEqual(typeof result[0].timeJoined, "number"); + assert.strictEqual(result[0].loginMethods[0].tenantIds.length, 1); + assert.strictEqual(Object.keys(result[0]).length, 8); } }); it("test with thirdPartyPasswordless, createCode test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -348,11 +374,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); it("thirdPartyPasswordless createNewCodeForDevice test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -457,11 +483,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); it("thirdPartyPasswordless consumeCode test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -501,13 +527,13 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); assert(resp.status === "OK"); - assert(resp.createdNewUser); + assert(resp.createdNewRecipeUser); assert(typeof resp.user.id === "string"); - assert(resp.user.email === "test@example.com"); - assert(resp.user.phoneNumber === undefined); + assert(resp.user.emails[0] === "test@example.com"); + assert(resp.user.phoneNumbers[0] === undefined); assert(typeof resp.user.timeJoined === "number"); - assert(Object.keys(resp).length === 3); - assert(Object.keys(resp.user).length === 4); + assert(Object.keys(resp).length === 4); + assert(Object.keys(resp.user).length === 8); } { @@ -550,12 +576,15 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); it("thirdPartyPasswordless, consumeCode test with EXPIRED_USER_INPUT_CODE_ERROR", async function () { - await setKeyValueInConfig("passwordless_code_lifetime", 1000); // one second lifetime - await startST(); + const connectionURI = await startST({ + coreConfig: { + passwordless_code_lifetime: 1000, // one second lifetime + }, + }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -605,11 +634,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct // updateUser it("thirdPartyPasswordless, updateUser contactMethod email test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -642,19 +671,19 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct { // update users email let response = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: userInfo.user.id, + recipeUserId: userInfo.user.loginMethods[0].recipeUserId, email: "test2@example.com", }); assert(response.status === "OK"); - let result = await ThirdPartyPasswordless.getUserById(userInfo.user.id); + let result = await STExpress.getUser(userInfo.user.id); - assert(result.email === "test2@example.com"); + assert(result.emails[0] === "test2@example.com"); } { // update user with invalid userId let response = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: "invalidUserId", + recipeUserId: STExpress.convertToRecipeUserId("invalidUserId"), email: "test2@example.com", }); @@ -668,7 +697,7 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); let result = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: userInfo2.user.id, + recipeUserId: userInfo2.user.loginMethods[0].recipeUserId, email: "test2@example.com", }); @@ -677,11 +706,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); it("thirdPartyPasswordless, updateUser contactMethod phone test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -719,14 +748,14 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct { // update users email let response = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: userInfo.user.id, + recipeUserId: userInfo.user.loginMethods[0].recipeUserId, phoneNumber: phoneNumber_2, }); assert(response.status === "OK"); - let result = await ThirdPartyPasswordless.getUserById(userInfo.user.id); + let result = await STExpress.getUser(userInfo.user.id); - assert(result.phoneNumber === phoneNumber_2); + assert(result.phoneNumbers[0] === phoneNumber_2); } { // update user with a phoneNumber that already exists @@ -736,7 +765,7 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); let result = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: userInfo2.user.id, + recipeUserId: userInfo2.user.loginMethods[0].recipeUserId, phoneNumber: phoneNumber_2, }); @@ -746,11 +775,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct // revokeAllCodes it("thirdPartyPasswordless, revokeAllCodes test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -817,11 +846,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); it("thirdPartyPasswordless, revokeCode test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -889,11 +918,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct // listCodesByEmail it("thirdPartyPasswordless, listCodesByEmail test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -945,11 +974,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct //listCodesByPhoneNumber it("thirdPartyPasswordless, listCodesByPhoneNumber test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1001,11 +1030,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct // listCodesByDeviceId and listCodesByPreAuthSessionId it("thirdPartyPasswordless, listCodesByDeviceId and listCodesByPreAuthSessionId test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1059,11 +1088,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct */ it("thirdPartyPasswordless, createMagicLink test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1105,11 +1134,11 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct // signInUp test it("thirdPartyPasswordless, signInUp test", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1143,13 +1172,13 @@ describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunct }); assert(result.status === "OK"); - assert(result.createdNewUser === true); - assert(Object.keys(result).length === 3); - - assert(result.user.phoneNumber === "+12345678901"); - assert(typeof result.user.id === "string"); - assert(typeof result.user.timeJoined === "number"); - assert(result.user.tenantIds.length === 1); - assert(Object.keys(result.user).length === 4); + assert(result.createdNewRecipeUser === true); + assert(Object.keys(result).length === 4); + + assert.strictEqual(result.user.phoneNumbers[0], "+12345678901"); + assert.strictEqual(typeof result.user.id, "string"); + assert.strictEqual(typeof result.user.timeJoined, "number"); + assert(result.user.loginMethods[0].tenantIds.length === 1); + assert.strictEqual(Object.keys(result.user).length, 8); }); }); diff --git a/test/thirdpartypasswordless/signinupFeature.test.js b/test/thirdpartypasswordless/signinupFeature.test.js index 643db6140..12c27193a 100644 --- a/test/thirdpartypasswordless/signinupFeature.test.js +++ b/test/thirdpartypasswordless/signinupFeature.test.js @@ -155,10 +155,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test with thirdPartyPasswordless, that if you disable the signInUp api, it does not work", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -234,10 +234,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test with thirdPartyPasswordless, minimum config without code for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -296,10 +296,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); let cookies1 = extractInfoFromResponse(response1); assert.notStrictEqual(cookies1.accessToken, undefined); @@ -313,7 +313,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur assert.notStrictEqual(cookies1.frontToken, "remove"); assert.strictEqual( - await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), + await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(response1.body.user.id), + response1.body.user.email + ), true ); @@ -338,10 +341,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur assert.notStrictEqual(response2, undefined); assert.strictEqual(response2.body.status, "OK"); - assert.strictEqual(response2.body.createdNewUser, false); - assert.strictEqual(response2.body.user.thirdParty.id, "custom"); - assert.strictEqual(response2.body.user.thirdParty.userId, "user"); - assert.strictEqual(response2.body.user.email, "email@test.com"); + assert.strictEqual(response2.body.createdNewRecipeUser, false); + assert.strictEqual(response2.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response2.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response2.body.user.emails[0], "email@test.com"); let cookies2 = extractInfoFromResponse(response2); assert.notStrictEqual(cookies2.accessToken, undefined); @@ -356,10 +359,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test with thirdPartyPasswordless, missing code and authCodeResponse", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -410,10 +413,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test for thirdPartyPasswordless, minimum config for thirdParty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -478,10 +481,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); let cookies1 = extractInfoFromResponse(response1); assert.notStrictEqual(cookies1.accessToken, undefined); @@ -495,7 +498,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur assert.notStrictEqual(cookies1.frontToken, "remove"); assert.strictEqual( - await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), + await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(response1.body.user.id), + response1.body.user.email + ), true ); @@ -524,10 +530,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur assert.notStrictEqual(response2, undefined); assert.strictEqual(response2.body.status, "OK"); - assert.strictEqual(response2.body.createdNewUser, false); - assert.strictEqual(response2.body.user.thirdParty.id, "custom"); - assert.strictEqual(response2.body.user.thirdParty.userId, "user"); - assert.strictEqual(response2.body.user.email, "email@test.com"); + assert.strictEqual(response2.body.createdNewRecipeUser, false); + assert.strictEqual(response2.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response2.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response2.body.user.emails[0], "email@test.com"); let cookies2 = extractInfoFromResponse(response2); assert.notStrictEqual(cookies2.accessToken, undefined); @@ -542,10 +548,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test with thirdPartyPasswordless, with minimum config for thirdparty module, email unverified", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -610,10 +616,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur ); assert.notStrictEqual(response1, undefined); assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); + assert.strictEqual(response1.body.createdNewRecipeUser, true); + assert.strictEqual(response1.body.user.thirdParty[0].id, "custom"); + assert.strictEqual(response1.body.user.thirdParty[0].userId, "user"); + assert.strictEqual(response1.body.user.emails[0], "email@test.com"); let cookies1 = extractInfoFromResponse(response1); assert.notStrictEqual(cookies1.accessToken, undefined); @@ -627,16 +633,19 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur assert.notStrictEqual(cookies1.frontToken, "remove"); assert.strictEqual( - await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), + await EmailVerification.isEmailVerified( + STExpress.convertToRecipeUserId(response1.body.user.id), + response1.body.user.email + ), false ); }); it("test with thirdPartyPasswordless, thirdparty provider doesn't exist in config", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -694,10 +703,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test with thirdPartyPasswordless, email not returned in getProfileInfo function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -757,10 +766,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test with thirdPartyPasswordless, error thrown from getProfileInfo function", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -826,10 +835,10 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test with thirdPartyPasswordless, invalid POST params for thirdparty module", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -957,11 +966,11 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur }); it("test with thirdPartyPasswordless, getUserById when user does not exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -983,7 +992,7 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur ], }); - assert.strictEqual(await ThirdPartyPasswordless.getUserById("randomID"), undefined); + assert.strictEqual(await STExpress.getUser("randomID"), undefined); const app = express(); @@ -1021,18 +1030,18 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur assert.strictEqual(response.statusCode, 200); let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyPasswordless.getUserById(signUpUserInfo.id); + let userInfo = await STExpress.getUser(signUpUserInfo.id); - assert.strictEqual(userInfo.email, signUpUserInfo.email); + assert.strictEqual(userInfo.emails[0], signUpUserInfo.emails[0]); assert.strictEqual(userInfo.id, signUpUserInfo.id); }); it("test getUserByThirdPartyInfo when user does not exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1059,7 +1068,14 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur return; } - assert.strictEqual(await ThirdPartyPasswordless.getUserByThirdPartyInfo("public", "custom", "user"), undefined); + assert.strictEqual( + ( + await STExpress.listUsersByAccountInfo("public", { + thirdParty: { id: "custom", userId: "user" }, + }) + ).length, + 0 + ); const app = express(); @@ -1092,9 +1108,11 @@ describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeatur assert.strictEqual(response.statusCode, 200); let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyPasswordless.getUserByThirdPartyInfo("public", "custom", "user"); + let userInfo = await STExpress.listUsersByAccountInfo("public", { + thirdParty: { id: "custom", userId: "user" }, + }); - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); + assert.strictEqual(userInfo[0].emails[0], signUpUserInfo.emails[0]); + assert.strictEqual(userInfo[0].id, signUpUserInfo.id); }); }); diff --git a/test/thirdpartypasswordless/signoutFeature.test.js b/test/thirdpartypasswordless/signoutFeature.test.js index 4ee6dce55..6a4425336 100644 --- a/test/thirdpartypasswordless/signoutFeature.test.js +++ b/test/thirdpartypasswordless/signoutFeature.test.js @@ -69,11 +69,11 @@ describe(`signoutTest: ${printPath("[test/thirdpartypasswordless/signoutFeature. }); it("test the default route and it should revoke the session", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -160,11 +160,11 @@ describe(`signoutTest: ${printPath("[test/thirdpartypasswordless/signoutFeature. }); it("test that disabling default route and calling the API returns 404", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -221,11 +221,11 @@ describe(`signoutTest: ${printPath("[test/thirdpartypasswordless/signoutFeature. }); it("test that calling the API without a session should return OK", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -275,13 +275,11 @@ describe(`signoutTest: ${printPath("[test/thirdpartypasswordless/signoutFeature. }); it("test that signout API reutrns try refresh token, refresh session and signout should return OK", async function () { - await setKeyValueInConfig("access_token_validity", 2); - - await startST(); + const connectionURI = await startST({ coreConfig: { access_token_validity: 2 } }); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartypasswordless/smsDelivery.test.js b/test/thirdpartypasswordless/smsDelivery.test.js index b49fd9130..6fe1eb9fc 100644 --- a/test/thirdpartypasswordless/smsDelivery.test.js +++ b/test/thirdpartypasswordless/smsDelivery.test.js @@ -40,10 +40,10 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test default backward compatibility api being called: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -111,14 +111,14 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test backward compatibility: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; let userInputCode = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -171,7 +171,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test custom override: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -180,7 +180,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes let appName = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -251,7 +251,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test twilio service: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -261,7 +261,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes let twilioAPICalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -353,10 +353,10 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test default backward compatibility api being called, error message sent back to user: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -428,7 +428,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test supertokens service: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -438,7 +438,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes let type = undefined; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -508,10 +508,10 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test default backward compatibility api being called, error message not sent back to user if response code is 429: passwordless login", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -577,10 +577,10 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test default backward compatibility api being called: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -666,7 +666,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test backward compatibility: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -674,7 +674,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes let sendCustomSMSCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -745,7 +745,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test custom override: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let urlWithLinkCode = undefined; @@ -756,7 +756,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes let loginCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -852,7 +852,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test twilio service: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -864,7 +864,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes let twilioAPICalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -993,10 +993,10 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test default backward compatibility api being called, error message sent back to user: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1092,7 +1092,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test supertokens service: resend code api", async function () { - await startST(); + const connectionURI = await startST(); let phoneNumber = undefined; let codeLifetime = undefined; let userInputCode = undefined; @@ -1103,7 +1103,7 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes let loginCalled = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -1193,10 +1193,10 @@ describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.tes }); it("test default backward compatibility api being called, error message not sent back to user if response code is 429: resend code api", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/thirdpartypasswordless/users.test.js b/test/thirdpartypasswordless/users.test.js index f35afa660..68ff8d9d1 100644 --- a/test/thirdpartypasswordless/users.test.js +++ b/test/thirdpartypasswordless/users.test.js @@ -67,10 +67,10 @@ describe(`usersTest: ${printPath("[test/thirdpartypasswordless/users.test.js]")} }); it("test getUsersOldestFirst", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -116,12 +116,12 @@ describe(`usersTest: ${printPath("[test/thirdpartypasswordless/users.test.js]")} users = await getUsersOldestFirst({ tenantId: "public", limit: 1 }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersOldestFirst({ tenantId: "public", limit: 1, paginationToken: users.nextPaginationToken }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test1@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test1@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersOldestFirst({ tenantId: "public", limit: 5, paginationToken: users.nextPaginationToken }); @@ -148,10 +148,10 @@ describe(`usersTest: ${printPath("[test/thirdpartypasswordless/users.test.js]")} }); it("test getUsersNewestFirst", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -197,12 +197,12 @@ describe(`usersTest: ${printPath("[test/thirdpartypasswordless/users.test.js]")} users = await getUsersNewestFirst({ tenantId: "public", limit: 1 }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test4@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test4@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersNewestFirst({ tenantId: "public", limit: 1, paginationToken: users.nextPaginationToken }); assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test3@gmail.com"); + assert.strictEqual(users.users[0].emails[0], "test3@gmail.com"); assert.strictEqual(typeof users.nextPaginationToken, "string"); users = await getUsersNewestFirst({ tenantId: "public", limit: 5, paginationToken: users.nextPaginationToken }); @@ -229,10 +229,10 @@ describe(`usersTest: ${printPath("[test/thirdpartypasswordless/users.test.js]")} }); it("test getUserCount", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userContext.test.js b/test/userContext.test.js index c24ca44dc..4483d98eb 100644 --- a/test/userContext.test.js +++ b/test/userContext.test.js @@ -50,12 +50,12 @@ describe(`userContext: ${printPath("[test/userContext.test.js]")}`, function () }); it("testing context across interface and recipe function", async function () { - await startST(); + const connectionURI = await startST(); let works = false; let signUpContextWorks = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -187,14 +187,14 @@ describe(`userContext: ${printPath("[test/userContext.test.js]")}`, function () }); it("testing default context across interface and recipe function", async function () { - await startST(); + const connectionURI = await startST(); let signInContextWorks = false; let signInAPIContextWorks = false; let createNewSessionContextWorks = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -290,14 +290,14 @@ describe(`userContext: ${printPath("[test/userContext.test.js]")}`, function () }); it("Test that SuperTokens.getRequestFromUserContext works as expected", async function () { - await startST(); + const connectionURI = await startST(); let signInContextWorks = false; let signInAPIContextWorks = false; let createNewSessionContextWorks = false; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/useridmapping/createUserIdMapping.test.js b/test/useridmapping/createUserIdMapping.test.js index e43031400..b8ef1c4c4 100644 --- a/test/useridmapping/createUserIdMapping.test.js +++ b/test/useridmapping/createUserIdMapping.test.js @@ -23,11 +23,11 @@ describe(`createUserIdMappingTest: ${printPath("[test/useridmapping/createUserId describe("createUserIdMappingTest", () => { it("create a userId mapping", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -74,11 +74,11 @@ describe(`createUserIdMappingTest: ${printPath("[test/useridmapping/createUserId }); it("create a userId mapping with an unknown superTokensUserId", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -106,11 +106,11 @@ describe(`createUserIdMappingTest: ${printPath("[test/useridmapping/createUserId }); it("create a userId mapping when a mapping already exists", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -188,11 +188,11 @@ describe(`createUserIdMappingTest: ${printPath("[test/useridmapping/createUserId }); it("create a userId mapping when userId already has usermetadata with and without force", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/useridmapping/deleteUserIdMapping.test.js b/test/useridmapping/deleteUserIdMapping.test.js index 3e2659d50..7e7220c04 100644 --- a/test/useridmapping/deleteUserIdMapping.test.js +++ b/test/useridmapping/deleteUserIdMapping.test.js @@ -23,11 +23,11 @@ describe(`deleteUserIdMappingTest: ${printPath("[test/useridmapping/deleteUserId describe("deleteUserIdMapping:", () => { it("delete an unknown userId mapping", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -67,11 +67,11 @@ describe(`deleteUserIdMappingTest: ${printPath("[test/useridmapping/deleteUserId }); it("delete a userId mapping with userIdType as SUPERTOKENS", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -121,11 +121,11 @@ describe(`deleteUserIdMappingTest: ${printPath("[test/useridmapping/deleteUserId }); it("delete a userId mapping with userIdType as EXTERNAL", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -175,11 +175,11 @@ describe(`deleteUserIdMappingTest: ${printPath("[test/useridmapping/deleteUserId }); it("delete a userId mapping with userIdType as ANY", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -256,11 +256,11 @@ describe(`deleteUserIdMappingTest: ${printPath("[test/useridmapping/deleteUserId }); it("delete a userId mapping when userMetadata exists with externalId with and without force", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/useridmapping/getUserIdMapping.test.js b/test/useridmapping/getUserIdMapping.test.js index 35f82236e..f4c47754d 100644 --- a/test/useridmapping/getUserIdMapping.test.js +++ b/test/useridmapping/getUserIdMapping.test.js @@ -22,11 +22,11 @@ describe(`getUserIdMappingTest: ${printPath("[test/useridmapping/getUserIdMappin describe("getUserIdMappingTest", () => { it("get userId mapping", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -115,11 +115,11 @@ describe(`getUserIdMappingTest: ${printPath("[test/useridmapping/getUserIdMappin }); it("get userId mapping when mapping does not exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -165,11 +165,11 @@ describe(`getUserIdMappingTest: ${printPath("[test/useridmapping/getUserIdMappin }); it("get userId mapping when externalUserIdInfo does not exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/useridmapping/recipeTests/emailpassword.test.js b/test/useridmapping/recipeTests/emailpassword.test.js index 98d176abc..28e7740d9 100644 --- a/test/useridmapping/recipeTests/emailpassword.test.js +++ b/test/useridmapping/recipeTests/emailpassword.test.js @@ -23,10 +23,10 @@ describe(`userIdMapping with emailpassword: ${printPath( describe("getUserById", () => { it("create an emailPassword user and map their userId, retrieve the user info using getUserById and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -54,7 +54,7 @@ describe(`userIdMapping with emailpassword: ${printPath( // retrieve the users info, the id should be the superTokens userId { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); + let response = await STExpress.getUser(superTokensUserId); assert.strictEqual(response.id, superTokensUserId); } @@ -68,28 +68,28 @@ describe(`userIdMapping with emailpassword: ${printPath( // retrieve the users info using the superTokensUserId, the id in the response should be the externalId { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); + let response = await STExpress.getUser(superTokensUserId); assert.ok(response !== undefined); assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); + assert.strictEqual(response.emails[0], email); } // retrieve the users info using the externalId, the id in the response should be the externalId { - let response = await EmailPasswordRecipe.getUserById(externalId); + let response = await STExpress.getUser(externalId); assert.ok(response !== undefined); assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); + assert.strictEqual(response.emails[0], email); } }); }); describe("getUserByEmail", () => { it("create an emailPassword user and map their userId, retrieve the user info using getUserByEmail and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -117,8 +117,8 @@ describe(`userIdMapping with emailpassword: ${printPath( // retrieve the users info, the id should be the superTokens userId { - let response = await EmailPasswordRecipe.getUserByEmail("public", email); - assert.strictEqual(response.id, superTokensUserId); + let response = await STExpress.listUsersByAccountInfo("public", { email }); + assert.strictEqual(response[0].id, superTokensUserId); } let externalId = "externalId"; @@ -131,20 +131,20 @@ describe(`userIdMapping with emailpassword: ${printPath( // retrieve the users info using email, the id in the response should be the externalId { - let response = await EmailPasswordRecipe.getUserByEmail("public", email); + let response = await STExpress.listUsersByAccountInfo("public", { email }); assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); + assert.strictEqual(response[0].id, externalId); + assert.strictEqual(response[0].emails[0], email); } }); }); describe("signIn", () => { it("create an emailPassword user and map their userId, signIn, check that the userRetrieved has the mapped userId", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -172,8 +172,8 @@ describe(`userIdMapping with emailpassword: ${printPath( // retrieve the users info, the id should be the superTokens userId { - let response = await EmailPasswordRecipe.getUserByEmail("public", email); - assert.strictEqual(response.id, superTokensUserId); + let response = await STExpress.listUsersByAccountInfo("public", { email }); + assert.strictEqual(response[0].id, superTokensUserId); } let externalId = "externalId"; @@ -193,10 +193,10 @@ describe(`userIdMapping with emailpassword: ${printPath( describe("password reset", () => { it("create an emailPassword user and map their userId, and do a password reset using the external id, check that it gets reset", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -224,8 +224,8 @@ describe(`userIdMapping with emailpassword: ${printPath( // retrieve the users info, the id should be the superTokens userId { - let response = await EmailPasswordRecipe.getUserByEmail("public", email); - assert.strictEqual(response.id, superTokensUserId); + let response = await STExpress.listUsersByAccountInfo("public", { email }); + assert.strictEqual(response[0].id, superTokensUserId); } // map the userId @@ -237,20 +237,26 @@ describe(`userIdMapping with emailpassword: ${printPath( // create the password resestToken let createResetPasswordTokenResponse = await EmailPasswordRecipe.createResetPasswordToken( "public", - externalId + externalId, + email ); assert.strictEqual(createResetPasswordTokenResponse.status, "OK"); // reset the password const newPassword = "newTestPass123"; - let resetPasswordUsingTokenResponse = await EmailPasswordRecipe.resetPasswordUsingToken( + let resetPasswordUsingTokenResponse = await EmailPasswordRecipe.consumePasswordResetToken( "public", - createResetPasswordTokenResponse.token, - newPassword + createResetPasswordTokenResponse.token ); assert.strictEqual(resetPasswordUsingTokenResponse.status, "OK"); assert.strictEqual(resetPasswordUsingTokenResponse.userId, externalId); + let resp = await EmailPasswordRecipe.updateEmailOrPassword({ + recipeUserId: STExpress.convertToRecipeUserId(externalId), + password: newPassword, + }); + assert.strictEqual(resp.status, "OK"); + // check that the password is reset by signing in let response = await EmailPasswordRecipe.signIn("public", email, newPassword); assert.strictEqual(response.status, "OK"); @@ -260,10 +266,10 @@ describe(`userIdMapping with emailpassword: ${printPath( describe("update email and password", () => { it("create an emailPassword user and map their userId, update their email and password using the externalId", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -291,8 +297,8 @@ describe(`userIdMapping with emailpassword: ${printPath( // retrieve the users info, the id should be the superTokens userId { - let response = await EmailPasswordRecipe.getUserByEmail("public", email); - assert.strictEqual(response.id, superTokensUserId); + let response = await STExpress.listUsersByAccountInfo("public", { email }); + assert.strictEqual(response[0].id, superTokensUserId); } // map the userId @@ -307,7 +313,7 @@ describe(`userIdMapping with emailpassword: ${printPath( { { const response = await EmailPasswordRecipe.updateEmailOrPassword({ - userId: externalId, + recipeUserId: STExpress.convertToRecipeUserId(externalId), email: updatedEmail, }); assert.strictEqual(response.status, "OK"); @@ -326,7 +332,7 @@ describe(`userIdMapping with emailpassword: ${printPath( { { const response = await EmailPasswordRecipe.updateEmailOrPassword({ - userId: externalId, + recipeUserId: STExpress.convertToRecipeUserId(externalId), password: updatedPassword, }); assert.strictEqual(response.status, "OK"); diff --git a/test/useridmapping/recipeTests/passwordless.test.js b/test/useridmapping/recipeTests/passwordless.test.js index 9d12f0223..d29b0c52f 100644 --- a/test/useridmapping/recipeTests/passwordless.test.js +++ b/test/useridmapping/recipeTests/passwordless.test.js @@ -23,10 +23,10 @@ describe(`userIdMapping with passwordless: ${printPath( describe("consumeCode", () => { it("create a passwordless user and map their userId, signIn again and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -108,10 +108,10 @@ describe(`userIdMapping with passwordless: ${printPath( describe("getUserById", () => { it("create a passwordless user and map their userId, call getUserById and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -171,9 +171,7 @@ describe(`userIdMapping with passwordless: ${printPath( externalUserId: externalId, }); - let response = await PasswordlessRecipe.getUserById({ - userId: externalId, - }); + let response = await STExpress.getUser(externalId); assert.ok(response !== undefined); assert.strictEqual(response.id, externalId); }); @@ -181,10 +179,10 @@ describe(`userIdMapping with passwordless: ${printPath( describe("getUserByEmail", () => { it("create a passwordless user and map their userId, call getUserByEmail and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -244,21 +242,20 @@ describe(`userIdMapping with passwordless: ${printPath( externalUserId: externalId, }); - let response = await PasswordlessRecipe.getUserByEmail({ - tenantId: "public", + let response = await STExpress.listUsersByAccountInfo("public", { email, }); assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); + assert.strictEqual(response[0].id, externalId); }); }); describe("getUserByPhoneNumber", () => { it("create a passwordless user and map their userId, call getUserByPhoneNumber and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -318,21 +315,20 @@ describe(`userIdMapping with passwordless: ${printPath( externalUserId: externalId, }); - let response = await PasswordlessRecipe.getUserByPhoneNumber({ - tenantId: "public", + let response = await STExpress.listUsersByAccountInfo("public", { phoneNumber, }); assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); + assert.strictEqual(response[0].id, externalId); }); }); describe("updateUser", () => { it("create a passwordless user and map their userId, call updateUser to add their email and retrieve the user to see if the changes are reflected", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -394,19 +390,18 @@ describe(`userIdMapping with passwordless: ${printPath( }); let updateUserResponse = await PasswordlessRecipe.updateUser({ - userId: externalId, + recipeUserId: STExpress.convertToRecipeUserId(externalId), email, }); assert.strictEqual(updateUserResponse.status, "OK"); // retrieve user - let response = await PasswordlessRecipe.getUserByPhoneNumber({ - tenantId: "public", + let response = await STExpress.listUsersByAccountInfo("public", { phoneNumber, }); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.phoneNumber, phoneNumber); - assert.strictEqual(response.email, email); + assert.strictEqual(response[0].id, externalId); + assert.strictEqual(response[0].phoneNumbers[0], phoneNumber); + assert.strictEqual(response[0].emails[0], email); }); }); }); diff --git a/test/useridmapping/recipeTests/supertokens.test.js b/test/useridmapping/recipeTests/supertokens.test.js index 0ee13c0d8..2b6425893 100644 --- a/test/useridmapping/recipeTests/supertokens.test.js +++ b/test/useridmapping/recipeTests/supertokens.test.js @@ -24,10 +24,10 @@ describe(`userIdMapping with supertokens recipe: ${printPath( describe("deleteUser", () => { it("create an emailPassword user and map their userId, then delete user with the externalId", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -55,7 +55,7 @@ describe(`userIdMapping with supertokens recipe: ${printPath( // retrieve the users info, the id should be the superTokens userId { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); + let response = await STExpress.getUser(superTokensUserId); assert.strictEqual(response.id, superTokensUserId); } @@ -69,10 +69,10 @@ describe(`userIdMapping with supertokens recipe: ${printPath( // retrieve the users info using the superTokensUserId, the id in the response should be the externalId { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); + let response = await STExpress.getUser(superTokensUserId); assert.ok(response !== undefined); assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); + assert.strictEqual(response.emails[0], email); } // add userMetadata to the user mapped with the externalId @@ -95,7 +95,7 @@ describe(`userIdMapping with supertokens recipe: ${printPath( // check that user does not exist { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); + let response = await STExpress.getUser(superTokensUserId); assert.ok(response === undefined); } // check that no metadata exists for the id @@ -108,10 +108,10 @@ describe(`userIdMapping with supertokens recipe: ${printPath( describe("getUsers", () => { it("create multiple users and map one of the users userId, retrieve all users and check that response will contain the externalId for the mapped user", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -155,7 +155,7 @@ describe(`userIdMapping with supertokens recipe: ${printPath( let response = await STExpress.getUsersNewestFirst({ tenantId: "public" }); assert.strictEqual(response.users.length, 4); // since the first user we created has their userId mapped we access the last element from the users array in the response - const oldestUsersId = response.users[response.users.length - 1].user.id; + const oldestUsersId = response.users[response.users.length - 1].id; assert.strictEqual(oldestUsersId, externalId); } @@ -164,7 +164,7 @@ describe(`userIdMapping with supertokens recipe: ${printPath( let response = await STExpress.getUsersOldestFirst({ tenantId: "public" }); assert.strictEqual(response.users.length, 4); - const oldestUsersId = response.users[0].user.id; + const oldestUsersId = response.users[0].id; assert.strictEqual(oldestUsersId, externalId); } }); diff --git a/test/useridmapping/recipeTests/thirdparty.test.js b/test/useridmapping/recipeTests/thirdparty.test.js index 216f12276..2dc22f6d0 100644 --- a/test/useridmapping/recipeTests/thirdparty.test.js +++ b/test/useridmapping/recipeTests/thirdparty.test.js @@ -23,10 +23,10 @@ describe(`userIdMapping with thirdparty: ${printPath( describe("signInUp", () => { it("create a thirdParty user and map their userId, signIn and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -67,7 +67,8 @@ describe(`userIdMapping with thirdparty: ${printPath( "public", "google", "tpId", - "test@example.com" + "test@example.com", + false ); assert.strictEqual(signInUpResponse.status, "OK"); @@ -84,21 +85,22 @@ describe(`userIdMapping with thirdparty: ${printPath( "public", "google", "tpId", - "test@example.com" + "test@example.com", + false ); assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.createdNewUser, false); + assert.strictEqual(response.createdNewRecipeUser, false); assert.strictEqual(response.user.id, externalId); }); }); describe("getUserById", () => { it("create a thirdParty user and map their userId, retrieve the user info using getUserById and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -139,7 +141,8 @@ describe(`userIdMapping with thirdparty: ${printPath( "public", "google", "tpId", - "test@example.com" + "test@example.com", + false ); assert.strictEqual(signInUpResponse.status, "OK"); @@ -153,7 +156,7 @@ describe(`userIdMapping with thirdparty: ${printPath( }); // retrieve the user - let response = await ThirdPartyRecipe.getUserById(externalId); + let response = await STExpress.getUser(externalId); assert.ok(response != undefined); assert.strictEqual(response.id, externalId); }); @@ -161,10 +164,10 @@ describe(`userIdMapping with thirdparty: ${printPath( describe("getUsersByEmail", () => { it("create a thirdParty user and map their userId, retrieve the user info using getUsersByEmail and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -205,7 +208,8 @@ describe(`userIdMapping with thirdparty: ${printPath( "public", "google", "tpId", - "test@example.com" + "test@example.com", + false ); assert.strictEqual(signInUpResponse.status, "OK"); @@ -219,7 +223,7 @@ describe(`userIdMapping with thirdparty: ${printPath( }); // retrieve the user - let response = await ThirdPartyRecipe.getUsersByEmail("public", "test@example.com"); + let response = await STExpress.listUsersByAccountInfo("public", { email: "test@example.com" }); assert.strictEqual(response.length, 1); assert.strictEqual(response[0].id, externalId); }); @@ -227,10 +231,10 @@ describe(`userIdMapping with thirdparty: ${printPath( describe("getUserByThirdPartyInfo", () => { it("create a thirdParty user and map their userId, retrieve the user info using getUserByThirdPartyInfo and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -273,7 +277,8 @@ describe(`userIdMapping with thirdparty: ${printPath( "public", thirdPartyId, thirdPartyUserId, - "test@example.com" + "test@example.com", + false ); assert.strictEqual(signInUpResponse.status, "OK"); @@ -287,9 +292,11 @@ describe(`userIdMapping with thirdparty: ${printPath( }); // retrieve the user - let response = await ThirdPartyRecipe.getUserByThirdPartyInfo("public", thirdPartyId, thirdPartyUserId); - assert.ok(response != undefined); - assert.strictEqual(response.id, externalId); + let response = await STExpress.listUsersByAccountInfo("public", { + thirdParty: { id: thirdPartyId, userId: thirdPartyUserId }, + }); + assert.ok(response.length === 1); + assert.strictEqual(response[0].id, externalId); }); }); }); diff --git a/test/useridmapping/recipeTests/thirdpartyemailpassword.test.js b/test/useridmapping/recipeTests/thirdpartyemailpassword.test.js index a69d548a8..82e63632c 100644 --- a/test/useridmapping/recipeTests/thirdpartyemailpassword.test.js +++ b/test/useridmapping/recipeTests/thirdpartyemailpassword.test.js @@ -23,10 +23,10 @@ describe(`userIdMapping with ThirdPartyEmailPassword: ${printPath( describe("getUserById", () => { it("create an emailPassword a thirdParty user and map their userIds, retrieve the user info using getUserById and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -79,10 +79,10 @@ describe(`userIdMapping with ThirdPartyEmailPassword: ${printPath( // retrieve the users info using the externalId, the id in the response should be the externalId { - let response = await ThirdPartyEmailPasswordRecipe.getUserById(superTokensUserId); + let response = await STExpress.getUser(superTokensUserId); assert.ok(response !== undefined); assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); + assert.strictEqual(response.emails[0], email); } } @@ -94,7 +94,8 @@ describe(`userIdMapping with ThirdPartyEmailPassword: ${printPath( "public", "google", "tpId", - email + email, + false ); // map the users id @@ -108,10 +109,10 @@ describe(`userIdMapping with ThirdPartyEmailPassword: ${printPath( // retrieve the users info using the externalId, the id in the response should be the externalId { - let response = await ThirdPartyEmailPasswordRecipe.getUserById(superTokensUserId); + let response = await STExpress.getUser(superTokensUserId); assert.ok(response !== undefined); assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); + assert.strictEqual(response.emails[0], email); } } }); diff --git a/test/useridmapping/recipeTests/thirdpartypasswordless.test.js b/test/useridmapping/recipeTests/thirdpartypasswordless.test.js index 8697db494..69ea84f8d 100644 --- a/test/useridmapping/recipeTests/thirdpartypasswordless.test.js +++ b/test/useridmapping/recipeTests/thirdpartypasswordless.test.js @@ -23,10 +23,10 @@ describe(`userIdMapping with thirdPartyPasswordless: ${printPath( describe("getUserById", () => { it("create a thirdParty and passwordless user and map their userIds, retrieve the user info using getUserById and check that the externalId is returned", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -79,7 +79,8 @@ describe(`userIdMapping with thirdPartyPasswordless: ${printPath( "public", "google", "tpId", - email + email, + false ); // map the users id @@ -93,10 +94,10 @@ describe(`userIdMapping with thirdPartyPasswordless: ${printPath( // retrieve the user info using the externalId, the id in the response should be the externalId { - let response = await ThirdPartyPasswordlessRecipe.getUserById(superTokensUserId); + let response = await STExpress.getUser(superTokensUserId); assert.ok(response !== undefined); assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); + assert.strictEqual(response.emails[0], email); } } @@ -129,7 +130,7 @@ describe(`userIdMapping with thirdPartyPasswordless: ${printPath( }); // retrieve the user info using the externalId, the id in the response should be the externalId - let response = await ThirdPartyPasswordlessRecipe.getUserById(externalId); + let response = await STExpress.getUser(externalId); assert.ok(response !== undefined); assert.strictEqual(response.id, externalId); } diff --git a/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.js b/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.js index 5daab9d4e..6f14f3f2b 100644 --- a/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.js +++ b/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.js @@ -24,11 +24,11 @@ describe(`updateOrDeleteUserIdMappingInfoTest: ${printPath( describe("updateOrDeleteUserIdMappingInfoTest", () => { it("update externalUserId mapping info with unknown userId", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -79,11 +79,11 @@ describe(`updateOrDeleteUserIdMappingInfoTest: ${printPath( }); it("update externalUserId mapping info with userIdType as SUPERTOKENS and EXTERNAL", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -182,11 +182,11 @@ describe(`updateOrDeleteUserIdMappingInfoTest: ${printPath( }); it("update externalUserId mapping info with userIdType as ANY", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/usermetadata/clearUserMetadata.test.js b/test/usermetadata/clearUserMetadata.test.js index f15f4e922..c277d022f 100644 --- a/test/usermetadata/clearUserMetadata.test.js +++ b/test/usermetadata/clearUserMetadata.test.js @@ -21,13 +21,13 @@ describe(`clearUserMetadataTest: ${printPath("[test/usermetadata/clearUserMetada describe("clearUserMetadata", () => { it("should return OK for unknown user id", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -50,7 +50,7 @@ describe(`clearUserMetadataTest: ${printPath("[test/usermetadata/clearUserMetada }); it("should clear stored userId", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; const testMetadata = { @@ -59,7 +59,7 @@ describe(`clearUserMetadataTest: ${printPath("[test/usermetadata/clearUserMetada STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/usermetadata/config.test.js b/test/usermetadata/config.test.js index 5c20e7eeb..875694e0c 100644 --- a/test/usermetadata/config.test.js +++ b/test/usermetadata/config.test.js @@ -19,10 +19,10 @@ describe(`configTest: ${printPath("[test/usermetadata/config.test.js]")}`, funct describe("recipe init", () => { it("should work fine without config", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/usermetadata/getUserMetadata.test.js b/test/usermetadata/getUserMetadata.test.js index 9c116dec9..dccda8a79 100644 --- a/test/usermetadata/getUserMetadata.test.js +++ b/test/usermetadata/getUserMetadata.test.js @@ -21,13 +21,13 @@ describe(`getUserMetadataTest: ${printPath("[test/usermetadata/getUserMetadata.t describe("getUserMetadata", () => { it("should return an empty object for unknown userIds", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -51,7 +51,7 @@ describe(`getUserMetadataTest: ${printPath("[test/usermetadata/getUserMetadata.t }); it("should return an object if it's created.", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; const testMetadata = { @@ -60,7 +60,7 @@ describe(`getUserMetadataTest: ${printPath("[test/usermetadata/getUserMetadata.t STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/usermetadata/override.test.js b/test/usermetadata/override.test.js index d03e53f0d..e856ed243 100644 --- a/test/usermetadata/override.test.js +++ b/test/usermetadata/override.test.js @@ -21,7 +21,7 @@ describe(`overrideTest: ${printPath("[test/usermetadata/override.test.js]")}`, f describe("recipe functions", () => { it("should work without an override config", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; const testUserContext = { hello: ":)" }; @@ -31,7 +31,7 @@ describe(`overrideTest: ${printPath("[test/usermetadata/override.test.js]")}`, f STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -58,7 +58,7 @@ describe(`overrideTest: ${printPath("[test/usermetadata/override.test.js]")}`, f }); it("should call user provided overrides", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; const testUserContext = { hello: ":)" }; @@ -72,7 +72,7 @@ describe(`overrideTest: ${printPath("[test/usermetadata/override.test.js]")}`, f STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/usermetadata/updateUserMetadata.test.js b/test/usermetadata/updateUserMetadata.test.js index a67fabf6f..c83fe7b0d 100644 --- a/test/usermetadata/updateUserMetadata.test.js +++ b/test/usermetadata/updateUserMetadata.test.js @@ -21,7 +21,7 @@ describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMeta describe("updateUserMetadata", () => { it("should create metadata for unknown user id", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; const testMetadata = { @@ -30,7 +30,7 @@ describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMeta STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -58,7 +58,7 @@ describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMeta }); it("should create metadata with utf8 encoding", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; const testMetadata = { @@ -67,7 +67,7 @@ describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMeta STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -95,7 +95,7 @@ describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMeta }); it("should create metadata for cleared user id", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; const testMetadata = { @@ -104,7 +104,7 @@ describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMeta STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -135,7 +135,7 @@ describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMeta }); it("should update metadata by shallow merge", async function () { - await startST(); + const connectionURI = await startST(); const testUserId = "userId"; const testMetadata = { @@ -166,7 +166,7 @@ describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMeta STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/addRoleToUser.test.js b/test/userroles/addRoleToUser.test.js index e9e4bf680..e50629799 100644 --- a/test/userroles/addRoleToUser.test.js +++ b/test/userroles/addRoleToUser.test.js @@ -22,11 +22,11 @@ describe(`addRoleToUserTest: ${printPath("[test/userroles/addRoleToUser.test.js] describe("addRoleToUserTest", () => { it("add a role to a user", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -70,11 +70,11 @@ describe(`addRoleToUserTest: ${printPath("[test/userroles/addRoleToUser.test.js] }); it("add duplicate role to the user", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -125,11 +125,11 @@ describe(`addRoleToUserTest: ${printPath("[test/userroles/addRoleToUser.test.js] }); it("add unknown role to the user", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/claims.test.js b/test/userroles/claims.test.js index 6dc785ea1..1b8f9b8e2 100644 --- a/test/userroles/claims.test.js +++ b/test/userroles/claims.test.js @@ -30,10 +30,10 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function describe("recipe init", () => { it("should add claims to session without config", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -50,16 +50,21 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function return this.skip(); } - const session = await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + const session = await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); assert.deepStrictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), []); assert.deepStrictEqual(await session.getClaimValue(UserRoles.PermissionClaim), []); }); it("should not add claims if disabled in config", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -82,16 +87,21 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function return this.skip(); } - const session = await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + const session = await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); assert.strictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), undefined); assert.strictEqual(await session.getClaimValue(UserRoles.PermissionClaim), undefined); }); it("should add claims to session with values", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -110,7 +120,12 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); await UserRoles.addRoleToUser("public", "userId", "test"); - const session = await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + const session = await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); assert.deepStrictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), ["test"]); assert.deepStrictEqual(await session.getClaimValue(UserRoles.PermissionClaim), ["a", "b"]); }); @@ -118,10 +133,10 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function describe("validation", () => { it("should validate roles", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -140,7 +155,12 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); await UserRoles.addRoleToUser("public", "userId", "test"); - const session = await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + const session = await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); await session.assertClaims([UserRoles.UserRoleClaim.validators.includes("test")]); @@ -161,10 +181,10 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function }); }); it("should validate roles after refetching", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -186,17 +206,22 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function return this.skip(); } - const session = await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + const session = await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); await UserRoles.addRoleToUser("public", "userId", "test"); await session.assertClaims([UserRoles.UserRoleClaim.validators.includes("test")]); }); it("should validate permissions", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -215,7 +240,12 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); await UserRoles.addRoleToUser("public", "userId", "test"); - const session = await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + const session = await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); await session.assertClaims([UserRoles.PermissionClaim.validators.includes("a")]); @@ -236,10 +266,10 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function }); }); it("should validate permissions after refetching", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -261,7 +291,12 @@ describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function return this.skip(); } - const session = await Session.createNewSession(mockRequest(), mockResponse(), "public", "userId"); + const session = await Session.createNewSession( + mockRequest(), + mockResponse(), + "public", + STExpress.convertToRecipeUserId("userId") + ); await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); await UserRoles.addRoleToUser("public", "userId", "test"); diff --git a/test/userroles/config.test.js b/test/userroles/config.test.js index cac2dc39d..db4cb5406 100644 --- a/test/userroles/config.test.js +++ b/test/userroles/config.test.js @@ -20,10 +20,10 @@ describe(`configTest: ${printPath("[test/userroles/config.test.js]")}`, function describe("recipe init", () => { it("should work fine without config", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/createNewRoleOrAddPermissions.test.js b/test/userroles/createNewRoleOrAddPermissions.test.js index 8aa5b81bd..5b28ae4a2 100644 --- a/test/userroles/createNewRoleOrAddPermissions.test.js +++ b/test/userroles/createNewRoleOrAddPermissions.test.js @@ -24,11 +24,11 @@ describe(`createNewRoleOrAddPermissionsTest: ${printPath( describe("createNewRoleOrAddPermissions", () => { it("create a new role", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -51,13 +51,13 @@ describe(`createNewRoleOrAddPermissionsTest: ${printPath( }); it("create the same role twice", async function () { - await startST(); + const connectionURI = await startST(); const role = "role"; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -88,14 +88,14 @@ describe(`createNewRoleOrAddPermissionsTest: ${printPath( }); it("create a role with permissions", async function () { - await startST(); + const connectionURI = await startST(); const role = "role"; const permissions = ["permission1"]; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -127,14 +127,14 @@ describe(`createNewRoleOrAddPermissionsTest: ${printPath( }); it("add new permissions to a role", async function () { - await startST(); + const connectionURI = await startST(); const role = "role"; const permissions = ["permission1"]; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -179,14 +179,14 @@ describe(`createNewRoleOrAddPermissionsTest: ${printPath( }); it("add duplicate permission", async function () { - await startST(); + const connectionURI = await startST(); const role = "role"; const permissions = ["permission1"]; STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/deleteRole.test.js b/test/userroles/deleteRole.test.js index 393fa8351..341a16f8d 100644 --- a/test/userroles/deleteRole.test.js +++ b/test/userroles/deleteRole.test.js @@ -22,11 +22,11 @@ describe(`deleteRole: ${printPath("[test/userroles/deleteRole.test.js]")}`, func describe("deleteRole", () => { it("create roles, add them to a user and delete one of the roles", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -78,11 +78,11 @@ describe(`deleteRole: ${printPath("[test/userroles/deleteRole.test.js]")}`, func }); it("delete a role that does not exist", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/getPermissionsForRole.test.js b/test/userroles/getPermissionsForRole.test.js index e6263eb0b..6efb2df65 100644 --- a/test/userroles/getPermissionsForRole.test.js +++ b/test/userroles/getPermissionsForRole.test.js @@ -22,11 +22,11 @@ describe(`getPermissionsForRole: ${printPath("[test/userroles/getPermissionsForR describe("getPermissionsForRole", () => { it("get permissions for a role", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -61,11 +61,11 @@ describe(`getPermissionsForRole: ${printPath("[test/userroles/getPermissionsForR }); it("get permissions for an unknown role", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/getRolesForUser.test.js b/test/userroles/getRolesForUser.test.js index 8709442e0..d172da7b8 100644 --- a/test/userroles/getRolesForUser.test.js +++ b/test/userroles/getRolesForUser.test.js @@ -22,11 +22,11 @@ describe(`getRolesForUser: ${printPath("[test/userroles/getRolesForUser.test.js] describe("getRolesForUser", () => { it("create roles, add them to a user check that the user has the roles", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/getRolesThatHavePermissions.test.js b/test/userroles/getRolesThatHavePermissions.test.js index eb91c771c..87c69ccee 100644 --- a/test/userroles/getRolesThatHavePermissions.test.js +++ b/test/userroles/getRolesThatHavePermissions.test.js @@ -24,11 +24,11 @@ describe(`getRolesThatHavePermissions: ${printPath( describe("getRolesThatHavePermissions", () => { it("get roles that have permissions", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -66,11 +66,11 @@ describe(`getRolesThatHavePermissions: ${printPath( }); it("get roles for unknown permission", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/getUsersThatHaveRole.test.js b/test/userroles/getUsersThatHaveRole.test.js index 66515724d..5a411e02d 100644 --- a/test/userroles/getUsersThatHaveRole.test.js +++ b/test/userroles/getUsersThatHaveRole.test.js @@ -22,11 +22,11 @@ describe(`getUsersThatHaveRole: ${printPath("[test/userroles/getUsersThatHaveRol describe("getUsersThatHaveRole", () => { it("get users for a role", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -67,11 +67,11 @@ describe(`getUsersThatHaveRole: ${printPath("[test/userroles/getUsersThatHaveRol }); it("get users for an unknown role", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/multitenantRole.test.js b/test/userroles/multitenantRole.test.js index 941de1ca5..7e7d20213 100644 --- a/test/userroles/multitenantRole.test.js +++ b/test/userroles/multitenantRole.test.js @@ -38,10 +38,10 @@ describe(`multitenant role: ${printPath("[test/userroles/multitenantRole.test.js }); it("test that different roles can be assigned for the same user for each tenant", async function () { - await startSTWithMultitenancy(); + const connectionURI = await startSTWithMultitenancy(); SuperTokens.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -62,10 +62,11 @@ describe(`multitenant role: ${printPath("[test/userroles/multitenantRole.test.js const user = await EmailPassword.signUp("public", "test@example.com", "password1"); const userId = user.user.id; + const recipeUserId = user.user.loginMethods[0].recipeUserId; - await Multitenancy.associateUserToTenant("t1", userId); - await Multitenancy.associateUserToTenant("t2", userId); - await Multitenancy.associateUserToTenant("t3", userId); + await Multitenancy.associateUserToTenant("t1", recipeUserId); + await Multitenancy.associateUserToTenant("t2", recipeUserId); + await Multitenancy.associateUserToTenant("t3", recipeUserId); await UserRolesRecipe.createNewRoleOrAddPermissions("role1", []); await UserRolesRecipe.createNewRoleOrAddPermissions("role2", []); diff --git a/test/userroles/removePermissionsFromRole.test.js b/test/userroles/removePermissionsFromRole.test.js index a1b19dc4f..467a3a7b7 100644 --- a/test/userroles/removePermissionsFromRole.test.js +++ b/test/userroles/removePermissionsFromRole.test.js @@ -22,11 +22,11 @@ describe(`getPermissionsForRole: ${printPath("[test/userroles/getPermissionsForR describe("getPermissionsForRole", () => { it("remove permissions from a role", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -69,11 +69,11 @@ describe(`getPermissionsForRole: ${printPath("[test/userroles/getPermissionsForR }); it("remove permissions from an unknown role", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/userroles/removeUserRole.test.js b/test/userroles/removeUserRole.test.js index 90974f16b..5548bfa0d 100644 --- a/test/userroles/removeUserRole.test.js +++ b/test/userroles/removeUserRole.test.js @@ -22,11 +22,11 @@ describe(`removeUserRoleTest: ${printPath("[test/userroles/removeUserRole.test.j describe("removeUserRole", () => { it("remove role from user", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -84,11 +84,11 @@ describe(`removeUserRoleTest: ${printPath("[test/userroles/removeUserRole.test.j }); it("remove a role the user does not have", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", @@ -124,11 +124,11 @@ describe(`removeUserRoleTest: ${printPath("[test/userroles/removeUserRole.test.j }); it("remove an unknown role from the user", async function () { - await startST(); + const connectionURI = await startST(); STExpress.init({ supertokens: { - connectionURI: "http://localhost:8080", + connectionURI, }, appInfo: { apiDomain: "api.supertokens.io", diff --git a/test/utils.js b/test/utils.js index b756b1022..42ab19a96 100644 --- a/test/utils.js +++ b/test/utils.js @@ -19,6 +19,7 @@ let fs = require("fs"); const { default: fetch } = require("cross-fetch"); let SuperTokens = require("../lib/build/supertokens").default; let SessionRecipe = require("../lib/build/recipe/session/recipe").default; +let AccountLinkingRecipe = require("../lib/build/recipe/accountlinking/recipe").default; let ThirPartyRecipe = require("../lib/build/recipe/thirdparty/recipe").default; let ThirPartyPasswordless = require("../lib/build/recipe/thirdpartypasswordless/recipe").default; let ThirdPartyEmailPasswordRecipe = require("../lib/build/recipe/thirdpartyemailpassword/recipe").default; @@ -39,6 +40,7 @@ const { wrapRequest } = require("../framework/express"); const { join } = require("path"); const users = require("./users.json"); +let assert = require("assert"); module.exports.printPath = function (path) { return `${createFormat([consoleOptions.yellow, consoleOptions.italic, consoleOptions.dim])}${path}${createFormat([ @@ -227,6 +229,7 @@ module.exports.stopST = async function (pid) { module.exports.resetAll = function () { SuperTokens.reset(); + AccountLinkingRecipe.reset(); SessionRecipe.reset(); ThirdPartyPasswordlessRecipe.reset(); ThirdPartyEmailPasswordRecipe.reset(); @@ -260,7 +263,16 @@ module.exports.killAllSTCoresOnly = async function () { } }; -module.exports.startST = async function (host = "localhost", port = 8080) { +module.exports.startST = async function (config = {}) { + const host = config.host ?? "localhost"; + const port = config.port ?? 8080; + const notUsingTestApp = + process.env.REAL_DB_TEST !== "true" || host !== "localhost" || port !== 8080 || config.noApp === true; + if (config.coreConfig && notUsingTestApp) { + for (const [k, v] of Object.entries(config.coreConfig)) { + await module.exports.setKeyValueInConfig(k, v); + } + } return new Promise(async (resolve, reject) => { let installationPath = process.env.INSTALL_PATH; let pidsBefore = await getListOfPids(); @@ -297,7 +309,49 @@ module.exports.startST = async function (host = "localhost", port = 8080) { } else { if (!returned) { returned = true; - resolve(nonIntersection[0]); + if (notUsingTestApp) { + return resolve(`http://${host}:${port}`); + } + try { + // Math.random is an unsafe random but it doesn't actually matter here + // const appId = configs.appId ?? `testapp-${Date.now()}-${Math.floor(Math.random() * 1000)}`; + const appId = config.appId ?? `testapp`; + + await module.exports.removeAppAndTenants(appId); + + const OPAQUE_KEY_WITH_MULTITENANCY_FEATURE = + "ijaleljUd2kU9XXWLiqFYv5br8nutTxbyBqWypQdv2N-BocoNriPrnYQd0NXPm8rVkeEocN9ayq0B7c3Pv-BTBIhAZSclXMlgyfXtlwAOJk=9BfESEleW6LyTov47dXu"; + + await fetch(`http://${host}:${port}/ee/license`, { + method: "PUT", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + licenseKey: OPAQUE_KEY_WITH_MULTITENANCY_FEATURE, + }), + }); + + // Create app + const createAppResp = await fetch(`http://${host}:${port}/recipe/multitenancy/app`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + appId, + emailPasswordEnabled: true, + thirdPartyEnabled: true, + passwordlessEnabled: true, + coreConfig: config.coreConfig, + }), + }); + const respBody = await createAppResp.json(); + assert.strictEqual(respBody.status, "OK"); + resolve(`http://${host}:${port}/appid-${appId}`); + } catch (err) { + reject(err); + } } } } @@ -308,12 +362,12 @@ module.exports.startST = async function (host = "localhost", port = 8080) { }); }; -module.exports.startSTWithMultitenancy = async function (host = "localhost", port = 8080) { - await module.exports.startST(host, port); +module.exports.startSTWithMultitenancy = async function (config) { + const connectionURI = await module.exports.startST(config); const OPAQUE_KEY_WITH_MULTITENANCY_FEATURE = "ijaleljUd2kU9XXWLiqFYv5br8nutTxbyBqWypQdv2N-BocoNriPrnYQd0NXPm8rVkeEocN9ayq0B7c3Pv-BTBIhAZSclXMlgyfXtlwAOJk=9BfESEleW6LyTov47dXu"; - await fetch(`http://${host}:${port}/ee/license`, { + await fetch(`${connectionURI}/ee/license`, { method: "PUT", headers: { "content-type": "application/json; charset=utf-8", @@ -322,6 +376,71 @@ module.exports.startSTWithMultitenancy = async function (host = "localhost", por licenseKey: OPAQUE_KEY_WITH_MULTITENANCY_FEATURE, }), }); + return connectionURI; +}; + +module.exports.startSTWithMultitenancyAndAccountLinking = async function (config) { + const connectionURI = await module.exports.startST(config); + + const OPAQUE_KEY_WITH_FEATURES = + "N2yITHflaFS4BPm7n0bnfFCjP4sJoTERmP0J=kXQ5YONtALeGnfOOe2rf2QZ0mfOh0aO3pBqfF-S0jb0ABpat6pySluTpJO6jieD6tzUOR1HrGjJO=50Ob3mHi21tQHJ"; + + await fetch(`${connectionURI}/ee/license`, { + method: "PUT", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + licenseKey: OPAQUE_KEY_WITH_FEATURES, + }), + }); + + return connectionURI; +}; + +module.exports.removeAppAndTenants = async function (appId) { + const tenantsResp = await fetch(`http://localhost:8080/appid-${appId}/recipe/multitenancy/tenant/list`); + if (tenantsResp.status === 401) { + const updateAppResp = await fetch(`http://localhost:8080/recipe/multitenancy/app`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + appId, + coreConfig: { api_keys: null }, + }), + }); + assert.strictEqual(updateAppResp.status, 200); + await module.exports.removeAppAndTenants(appId); + } else if (tenantsResp.status === 200) { + const tenants = (await tenantsResp.json()).tenants; + for (const t of tenants) { + if (t.tenantId !== "public") { + await fetch(`http://localhost:8080/appid-${appId}/recipe/multitenancy/tenant/remove`, { + method: "POST", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + tenantId: t.tenantId, + }), + }); + } + } + + const removeResp = await fetch(`http://localhost:8080/recipe/multitenancy/app/remove`, { + method: "POST", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + appId, + }), + }); + const removeRespBody = await removeResp.json(); + assert.strictEqual(removeRespBody.status, "OK"); + } }; async function getListOfPids() { @@ -471,7 +590,7 @@ module.exports.emailVerifyTokenRequest = async function (app, accessToken, antiC .set("Cookie", ["sAccessToken=" + accessToken]) .set("anti-csrf", antiCsrf) .send({ - userId, + userId: typeof userId === "string" ? userId : userId.getAsString(), }) .end((err, res) => { if (err) { @@ -637,7 +756,11 @@ module.exports.createUsers = async (emailpassword = null, passwordless = null, t } if (user.recipe === "thirdparty" && thirdparty !== null) { - await thirdparty.manuallyCreateOrUpdateUser("public", user.provider, user.userId, user.email); + await thirdparty.manuallyCreateOrUpdateUser("public", user.provider, user.userId, user.email, false); } } }; + +module.exports.assertJSONEquals = (actual, expected) => { + assert.deepStrictEqual(JSON.parse(JSON.stringify(actual)), JSON.parse(JSON.stringify(expected))); +}; diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index 1e8767359..79d8c9bb1 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -1,5 +1,5 @@ import * as express from "express"; -import Supertokens from "../.."; +import Supertokens, { RecipeUserId, User, getUser } from "../.."; import Session, { RecipeInterface, SessionClaimValidator, VerifySessionOptions } from "../../recipe/session"; import EmailVerification from "../../recipe/emailverification"; import EmailPassword from "../../recipe/emailpassword"; @@ -28,6 +28,7 @@ import { BooleanClaim, PrimitiveClaim } from "../../recipe/session/claims"; import UserRoles from "../../recipe/userroles"; import Dashboard from "../../recipe/dashboard"; import JWT from "../../recipe/jwt"; +import AccountLinking from "../../recipe/accountlinking"; UserRoles.init({ override: { @@ -935,6 +936,7 @@ let sessionConfig: SessionTypeInput = { getAccessTokenPayload: session.getAccessTokenPayload, getSessionDataFromDatabase: session.getSessionDataFromDatabase, getUserId: session.getUserId, + getRecipeUserId: session.getRecipeUserId, getTenantId: session.getTenantId, revokeSession: session.revokeSession, updateSessionDataInDatabase: session.updateSessionDataInDatabase, @@ -965,7 +967,6 @@ let sessionConfig: SessionTypeInput = { getClaimValue: originalImpl.getClaimValue, removeClaim: originalImpl.removeClaim, validateClaims: originalImpl.validateClaims, - validateClaimsInJWTPayload: originalImpl.validateClaimsInJWTPayload, }; }, }, @@ -1160,11 +1161,15 @@ Supertokens.init({ // we check if the email exists in SuperTokens. If not, // then the sign in should be handled by you. if ( - (await supertokensImpl.getUserByEmail({ - email: input.email, - tenantId: input.tenantId, - userContext: input.userContext, - })) === undefined + ( + await Supertokens.listUsersByAccountInfo( + "public", + { + email: input.email, + }, + input.userContext + ) + ).length === 0 ) { // TODO: sign in from your db // example return value if credentials don't match @@ -1179,24 +1184,6 @@ Supertokens.init({ // all new users are created in SuperTokens; return supertokensImpl.signUp(input); }, - getUserByEmail: async (input) => { - let superTokensUser = await supertokensImpl.getUserByEmail(input); - if (superTokensUser === undefined) { - let email = input.email; - // TODO: fetch and return user info from your database... - } else { - return superTokensUser; - } - }, - getUserById: async (input) => { - let superTokensUser = await supertokensImpl.getUserById(input); - if (superTokensUser === undefined) { - let userId = input.userId; - // TODO: fetch and return user info from your database... - } else { - return superTokensUser; - } - }, }; }, apis: (oI) => { @@ -1298,7 +1285,12 @@ EmailPassword.init({ if (isAllowed) { // import Session from "supertokens-node/recipe/session" - let session = await Session.createNewSession(options.req, options.res, "public", user.id); + let session = await Session.createNewSession( + options.req, + options.res, + "public", + Supertokens.convertToRecipeUserId(user.id) + ); return { status: "OK", session, @@ -1338,7 +1330,7 @@ Session.init({ input.accessTokenPayload = stringClaim.removeFromPayload(input.accessTokenPayload); input.accessTokenPayload = { ...input.accessTokenPayload, - ...(await boolClaim.build(input.userId, input.tenantId, input.userContext)), + ...(await boolClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)), lastTokenRefresh: Date.now(), }; return originalImplementation.createNewSession(input); @@ -1359,25 +1351,14 @@ Session.validateClaimsForSessionHandle( { test: 1 } ); -Session.validateClaimsInJWTPayload("public", "userId", {}); -Session.validateClaimsInJWTPayload("public", "userId", {}, (globalClaimValidators) => [ - ...globalClaimValidators, - boolClaim.validators.isTrue(), -]); -Session.validateClaimsInJWTPayload( - "public", - "userId", - {}, - (globalClaimValidators, userId) => [...globalClaimValidators, stringClaim.validators.startsWith(userId)], - { test: 1 } -); EmailVerification.sendEmail({ tenantId: "public", emailVerifyLink: "", type: "EMAIL_VERIFICATION", user: { - email: "", id: "", + email: "", + recipeUserId: Supertokens.convertToRecipeUserId(""), }, }); @@ -1388,6 +1369,7 @@ ThirdPartyEmailPassword.sendEmail({ user: { email: "", id: "", + recipeUserId: Supertokens.convertToRecipeUserId(""), }, }); ThirdPartyEmailPassword.sendEmail({ @@ -1397,6 +1379,7 @@ ThirdPartyEmailPassword.sendEmail({ user: { email: "", id: "", + recipeUserId: Supertokens.convertToRecipeUserId(""), }, userContext: {}, }); @@ -1561,7 +1544,8 @@ Passwordless.init({ }); return { status: "OK", - createdNewUser: user.createdNewUser, + createdNewRecipeUser: user.createdNewRecipeUser, + recipeUserId: user.recipeUserId, user: user.user, }; } @@ -1689,7 +1673,10 @@ async function getSessionWithoutRequestWithErrorHandler(req: express.Request, re async function createNewSessionWithoutRequestResponse(req: express.Request, resp: express.Response) { const userId = "user-id"; // This would be fetched from somewhere - const session = await Session.createNewSessionWithoutRequestResponse("public", userId); + const session = await Session.createNewSessionWithoutRequestResponse( + "public", + Supertokens.convertToRecipeUserId(userId) + ); const tokens = session.getAllSessionTokensDangerously(); if (tokens.accessAndFrontTokenUpdated) { @@ -1745,6 +1732,8 @@ ThirdPartyPasswordless.init({ flowType: "MAGIC_LINK", }); +const recipeUserId = new Supertokens.RecipeUserId("asdf"); + Session.init({ override: { openIdFeature: { @@ -1763,3 +1752,78 @@ Session.init({ }, }, }); + +async function accountLinkingFuncsTest() { + const signUpResp = await EmailPassword.signUp("public", "asdf@asdf.asfd", "testpw"); + if (signUpResp.status !== "OK") { + return signUpResp; + } + + let user: User; + if ( + !signUpResp.user.isPrimaryUser && + (await AccountLinking.canCreatePrimaryUser(signUpResp.user.loginMethods[0].recipeUserId)) + ) { + const createResp = await AccountLinking.createPrimaryUser(Supertokens.convertToRecipeUserId("asdf")); + if (createResp.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + throw new Error(createResp.status); + } + if (createResp.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { + user = (await getUser(createResp.primaryUserId))!; + } else { + user = createResp.user; + } + } else { + user = signUpResp.user; + } + + const signUpResp2 = await EmailPassword.signUp("public", "asdf2@asdf.asfd", "testpw"); + if (signUpResp2.status !== "OK") { + return signUpResp2; + } + const linkResp = await AccountLinking.linkAccounts(signUpResp2.recipeUserId, user.id); + + if (linkResp.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + throw new Error("Should never happen"); + } + if ( + linkResp.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" || + linkResp.status === "OK" + ) { + user = linkResp.user; + } else { + throw new Error(linkResp.status); + } + + const unlinkResp = await AccountLinking.unlinkAccount(signUpResp2.recipeUserId); + + if (unlinkResp.wasRecipeUserDeleted) { + console.log("User deleted: " + signUpResp2.recipeUserId.getAsString()); + } + + const tpSignUp = await ThirdParty.manuallyCreateOrUpdateUser("public", "mytp", "tpuser", "asfd@asfd.asdf", false); + if (tpSignUp.status !== "OK") { + return tpSignUp; + } + // This should be true + const canLink = await AccountLinking.canLinkAccounts(tpSignUp.recipeUserId, user.id); + + // This should be the same as the primary user above + const toLink = await AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId("public", tpSignUp.recipeUserId); + + // This should be the same primary user as toLink updated with the new link + const linkResult = await AccountLinking.createPrimaryUserIdOrLinkAccounts("public", tpSignUp.recipeUserId); + + return { + canChangeEmail: await AccountLinking.isEmailChangeAllowed(tpSignUp.recipeUserId, "asfd@asfd.asfd", true), + canSignIn: await AccountLinking.isSignInAllowed("public", tpSignUp.recipeUserId), + canSignUp: await AccountLinking.isSignUpAllowed( + "public", + { + recipeId: "passwordless", + email: "asdf@asdf.asdf", + }, + true + ), + }; +}