Skip to content

Commit

Permalink
feat!: account linking (#729)
Browse files Browse the repository at this point in the history
* feat: account-linking initial changes

* feat: add translations + initial account linking tests

* test: add tests where the provider list doesn't match on the FE and BE

* test: add account-linking e2e tests

* fix: password reset not allowed error message

* test: update tests for account-linking

* docs: add account-linking example

* feat: updated error codes/messages and related tests

* fix: remove unneccessary fields from PASSWORD_RESET_SUCCESSFUL and submitNewPassword return types

* test: making tests backwards compatible

* chore: update changelog

* docs(example): small simplifiactions to account linking example

* docs(examples): with-account-linking docs/small simplifications

* feat: remove EMAIL_ALREADY_USED_IN_ANOTHER_ACCOUNT + update version number

* feat: add user to success redirection CB and matching node/web-js changes

* docs: add missing translation keys and changelog

* refactor: some cleanup and tests updates

* test: update test server to better handle in-memory tests

* chore: extended changelog

* test: make tests more backwards compatible

* test: fix tests

* test: update test server

* test: fix after cb in case whole suite was skipped

* docs: update examples

* chore: update changelog + implement review comments

* chore: add other BE sdks to multitenancy changelog

* chore: update web-js dep version to a branch + rebuild
  • Loading branch information
porcellus authored Sep 20, 2023
1 parent 5818082 commit 2062169
Show file tree
Hide file tree
Showing 162 changed files with 5,126 additions and 3,475 deletions.
100 changes: 100 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [0.35.0] - 2023-09-XX

### Overview

#### Introducing account-linking

With this release, we are introducing AccountLinking, 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

### Breaking changes

- Added support for FDI 1.18 (Node SDK>= 16.0.0), but keeping support FDI version1.17 (node >= 15.0.0, golang>=0.13, python>=0.15.0)
- User type has changed across recipes and functions: recipe specific user types have been removed and replaced by a generic one that contains more information
- `createdNewUser` has been renamed to `createdNewRecipeUser`
- `createCode`, `consumeCode`, `createPasswordlessCode` and `consumePasswordlessCode` can now return status: `SIGN_IN_UP_NOT_ALLOWED`
- `signInAndUp` and `thirdPartySignInAndUp` can now return new status: `SIGN_IN_UP_NOT_ALLOWED`
- `sendPasswordResetEmail` can now return `status: "PASSWORD_RESET_NOT_ALLOWED"`
- `signIn` and `emailPasswordSignIn` can now return `SIGN_IN_NOT_ALLOWED`
- `signUp` and `emailPasswordSignUp` can now return `SIGN_UP_NOT_ALLOWED`
- The context param of `getRedirectionURL` gets an optional `user` prop (it's always defined if `createdNewRecipeUser` is set to true)
- Added new language translation keys
- We've added error messages for all of the above error statuses. Please see the new UI [here](https://supertokens.com/docs/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking#support-status-codes). You can change the text using the language translation feature

### Migration

#### New User structure

We've added a generic `User` type 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`
- `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`

#### Checking if a user signed up or signed in

- When calling 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;
```

- When calling the emailpassword sign up API, you can check if a user signed up by:

```
const isNewUser = res.user.loginMethods.length === 1;
```

- In `getRedirectionURL`

```
EmailPassword.init({ // This looks the same for other recipes
// Other config options.
async getRedirectionURL(context) {
if (context.action === "SUCCESS") {
if (context.isNewRecipeUser && context.user.loginMethods.length === 1) {
// new primary user
} else {
// only a recipe user was created
}
}
return undefined;
}
})
```

- In `onHandleEvent`:

```
EmailPassword.init({ // This looks the same for other recipes
// Other config options.
onHandleEvent(context: EmailPasswordOnHandleEventContext) {
if (context.action === "SUCCESS") {
if (context.isNewRecipeUser && context.user.loginMethods.length === 1) {
// new primary user
} else {
// only a recipe user was created
}
}
},
})
```

## [0.34.2] - 2023-08-27

### Fixes
Expand Down Expand Up @@ -40,6 +138,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Only supporting FDI 1.17
- Backend SDKs have to be updated first to a version that supports multi-tenancy for thirdparty
- supertokens-node: >= 15.0.0
- supertokens-golang: >= 0.13.0
- supertokens-python: >= 0.15.0
- In ThirdParty recipe,

- Changed signatures of the functions `getAuthorisationURLWithQueryParamsAndSetState`
Expand Down
26 changes: 19 additions & 7 deletions examples/for-tests-react-16/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ function getEmailPasswordConfigs({ disableDefaultUI }) {
getRedirectionURL: async (context) => {
console.log(`ST_LOGS EMAIL_PASSWORD GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("emailpassword", context.isNewUser);
setIsNewUserToStorage("emailpassword", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -653,6 +653,10 @@ function getThirdPartyPasswordlessConfigs({ staticProviderList, disableDefaultUI
name: "Auth0",
getRedirectURL: thirdPartyRedirectURL !== null ? () => thirdPartyRedirectURL : undefined,
},
{
id: "mock-provider",
name: "Mock Provider",
},
];
if (staticProviderList) {
const ids = JSON.parse(staticProviderList);
Expand Down Expand Up @@ -728,7 +732,7 @@ function getThirdPartyPasswordlessConfigs({ staticProviderList, disableDefaultUI
getRedirectionURL: async (context) => {
console.log(`ST_LOGS THIRDPARTYPASSWORDLESS GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("thirdpartypasswordless", context.isNewUser);
setIsNewUserToStorage("thirdpartypasswordless", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -812,7 +816,7 @@ function getPasswordlessConfigs({ disableDefaultUI }) {
getRedirectionURL: async (context) => {
console.log(`ST_LOGS PASSWORDLESS GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("passwordless", context.isNewUser);
setIsNewUserToStorage("passwordless", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -855,6 +859,10 @@ function getThirdPartyConfigs({ staticProviderList, disableDefaultUI, thirdParty
name: "Auth0",
getRedirectURL: thirdPartyRedirectURL !== null ? () => thirdPartyRedirectURL : undefined,
},
{
id: "mock-provider",
name: "Mock Provider",
},
];
if (staticProviderList) {
const ids = JSON.parse(staticProviderList);
Expand All @@ -874,7 +882,7 @@ function getThirdPartyConfigs({ staticProviderList, disableDefaultUI, thirdParty
getRedirectionURL: async (context) => {
console.log(`ST_LOGS THIRD_PARTY GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("thirdparty", context.isNewUser);
setIsNewUserToStorage("thirdparty", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -940,6 +948,10 @@ function getThirdPartyEmailPasswordConfigs({ staticProviderList, disableDefaultU
name: "Auth0",
getRedirectURL: thirdPartyRedirectURL !== null ? () => thirdPartyRedirectURL : undefined,
},
{
id: "mock-provider",
name: "Mock Provider",
},
];
if (staticProviderList) {
const ids = JSON.parse(staticProviderList);
Expand All @@ -953,7 +965,7 @@ function getThirdPartyEmailPasswordConfigs({ staticProviderList, disableDefaultU
getRedirectionURL: async (context) => {
console.log(`ST_LOGS THIRD_PARTY_EMAIL_PASSWORD GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("thirdpartyemailpassword", context.isNewUser);
setIsNewUserToStorage("thirdpartyemailpassword", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -1104,8 +1116,8 @@ function getThirdPartyEmailPasswordConfigs({ staticProviderList, disableDefaultU
});
}

function setIsNewUserToStorage(recipeName, isNewUser) {
localStorage.setItem("isNewUserCheck", `${recipeName}-${isNewUser}`);
function setIsNewUserToStorage(recipeName, isNewRecipeUser) {
localStorage.setItem("isNewUserCheck", `${recipeName}-${isNewRecipeUser}`);
}

window.SuperTokens = SuperTokens;
Expand Down
26 changes: 19 additions & 7 deletions examples/for-tests/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ function getEmailPasswordConfigs({ disableDefaultUI }) {
getRedirectionURL: async (context) => {
console.log(`ST_LOGS EMAIL_PASSWORD GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("emailpassword", context.isNewUser);
setIsNewUserToStorage("emailpassword", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -658,6 +658,10 @@ function getThirdPartyPasswordlessConfigs({ staticProviderList, disableDefaultUI
name: "Auth0",
getRedirectURL: thirdPartyRedirectURL !== null ? () => thirdPartyRedirectURL : undefined,
},
{
id: "mock-provider",
name: "Mock Provider",
},
];
if (staticProviderList) {
const ids = JSON.parse(staticProviderList);
Expand Down Expand Up @@ -745,7 +749,7 @@ function getThirdPartyPasswordlessConfigs({ staticProviderList, disableDefaultUI
getRedirectionURL: async (context) => {
console.log(`ST_LOGS THIRDPARTYPASSWORDLESS GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("thirdpartypasswordless", context.isNewUser);
setIsNewUserToStorage("thirdpartypasswordless", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -838,7 +842,7 @@ function getPasswordlessConfigs({ disableDefaultUI }) {
getRedirectionURL: async (context) => {
console.log(`ST_LOGS PASSWORDLESS GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("passwordless", context.isNewUser);
setIsNewUserToStorage("passwordless", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -891,6 +895,10 @@ function getThirdPartyConfigs({ staticProviderList, disableDefaultUI, thirdParty
</button>
),
},
{
id: "mock-provider",
name: "Mock Provider",
},
];
if (staticProviderList) {
const ids = JSON.parse(staticProviderList);
Expand Down Expand Up @@ -922,7 +930,7 @@ function getThirdPartyConfigs({ staticProviderList, disableDefaultUI, thirdParty
getRedirectionURL: async (context) => {
console.log(`ST_LOGS THIRD_PARTY GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("thirdparty", context.isNewUser);
setIsNewUserToStorage("thirdparty", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -988,6 +996,10 @@ function getThirdPartyEmailPasswordConfigs({ staticProviderList, disableDefaultU
name: "Auth0",
getRedirectURL: thirdPartyRedirectURL !== null ? () => thirdPartyRedirectURL : undefined,
},
{
id: "mock-provider",
name: "Mock Provider",
},
];
if (staticProviderList) {
const ids = JSON.parse(staticProviderList);
Expand All @@ -1014,7 +1026,7 @@ function getThirdPartyEmailPasswordConfigs({ staticProviderList, disableDefaultU
getRedirectionURL: async (context) => {
console.log(`ST_LOGS THIRD_PARTY_EMAIL_PASSWORD GET_REDIRECTION_URL ${context.action}`);
if (context.action === "SUCCESS") {
setIsNewUserToStorage("thirdpartyemailpassword", context.isNewUser);
setIsNewUserToStorage("thirdpartyemailpassword", context.isNewRecipeUser);
return context.redirectToPath || "/dashboard";
}
},
Expand Down Expand Up @@ -1165,8 +1177,8 @@ function getThirdPartyEmailPasswordConfigs({ staticProviderList, disableDefaultU
});
}

function setIsNewUserToStorage(recipeName, isNewUser) {
localStorage.setItem("isNewUserCheck", `${recipeName}-${isNewUser}`);
function setIsNewUserToStorage(recipeName, isNewRecipeUser) {
localStorage.setItem("isNewUserCheck", `${recipeName}-${isNewRecipeUser}`);
}

window.SuperTokens = SuperTokens;
Expand Down
66 changes: 65 additions & 1 deletion examples/with-account-linking/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,65 @@
# Account linking is not supported yet, we are actively working on the feature.
![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png)

# SuperTokens Google one tap Demo app

This demo app demonstrates the following use cases:

- Thirdparty Login / Sign-up
- Email Password Login / Sign-up
- Logout
- Session management & Calling APIs
- Account linking

## Project setup

Clone the repo, enter the directory, and use `npm` to install the project dependencies:

```bash
git clone https://github.com/supertokens/supertokens-auth-react
cd supertokens-auth-react/examples/with-account-linking
npm install
cd frontend && npm install && cd ../
cd backend && npm install && cd ../
```

## Run the demo app

This compiles and serves the React app and starts the backend API server on port 3001.

```bash
npm run start
```

The app will start on `http://localhost:3000`

## How it works

We are adding a new (`/link`) page where the user can add new login methods to their current user, plus enabling automatic account linking.

### On the frontend

The demo uses the pre-built UI, but you can always build your own UI instead.

- We do not need any extra configuration to enable account linking
- To enable manual linking through a custom callback page, we add `getRedirectURL` to the configuration of the social login providers.
- We add a custom page (`/link`) that will:
- Get and show the login methods belonging to the current user
- Show a password form (if available) that calls `/addPassword` to add an email+password login method to the current user.
- Show a phone number form (if available) that calls `/addPhoneNumber` to associate a phone number with the current user.
- Show an "Add Google account" that start a login process through Google
- We add a custom page (`/link/tpcallback/:thirdPartyId`) that will:
- Call `/addThirdPartyUser` through a customized `ThirdPartyEmailPassword.thirdPartySignInAndUp` call

### On the backend

- We enable account linking by initializing the recipe and providing a `shouldDoAutomaticAccountLinking` implementation
- We add `/addPassword`, `/addPhoneNumber` and `/addThirdPartyUser` to enable manual linking from the frontend
- We add `/userInfo` so the frontend can list/show the login methods belonging to the current user.

## Author

Created with :heart: by the folks at supertokens.com.

## License

This project is licensed under the Apache 2.0 license.
Loading

0 comments on commit 2062169

Please sign in to comment.