From 82194ce8cad6b7a39c971757b53d47e1b5c49706 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 21 Aug 2023 17:47:16 +0530 Subject: [PATCH 01/81] start working on code snippet correction for accoutn linking --- v2/emailpassword/advanced-customizations/user-context.mdx | 3 ++- .../common-customizations/change-email-post-login.mdx | 2 +- v2/src/plugins/codeTypeChecking/jsEnv/package.json | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/v2/emailpassword/advanced-customizations/user-context.mdx b/v2/emailpassword/advanced-customizations/user-context.mdx index 5d1b536d3..3bb9a262a 100644 --- a/v2/emailpassword/advanced-customizations/user-context.mdx +++ b/v2/emailpassword/advanced-customizations/user-context.mdx @@ -219,7 +219,8 @@ SuperTokens.init({ antiCsrfToken: undefined, refreshToken: undefined, }), - getTenantId: () => "public" + getTenantId: () => "public", + getRecipeUserId: () => SuperTokens.convertToRecipeUserId(""), }; } return originalImplementation.createNewSession(input); diff --git a/v2/emailpassword/common-customizations/change-email-post-login.mdx b/v2/emailpassword/common-customizations/change-email-post-login.mdx index 6791de111..525c110ec 100644 --- a/v2/emailpassword/common-customizations/change-email-post-login.mdx +++ b/v2/emailpassword/common-customizations/change-email-post-login.mdx @@ -126,7 +126,7 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Update the email let resp = await EmailPassword.updateEmailOrPassword({ - userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: email, }); diff --git a/v2/src/plugins/codeTypeChecking/jsEnv/package.json b/v2/src/plugins/codeTypeChecking/jsEnv/package.json index 3dce7ab6e..14eac1d02 100644 --- a/v2/src/plugins/codeTypeChecking/jsEnv/package.json +++ b/v2/src/plugins/codeTypeChecking/jsEnv/package.json @@ -53,7 +53,7 @@ "socket.io": "^4.6.1", "socketio": "^1.0.0", "supertokens-auth-react": "^0.34.0", - "supertokens-node": "^15.0.0", + "supertokens-node": "github:supertokens/supertokens-node#account-linking", "supertokens-node7": "npm:supertokens-node@7.3", "supertokens-react-native": "^4.0.0", "supertokens-web-js": "^0.7.0", @@ -62,4 +62,4 @@ "supertokens-website-script": "github:supertokens/supertokens-website#17.0", "typescript": "^4.9.5" } -} \ No newline at end of file +} From 9f8d34e3d04d611269d98adbce50dc9d13a01a9e Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 22 Aug 2023 14:19:12 +0530 Subject: [PATCH 02/81] fixes more snippets --- .../change-email-post-login.mdx | 99 +++++---- .../common-customizations/change-password.mdx | 19 +- .../disable-sign-up/emailpassword-changes.mdx | 28 +-- .../changing-email-verification-status.mdx | 10 +- .../generate-link-manually.mdx | 5 +- .../handling-email-verification-success.mdx | 2 +- .../common-customizations/get-user-info.mdx | 199 ++++++++++++++---- .../handling-signin-success.mdx | 14 +- .../handling-signup-success.mdx | 29 ++- .../reset-password/generate-link-manually.mdx | 4 +- .../sessions/claims/claim-validators.mdx | 2 +- .../emailpassword-changes.mdx | 52 +++-- .../ep-migration-without-password-hash.mdx | 44 ++-- .../backend-signup-override.mdx | 2 +- .../changing-email-verification-status.mdx | 10 +- .../generate-link-manually.mdx | 5 +- .../handling-email-verification-success.mdx | 2 +- .../sessions/claims/claim-validators.mdx | 2 +- .../sessions/claims/claim-validators.mdx | 2 +- .../changing-email-verification-status.mdx | 10 +- .../generate-link-manually.mdx | 5 +- .../handling-email-verification-success.mdx | 2 +- .../common-customizations/get-user-info.mdx | 175 ++++++++++++--- .../sessions/claims/claim-validators.mdx | 2 +- .../common-customizations/change-password.mdx | 19 +- .../disable-sign-up/emailpassword-changes.mdx | 28 +-- .../changing-email-verification-status.mdx | 10 +- .../generate-link-manually.mdx | 5 +- .../handling-email-verification-success.mdx | 2 +- .../common-customizations/get-user-info.mdx | 175 ++++++++++++--- .../reset-password/generate-link-manually.mdx | 4 +- .../sessions/claims/claim-validators.mdx | 2 +- .../changing-email-verification-status.mdx | 10 +- .../generate-link-manually.mdx | 5 +- .../handling-email-verification-success.mdx | 2 +- .../common-customizations/get-user-info.mdx | 175 ++++++++++++--- .../sessions/claims/claim-validators.mdx | 2 +- 37 files changed, 840 insertions(+), 323 deletions(-) diff --git a/v2/emailpassword/common-customizations/change-email-post-login.mdx b/v2/emailpassword/common-customizations/change-email-post-login.mdx index 525c110ec..115596af0 100644 --- a/v2/emailpassword/common-customizations/change-email-post-login.mdx +++ b/v2/emailpassword/common-customizations/change-email-post-login.mdx @@ -389,6 +389,7 @@ import EmailVerification from "supertokens-node/recipe/emailverification"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import supertokens from "supertokens-node"; let app = express(); @@ -406,32 +407,39 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. - let isVerified = await EmailVerification.isEmailVerified(session.getUserId(), email); + let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. - let user = (await EmailPassword.getUserById(session.getUserId()))!; - for (let i = 0; i < user?.tenantIds.length; i++) { - // Since once user can be shared across many tenants, we need to check if - // the email already exists in any of the tenants. - let userWithEmail = await EmailPassword.getUserByEmail(user.tenantIds[i], email); - if (userWithEmail?.id !== session.getUserId()) { - // TODO handle error, email already exists with another user. - return + let user = (await supertokens.getUser(session.getUserId()))!; + let usersWithSameEmail = await supertokens.listUsersByAccountInfo({ + email + }); + for (let i = 0; i < usersWithSameEmail.length; i++) { + // Since one user can be shared across many tenants, we need to check if + // the email already exists in any of the tenants that belongs to this user. + let currUser = usersWithSameEmail[i]; + for (let y = 0; y < user.tenantIds.length; y++) { + if (currUser.tenantIds.includes(user.tenantIds[y])) { + if (currUser?.id !== session.getUserId()) { + // TODO handle error, email already exists with another user. + return + } + } } } - + // Now we create and send the email verification link to the user for the new email. await EmailVerification.sendEmailVerificationEmail(session.getTenantId(), session.getUserId(), email); // TODO send successful response that email verification email sent. - return + return } // Since the email is verified, we try and do an update let resp = await EmailPassword.updateEmailOrPassword({ - userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: email, }); @@ -441,7 +449,7 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO handle error, email already exists with another user. - return + return } throw new Error("Should never come here"); @@ -455,7 +463,6 @@ function isValidEmail(email: string) { ); return regexp.test(email); } - ``` @@ -710,39 +717,39 @@ import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - EmailPassword.init(), - EmailVerification.init({ - mode: "REQUIRED", - override: { - apis: (oI) => { - return { - ...oI, - verifyEmailPOST: async function (input) { - // highlight-start - let response = await oI.verifyEmailPOST!(input); - if (response.status === "OK") { - // This will update the email of the user to the one - // that was just marked as verified by the token. - await EmailPassword.updateEmailOrPassword({ - userId: response.user.id, - email: response.user.email, - }); - } - return response; - // highlight-end - }, - }; + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: "REQUIRED", + override: { + apis: (oI) => { + return { + ...oI, + verifyEmailPOST: async function (input) { + // highlight-start + let response = await oI.verifyEmailPOST!(input); + if (response.status === "OK") { + // This will update the email of the user to the one + // that was just marked as verified by the token. + await EmailPassword.updateEmailOrPassword({ + recipeUserId: response.user.recipeUserId, + email: response.user.email, + }); + } + return response; + // highlight-end + }, + }; + }, }, - }, - }), - Session.init(), - ], + }), + Session.init(), + ], }); ``` diff --git a/v2/emailpassword/common-customizations/change-password.mdx b/v2/emailpassword/common-customizations/change-password.mdx index bd12ed2a6..b8cd54428 100644 --- a/v2/emailpassword/common-customizations/change-password.mdx +++ b/v2/emailpassword/common-customizations/change-password.mdx @@ -108,12 +108,12 @@ import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRec import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import supertokens from "supertokens-node"; let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { - - // highlight-start +// highlight-start // get the supertokens session object from the req let session = req.session @@ -123,18 +123,21 @@ app.post("/change-password", verifySession(), async (req: SessionRequest, res: e // retrive the new password from the request body let updatedPassword = req.body.newPassword - // get the user's Id from the session - let userId = session!.getUserId() - // get the signed in user's email from the getUserById function - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId) + let userInfo = await supertokens.getUser(session!.getUserId()) if (userInfo === undefined) { throw new Error("Should never come here") } + let loginMethod = userInfo.loginMethods.find((lM) => lM.recipeUserId.getAsString() === session!.getRecipeUserId().getAsString() && lM.recipeId === "emailpassword"); + if (loginMethod === undefined) { + throw new Error("Should never come here") + } + const email = loginMethod.email!; + // call signin to check that input password is correct - let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeSignIn}(session!.getTenantId(), userInfo.email, oldPassword) + let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeSignIn}(session!.getTenantId(), email, oldPassword) if (isPasswordValid.status !== "OK") { // TODO: handle incorrect password error @@ -144,7 +147,7 @@ app.post("/change-password", verifySession(), async (req: SessionRequest, res: e // update the user's password using updateEmailOrPassword let response = await ^{recipeNameCapitalLetters}.updateEmailOrPassword({ - userId, + recipeUserId: session!.getRecipeUserId(), password: updatedPassword, tenantIdForPasswordPolicy: session!.getTenantId() }) diff --git a/v2/emailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx b/v2/emailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx index 0297296c7..1d2d07b6e 100644 --- a/v2/emailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx +++ b/v2/emailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx @@ -200,7 +200,7 @@ app.post("/create-user", verifySession({ } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); res.send("Success"); }); @@ -245,7 +245,7 @@ server.route({ } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); res.response("Success").code(200); } @@ -281,7 +281,7 @@ fastify.post("/create-user", { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); res.code(200).send("Success"); }); @@ -310,7 +310,7 @@ async function createUser(awsEvent: SessionEventV2) { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); return { statusCode: '200', @@ -356,7 +356,7 @@ router.post("/create-user", verifySession({ } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); ctx.status = 200; ctx.body = "Success"; @@ -394,7 +394,7 @@ class LikeComment { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); // TODO: send 200 response to the client } @@ -436,7 +436,7 @@ export default async function createUser(req: SessionRequest, res: any) { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); res.status(200).json({ message: 'Success' }) } @@ -474,7 +474,7 @@ export class CreateUserController { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); // TODO: send 200 response to the client } @@ -873,23 +873,13 @@ SuperTokens.init({ ...originalImplementation, updateEmailOrPassword: async function (input) { // This can be called on the backend - // in your own APIs + // in your own APIs or in the password reset flow if (input.password === FAKE_PASSWORD) { throw new Error("Use a different password") } return originalImplementation.updateEmailOrPassword(input); }, - resetPasswordUsingToken: async function (input) { - // This is called during the password reset flow - // when the user enters their new password - if (input.newPassword === FAKE_PASSWORD) { - return { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" - } - } - return originalImplementation.resetPasswordUsingToken(input); - }, ^{nodeEmailPasswordSignInFunction}: async function (input) { // This is called in the email password sign in API if (input.password === FAKE_PASSWORD) { diff --git a/v2/emailpassword/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/emailpassword/common-customizations/email-verification/changing-email-verification-status.mdx index d485b1f58..3e59118f3 100644 --- a/v2/emailpassword/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/emailpassword/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,11 +23,12 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function manuallyVerifyEmail(userId: string) { +async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { try { // Create an email verification token for the user - const tokenRes = await EmailVerification.createEmailVerificationToken("public", userId); + const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); // If the token creation is successful, use the token to verify the user's email if (tokenRes.status === "OK") { @@ -127,11 +128,12 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensType from "supertokens-node/types"; -async function manuallyUnverifyEmail(userId: string) { +async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { try { // Set email verification status to false - await EmailVerification.unverifyEmail(userId); + await EmailVerification.unverifyEmail(recipeUserId); } catch (err) { console.error(err); } diff --git a/v2/emailpassword/common-customizations/email-verification/generate-link-manually.mdx b/v2/emailpassword/common-customizations/email-verification/generate-link-manually.mdx index 1c6aedd73..536aa4e9c 100644 --- a/v2/emailpassword/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/emailpassword/common-customizations/email-verification/generate-link-manually.mdx @@ -21,11 +21,12 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function createEmailVerificationLink(userId: string, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { try { // Create an email verification link for the user - const linkResponse = await EmailVerification.createEmailVerificationLink("public", userId, email); + const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); diff --git a/v2/emailpassword/common-customizations/email-verification/handling-email-verification-success.mdx b/v2/emailpassword/common-customizations/email-verification/handling-email-verification-success.mdx index 91a12bbe6..bb65d945c 100644 --- a/v2/emailpassword/common-customizations/email-verification/handling-email-verification-success.mdx +++ b/v2/emailpassword/common-customizations/email-verification/handling-email-verification-success.mdx @@ -47,7 +47,7 @@ SuperTokens.init({ // Then we check if it was successfully completed if (response.status === "OK") { - let { id, email } = response.user; + let { recipeUserId, email } = response.user; // TODO: post email verification logic } return response; diff --git a/v2/emailpassword/common-customizations/get-user-info.mdx b/v2/emailpassword/common-customizations/get-user-info.mdx index f53fc606e..c4ce4aaf4 100644 --- a/v2/emailpassword/common-customizations/get-user-info.mdx +++ b/v2/emailpassword/common-customizations/get-user-info.mdx @@ -37,15 +37,29 @@ There are several ways to fetch information about a user: -You can get a user's information on the backend using the `^{getUserByEmailNode}` and `getUserById` functions: +You can get a user's information on the backend using the `listUsersByAccountInfo` function: ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; async function getUserInfo() { - // Note that usersInfo has type User - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - let usersInfo = await ^{recipeNameCapitalLetters}.^{getUserByEmailNode}("public", "test@example.com"); + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + email: "test@example.com" + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` @@ -134,17 +148,30 @@ If you are using our multi tenancy feature, you can pass in a different tenantId ```tsx import express from "express"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from 'supertokens-node/framework/express'; +import supertokens from "supertokens-node"; let app = express(); app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId) - // ... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }) ``` @@ -152,10 +179,10 @@ app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import Hapi from "@hapi/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; +import supertokens from "supertokens-node"; let server = Hapi.server({ port: 8000 }); @@ -172,10 +199,23 @@ server.route({ // @ts-ignore handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } }) ``` @@ -186,9 +226,9 @@ server.route({ ```tsx import Fastify from "fastify"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from 'supertokens-node/framework/fastify'; +import supertokens from "supertokens-node"; const fastify = Fastify(); @@ -196,10 +236,23 @@ fastify.post("/like-comment", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -208,15 +261,29 @@ fastify.post("/like-comment", { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import supertokens from "supertokens-node"; async function getUserInfo(awsEvent: SessionEvent) { let userId = awsEvent.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }; exports.handler = verifySession(getUserInfo); @@ -228,18 +295,31 @@ exports.handler = verifySession(getUserInfo); ```tsx import KoaRouter from "koa-router"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; +import supertokens from "supertokens-node"; let router = new KoaRouter(); router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -250,10 +330,10 @@ router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, get, response } from "@loopback/rest"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { SessionContext } from "supertokens-node/framework/loopback"; +import supertokens from "supertokens-node"; class GetUserInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @@ -262,10 +342,23 @@ class GetUserInfo { @response(200) async handler() { let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } } ``` @@ -275,10 +368,10 @@ class GetUserInfo { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { superTokensNextWrapper } from 'supertokens-node/nextjs' import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( @@ -290,10 +383,23 @@ export default async function likeComment(req: SessionRequest, res: any) { ) let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` @@ -307,8 +413,8 @@ import { Controller, Post, UseGuards, Request, Response } from "@nestjs/common"; import { AuthGuard } from './auth/auth.guard'; // @ts-ignore import { Session } from './auth/session.decorator'; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; @Controller() export class ExampleController { @@ -316,10 +422,23 @@ export class ExampleController { @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Request() req: SessionRequest, @Session() session: Session, @Response({passthrough: true}) res: Response): Promise { let userId = session.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ return true; } } diff --git a/v2/emailpassword/common-customizations/handling-signin-success.mdx b/v2/emailpassword/common-customizations/handling-signin-success.mdx index b645f1147..f469b02a9 100644 --- a/v2/emailpassword/common-customizations/handling-signin-success.mdx +++ b/v2/emailpassword/common-customizations/handling-signin-success.mdx @@ -115,7 +115,19 @@ SuperTokens.init({ // Post sign up response, we check if it was successful if (response.status === "OK") { - let { id, email } = response.user; + /** + * + * response.user contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ // TODO: post sign in logic } diff --git a/v2/emailpassword/common-customizations/handling-signup-success.mdx b/v2/emailpassword/common-customizations/handling-signup-success.mdx index e8b97ef0a..66299acbc 100644 --- a/v2/emailpassword/common-customizations/handling-signup-success.mdx +++ b/v2/emailpassword/common-customizations/handling-signup-success.mdx @@ -117,7 +117,19 @@ SuperTokens.init({ // Post sign up response, we check if it was successful if (response.status === "OK") { - let { id, email } = response.user; + /** + * + * response.user contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ // TODO: post sign up logic } return response; @@ -282,7 +294,20 @@ SuperTokens.init({ // Post sign up response, we check if it was successful if (response.status === "OK") { - let { id, email } = response.user; + /** + * + * response.user contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ + // TODO: sign up successful // here we fetch a custom form field for the user's name. diff --git a/v2/emailpassword/common-customizations/reset-password/generate-link-manually.mdx b/v2/emailpassword/common-customizations/reset-password/generate-link-manually.mdx index 706bfbca8..65c75db79 100644 --- a/v2/emailpassword/common-customizations/reset-password/generate-link-manually.mdx +++ b/v2/emailpassword/common-customizations/reset-password/generate-link-manually.mdx @@ -22,8 +22,8 @@ You can use our backend SDK to generate the reset password link as shown below: ```tsx import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; -async function createResetPasswordLink(userId: string) { - const linkResponse = await ^{recipeNameCapitalLetters}.createResetPasswordLink("public", userId); +async function createResetPasswordLink(userId: string, email: string) { + const linkResponse = await ^{recipeNameCapitalLetters}.createResetPasswordLink("public", userId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); diff --git a/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx b/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..f59533d1f 100644 --- a/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx @@ -346,7 +346,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) }; /* diff --git a/v2/emailpassword/common-customizations/username-password/emailpassword-changes.mdx b/v2/emailpassword/common-customizations/username-password/emailpassword-changes.mdx index 39dbd897e..1f1c46e76 100644 --- a/v2/emailpassword/common-customizations/username-password/emailpassword-changes.mdx +++ b/v2/emailpassword/common-customizations/username-password/emailpassword-changes.mdx @@ -619,12 +619,12 @@ We want the user to be able to sign in using their email or username along with import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; -let emailUserMap: {[key: string]: string} = {} +let emailUserMap: { [key: string]: string } = {} async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. - + // this is just a placeholder implementation return emailUserMap[email]; } @@ -664,9 +664,15 @@ SuperTokens.init({ if (isInputEmail(input.email)) { let userId = await getUserUsingEmail(input.email); if (userId !== undefined) { - let superTokensUser = await EmailPassword.getUserById(userId); + let superTokensUser = await SuperTokens.getUser(userId); if (superTokensUser !== undefined) { - input.email = superTokensUser.email + // we find the right login method for this user + // based on the user ID. + let loginMethod = superTokensUser.loginMethods.find(lM => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword"); + + if (loginMethod !== undefined) { + input.email = loginMethod.email! + } } } } @@ -875,13 +881,14 @@ We allow the user to enter either their username or their email when starting th ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; +import supertokensTypes from "supertokens-node/types"; -let emailUserMap: {[key: string]: string} = {} +let emailUserMap: { [key: string]: string } = {} async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. - + // this is just a placeholder implementation return emailUserMap[email]; } @@ -942,23 +949,34 @@ SuperTokens.init({ if (isInputEmail(emailOrUsername)) { let userId = await getUserUsingEmail(emailOrUsername); if (userId !== undefined) { - let superTokensUser = await EmailPassword.getUserById(userId); + let superTokensUser = await SuperTokens.getUser(userId); if (superTokensUser !== undefined) { - // we replace the input form field's array item - // to contain the username instead of the email. - input.formFields = input.formFields.filter(i => i.id !== "email") - input.formFields = [...input.formFields, { - id: "email", - value: superTokensUser.email - }] + // we find the right login method for this user + // based on the user ID. + let loginMethod = superTokensUser.loginMethods.find(lM => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword"); + if (loginMethod !== undefined) { + // we replace the input form field's array item + // to contain the username instead of the email. + input.formFields = input.formFields.filter(i => i.id !== "email") + input.formFields = [...input.formFields, { + id: "email", + value: loginMethod.email! + }] + } } } } let username = input.formFields.find(i => i.id === "email")!.value; - let superTokensUser = await EmailPassword.getUserByEmail(username, input.tenantId); - if (superTokensUser !== undefined) { - if ((await getEmailUsingUserId(superTokensUser.id)) === undefined) { + let superTokensUsers: supertokensTypes.User[] = await SuperTokens.listUsersByAccountInfo(input.tenantId, { + email: username + }); + // from the list of users that have this email, we now find the one + // that has this email with the email password login method. + let targetUser = superTokensUsers.find(u => u.loginMethods.find(lM => lM.hasSameEmailAs(username) && lM.recipeId === "emailpassword") !== undefined); + + if (targetUser !== undefined) { + if ((await getEmailUsingUserId(targetUser.id)) === undefined) { return { status: "GENERAL_ERROR", message: "You need to add an email to your account for resetting your password. Please contact support." diff --git a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx index f5221198c..ff6bd3854 100644 --- a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx +++ b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx @@ -191,8 +191,14 @@ EmailPassword.init({ ...originalImplementation, signIn: async function (input) { // Check if the user exists in SuperTokens - let supertokensUser = await EmailPassword.getUserByEmail(input.email, input.userContext); + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }, undefined, input.userContext); + let supertokensUser = supertokensUsersWithSameEmail.find(u => { + return u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "emailpassword") !== undefined; + }) if (supertokensUser === undefined) { + // EmailPassword user with the input email does not exist in SuperTokens // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(input.email, input.password) @@ -202,7 +208,7 @@ EmailPassword.init({ status: "WRONG_CREDENTIALS_ERROR" } } - + // Call the signup function to create a new SuperTokens user. let signUpResponse = await EmailPassword.signUp(input.email, input.password, input.userContext) if (signUpResponse.status !== "OK") { @@ -216,8 +222,9 @@ EmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { + let loginMethod = signUpResponse.user.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(input.email)); // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(signUpResponse.user.id, input.email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, loginMethod!.recipeUserId, input.email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email @@ -227,7 +234,7 @@ EmailPassword.init({ return signUpResponse; } - + return originalImplementation.signIn(input) } } @@ -473,7 +480,12 @@ EmailPassword.init({ let email = input.formFields.find(i => i.id === "email")!.value; // Check if the user exists in SuperTokens - let supertokensUser = await EmailPassword.getUserByEmail(email, input.userContext); + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { + email + }, undefined, input.userContext); + let supertokensUser = supertokensUsersWithSameEmail.find(u => { + return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; + }) if (supertokensUser === undefined) { // check if the user exists in the external provider let legacyUserInfo = await retrieveUserDataFromExternalProvider(email) @@ -492,8 +504,9 @@ EmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { + let loginMethod = signUpResponse.user.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(email)); // generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(legacyUserInfo.user_id, email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, loginMethod!.recipeUserId, email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // verify the user's email @@ -502,7 +515,7 @@ EmailPassword.init({ } // We also need to identify that the user is using a temporary password. We do through the userMetadata recipe - UserMetadata.updateUserMetadata(legacyUserInfo.user_id,{isUsingTemporaryPassword: true}) + UserMetadata.updateUserMetadata(legacyUserInfo.user_id, { isUsingTemporaryPassword: true }) } else { throw new Error("Should never come here") @@ -783,10 +796,10 @@ EmailPassword.init({ passwordResetPOST: async function (input) { let response = await originalImplementation.passwordResetPOST!(input); if (response.status === "OK") { - let usermetadata = await UserMetadata.getUserMetadata(response.userId!, input.userContext) + let usermetadata = await UserMetadata.getUserMetadata(response.user.id, input.userContext) if (usermetadata.status === "OK" && usermetadata.metadata.isUsingTemporaryPassword) { // Since the password reset we can remove the isUsingTemporaryPassword flag - await UserMetadata.updateUserMetadata(response.userId!, { isUsingTemporaryPassword: null }) + await UserMetadata.updateUserMetadata(response.user.id, { isUsingTemporaryPassword: null }) } } return response @@ -945,7 +958,12 @@ EmailPassword.init({ ...originalImplementation, signIn: async function (input) { // Check if the user exists in SuperTokens - let supertokensUser = await EmailPassword.getUserByEmail(input.email, input.userContext); + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }, undefined, input.userContext); + let supertokensUser = supertokensUsersWithSameEmail.find(u => { + return u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "emailpassword") !== undefined; + }) if (supertokensUser === undefined) { // EmailPassword user with the input email does not exist in SuperTokens // Check if the input credentials are valid in the external provider @@ -969,8 +987,9 @@ EmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { + let loginMethod = signUpResponse.user.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(input.email)); // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(signUpResponse.user.id, input.email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, loginMethod!.recipeUserId, input.email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email @@ -987,9 +1006,10 @@ EmailPassword.init({ // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(input.email, input.password); if (legacyUserInfo) { + let loginMethod = supertokensUser.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(input.email)); // Update the user's password with the correct password EmailPassword.updateEmailOrPassword({ - userId: supertokensUser.id, + recipeUserId: loginMethod!.recipeUserId, password: input.password, applyPasswordPolicy: false }) diff --git a/v2/emailpassword/supabase-intergration/backend-signup-override.mdx b/v2/emailpassword/supabase-intergration/backend-signup-override.mdx index d1378aa82..1440bb6d0 100644 --- a/v2/emailpassword/supabase-intergration/backend-signup-override.mdx +++ b/v2/emailpassword/supabase-intergration/backend-signup-override.mdx @@ -61,7 +61,7 @@ let backendConfig = (): TypeInput => { // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") - .insert({ email: response.user.email, user_id: response.user.id }); + .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { diff --git a/v2/passwordless/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/passwordless/common-customizations/email-verification/changing-email-verification-status.mdx index d485b1f58..3e59118f3 100644 --- a/v2/passwordless/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/passwordless/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,11 +23,12 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function manuallyVerifyEmail(userId: string) { +async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { try { // Create an email verification token for the user - const tokenRes = await EmailVerification.createEmailVerificationToken("public", userId); + const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); // If the token creation is successful, use the token to verify the user's email if (tokenRes.status === "OK") { @@ -127,11 +128,12 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensType from "supertokens-node/types"; -async function manuallyUnverifyEmail(userId: string) { +async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { try { // Set email verification status to false - await EmailVerification.unverifyEmail(userId); + await EmailVerification.unverifyEmail(recipeUserId); } catch (err) { console.error(err); } diff --git a/v2/passwordless/common-customizations/email-verification/generate-link-manually.mdx b/v2/passwordless/common-customizations/email-verification/generate-link-manually.mdx index 1c6aedd73..536aa4e9c 100644 --- a/v2/passwordless/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/passwordless/common-customizations/email-verification/generate-link-manually.mdx @@ -21,11 +21,12 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function createEmailVerificationLink(userId: string, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { try { // Create an email verification link for the user - const linkResponse = await EmailVerification.createEmailVerificationLink("public", userId, email); + const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); diff --git a/v2/passwordless/common-customizations/email-verification/handling-email-verification-success.mdx b/v2/passwordless/common-customizations/email-verification/handling-email-verification-success.mdx index 91a12bbe6..bb65d945c 100644 --- a/v2/passwordless/common-customizations/email-verification/handling-email-verification-success.mdx +++ b/v2/passwordless/common-customizations/email-verification/handling-email-verification-success.mdx @@ -47,7 +47,7 @@ SuperTokens.init({ // Then we check if it was successfully completed if (response.status === "OK") { - let { id, email } = response.user; + let { recipeUserId, email } = response.user; // TODO: post email verification logic } return response; diff --git a/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx b/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..f59533d1f 100644 --- a/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx @@ -346,7 +346,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) }; /* diff --git a/v2/session/common-customizations/sessions/claims/claim-validators.mdx b/v2/session/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..f59533d1f 100644 --- a/v2/session/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/session/common-customizations/sessions/claims/claim-validators.mdx @@ -346,7 +346,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) }; /* diff --git a/v2/thirdparty/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/thirdparty/common-customizations/email-verification/changing-email-verification-status.mdx index d485b1f58..3e59118f3 100644 --- a/v2/thirdparty/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/thirdparty/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,11 +23,12 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function manuallyVerifyEmail(userId: string) { +async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { try { // Create an email verification token for the user - const tokenRes = await EmailVerification.createEmailVerificationToken("public", userId); + const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); // If the token creation is successful, use the token to verify the user's email if (tokenRes.status === "OK") { @@ -127,11 +128,12 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensType from "supertokens-node/types"; -async function manuallyUnverifyEmail(userId: string) { +async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { try { // Set email verification status to false - await EmailVerification.unverifyEmail(userId); + await EmailVerification.unverifyEmail(recipeUserId); } catch (err) { console.error(err); } diff --git a/v2/thirdparty/common-customizations/email-verification/generate-link-manually.mdx b/v2/thirdparty/common-customizations/email-verification/generate-link-manually.mdx index 1c6aedd73..536aa4e9c 100644 --- a/v2/thirdparty/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/thirdparty/common-customizations/email-verification/generate-link-manually.mdx @@ -21,11 +21,12 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function createEmailVerificationLink(userId: string, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { try { // Create an email verification link for the user - const linkResponse = await EmailVerification.createEmailVerificationLink("public", userId, email); + const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); diff --git a/v2/thirdparty/common-customizations/email-verification/handling-email-verification-success.mdx b/v2/thirdparty/common-customizations/email-verification/handling-email-verification-success.mdx index 91a12bbe6..bb65d945c 100644 --- a/v2/thirdparty/common-customizations/email-verification/handling-email-verification-success.mdx +++ b/v2/thirdparty/common-customizations/email-verification/handling-email-verification-success.mdx @@ -47,7 +47,7 @@ SuperTokens.init({ // Then we check if it was successfully completed if (response.status === "OK") { - let { id, email } = response.user; + let { recipeUserId, email } = response.user; // TODO: post email verification logic } return response; diff --git a/v2/thirdparty/common-customizations/get-user-info.mdx b/v2/thirdparty/common-customizations/get-user-info.mdx index 451e7dc91..9b8f2952a 100644 --- a/v2/thirdparty/common-customizations/get-user-info.mdx +++ b/v2/thirdparty/common-customizations/get-user-info.mdx @@ -131,17 +131,30 @@ Notice that we pass in the `"public"` tenantId to the function call above. This ```tsx import express from "express"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from 'supertokens-node/framework/express'; +import supertokens from "supertokens-node"; let app = express(); app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId) - // ... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }) ``` @@ -149,10 +162,10 @@ app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import Hapi from "@hapi/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; +import supertokens from "supertokens-node"; let server = Hapi.server({ port: 8000 }); @@ -169,10 +182,23 @@ server.route({ // @ts-ignore handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } }) ``` @@ -183,9 +209,9 @@ server.route({ ```tsx import Fastify from "fastify"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from 'supertokens-node/framework/fastify'; +import supertokens from "supertokens-node"; const fastify = Fastify(); @@ -193,10 +219,23 @@ fastify.post("/like-comment", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -205,15 +244,29 @@ fastify.post("/like-comment", { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import supertokens from "supertokens-node"; async function getUserInfo(awsEvent: SessionEvent) { let userId = awsEvent.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }; exports.handler = verifySession(getUserInfo); @@ -225,18 +278,31 @@ exports.handler = verifySession(getUserInfo); ```tsx import KoaRouter from "koa-router"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; +import supertokens from "supertokens-node"; let router = new KoaRouter(); router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -247,10 +313,10 @@ router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, get, response } from "@loopback/rest"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { SessionContext } from "supertokens-node/framework/loopback"; +import supertokens from "supertokens-node"; class GetUserInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @@ -259,10 +325,23 @@ class GetUserInfo { @response(200) async handler() { let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } } ``` @@ -272,10 +351,10 @@ class GetUserInfo { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { superTokensNextWrapper } from 'supertokens-node/nextjs' import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( @@ -287,10 +366,23 @@ export default async function likeComment(req: SessionRequest, res: any) { ) let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` @@ -304,8 +396,8 @@ import { Controller, Post, UseGuards, Request, Response } from "@nestjs/common"; import { AuthGuard } from './auth/auth.guard'; // @ts-ignore import { Session } from './auth/session.decorator'; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; @Controller() export class ExampleController { @@ -313,10 +405,23 @@ export class ExampleController { @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Request() req: SessionRequest, @Session() session: Session, @Response({passthrough: true}) res: Response): Promise { let userId = session.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ return true; } } diff --git a/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..f59533d1f 100644 --- a/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx @@ -346,7 +346,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) }; /* diff --git a/v2/thirdpartyemailpassword/common-customizations/change-password.mdx b/v2/thirdpartyemailpassword/common-customizations/change-password.mdx index f3dd0bdf7..d5cfc7c1a 100644 --- a/v2/thirdpartyemailpassword/common-customizations/change-password.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/change-password.mdx @@ -108,12 +108,12 @@ import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRec import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import supertokens from "supertokens-node"; let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { - - // highlight-start +// highlight-start // get the supertokens session object from the req let session = req.session @@ -123,18 +123,21 @@ app.post("/change-password", verifySession(), async (req: SessionRequest, res: e // retrive the new password from the request body let updatedPassword = req.body.newPassword - // get the user's Id from the session - let userId = session!.getUserId() - // get the signed in user's email from the getUserById function - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId) + let userInfo = await supertokens.getUser(session!.getUserId()) if (userInfo === undefined) { throw new Error("Should never come here") } + let loginMethod = userInfo.loginMethods.find((lM) => lM.recipeUserId.getAsString() === session!.getRecipeUserId().getAsString() && lM.recipeId === "emailpassword"); + if (loginMethod === undefined) { + throw new Error("Should never come here") + } + const email = loginMethod.email!; + // call signin to check that input password is correct - let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeSignIn}(session!.getTenantId(), userInfo.email, oldPassword) + let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeSignIn}(session!.getTenantId(), email, oldPassword) if (isPasswordValid.status !== "OK") { // TODO: handle incorrect password error @@ -144,7 +147,7 @@ app.post("/change-password", verifySession(), async (req: SessionRequest, res: e // update the user's password using updateEmailOrPassword let response = await ^{recipeNameCapitalLetters}.updateEmailOrPassword({ - userId, + recipeUserId: session!.getRecipeUserId(), password: updatedPassword, tenantIdForPasswordPolicy: session!.getTenantId() }) diff --git a/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx b/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx index 4d975fc8c..1dee00826 100644 --- a/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx @@ -199,7 +199,7 @@ app.post("/create-user", verifySession({ } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); res.send("Success"); }); @@ -244,7 +244,7 @@ server.route({ } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); res.response("Success").code(200); } @@ -280,7 +280,7 @@ fastify.post("/create-user", { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); res.code(200).send("Success"); }); @@ -309,7 +309,7 @@ async function createUser(awsEvent: SessionEventV2) { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); return { statusCode: '200', @@ -355,7 +355,7 @@ router.post("/create-user", verifySession({ } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); ctx.status = 200; ctx.body = "Success"; @@ -393,7 +393,7 @@ class LikeComment { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); // TODO: send 200 response to the client } @@ -435,7 +435,7 @@ export default async function createUser(req: SessionRequest, res: any) { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); res.status(200).json({ message: 'Success' }) } @@ -473,7 +473,7 @@ export class CreateUserController { } // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id); + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); // TODO: send 200 response to the client } @@ -872,23 +872,13 @@ SuperTokens.init({ ...originalImplementation, updateEmailOrPassword: async function (input) { // This can be called on the backend - // in your own APIs + // in your own APIs or in the password reset flow if (input.password === FAKE_PASSWORD) { throw new Error("Use a different password") } return originalImplementation.updateEmailOrPassword(input); }, - resetPasswordUsingToken: async function (input) { - // This is called during the password reset flow - // when the user enters their new password - if (input.newPassword === FAKE_PASSWORD) { - return { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" - } - } - return originalImplementation.resetPasswordUsingToken(input); - }, ^{nodeEmailPasswordSignInFunction}: async function (input) { // This is called in the email password sign in API if (input.password === FAKE_PASSWORD) { diff --git a/v2/thirdpartyemailpassword/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/thirdpartyemailpassword/common-customizations/email-verification/changing-email-verification-status.mdx index f611bc638..00043910f 100644 --- a/v2/thirdpartyemailpassword/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,11 +23,12 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function manuallyVerifyEmail(userId: string) { +async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { try { // Create an email verification token for the user - const tokenRes = await EmailVerification.createEmailVerificationToken("public", userId); + const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); // If the token creation is successful, use the token to verify the user's email if (tokenRes.status === "OK") { @@ -127,11 +128,12 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensType from "supertokens-node/types"; -async function manuallyUnverifyEmail(userId: string) { +async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { try { // Set email verification status to false - await EmailVerification.unverifyEmail(userId); + await EmailVerification.unverifyEmail(recipeUserId); } catch (err) { console.error(err); } diff --git a/v2/thirdpartyemailpassword/common-customizations/email-verification/generate-link-manually.mdx b/v2/thirdpartyemailpassword/common-customizations/email-verification/generate-link-manually.mdx index 3652a399e..9947926cd 100644 --- a/v2/thirdpartyemailpassword/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/email-verification/generate-link-manually.mdx @@ -21,11 +21,12 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function createEmailVerificationLink(userId: string, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { try { // Create an email verification link for the user - const linkResponse = await EmailVerification.createEmailVerificationLink("public", userId, email); + const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); diff --git a/v2/thirdpartyemailpassword/common-customizations/email-verification/handling-email-verification-success.mdx b/v2/thirdpartyemailpassword/common-customizations/email-verification/handling-email-verification-success.mdx index 91a12bbe6..bb65d945c 100644 --- a/v2/thirdpartyemailpassword/common-customizations/email-verification/handling-email-verification-success.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/email-verification/handling-email-verification-success.mdx @@ -47,7 +47,7 @@ SuperTokens.init({ // Then we check if it was successfully completed if (response.status === "OK") { - let { id, email } = response.user; + let { recipeUserId, email } = response.user; // TODO: post email verification logic } return response; diff --git a/v2/thirdpartyemailpassword/common-customizations/get-user-info.mdx b/v2/thirdpartyemailpassword/common-customizations/get-user-info.mdx index e30a20be2..309595d3e 100644 --- a/v2/thirdpartyemailpassword/common-customizations/get-user-info.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/get-user-info.mdx @@ -131,17 +131,30 @@ Notice that we pass in the `"public"` tenantId to the function call above. This ```tsx import express from "express"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from 'supertokens-node/framework/express'; +import supertokens from "supertokens-node"; let app = express(); app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId) - // ... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }) ``` @@ -149,10 +162,10 @@ app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import Hapi from "@hapi/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; +import supertokens from "supertokens-node"; let server = Hapi.server({ port: 8000 }); @@ -169,10 +182,23 @@ server.route({ // @ts-ignore handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } }) ``` @@ -183,9 +209,9 @@ server.route({ ```tsx import Fastify from "fastify"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from 'supertokens-node/framework/fastify'; +import supertokens from "supertokens-node"; const fastify = Fastify(); @@ -193,10 +219,23 @@ fastify.post("/like-comment", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -205,15 +244,29 @@ fastify.post("/like-comment", { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import supertokens from "supertokens-node"; async function getUserInfo(awsEvent: SessionEvent) { let userId = awsEvent.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }; exports.handler = verifySession(getUserInfo); @@ -225,18 +278,31 @@ exports.handler = verifySession(getUserInfo); ```tsx import KoaRouter from "koa-router"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; +import supertokens from "supertokens-node"; let router = new KoaRouter(); router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -247,10 +313,10 @@ router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, get, response } from "@loopback/rest"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { SessionContext } from "supertokens-node/framework/loopback"; +import supertokens from "supertokens-node"; class GetUserInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @@ -259,10 +325,23 @@ class GetUserInfo { @response(200) async handler() { let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } } ``` @@ -272,10 +351,10 @@ class GetUserInfo { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { superTokensNextWrapper } from 'supertokens-node/nextjs' import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( @@ -287,10 +366,23 @@ export default async function likeComment(req: SessionRequest, res: any) { ) let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` @@ -304,8 +396,8 @@ import { Controller, Post, UseGuards, Request, Response } from "@nestjs/common"; import { AuthGuard } from './auth/auth.guard'; // @ts-ignore import { Session } from './auth/session.decorator'; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; @Controller() export class ExampleController { @@ -313,10 +405,23 @@ export class ExampleController { @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Request() req: SessionRequest, @Session() session: Session, @Response({passthrough: true}) res: Response): Promise { let userId = session.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ return true; } } diff --git a/v2/thirdpartyemailpassword/common-customizations/reset-password/generate-link-manually.mdx b/v2/thirdpartyemailpassword/common-customizations/reset-password/generate-link-manually.mdx index 706bfbca8..65c75db79 100644 --- a/v2/thirdpartyemailpassword/common-customizations/reset-password/generate-link-manually.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/reset-password/generate-link-manually.mdx @@ -22,8 +22,8 @@ You can use our backend SDK to generate the reset password link as shown below: ```tsx import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; -async function createResetPasswordLink(userId: string) { - const linkResponse = await ^{recipeNameCapitalLetters}.createResetPasswordLink("public", userId); +async function createResetPasswordLink(userId: string, email: string) { + const linkResponse = await ^{recipeNameCapitalLetters}.createResetPasswordLink("public", userId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..f59533d1f 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx @@ -346,7 +346,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) }; /* diff --git a/v2/thirdpartypasswordless/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/thirdpartypasswordless/common-customizations/email-verification/changing-email-verification-status.mdx index d485b1f58..3e59118f3 100644 --- a/v2/thirdpartypasswordless/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/thirdpartypasswordless/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,11 +23,12 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function manuallyVerifyEmail(userId: string) { +async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { try { // Create an email verification token for the user - const tokenRes = await EmailVerification.createEmailVerificationToken("public", userId); + const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); // If the token creation is successful, use the token to verify the user's email if (tokenRes.status === "OK") { @@ -127,11 +128,12 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensType from "supertokens-node/types"; -async function manuallyUnverifyEmail(userId: string) { +async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { try { // Set email verification status to false - await EmailVerification.unverifyEmail(userId); + await EmailVerification.unverifyEmail(recipeUserId); } catch (err) { console.error(err); } diff --git a/v2/thirdpartypasswordless/common-customizations/email-verification/generate-link-manually.mdx b/v2/thirdpartypasswordless/common-customizations/email-verification/generate-link-manually.mdx index 1c6aedd73..536aa4e9c 100644 --- a/v2/thirdpartypasswordless/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/thirdpartypasswordless/common-customizations/email-verification/generate-link-manually.mdx @@ -21,11 +21,12 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; +import supertokensTypes from "supertokens-node/types"; -async function createEmailVerificationLink(userId: string, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { try { // Create an email verification link for the user - const linkResponse = await EmailVerification.createEmailVerificationLink("public", userId, email); + const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); diff --git a/v2/thirdpartypasswordless/common-customizations/email-verification/handling-email-verification-success.mdx b/v2/thirdpartypasswordless/common-customizations/email-verification/handling-email-verification-success.mdx index 91a12bbe6..bb65d945c 100644 --- a/v2/thirdpartypasswordless/common-customizations/email-verification/handling-email-verification-success.mdx +++ b/v2/thirdpartypasswordless/common-customizations/email-verification/handling-email-verification-success.mdx @@ -47,7 +47,7 @@ SuperTokens.init({ // Then we check if it was successfully completed if (response.status === "OK") { - let { id, email } = response.user; + let { recipeUserId, email } = response.user; // TODO: post email verification logic } return response; diff --git a/v2/thirdpartypasswordless/common-customizations/get-user-info.mdx b/v2/thirdpartypasswordless/common-customizations/get-user-info.mdx index cb1ad65f8..9a9441fda 100644 --- a/v2/thirdpartypasswordless/common-customizations/get-user-info.mdx +++ b/v2/thirdpartypasswordless/common-customizations/get-user-info.mdx @@ -194,17 +194,30 @@ Notice that we pass in the `"public"` tenantId to the function call above. This ```tsx import express from "express"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from 'supertokens-node/framework/express'; +import supertokens from "supertokens-node"; let app = express(); app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId) - // ... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }) ``` @@ -212,10 +225,10 @@ app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import Hapi from "@hapi/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; +import supertokens from "supertokens-node"; let server = Hapi.server({ port: 8000 }); @@ -232,10 +245,23 @@ server.route({ // @ts-ignore handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } }) ``` @@ -246,9 +272,9 @@ server.route({ ```tsx import Fastify from "fastify"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from 'supertokens-node/framework/fastify'; +import supertokens from "supertokens-node"; const fastify = Fastify(); @@ -256,10 +282,23 @@ fastify.post("/like-comment", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -268,15 +307,29 @@ fastify.post("/like-comment", { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import supertokens from "supertokens-node"; async function getUserInfo(awsEvent: SessionEvent) { let userId = awsEvent.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }; exports.handler = verifySession(getUserInfo); @@ -288,18 +341,31 @@ exports.handler = verifySession(getUserInfo); ```tsx import KoaRouter from "koa-router"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; +import supertokens from "supertokens-node"; let router = new KoaRouter(); router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -310,10 +376,10 @@ router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, get, response } from "@loopback/rest"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { SessionContext } from "supertokens-node/framework/loopback"; +import supertokens from "supertokens-node"; class GetUserInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @@ -322,10 +388,23 @@ class GetUserInfo { @response(200) async handler() { let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } } ``` @@ -335,10 +414,10 @@ class GetUserInfo { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { superTokensNextWrapper } from 'supertokens-node/nextjs' import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( @@ -350,10 +429,23 @@ export default async function likeComment(req: SessionRequest, res: any) { ) let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` @@ -367,8 +459,8 @@ import { Controller, Post, UseGuards, Request, Response } from "@nestjs/common"; import { AuthGuard } from './auth/auth.guard'; // @ts-ignore import { Session } from './auth/session.decorator'; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; @Controller() export class ExampleController { @@ -376,10 +468,23 @@ export class ExampleController { @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Request() req: SessionRequest, @Session() session: Session, @Response({passthrough: true}) res: Response): Promise { let userId = session.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById(userId); - //.... + + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ return true; } } diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..f59533d1f 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx @@ -346,7 +346,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) }; /* From c4db15a19da72445310539a267379f398393ca1a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 22 Aug 2023 14:56:02 +0530 Subject: [PATCH 03/81] more changes --- .../ep-migration-without-password-hash.mdx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx index ff6bd3854..33da354b4 100644 --- a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx +++ b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx @@ -222,9 +222,8 @@ EmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { - let loginMethod = signUpResponse.user.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(input.email)); // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, loginMethod!.recipeUserId, input.email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, input.email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email @@ -504,9 +503,8 @@ EmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { - let loginMethod = signUpResponse.user.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(email)); // generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, loginMethod!.recipeUserId, email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // verify the user's email @@ -987,9 +985,8 @@ EmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { - let loginMethod = signUpResponse.user.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(input.email)); // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, loginMethod!.recipeUserId, input.email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, input.email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email From 82428704fc704db147f47cb78bb9eb32463435a3 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 22 Aug 2023 18:02:57 +0530 Subject: [PATCH 04/81] fixes --- v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt b/v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt index f3e86b4d1..493ceb979 100644 --- a/v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt +++ b/v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt @@ -84,5 +84,5 @@ tzdata==2021.5 urllib3==2.0.4 uvicorn==0.18.2 Werkzeug==2.0.3 -wrapt==1.15.0 +wrapt==1.13.0 zipp==3.7.0 From 1d4777086f4143d65817c171597ab3de68b26cdf Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 22 Aug 2023 18:03:31 +0530 Subject: [PATCH 05/81] fixes --- v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt b/v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt index f3e86b4d1..493ceb979 100644 --- a/v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt +++ b/v2/src/plugins/codeTypeChecking/pythonEnv/requirements.txt @@ -84,5 +84,5 @@ tzdata==2021.5 urllib3==2.0.4 uvicorn==0.18.2 Werkzeug==2.0.3 -wrapt==1.15.0 +wrapt==1.13.0 zipp==3.7.0 From 2e8e2c29ce1fb4091bbaa3524451dcdb53f981c8 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 23 Aug 2023 12:28:33 +0530 Subject: [PATCH 06/81] fixes a snippet --- .../change-email-post-login.mdx | 24 +++++++++---------- .../codeTypeChecking/jsEnv/package.json | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/v2/emailpassword/common-customizations/change-email-post-login.mdx b/v2/emailpassword/common-customizations/change-email-post-login.mdx index 115596af0..24b549c6f 100644 --- a/v2/emailpassword/common-customizations/change-email-post-login.mdx +++ b/v2/emailpassword/common-customizations/change-email-post-login.mdx @@ -413,19 +413,17 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. let user = (await supertokens.getUser(session.getUserId()))!; - let usersWithSameEmail = await supertokens.listUsersByAccountInfo({ - email - }); - for (let i = 0; i < usersWithSameEmail.length; i++) { - // Since one user can be shared across many tenants, we need to check if - // the email already exists in any of the tenants that belongs to this user. - let currUser = usersWithSameEmail[i]; - for (let y = 0; y < user.tenantIds.length; y++) { - if (currUser.tenantIds.includes(user.tenantIds[y])) { - if (currUser?.id !== session.getUserId()) { - // TODO handle error, email already exists with another user. - return - } + for (let i = 0; i < user.tenantIds.length; i++) { + let usersWithSameEmail = await supertokens.listUsersByAccountInfo(user.tenantIds[i], { + email + }); + for (let y = 0; y < usersWithSameEmail.length; y++) { + // Since one user can be shared across many tenants, we need to check if + // the email already exists in any of the tenants that belongs to this user. + let currUser = usersWithSameEmail[y]; + if (currUser?.id !== session.getUserId()) { + // TODO handle error, email already exists with another user. + return } } } diff --git a/v2/src/plugins/codeTypeChecking/jsEnv/package.json b/v2/src/plugins/codeTypeChecking/jsEnv/package.json index 14eac1d02..3302eef6b 100644 --- a/v2/src/plugins/codeTypeChecking/jsEnv/package.json +++ b/v2/src/plugins/codeTypeChecking/jsEnv/package.json @@ -53,7 +53,7 @@ "socket.io": "^4.6.1", "socketio": "^1.0.0", "supertokens-auth-react": "^0.34.0", - "supertokens-node": "github:supertokens/supertokens-node#account-linking", + "supertokens-node": "github:supertokens/supertokens-node#feat/account_linking/optimizations", "supertokens-node7": "npm:supertokens-node@7.3", "supertokens-react-native": "^4.0.0", "supertokens-web-js": "^0.7.0", @@ -62,4 +62,4 @@ "supertokens-website-script": "github:supertokens/supertokens-website#17.0", "typescript": "^4.9.5" } -} +} \ No newline at end of file From 0ce8c000510b906266d3abdb519e69493a9b85af Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 23 Aug 2023 13:24:23 +0530 Subject: [PATCH 07/81] more fixes --- v2/mfa/backend/first-factor.mdx | 4 +-- v2/mfa/backend/second-factor.mdx | 27 ++++++++++--------- .../advanced-customizations/user-context.mdx | 5 ++-- .../advanced-customizations/user-context.mdx | 4 ++- .../advanced-customizations/user-context.mdx | 3 ++- .../advanced-customizations/user-context.mdx | 3 ++- 6 files changed, 25 insertions(+), 21 deletions(-) diff --git a/v2/mfa/backend/first-factor.mdx b/v2/mfa/backend/first-factor.mdx index 07bed0f70..452759e92 100644 --- a/v2/mfa/backend/first-factor.mdx +++ b/v2/mfa/backend/first-factor.mdx @@ -374,8 +374,6 @@ After sign up or sign in of the first factor, the existence of the session signi ```tsx import Session from "supertokens-node/recipe/session"; -import UserMetadata from "supertokens-node/recipe/usermetadata"; -import Passwordless from "supertokens-node/recipe/passwordless"; import { BooleanClaim } from "supertokens-node/recipe/session/claims"; /* @@ -401,7 +399,7 @@ Session.init({ accessTokenPayload: { ...input.accessTokenPayload, // highlight-next-line - ...(await SecondFactorClaim.build(input.userId, input.tenantId, input.userContext)), + ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)), }, }); }, diff --git a/v2/mfa/backend/second-factor.mdx b/v2/mfa/backend/second-factor.mdx index b4929c8b7..af6024a59 100644 --- a/v2/mfa/backend/second-factor.mdx +++ b/v2/mfa/backend/second-factor.mdx @@ -688,7 +688,7 @@ Session.init({ ...input, accessTokenPayload: { ...input.accessTokenPayload, - ...(await SecondFactorClaim.build(input.userId, input.tenantId, input.userContext)), + ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)), }, }); }, @@ -938,6 +938,7 @@ To make it secure, we override the `createCodePOST` API and check that the input import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import Passwordless from "supertokens-node/recipe/passwordless"; +import SuperTokens from "supertokens-node"; Passwordless.init({ flowType: "USER_INPUT_CODE", @@ -968,11 +969,11 @@ Passwordless.init({ if (userMetadata.metadata.passwordlessUserId !== undefined) { // the flow will come here during a login attempt, since we // associate the passwordless userId to the user on sign up - let passwordlessUserInfo = await Passwordless.getUserById({ - userId: userMetadata.metadata.passwordlessUserId as string, - userContext: input.userContext, - }); - phoneNumber = passwordlessUserInfo?.phoneNumber; + let passwordlessUserInfo = await SuperTokens.getUser( + userMetadata.metadata.passwordlessUserId as string, + input.userContext, + ); + phoneNumber = passwordlessUserInfo?.phoneNumbers[0]; } if (phoneNumber !== undefined) { @@ -1191,7 +1192,7 @@ We do this by modifying the `createNewSession` function in the `Session.init` ca ```tsx import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -import Passwordless from "supertokens-node/recipe/passwordless"; +import SuperTokens from "supertokens-node"; import { BooleanClaim } from "supertokens-node/recipe/session/claims"; // typecheck-only, removed from output declare const SecondFactorClaim: BooleanClaim; // typecheck-only, removed from output @@ -1217,11 +1218,11 @@ Session.init({ let phoneNumber: string | undefined = undefined; if (userMetadata.metadata.passwordlessUserId !== undefined) { // We get the phone number associated with the passwordless userId. - let passwordlessUserInfo = await Passwordless.getUserById({ - userId: userMetadata.metadata.passwordlessUserId as string, - userContext: input.userContext, - }); - phoneNumber = passwordlessUserInfo?.phoneNumber; + let passwordlessUserInfo = await SuperTokens.getUser( + userMetadata.metadata.passwordlessUserId as string, + input.userContext, + ); + phoneNumber = passwordlessUserInfo?.phoneNumbers[0]; } // highlight-end @@ -1229,7 +1230,7 @@ Session.init({ ...input, accessTokenPayload: { ...input.accessTokenPayload, - ...(await SecondFactorClaim.build(input.userId, input.tenantId, input.userContext)), + ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)), // highlight-next-line phoneNumber, }, diff --git a/v2/passwordless/advanced-customizations/user-context.mdx b/v2/passwordless/advanced-customizations/user-context.mdx index eab044bb4..1854048ad 100644 --- a/v2/passwordless/advanced-customizations/user-context.mdx +++ b/v2/passwordless/advanced-customizations/user-context.mdx @@ -55,7 +55,7 @@ SuperTokens.init({ ...originalImplementation, consumeCode: async function (input) { let resp = await originalImplementation.consumeCode(input); - if (resp.status === "OK" && resp.createdNewUser) { + if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1) { /* * This is called during the consume code API, * but before calling the createNewSession function. @@ -267,7 +267,8 @@ SuperTokens.init({ antiCsrfToken: undefined, refreshToken: undefined, }), - getTenantId: () => "public" + getTenantId: () => "public", + getRecipeUserId: () => SuperTokens.convertToRecipeUserId(""), }; } return originalImplementation.createNewSession(input); diff --git a/v2/thirdparty/advanced-customizations/user-context.mdx b/v2/thirdparty/advanced-customizations/user-context.mdx index 3fc1091f8..ae4b8b553 100644 --- a/v2/thirdparty/advanced-customizations/user-context.mdx +++ b/v2/thirdparty/advanced-customizations/user-context.mdx @@ -219,6 +219,7 @@ import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import Session from "supertokens-node/recipe/session"; + SuperTokens.init({ appInfo: { apiDomain: "...", @@ -268,7 +269,8 @@ SuperTokens.init({ antiCsrfToken: undefined, refreshToken: undefined, }), - getTenantId: () => "public" + getTenantId: () => "public", + getRecipeUserId: () => SuperTokens.convertToRecipeUserId(""), }; } return originalImplementation.createNewSession(input); diff --git a/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx b/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx index 8c36f4e50..292fecd75 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx @@ -299,7 +299,8 @@ SuperTokens.init({ antiCsrfToken: undefined, refreshToken: undefined, }), - getTenantId: () => "public" + getTenantId: () => "public", + getRecipeUserId: () => SuperTokens.convertToRecipeUserId(""), }; } return originalImplementation.createNewSession(input); diff --git a/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx b/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx index e0445a10d..ed5720dbc 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx @@ -334,7 +334,8 @@ SuperTokens.init({ antiCsrfToken: undefined, refreshToken: undefined, }), - getTenantId: () => "public" + getTenantId: () => "public", + getRecipeUserId: () => SuperTokens.convertToRecipeUserId(""), }; } return originalImplementation.createNewSession(input); From 6c9c2b4876e10a7b7ea1c0c1ac8edd2d0d00493f Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 23 Aug 2023 14:29:34 +0530 Subject: [PATCH 08/81] more changes --- .../changing-email-verification-status.mdx | 8 ++++---- .../email-verification/generate-link-manually.mdx | 4 ++-- .../ep-migration-without-password-hash.mdx | 3 ++- .../changing-email-verification-status.mdx | 8 ++++---- .../email-verification/generate-link-manually.mdx | 4 ++-- .../changing-email-verification-status.mdx | 8 ++++---- .../email-verification/generate-link-manually.mdx | 4 ++-- .../changing-email-verification-status.mdx | 8 ++++---- .../email-verification/generate-link-manually.mdx | 4 ++-- .../changing-email-verification-status.mdx | 8 ++++---- .../email-verification/generate-link-manually.mdx | 4 ++-- 11 files changed, 32 insertions(+), 31 deletions(-) diff --git a/v2/emailpassword/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/emailpassword/common-customizations/email-verification/changing-email-verification-status.mdx index 3e59118f3..9ab9353d5 100644 --- a/v2/emailpassword/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/emailpassword/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,9 +23,9 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { +async function manuallyVerifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Create an email verification token for the user const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); @@ -128,9 +128,9 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensType from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { +async function manuallyUnverifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Set email verification status to false await EmailVerification.unverifyEmail(recipeUserId); diff --git a/v2/emailpassword/common-customizations/email-verification/generate-link-manually.mdx b/v2/emailpassword/common-customizations/email-verification/generate-link-manually.mdx index 536aa4e9c..27238bb92 100644 --- a/v2/emailpassword/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/emailpassword/common-customizations/email-verification/generate-link-manually.mdx @@ -21,9 +21,9 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokens.RecipeUserId, email: string) { try { // Create an email verification link for the user const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); diff --git a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx index 33da354b4..8727d84bc 100644 --- a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx +++ b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx @@ -1016,7 +1016,8 @@ EmailPassword.init({ return { status: "OK", - user: supertokensUser + user: supertokensUser, + recipeUserId: loginMethod!.recipeUserId } } return { diff --git a/v2/passwordless/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/passwordless/common-customizations/email-verification/changing-email-verification-status.mdx index 3e59118f3..9ab9353d5 100644 --- a/v2/passwordless/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/passwordless/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,9 +23,9 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { +async function manuallyVerifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Create an email verification token for the user const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); @@ -128,9 +128,9 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensType from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { +async function manuallyUnverifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Set email verification status to false await EmailVerification.unverifyEmail(recipeUserId); diff --git a/v2/passwordless/common-customizations/email-verification/generate-link-manually.mdx b/v2/passwordless/common-customizations/email-verification/generate-link-manually.mdx index 536aa4e9c..27238bb92 100644 --- a/v2/passwordless/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/passwordless/common-customizations/email-verification/generate-link-manually.mdx @@ -21,9 +21,9 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokens.RecipeUserId, email: string) { try { // Create an email verification link for the user const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); diff --git a/v2/thirdparty/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/thirdparty/common-customizations/email-verification/changing-email-verification-status.mdx index 3e59118f3..9ab9353d5 100644 --- a/v2/thirdparty/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/thirdparty/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,9 +23,9 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { +async function manuallyVerifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Create an email verification token for the user const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); @@ -128,9 +128,9 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensType from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { +async function manuallyUnverifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Set email verification status to false await EmailVerification.unverifyEmail(recipeUserId); diff --git a/v2/thirdparty/common-customizations/email-verification/generate-link-manually.mdx b/v2/thirdparty/common-customizations/email-verification/generate-link-manually.mdx index 536aa4e9c..27238bb92 100644 --- a/v2/thirdparty/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/thirdparty/common-customizations/email-verification/generate-link-manually.mdx @@ -21,9 +21,9 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokens.RecipeUserId, email: string) { try { // Create an email verification link for the user const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); diff --git a/v2/thirdpartyemailpassword/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/thirdpartyemailpassword/common-customizations/email-verification/changing-email-verification-status.mdx index 00043910f..2c38e3615 100644 --- a/v2/thirdpartyemailpassword/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,9 +23,9 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { +async function manuallyVerifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Create an email verification token for the user const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); @@ -128,9 +128,9 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensType from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { +async function manuallyUnverifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Set email verification status to false await EmailVerification.unverifyEmail(recipeUserId); diff --git a/v2/thirdpartyemailpassword/common-customizations/email-verification/generate-link-manually.mdx b/v2/thirdpartyemailpassword/common-customizations/email-verification/generate-link-manually.mdx index 9947926cd..0a0e1daf2 100644 --- a/v2/thirdpartyemailpassword/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/email-verification/generate-link-manually.mdx @@ -21,9 +21,9 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokens.RecipeUserId, email: string) { try { // Create an email verification link for the user const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); diff --git a/v2/thirdpartypasswordless/common-customizations/email-verification/changing-email-verification-status.mdx b/v2/thirdpartypasswordless/common-customizations/email-verification/changing-email-verification-status.mdx index 3e59118f3..9ab9353d5 100644 --- a/v2/thirdpartypasswordless/common-customizations/email-verification/changing-email-verification-status.mdx +++ b/v2/thirdpartypasswordless/common-customizations/email-verification/changing-email-verification-status.mdx @@ -23,9 +23,9 @@ To manually mark an email as verified, you need to first create an email verific ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyVerifyEmail(recipeUserId: supertokensTypes.RecipeUserId) { +async function manuallyVerifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Create an email verification token for the user const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); @@ -128,9 +128,9 @@ To manually mark an email as unverified, you need to first retrieve the user's e ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensType from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function manuallyUnverifyEmail(recipeUserId: supertokensType.RecipeUserId) { +async function manuallyUnverifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Set email verification status to false await EmailVerification.unverifyEmail(recipeUserId); diff --git a/v2/thirdpartypasswordless/common-customizations/email-verification/generate-link-manually.mdx b/v2/thirdpartypasswordless/common-customizations/email-verification/generate-link-manually.mdx index 536aa4e9c..27238bb92 100644 --- a/v2/thirdpartypasswordless/common-customizations/email-verification/generate-link-manually.mdx +++ b/v2/thirdpartypasswordless/common-customizations/email-verification/generate-link-manually.mdx @@ -21,9 +21,9 @@ You can use our backend SDK to generate the email verification link as shown bel ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; -import supertokensTypes from "supertokens-node/types"; +import supertokens from "supertokens-node"; -async function createEmailVerificationLink(recipeUserId: supertokensTypes.RecipeUserId, email: string) { +async function createEmailVerificationLink(recipeUserId: supertokens.RecipeUserId, email: string) { try { // Create an email verification link for the user const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); From 7750d7101eaf8bec69f2663182b4df46e2996305 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 24 Aug 2023 13:25:52 +0530 Subject: [PATCH 09/81] more fixes --- .../common-customizations/change-email.mdx | 98 ++++++++++--------- .../passwordless-via-allow-list.mdx | 19 ++-- .../passwordless-via-invite-link.mdx | 39 ++++---- 3 files changed, 81 insertions(+), 75 deletions(-) diff --git a/v2/passwordless/common-customizations/change-email.mdx b/v2/passwordless/common-customizations/change-email.mdx index 160476d0f..71bd51442 100644 --- a/v2/passwordless/common-customizations/change-email.mdx +++ b/v2/passwordless/common-customizations/change-email.mdx @@ -126,7 +126,7 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Update the email let resp = await Passwordless.updateUser({ - userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: email }) @@ -388,6 +388,7 @@ import EmailVerification from "supertokens-node/recipe/emailverification"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import supertokens from "supertokens-node"; let app = express(); @@ -405,32 +406,36 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. - let isVerified = await EmailVerification.isEmailVerified(session.getUserId(), email); + let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. - let user = (await Passwordless.getUserById({ userId: session.getUserId() }))!; + let user = (await supertokens.getUser(session.getUserId()))!; for (let i = 0; i < user?.tenantIds.length; i++) { // Since once user can be shared across many tenants, we need to check if // the email already exists in any of the tenants. - let userWithEmail = await Passwordless.getUserByEmail({ email, tenantId: user.tenantIds[i] }); - if (userWithEmail?.id !== session.getUserId()) { - // TODO handle error, email already exists with another user. - return + let usersWithEmail = await supertokens.listUsersByAccountInfo(user?.tenantIds[i], { + email + }) + for (let y = 0; y < usersWithEmail.length; y++) { + if (usersWithEmail[y].id !== session.getUserId()) { + // TODO handle error, email already exists with another user. + return + } } } - + // Now we create and send the email verification link to the user for the new email. await EmailVerification.sendEmailVerificationEmail(session.getTenantId(), session.getUserId(), email); // TODO send successful response that email verification email sent. - return + return } // Since the email is verified, we try and do an update let resp = await Passwordless.updateUser({ - userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: email, }); @@ -440,7 +445,7 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO handle error, email already exists with another user. - return + return } throw new Error("Should never come here"); @@ -454,7 +459,6 @@ function isValidEmail(email: string) { ); return regexp.test(email); } - ``` @@ -670,42 +674,42 @@ import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - Passwordless.init({ - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - contactMethod: "EMAIL_OR_PHONE" - }), - EmailVerification.init({ - mode: "REQUIRED", - override: { - apis: (oI) => { - return { - ...oI, - verifyEmailPOST: async function (input) { - // highlight-start - let response = await oI.verifyEmailPOST!(input); - if (response.status === "OK") { - // This will update the email of the user to the one - // that was just marked as verified by the token. - await Passwordless.updateUser({ - userId: response.user.id, - email: response.user.email, - }); - } - return response; - // highlight-end - }, - }; + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + contactMethod: "EMAIL_OR_PHONE" + }), + EmailVerification.init({ + mode: "REQUIRED", + override: { + apis: (oI) => { + return { + ...oI, + verifyEmailPOST: async function (input) { + // highlight-start + let response = await oI.verifyEmailPOST!(input); + if (response.status === "OK") { + // This will update the email of the user to the one + // that was just marked as verified by the token. + await Passwordless.updateUser({ + recipeUserId: response.user.recipeUserId, + email: response.user.email, + }); + } + return response; + // highlight-end + }, + }; + }, }, - }, - }), - Session.init(), - ], + }), + Session.init(), + ], }); ``` diff --git a/v2/passwordless/common-customizations/disable-sign-up/passwordless-via-allow-list.mdx b/v2/passwordless/common-customizations/disable-sign-up/passwordless-via-allow-list.mdx index 72de88161..3bf7a5587 100644 --- a/v2/passwordless/common-customizations/disable-sign-up/passwordless-via-allow-list.mdx +++ b/v2/passwordless/common-customizations/disable-sign-up/passwordless-via-allow-list.mdx @@ -188,23 +188,24 @@ After that, we override the `createCodePOST` API to check if the input email / p ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import supertokens from "supertokens-node"; declare let isEmailAllowed: (email: string) => Promise // typecheck-only, removed from output declare let isPhoneNumberAllowed: (email: string) => Promise // typecheck-only, removed from output -^{recipeNameCapitalLetters}.init({ - ^{nodeRecipeInitDefault} +Passwordless.init({ + contactMethod: "EMAIL", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // typecheck-only, removed from output override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { - let existingUser = await Passwordless.getUserByEmail({ + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email, - tenantId: input.tenantId }); - if (existingUser === undefined) { + let userWithPasswordles = existingUsers.find(u => u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined); + if (userWithPasswordles === undefined) { // this is sign up attempt if (!(await isEmailAllowed(input.email))) { return { @@ -214,11 +215,11 @@ declare let isPhoneNumberAllowed: (email: string) => Promise // typeche } } } else { - let existingUser = await ^{recipeNameCapitalLetters}.getUserByPhoneNumber({ + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { phoneNumber: input.phoneNumber, - tenantId: input.tenantId }); - if (existingUser === undefined) { + let userWithPasswordles = existingUsers.find(u => u.loginMethods.find(lM => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless") !== undefined); + if (userWithPasswordles === undefined) { // this is sign up attempt if (!(await isPhoneNumberAllowed(input.phoneNumber))) { return { diff --git a/v2/passwordless/common-customizations/disable-sign-up/passwordless-via-invite-link.mdx b/v2/passwordless/common-customizations/disable-sign-up/passwordless-via-invite-link.mdx index d86c932e4..a4bccdbbd 100644 --- a/v2/passwordless/common-customizations/disable-sign-up/passwordless-via-invite-link.mdx +++ b/v2/passwordless/common-customizations/disable-sign-up/passwordless-via-invite-link.mdx @@ -27,38 +27,39 @@ We start by overriding the `createCodePOST` API to check if the input email / ph ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import supertokens from "supertokens-node"; -^{recipeNameCapitalLetters}.init({ - ^{nodeRecipeInitDefault} +Passwordless.init({ + contactMethod: "EMAIL", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // typecheck-only, removed from output override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { - let existingUser = await ^{recipeNameCapitalLetters}.getUserByEmail({ - email: input.email, - tenantId: input.tenantId + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email }); - if (existingUser === undefined) { + let existingPasswordlessUser = existingUsers.find(user => user.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined); + if (existingPasswordlessUser === undefined) { // this is sign up attempt - return { - status: "GENERAL_ERROR", - message: "Sign up disabled. Please contact the admin." - } + return { + status: "GENERAL_ERROR", + message: "Sign up disabled. Please contact the admin." + } } } else { - let existingUser = await ^{recipeNameCapitalLetters}.getUserByPhoneNumber({ - phoneNumber: input.phoneNumber, - tenantId: input.tenantId + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + phoneNumber: input.phoneNumber }); - if (existingUser === undefined) { + let existingPasswordlessUser = existingUsers.find(user => user.loginMethods.find(lM => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless") !== undefined); + if (existingPasswordlessUser === undefined) { // this is sign up attempt - return { - status: "GENERAL_ERROR", - message: "Sign up disabled. Please contact the admin." - } + return { + status: "GENERAL_ERROR", + message: "Sign up disabled. Please contact the admin." + } } } return await originalImplementation.createCodePOST!(input); From fb6c8d8a6561094ac3f0616d2cd7878650e60183 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 24 Aug 2023 14:01:27 +0530 Subject: [PATCH 10/81] fixes more docs --- .../common-customizations/get-user-info.mdx | 212 ++++++++++++++---- .../handling-signinup-success.mdx | 4 +- .../backend-signup-override.mdx | 2 +- 3 files changed, 173 insertions(+), 45 deletions(-) diff --git a/v2/passwordless/common-customizations/get-user-info.mdx b/v2/passwordless/common-customizations/get-user-info.mdx index 450d858b7..13d884b21 100644 --- a/v2/passwordless/common-customizations/get-user-info.mdx +++ b/v2/passwordless/common-customizations/get-user-info.mdx @@ -40,13 +40,27 @@ There are several ways to fetch information about a user: You can get a user's information on the backend using the `^{getUserByEmailNode}`, `getUserByPhoneNumber` and `getUserById` functions: ```tsx - -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; async function handler() { - const userInfo = await ^{recipeNameCapitalLetters}.^{getUserByEmailNode}({email: "test@example.com", tenantId: "public"}); + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + email: "test@example.com" + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } - ``` @@ -116,10 +130,26 @@ Notice that we pass in the `"public"` tenantId to the function call above. This ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; async function handler() { - const userInfo = await ^{recipeNameCapitalLetters}.getUserByPhoneNumber({phoneNumber: "+1234567891", tenantId: "public"}); + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + phoneNumber: "+1234567890" + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` @@ -193,17 +223,30 @@ Notice that we pass in the `"public"` tenantId to the function call above. This ```tsx import express from "express"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from 'supertokens-node/framework/express'; let app = express(); app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki + // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById({userId}) - // ... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }) ``` @@ -211,7 +254,7 @@ app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import Hapi from "@hapi/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; @@ -231,10 +274,22 @@ server.route({ // @ts-ignore handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById({userId}); - //... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } }) ``` @@ -245,7 +300,7 @@ server.route({ ```tsx import Fastify from "fastify"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from 'supertokens-node/framework/fastify'; @@ -255,10 +310,22 @@ fastify.post("/like-comment", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById({userId}); - //.... + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -267,15 +334,28 @@ fastify.post("/like-comment", { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function getUserInfo(awsEvent: SessionEvent) { let userId = awsEvent.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - let userInfo = await ^{recipeNameCapitalLetters}.getUserById({userId}); - //.... + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }; exports.handler = verifySession(getUserInfo); @@ -287,7 +367,7 @@ exports.handler = verifySession(getUserInfo); ```tsx import KoaRouter from "koa-router"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; @@ -295,10 +375,22 @@ let router = new KoaRouter(); router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById({userId}); - //.... + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` @@ -309,7 +401,7 @@ router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, get, response } from "@loopback/rest"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { SessionContext } from "supertokens-node/framework/loopback"; @@ -321,10 +413,22 @@ class GetUserInfo { @response(200) async handler() { let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById({userId}); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } } ``` @@ -334,7 +438,7 @@ class GetUserInfo { ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; import { superTokensNextWrapper } from 'supertokens-node/nextjs' import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; @@ -349,10 +453,22 @@ export default async function likeComment(req: SessionRequest, res: any) { ) let userId = req.session!.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki // highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById({userId}); - //.... + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` @@ -366,7 +482,7 @@ import { Controller, Post, UseGuards, Request, Response } from "@nestjs/common"; import { AuthGuard } from './auth/auth.guard'; // @ts-ignore import { Session } from './auth/session.decorator'; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; import { SessionRequest } from "supertokens-node/framework/express"; @Controller() @@ -375,11 +491,23 @@ export class ExampleController { @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Request() req: SessionRequest, @Session() session: Session, @Response({passthrough: true}) res: Response): Promise { let userId = session.getUserId(); - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - //highlight-next-line - let userInfo = await ^{recipeNameCapitalLetters}.getUserById({userId}); - //.... - return true; + // highlight-next-line + let userInfo = await supertokens.getUser(userId) + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ + return true; } } ``` diff --git a/v2/passwordless/common-customizations/handling-signinup-success.mdx b/v2/passwordless/common-customizations/handling-signinup-success.mdx index 328931336..182c90439 100644 --- a/v2/passwordless/common-customizations/handling-signinup-success.mdx +++ b/v2/passwordless/common-customizations/handling-signinup-success.mdx @@ -123,9 +123,9 @@ SuperTokens.init({ // Post sign up response, we check if it was successful if (response.status === "OK") { - let { id, email, phoneNumber } = response.user; + let { id, emails, phoneNumbers } = response.user; - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: post sign up logic } else { // TODO: post sign in logic diff --git a/v2/passwordless/supabase-intergration/backend-signup-override.mdx b/v2/passwordless/supabase-intergration/backend-signup-override.mdx index 93483fbbd..c26db66a1 100644 --- a/v2/passwordless/supabase-intergration/backend-signup-override.mdx +++ b/v2/passwordless/supabase-intergration/backend-signup-override.mdx @@ -51,7 +51,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.consumeCodePOST(input); - if (response.status === "OK" && response.createdNewUser) { + if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); From d8d924d626fbc9d758946c518973612411b7b140 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 24 Aug 2023 14:13:10 +0530 Subject: [PATCH 11/81] fixes more docs --- .../supabase-intergration/backend-signup-override.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/passwordless/supabase-intergration/backend-signup-override.mdx b/v2/passwordless/supabase-intergration/backend-signup-override.mdx index c26db66a1..56445160a 100644 --- a/v2/passwordless/supabase-intergration/backend-signup-override.mdx +++ b/v2/passwordless/supabase-intergration/backend-signup-override.mdx @@ -62,7 +62,7 @@ let backendConfig = (): TypeInput => { // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") - .insert({ email: response.user.email, user_id: response.user.id }); + .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { throw error; From 470617ce4baa3da929afb4ffd74b1a2946cc270a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 24 Aug 2023 15:54:22 +0530 Subject: [PATCH 12/81] more fixes --- v2/phonepassword/backend/passwordless-customisation.mdx | 8 ++++---- v2/phonepassword/backend/session-customisation.mdx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/v2/phonepassword/backend/passwordless-customisation.mdx b/v2/phonepassword/backend/passwordless-customisation.mdx index 421d765ab..0636bebf2 100644 --- a/v2/phonepassword/backend/passwordless-customisation.mdx +++ b/v2/phonepassword/backend/passwordless-customisation.mdx @@ -191,7 +191,7 @@ supertokens.init({ // @ts-ignore appInfo: { /*...*/ }, recipeList: [ - EmailPassword.init({ /* ... */}), + EmailPassword.init({ /* ... */ }), // @ts-ignore Passwordless.init({ /* ... */ }), Session.init({ @@ -209,13 +209,13 @@ supertokens.init({ // highlight-end // we also get the phone number of the user and save it in the // session so that the OTP can be sent to it directly - let userInfo = await EmailPassword.getUserById(input.userId, input.userContext); + let userInfo = await supertokens.getUser(input.userId, input.userContext); return originalImplementation.createNewSession({ ...input, accessTokenPayload: { ...input.accessTokenPayload, - ...PhoneVerifiedClaim.build(input.userId, input.tenantId, input.userContext), - phoneNumber: userInfo?.email, + ...PhoneVerifiedClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext), + phoneNumber: userInfo?.emails[0], }, }); } diff --git a/v2/phonepassword/backend/session-customisation.mdx b/v2/phonepassword/backend/session-customisation.mdx index 4512bbfb9..a337bf98c 100644 --- a/v2/phonepassword/backend/session-customisation.mdx +++ b/v2/phonepassword/backend/session-customisation.mdx @@ -37,7 +37,7 @@ supertokens.init({ // @ts-ignore appInfo: { /*...*/ }, recipeList: [ - EmailPassword.init({ /* ... */}), + EmailPassword.init({ /* ... */ }), Session.init({ override: { functions: (originalImplementation) => { @@ -47,13 +47,13 @@ supertokens.init({ createNewSession: async function (input) { // we also get the phone number of the user and save it in the // session so that the OTP can be sent to it directly - let userInfo = await EmailPassword.getUserById(input.userId, input.userContext); + let userInfo = await supertokens.getUser(input.userId, input.userContext); return originalImplementation.createNewSession({ ...input, accessTokenPayload: { ...input.accessTokenPayload, - ...PhoneVerifiedClaim.build(input.userId, input.tenantId, input.userContext), - phoneNumber: userInfo?.email, + ...PhoneVerifiedClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext), + phoneNumber: userInfo?.emails[0], }, }); }, From 68d848fa879f703902df8eee3cf325f747e79f66 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 24 Aug 2023 16:19:22 +0530 Subject: [PATCH 13/81] more fixes --- .../sessions/new-session.mdx | 27 ++++++++++++------- v2/session/testing/testing-with-postman.mdx | 3 ++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/v2/session/common-customizations/sessions/new-session.mdx b/v2/session/common-customizations/sessions/new-session.mdx index 4a5806006..a14cb126b 100644 --- a/v2/session/common-customizations/sessions/new-session.mdx +++ b/v2/session/common-customizations/sessions/new-session.mdx @@ -26,6 +26,7 @@ Create a new session after verifying user's credentials in the login API, or aft import express from "express"; // highlight-next-line import Session from "supertokens-node/recipe/session"; +import supertokens from "supertokens-node"; let app = express(); @@ -36,7 +37,7 @@ app.post("/login", async (req, res) => { let userId = "userId"; // get from db // highlight-next-line - await Session.createNewSession(req, res, "public", userId); + await Session.createNewSession(req, res, "public", supertokens.convertToRecipeUserId(userId)); /* a new session has been created. * - an access & refresh token has been attached to the response's cookie @@ -54,6 +55,7 @@ app.post("/login", async (req, res) => { import Hapi from "@hapi/hapi"; // highlight-next-line import Session from "supertokens-node/recipe/session"; +import supertokens from "supertokens-node"; let server = Hapi.server({ port: 8000 }); @@ -66,7 +68,7 @@ server.route({ let userId = "userId"; // get from db // highlight-next-line - await Session.createNewSession(req, res, "public", userId); + await Session.createNewSession(req, res, "public", supertokens.convertToRecipeUserId(userId)); /* a new session has been created. * - an access & refresh token has been attached to the response's cookie @@ -83,6 +85,7 @@ server.route({ import Fastify from "fastify"; // highlight-next-line import Session from "supertokens-node/recipe/session"; +import supertokens from "supertokens-node"; let fastify = Fastify(); @@ -93,7 +96,7 @@ fastify.post("/login", async (req, res) => { let userId = "userId"; // get from db // highlight-next-line - await Session.createNewSession(req, res, "public", userId); + await Session.createNewSession(req, res, "public", supertokens.convertToRecipeUserId(userId)); /* a new session has been created. * - an access & refresh token has been attached to the response's cookie @@ -112,6 +115,7 @@ fastify.post("/login", async (req, res) => { import { middleware } from "supertokens-node/framework/awsLambda" import Session from "supertokens-node/recipe/session"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import supertokens from "supertokens-node"; async function login(awsEvent: SessionEvent) { // verify user's credentials... @@ -119,7 +123,7 @@ async function login(awsEvent: SessionEvent) { let userId = "userId"; // get from db // highlight-next-line - await Session.createNewSession(awsEvent, awsEvent, "public", userId); + await Session.createNewSession(awsEvent, awsEvent, "public", supertokens.convertToRecipeUserId(userId)); /* a new session has been created. * - an access & refresh token has been attached to the response's cookie @@ -142,6 +146,7 @@ exports.handler = middleware(login); import KoaRouter from "koa-router"; // highlight-next-line import Session from "supertokens-node/recipe/session"; +import supertokens from "supertokens-node"; let router = new KoaRouter(); @@ -152,7 +157,7 @@ router.post("/login", async (ctx, next) => { let userId = "userId"; // get from db // highlight-next-line - await Session.createNewSession(ctx, ctx, "public", userId); + await Session.createNewSession(ctx, ctx, "public", supertokens.convertToRecipeUserId(userId)); /* a new session has been created. * - an access & refresh token has been attached to the response's cookie @@ -171,6 +176,7 @@ import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; // highlight-next-line import Session from "supertokens-node/recipe/session"; +import supertokens from "supertokens-node"; class Login { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @@ -182,7 +188,7 @@ class Login { let userId = "userId"; // get from db // highlight-next-line - await Session.createNewSession(this.ctx, this.ctx, "public", userId); + await Session.createNewSession(this.ctx, this.ctx, "public", supertokens.convertToRecipeUserId(userId)); return { message: "User logged in!" }; } } @@ -196,6 +202,7 @@ import { superTokensNextWrapper } from 'supertokens-node/nextjs' // highlight-next-line import { createNewSession } from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; +import supertokens from "supertokens-node"; export default async function superTokens(req: SessionRequest, res: any) { // verify user's credentials... @@ -204,7 +211,7 @@ export default async function superTokens(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { // highlight-next-line - await createNewSession(req, res, "public", userId); + await createNewSession(req, res, "public", supertokens.convertToRecipeUserId(userId)); }, req, res @@ -222,6 +229,7 @@ export default async function superTokens(req: SessionRequest, res: any) { import { Controller, Post, Res, Req } from "@nestjs/common"; import type { Response, Request } from "express"; import { createNewSession } from "supertokens-node/recipe/session"; +import supertokens from "supertokens-node"; @Controller() export class ExampleController { @@ -230,7 +238,7 @@ export class ExampleController { async postLogin(@Req() req: Request, @Res() res: Response): Promise<{ message: string }> { let userId = "userId"; // get from db - await createNewSession(req, res, "public", userId); + await createNewSession(req, res, "public", supertokens.convertToRecipeUserId(userId)); /* a new session has been created. * - an access & refresh token has been attached to the response's cookie @@ -378,6 +386,7 @@ In the above version of the `createNewSession` function, we pass it the `request import express from "express"; // highlight-next-line import Session from "supertokens-node/recipe/session"; +import supertokens from "supertokens-node"; let app = express(); @@ -387,7 +396,7 @@ app.post("/login", async (req, res) => { let userId = "userId"; // get from db - let session = await Session.createNewSessionWithoutRequestResponse("public", userId); + let session = await Session.createNewSessionWithoutRequestResponse("public", supertokens.convertToRecipeUserId(userId)); // we can fetch the session tokens from the session object as follows: const tokens = session.getAllSessionTokensDangerously(); diff --git a/v2/session/testing/testing-with-postman.mdx b/v2/session/testing/testing-with-postman.mdx index 3b46626a6..c7b44fc57 100644 --- a/v2/session/testing/testing-with-postman.mdx +++ b/v2/session/testing/testing-with-postman.mdx @@ -36,12 +36,13 @@ For example, in your backend API you can use the `Session.createNewSession` func ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; +import supertokens from "supertokens-node"; let app = express(); // in you backend app.post("/create-new-session", async (req, res) => { - await Session.createNewSession(req, res, "public", "test-user", {}, {}) + await Session.createNewSession(req, res, "public", supertokens.convertToRecipeUserId("test-user"), {}, {}) res.send({ "message": "New user session created" }) From 153d0bfa597ca2f31fd300e2b8bbc474639c97f7 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 24 Aug 2023 18:37:14 +0530 Subject: [PATCH 14/81] more fixes --- .../advanced-customizations/user-context.mdx | 3 ++- .../implementing-deduplication.mdx | 11 ++++++++-- .../common-customizations/get-user-info.mdx | 22 +++++++++++++++---- .../handling-signinup-success.mdx | 4 ++-- .../backend-signup-override.mdx | 5 +++-- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/v2/thirdparty/advanced-customizations/user-context.mdx b/v2/thirdparty/advanced-customizations/user-context.mdx index 227461bdf..8953dea55 100644 --- a/v2/thirdparty/advanced-customizations/user-context.mdx +++ b/v2/thirdparty/advanced-customizations/user-context.mdx @@ -56,7 +56,8 @@ SuperTokens.init({ ...originalImplementation, signInUp: async function (input) { let resp = await originalImplementation.signInUp(input); - if (resp.status === "OK" && resp.createdNewUser) { + if (resp.status === "OK" && resp.createdNewRecipeUser && + resp.user.loginMethods.length === 1) { /* * This is called during the signInUp API for third party login, * but before calling the createNewSession function. diff --git a/v2/thirdparty/common-customizations/deduplication/implementing-deduplication.mdx b/v2/thirdparty/common-customizations/deduplication/implementing-deduplication.mdx index 7578276a6..8c22bbb2e 100644 --- a/v2/thirdparty/common-customizations/deduplication/implementing-deduplication.mdx +++ b/v2/thirdparty/common-customizations/deduplication/implementing-deduplication.mdx @@ -22,6 +22,7 @@ Add the following override logic to `ThirdParty.init` on the backend ```tsx import ThirdParty from "supertokens-node/recipe/thirdparty"; +import supertokens from "supertokens-node"; ThirdParty.init({ signInAndUpFeature: { @@ -32,12 +33,18 @@ ThirdParty.init({ return { ...originalImplementation, signInUp: async function (input) { - let existingUsers = await ThirdParty.getUsersByEmail(input.tenantId, input.email); + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.signInUp(input); } - if (existingUsers.find(i => i.thirdParty.id === input.thirdPartyId && i.thirdParty.userId === input.thirdPartyUserId)) { + if (existingUsers.find(u => + u.loginMethods.find(lM => lM.hasSameThirdPartyInfoAs({ + id: input.thirdPartyId, + userId: input.thirdPartyUserId + }) && lM.recipeId === "thirdparty") !== undefined)) { // this means we are trying to sign in with the same social login. So we allow it return originalImplementation.signInUp(input); } diff --git a/v2/thirdparty/common-customizations/get-user-info.mdx b/v2/thirdparty/common-customizations/get-user-info.mdx index 9b8f2952a..d638c3319 100644 --- a/v2/thirdparty/common-customizations/get-user-info.mdx +++ b/v2/thirdparty/common-customizations/get-user-info.mdx @@ -40,12 +40,26 @@ There are several ways to fetch information about a user: You can get a user's information on the backend using the `^{getUserByEmailNode}` and `getUserById` functions: ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; async function getUserInfo() { - // Note that usersInfo has type User[] - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - let usersInfo = await ^{recipeNameCapitalLetters}.^{getUserByEmailNode}("public", "test@example.com"); + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + email: "test@example.com" + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` diff --git a/v2/thirdparty/common-customizations/handling-signinup-success.mdx b/v2/thirdparty/common-customizations/handling-signinup-success.mdx index 1ea266900..7ba45833c 100644 --- a/v2/thirdparty/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdparty/common-customizations/handling-signinup-success.mdx @@ -117,13 +117,13 @@ SuperTokens.init({ // Post sign up response, we check if it was successful if (response.status === "OK") { - let { id, email } = response.user; + let { id, emails } = response.user; // This is the response from the OAuth 2 provider that contains their tokens or user info. let providerAccessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: Post sign up logic } else { // TODO: Post sign in logic diff --git a/v2/thirdparty/supabase-intergration/backend-signup-override.mdx b/v2/thirdparty/supabase-intergration/backend-signup-override.mdx index ab7d0921a..40a559cf3 100644 --- a/v2/thirdparty/supabase-intergration/backend-signup-override.mdx +++ b/v2/thirdparty/supabase-intergration/backend-signup-override.mdx @@ -53,7 +53,8 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.signInUpPOST(input); // check that there is no issue with sign up and that a new user is created - if (response.status === "OK" && response.createdNewUser) { + if (response.status === "OK" && response.createdNewRecipeUser && + response.user.loginMethods.length === 1) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); @@ -64,7 +65,7 @@ let backendConfig = (): TypeInput => { // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") - .insert({ email: response.user.email, user_id: response.user.id }); + .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { From 7b6707aa2b79cbd1456b13733bc7c28708a6aa0f Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 24 Aug 2023 19:01:53 +0530 Subject: [PATCH 15/81] more changes --- .../advanced-customizations/user-context.mdx | 26 +++++++------- .../advanced-customizations/user-context.mdx | 35 ++++++++----------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/v2/emailpassword/advanced-customizations/user-context.mdx b/v2/emailpassword/advanced-customizations/user-context.mdx index b24df9209..c28c1c401 100644 --- a/v2/emailpassword/advanced-customizations/user-context.mdx +++ b/v2/emailpassword/advanced-customizations/user-context.mdx @@ -26,7 +26,7 @@ In order to achieve this, all the API interface and recipe interface functions t ## Example use -Let's take the example mentioned above and implement it in the context of this recipe. First, we override the sign up APIs to add information into the context indicating that it's a sign up API call: +Let's take the example mentioned above and implement it in the context of this recipe. First, we override the sign up function to add information into the context indicating that it's a sign up API call: @@ -48,22 +48,22 @@ SuperTokens.init({ EmailPassword.init({ // highlight-start override: { - apis: (originalImplementation) => { + functions: (originalImplementation) => { return { ...originalImplementation, // override sign up using email / password - signUpPOST: async function (input) { - if (originalImplementation.signUpPOST === undefined) { - throw new Error("Should never come here"); + signUp: async function (input) { + let resp = await originalImplementation.signUp(input); + if (resp.status === "OK" && resp.user.loginMethods.length === 1) { + /* + * This is called during the sign up API for email password login, + * but before calling the createNewSession function. + * We override the recipe function as shown here, + * and then set the relevant context only if it's a new user. + */ + input.userContext.isSignUp = true; } - - // by default, the userContext object is {}, - // we change it to {isSignUp: true}, since this is the - // sign up API, and this will tell the createNewSession function - // (being called inside originalImplementation.signUpPOST) - // to not create a new session in case userContext.isSignUp === true - input.userContext.isSignUp = true; - return originalImplementation.signUpPOST(input); + return resp; }, } } diff --git a/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx b/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx index 2fe9a65db..ff6ffd947 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx @@ -26,7 +26,7 @@ In order to achieve this, all the API interface and recipe interface functions t ## Example use -Let's take the example mentioned above and implement it in the context of this recipe. First, we override the sign up APIs to add information into the context indicating that it's a sign up API call: +Let's take the example mentioned above and implement it in the context of this recipe. First, we override the sign up functions to add information into the context indicating that it's a sign up API call: @@ -34,7 +34,6 @@ Let's take the example mentioned above and implement it in the context of this r ```tsx import SuperTokens from "supertokens-node"; import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; -import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { @@ -49,31 +48,25 @@ SuperTokens.init({ ThirdPartyEmailPassword.init({ // highlight-start override: { - apis: (originalImplementation) => { + functions: (originalImplementation) => { return { ...originalImplementation, - // override sign up using email / password - emailPasswordSignUpPOST: async function (input) { - if (originalImplementation.emailPasswordSignUpPOST === undefined) { - throw new Error("Should never come here"); + emailPasswordSignUp: async function (input) { + let resp = await originalImplementation.emailPasswordSignUp(input); + if (resp.status === "OK" && resp.user.loginMethods.length === 1) { + /* + * This is called during the sign up API for email password login, + * but before calling the createNewSession function. + * We override the recipe function as shown here, + * and then set the relevant context only if it's a new user. + */ + input.userContext.isSignUp = true; } - - // by default, the userContext object is {}, - // we change it to {isSignUp: true}, since this is one of the - // sign up API, and this will tell the createNewSession function - // (being called inside originalImplementation.emailPasswordSignUpPOST) - // to not create a new session in case userContext.isSignUp === true - input.userContext.isSignUp = true; - return originalImplementation.emailPasswordSignUpPOST(input); + return resp; }, - } - }, - functions: (originalImplementation) => { - return { - ...originalImplementation, thirdPartySignInUp: async function (input) { let resp = await originalImplementation.thirdPartySignInUp(input); - if (resp.status === "OK" && resp.createdNewUser) { + if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1) { /* * This is called during the signInUp API for third party login, * but before calling the createNewSession function. From 1845b3f30c6d7ec2dc1a7f4f7600317c3707dfb8 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 25 Aug 2023 13:22:24 +0530 Subject: [PATCH 16/81] fixes more snippets --- .../change-email-post-login.mdx | 48 +++++++++++-------- .../implementing-deduplication.mdx | 15 ++++-- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx b/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx index 82c572c81..9425b3f5a 100644 --- a/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx @@ -110,6 +110,7 @@ import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpass import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import supertokens from "supertokens-node"; let app = express(); @@ -122,28 +123,31 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Validate the input email if (!isValidEmail(email)) { // TODO: handle invalid email error - return + return } // Check that the account to be updated is not a social account { let userId = session!.getUserId(); - let userAccount = await ThirdPartyEmailPassword.getUserById(userId!); - if (userAccount!.thirdParty !== undefined) { + let userAccount = await supertokens.getUser(userId!); + let loginMethod = userAccount?.loginMethods.find(lM => { + return lM.recipeUserId.getAsString() === session.getRecipeUserId().getAsString() + })! + if (loginMethod.recipeId === "thirdparty") { // TODO: handle error, cannot update email for third party users. - return + return } } - + // Update the email let resp = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: email, }); if (resp.status === "OK") { // TODO: send successfully updated email response - return + return } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO: handle error that email exists with another account. @@ -160,7 +164,6 @@ function isValidEmail(email: string) { ); return regexp.test(email); } - ``` @@ -426,6 +429,7 @@ import EmailVerification from "supertokens-node/recipe/emailverification"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import supertokens from "supertokens-node"; let app = express(); @@ -443,10 +447,13 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Check if the user's account is not a third party account { - let sessionId = session!.getUserId(); - let userAccount = await ThirdPartyEmailPassword.getUserById(sessionId!); - if (userAccount!.thirdParty !== undefined) { - // TODO handle error, cannot update password for third party users. + let userId = session!.getUserId(); + let userAccount = await supertokens.getUser(userId!); + let loginMethod = userAccount?.loginMethods.find(lM => { + return lM.recipeUserId.getAsString() === session.getRecipeUserId().getAsString() + })! + if (loginMethod.recipeId === "thirdparty") { + // TODO: handle error, cannot update email for third party users. return } } @@ -454,16 +461,18 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. - let isVerified = await EmailVerification.isEmailVerified(session.getUserId(), email); + let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. - let user = (await ThirdPartyEmailPassword.getUserById(session.getUserId()))!; + let user = (await supertokens.getUser(session.getUserId()))!; for (let i = 0; i < user?.tenantIds.length; i++) { // Since once user can be shared across many tenants, we need to check if // the email already exists in any of the tenants. - let usersWithEmail = await ThirdPartyEmailPassword.getUsersByEmail(user.tenantIds[i], email); + let usersWithEmail = await supertokens.listUsersByAccountInfo(user.tenantIds[i], { + email + }); for (let y = 0; y < usersWithEmail.length; y++) { if (usersWithEmail[y].id !== session.getUserId()) { // TODO handle error, email already exists with another user. @@ -476,18 +485,18 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr await EmailVerification.sendEmailVerificationEmail(session.getTenantId(), session.getUserId(), email); // TODO send successful response that email verification email sent. - return + return } // Since the email is verified, we try and do an update let resp = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: email, }); if (resp.status === "OK") { // TODO: send successful response that email has been updated - return + return } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO: handle error, email already exists for another account @@ -504,7 +513,6 @@ function isValidEmail(email: string) { ); return regexp.test(email); } - ``` @@ -800,7 +808,7 @@ SuperTokens.init({ // This will update the email of the user to the one // that was just marked as verified by the token. await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: response.user.id, + recipeUserId: response.user.recipeUserId, email: response.user.email, }); } diff --git a/v2/thirdpartyemailpassword/common-customizations/deduplication/implementing-deduplication.mdx b/v2/thirdpartyemailpassword/common-customizations/deduplication/implementing-deduplication.mdx index e7e5a73cc..b49024604 100644 --- a/v2/thirdpartyemailpassword/common-customizations/deduplication/implementing-deduplication.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/deduplication/implementing-deduplication.mdx @@ -22,6 +22,7 @@ Add the following override logic to `ThirdPartyEmailPassword.init` on the backen ```tsx import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; +import supertokens from "supertokens-node"; ThirdPartyEmailPassword.init({ override: { @@ -29,7 +30,9 @@ ThirdPartyEmailPassword.init({ return { ...originalImplementation, emailPasswordSignUp: async function (input) { - let existingUsers = await ThirdPartyEmailPassword.getUsersByEmail(input.tenantId, input.email); + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.emailPasswordSignUp(input); @@ -39,12 +42,18 @@ ThirdPartyEmailPassword.init({ } }, thirdPartySignInUp: async function (input) { - let existingUsers = await ThirdPartyEmailPassword.getUsersByEmail(input.tenantId, input.email); + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.thirdPartySignInUp(input); } - if (existingUsers.find(i => i.thirdParty !== undefined && i.thirdParty.id === input.thirdPartyId && i.thirdParty.userId === input.thirdPartyUserId)) { + if (existingUsers.find(u => + u.loginMethods.find(lM => lM.hasSameThirdPartyInfoAs({ + id: input.thirdPartyId, + userId: input.thirdPartyUserId + }) && lM.recipeId === "thirdparty") !== undefined)) { // this means we are trying to sign in with the same social login. So we allow it return originalImplementation.thirdPartySignInUp(input); } From ec99d3c4b74a0d5e687f94469a2e0a389d94c4d4 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 25 Aug 2023 13:39:03 +0530 Subject: [PATCH 17/81] more info --- .../disable-sign-up/thirdparty-changes.mdx | 5 ++++- .../disable-sign-up/thirdparty-changes.mdx | 5 ++++- .../disable-sign-up/thirdparty-changes.mdx | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/v2/thirdparty/common-customizations/disable-sign-up/thirdparty-changes.mdx b/v2/thirdparty/common-customizations/disable-sign-up/thirdparty-changes.mdx index 5bba47789..1d1c50528 100644 --- a/v2/thirdparty/common-customizations/disable-sign-up/thirdparty-changes.mdx +++ b/v2/thirdparty/common-customizations/disable-sign-up/thirdparty-changes.mdx @@ -129,6 +129,7 @@ After that, we override the `signInUpPOST` API and the `signInUp` recipe functio ```tsx import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; declare let isEmailAllowed: (email: string) => Promise // typecheck-only, removed from output ^{recipeNameCapitalLetters}.init({ @@ -138,7 +139,9 @@ declare let isEmailAllowed: (email: string) => Promise // typecheck-onl return { ...originalImplementation, ^{nodeThirdPartySignInUp}: async function (input) { - let existingUsers = await ^{recipeNameCapitalLetters}.getUsersByEmail(input.tenantId, input.email); + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); if (existingUsers.length === 0) { // this means that the email is new and is a sign up if (!(await isEmailAllowed(input.email))) { diff --git a/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/thirdparty-changes.mdx b/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/thirdparty-changes.mdx index 957acbf08..1bd7c0010 100644 --- a/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/thirdparty-changes.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/thirdparty-changes.mdx @@ -130,6 +130,7 @@ After that, we override the `thirdPartySignInUpPOST` API and the `thirdPartySign ```tsx import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; declare let isEmailAllowed: (email: string) => Promise // typecheck-only, removed from output ^{recipeNameCapitalLetters}.init({ @@ -139,7 +140,9 @@ declare let isEmailAllowed: (email: string) => Promise // typecheck-onl return { ...originalImplementation, ^{nodeThirdPartySignInUp}: async function (input) { - let existingUsers = await ^{recipeNameCapitalLetters}.getUsersByEmail(input.tenantId, input.email); + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); if (existingUsers.length === 0) { // this means that the email is new and is a sign up if (!(await isEmailAllowed(input.email))) { diff --git a/v2/thirdpartypasswordless/common-customizations/disable-sign-up/thirdparty-changes.mdx b/v2/thirdpartypasswordless/common-customizations/disable-sign-up/thirdparty-changes.mdx index 957acbf08..1bd7c0010 100644 --- a/v2/thirdpartypasswordless/common-customizations/disable-sign-up/thirdparty-changes.mdx +++ b/v2/thirdpartypasswordless/common-customizations/disable-sign-up/thirdparty-changes.mdx @@ -130,6 +130,7 @@ After that, we override the `thirdPartySignInUpPOST` API and the `thirdPartySign ```tsx import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; declare let isEmailAllowed: (email: string) => Promise // typecheck-only, removed from output ^{recipeNameCapitalLetters}.init({ @@ -139,7 +140,9 @@ declare let isEmailAllowed: (email: string) => Promise // typecheck-onl return { ...originalImplementation, ^{nodeThirdPartySignInUp}: async function (input) { - let existingUsers = await ^{recipeNameCapitalLetters}.getUsersByEmail(input.tenantId, input.email); + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); if (existingUsers.length === 0) { // this means that the email is new and is a sign up if (!(await isEmailAllowed(input.email))) { From 0be3ffda6beb06e40684a19e5b44a53bfa36feb9 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 25 Aug 2023 14:10:56 +0530 Subject: [PATCH 18/81] more changes to code --- .../common-customizations/get-user-info.mdx | 2 +- .../common-customizations/get-user-info.mdx | 24 +++++++++++++++---- .../handling-signinup-success.mdx | 4 ++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/v2/thirdparty/common-customizations/get-user-info.mdx b/v2/thirdparty/common-customizations/get-user-info.mdx index d638c3319..a866fce7c 100644 --- a/v2/thirdparty/common-customizations/get-user-info.mdx +++ b/v2/thirdparty/common-customizations/get-user-info.mdx @@ -37,7 +37,7 @@ There are several ways to fetch information about a user: -You can get a user's information on the backend using the `^{getUserByEmailNode}` and `getUserById` functions: +You can get a user's information on the backend using the `listUsersByAccountInfo` functions: ```tsx import supertokens from "supertokens-node"; diff --git a/v2/thirdpartyemailpassword/common-customizations/get-user-info.mdx b/v2/thirdpartyemailpassword/common-customizations/get-user-info.mdx index 309595d3e..3480e85e9 100644 --- a/v2/thirdpartyemailpassword/common-customizations/get-user-info.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/get-user-info.mdx @@ -38,15 +38,29 @@ There are several ways to fetch information about a user: -You can get a user's information on the backend using the `^{getUserByEmailNode}` and `getUserById` functions: +You can get a user's information on the backend using the `listUsersByAccountInfo` functions: ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; async function getUserInfo() { - // Note that usersInfo has type User[] - // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - let usersInfo = await ^{recipeNameCapitalLetters}.^{getUserByEmailNode}("public", "test@example.com"); + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + email: "test@example.com" + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` diff --git a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx index 897692b6a..21c701a17 100644 --- a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx @@ -152,7 +152,7 @@ SuperTokens.init({ let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: some post sign up logic } else { // TODO: some post sign in logic @@ -415,7 +415,7 @@ SuperTokens.init({ // Post sign up response, we check if it was successful if (response.status === "OK") { - let { id, email } = response.user; + let { id, emails } = response.user; // TODO: sign up successful // here we fetch a custom form field for the user's name. From 1b0e46ff9f3f01e567f005df636d66d72352802a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 25 Aug 2023 14:32:39 +0530 Subject: [PATCH 19/81] more fixes --- .../ep-migration-without-password-hash.mdx | 68 ++++++++----------- .../backend-signup-override.mdx | 6 +- .../advanced-customizations/user-context.mdx | 4 +- 3 files changed, 33 insertions(+), 45 deletions(-) diff --git a/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx b/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx index 9492adffb..8c4ea6071 100644 --- a/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx +++ b/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx @@ -188,16 +188,12 @@ ThirdPartyEmailPassword.init({ ...originalImplementation, emailPasswordSignIn: async function (input) { // Check if an email-password user with the input email exists in SuperTokens - let superTokensUsers = await ThirdPartyEmailPassword.getUsersByEmail(input.tenantId, input.email, input.userContext); - let emailPasswordUser = undefined; - - for (let i = 0; i < superTokensUsers.length; i++) { - // if the thirdParty field in the user object is undefined, then the user is an EmailPassword account. - if (superTokensUsers[i].thirdParty === undefined) { - emailPasswordUser = superTokensUsers[i] - break; - } - } + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }, undefined, input.userContext); + let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { + return u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "emailpassword") !== undefined; + }) if (emailPasswordUser === undefined) { // EmailPassword user with the input email does not exist in SuperTokens @@ -224,7 +220,7 @@ ThirdPartyEmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.user.id, input.email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, input.email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email @@ -503,17 +499,12 @@ ThirdPartyEmailPassword.init({ let email = input.formFields.find(i => i.id === "email")!.value; // check if user exists in SuperTokens - let superTokensUsers = await ThirdPartyEmailPassword.getUsersByEmail(email, input.userContext); - - let emailPasswordUser = undefined; - - for (let i = 0; i < superTokensUsers.length; i++) { - // if the thirdParty field in the user object is undefined, then the user is an EmailPassword account. - if (superTokensUsers[i].thirdParty === undefined) { - emailPasswordUser = superTokensUsers[i] - break; - } - } + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { + email + }, undefined, input.userContext); + let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { + return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; + }) if (emailPasswordUser === undefined) { // User does not exist in SuperTokens @@ -535,7 +526,7 @@ ThirdPartyEmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserData.isEmailVerified) { // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, legacyUserData.user_id, email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signupResponse.recipeUserId, email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email @@ -544,7 +535,7 @@ ThirdPartyEmailPassword.init({ } // We also need to identify that the user is using a temporary password. We do through the userMetadata recipe - UserMetadata.updateUserMetadata(legacyUserData.user_id,{isUsingTemporaryPassword: true}) + UserMetadata.updateUserMetadata(legacyUserData.user_id, { isUsingTemporaryPassword: true }) } else { throw new Error("Should never come here") } @@ -858,10 +849,10 @@ ThirdPartyEmailPassword.init({ passwordResetPOST: async function (input) { let response = await originalImplementation.passwordResetPOST!(input); if (response.status === "OK") { - let usermetadata = await UserMetadata.getUserMetadata(response.userId!, input.userContext) + let usermetadata = await UserMetadata.getUserMetadata(response.user.id, input.userContext) if (usermetadata.status === "OK" && usermetadata.metadata.isUsingTemporaryPassword) { // Since the password reset we can remove the isUsingTemporaryPassword flag - await UserMetadata.updateUserMetadata(response.userId!, { isUsingTemporaryPassword: null }) + await UserMetadata.updateUserMetadata(response.user.id, { isUsingTemporaryPassword: null }) } } return response @@ -870,7 +861,6 @@ ThirdPartyEmailPassword.init({ } } }) - ``` @@ -1029,16 +1019,12 @@ ThirdPartyEmailPassword.init({ ...originalImplementation, emailPasswordSignIn: async function (input) { // Check if an email-password user with the input email exists in SuperTokens - let superTokensUsers = await this.getUsersByEmail({ email: input.email, tenantId: input.tenantId, userContext: input.userContext }); - let emailPasswordUser = undefined; - - for (let i = 0; i < superTokensUsers.length; i++) { - // if the thirdParty field in the user object is undefined, then the user is an EmailPassword account. - if (superTokensUsers[i].thirdParty === undefined) { - emailPasswordUser = superTokensUsers[i] - break; - } - } + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }, undefined, input.userContext); + let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { + return u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "emailpassword") !== undefined; + }) if (emailPasswordUser === undefined) { // EmailPassword user with the input email does not exist in SuperTokens @@ -1065,7 +1051,7 @@ ThirdPartyEmailPassword.init({ // We will also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.user.id, input.email, input.userContext); + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, input.email, input.userContext); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email @@ -1082,9 +1068,10 @@ ThirdPartyEmailPassword.init({ // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(input.email, input.password); if (legacyUserInfo) { + let loginMethod = emailPasswordUser.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(input.email)); // Update the user's password with the correct password ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: emailPasswordUser.id, + recipeUserId: loginMethod!.recipeUserId, password: input.password, applyPasswordPolicy: false }) @@ -1094,7 +1081,8 @@ ThirdPartyEmailPassword.init({ return { status: "OK", - user: emailPasswordUser + user: emailPasswordUser, + recipeUserId: loginMethod!.recipeUserId } } return { diff --git a/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx b/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx index 44f7f4a44..ad0e759b8 100644 --- a/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx +++ b/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx @@ -63,7 +63,7 @@ let backendConfig = (): TypeInput => { // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") - .insert({ email: response.user.email, user_id: response.user.id }); + .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { @@ -139,7 +139,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.thirdPartySignInUpPOST(input); // check that there is no issue with sign up and that a new user is created - if (response.status === "OK" && response.createdNewUser) { + if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); @@ -150,7 +150,7 @@ let backendConfig = (): TypeInput => { // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") - .insert({ email: response.user.email, user_id: response.user.id }); + .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { diff --git a/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx b/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx index 1903b8893..c07630807 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx @@ -55,7 +55,7 @@ SuperTokens.init({ ...originalImplementation, consumeCode: async function (input) { let resp = await originalImplementation.consumeCode(input); - if (resp.status === "OK" && resp.createdNewUser) { + if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1) { /* * This is called during the consume code API, * but before calling the createNewSession function. @@ -79,7 +79,7 @@ SuperTokens.init({ thirdPartySignInUp: async function (input) { let resp = await originalImplementation.thirdPartySignInUp(input); - if (resp.status === "OK" && resp.createdNewUser) { + if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1) { /* * This is called during the signInUp API for third party login, * but before calling the createNewSession function. From 56aa540bccb6e6b500aad64673a56def7dddb11d Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 25 Aug 2023 15:53:20 +0530 Subject: [PATCH 20/81] more fixes --- .../multi-tenancy/common-domain-login.mdx | 2 +- .../multi-tenancy/sub-domain-login.mdx | 2 +- .../multi-tenancy/common-domain-login.mdx | 2 +- .../multi-tenancy/sub-domain-login.mdx | 2 +- .../multi-tenancy/common-domain-login.mdx | 2 +- .../multi-tenancy/sub-domain-login.mdx | 2 +- .../multi-tenancy/common-domain-login.mdx | 2 +- .../multi-tenancy/sub-domain-login.mdx | 2 +- .../common-customizations/change-email.mdx | 50 +++++++++++-------- .../multi-tenancy/common-domain-login.mdx | 2 +- .../multi-tenancy/sub-domain-login.mdx | 2 +- 11 files changed, 39 insertions(+), 31 deletions(-) diff --git a/v2/emailpassword/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/emailpassword/common-customizations/multi-tenancy/common-domain-login.mdx index 3c01bbfca..6d8a8d318 100644 --- a/v2/emailpassword/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/emailpassword/common-customizations/multi-tenancy/common-domain-login.mdx @@ -46,7 +46,7 @@ npx create-supertokens-app@latest --recipe=multitenancy ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/emailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/emailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx index 58d252b8a..bbca9ff7f 100644 --- a/v2/emailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/emailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -40,7 +40,7 @@ An example app for this setup with our **pre built UI** can be found on [our git ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/passwordless/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/passwordless/common-customizations/multi-tenancy/common-domain-login.mdx index 78f3997f3..2593c2978 100644 --- a/v2/passwordless/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/passwordless/common-customizations/multi-tenancy/common-domain-login.mdx @@ -46,7 +46,7 @@ npx create-supertokens-app@latest --recipe=multitenancy ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/passwordless/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/passwordless/common-customizations/multi-tenancy/sub-domain-login.mdx index 2086a6c08..673e21dd6 100644 --- a/v2/passwordless/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/passwordless/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -40,7 +40,7 @@ An example app for this setup with our **pre built UI** can be found on [our git ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/thirdparty/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/thirdparty/common-customizations/multi-tenancy/common-domain-login.mdx index 5582084e7..aec347cdf 100644 --- a/v2/thirdparty/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/thirdparty/common-customizations/multi-tenancy/common-domain-login.mdx @@ -46,7 +46,7 @@ npx create-supertokens-app@latest --recipe=multitenancy ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/thirdparty/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/thirdparty/common-customizations/multi-tenancy/sub-domain-login.mdx index 87d81e3a4..e195ae5cb 100644 --- a/v2/thirdparty/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/thirdparty/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -40,7 +40,7 @@ An example app for this setup with our **pre built UI** can be found on [our git ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/common-domain-login.mdx index 342be8ce1..e39d862f7 100644 --- a/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/common-domain-login.mdx @@ -46,7 +46,7 @@ npx create-supertokens-app@latest --recipe=multitenancy ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx index 4aae21271..2120f7393 100644 --- a/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -40,7 +40,7 @@ An example app for this setup with our **pre built UI** can be found on [our git ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/thirdpartypasswordless/common-customizations/change-email.mdx b/v2/thirdpartypasswordless/common-customizations/change-email.mdx index f0b85e477..469f4812a 100644 --- a/v2/thirdpartypasswordless/common-customizations/change-email.mdx +++ b/v2/thirdpartypasswordless/common-customizations/change-email.mdx @@ -110,6 +110,7 @@ import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordle import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import supertokens from "supertokens-node"; let app = express(); @@ -122,7 +123,7 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Validate the input email if (!isValidEmail(email)) { // TODO: handle invalid email error - return + return } @@ -130,23 +131,26 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Check that the account to be updated is not a social account { let userId = session!.getUserId(); - let userAccount = await ThirdPartyPasswordless.getUserById(userId!); + let userAccount = await supertokens.getUser(userId!); + let loginMethod = userAccount?.loginMethods.find(lM => { + return lM.recipeUserId.getAsString() === session.getRecipeUserId().getAsString() + })! - if ("thirdParty" in userAccount!) { + if (loginMethod.recipeId === "thirdparty") { // TODO: handle error, cannot update email for third party users. - return + return } } - + // Update the email let resp = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: email, }); if (resp.status === "OK") { // TODO: send successfully updated email response - return + return } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO: handle error that email exists with another account. @@ -163,7 +167,6 @@ function isValidEmail(email: string) { ); return regexp.test(email); } - ``` @@ -427,6 +430,7 @@ import EmailVerification from "supertokens-node/recipe/emailverification"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import supertokens from "supertokens-node"; let app = express(); @@ -444,10 +448,13 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Check if the user's account is not a third party account { - let sessionId = session!.getUserId(); - let userAccount = await ThirdPartyPasswordless.getUserById(sessionId!); - if ( "thirdParty" in userAccount!) { - // TODO handle error, cannot update password for third party users. + let userId = session!.getUserId(); + let userAccount = await supertokens.getUser(userId); + let loginMethod = userAccount?.loginMethods.find(lM => { + return lM.recipeUserId.getAsString() === session.getRecipeUserId().getAsString() + })! + if (loginMethod.recipeId === "thirdparty") { + // TODO: handle error, cannot update email for third party users. return } } @@ -455,16 +462,18 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. - let isVerified = await EmailVerification.isEmailVerified(session.getUserId(), email); + let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. - let user = (await ThirdPartyPasswordless.getUserById(session.getUserId()))!; + let user = (await supertokens.getUser(session.getUserId()))!; for (let i = 0; i < user?.tenantIds.length; i++) { // Since once user can be shared across many tenants, we need to check if // the email already exists in any of the tenants. - let usersWithEmail = await ThirdPartyPasswordless.getUsersByEmail(user.tenantIds[i], email); + let usersWithEmail = await supertokens.listUsersByAccountInfo(user.tenantIds[i], { + email + }); for (let y = 0; y < usersWithEmail.length; y++) { if (usersWithEmail[y].id !== session.getUserId()) { // TODO handle error, email already exists with another user. @@ -472,23 +481,23 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr } } } - + // Now we create and send the email verification link to the user for the new email. await EmailVerification.sendEmailVerificationEmail(session.getTenantId(), session.getUserId(), email); // TODO send successful response that email verification email sent. - return + return } // Since the email is verified, we try and do an update let resp = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: session.getUserId(), + recipeUserId: session.getRecipeUserId(), email: email, }); if (resp.status === "OK") { // TODO: send successful response that email has been updated - return + return } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO: handle error, email already exists for another account @@ -505,7 +514,6 @@ function isValidEmail(email: string) { ); return regexp.test(email); } - ``` @@ -753,7 +761,7 @@ SuperTokens.init({ // This will update the email of the user to the one // that was just marked as verified by the token. await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: response.user.id, + recipeUserId: response.user.recipeUserId, email: response.user.email, }); } diff --git a/v2/thirdpartypasswordless/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/thirdpartypasswordless/common-customizations/multi-tenancy/common-domain-login.mdx index 449fbe867..6d8317724 100644 --- a/v2/thirdpartypasswordless/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/thirdpartypasswordless/common-customizations/multi-tenancy/common-domain-login.mdx @@ -46,7 +46,7 @@ npx create-supertokens-app@latest --recipe=multitenancy ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). diff --git a/v2/thirdpartypasswordless/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/thirdpartypasswordless/common-customizations/multi-tenancy/sub-domain-login.mdx index fbc0f73cf..0b24e21ba 100644 --- a/v2/thirdpartypasswordless/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/thirdpartypasswordless/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -40,7 +40,7 @@ An example app for this setup with our **pre built UI** can be found on [our git ## Step 1: Creating a new tenant -Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config). +Whenever you want to onboard a new customer, you should [create and configure a tenantId for them in the SuperTokens core](./new-tenant-config.mdx). From 921292a5f5ed32465f7bef22b422e672b0817919 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 25 Aug 2023 18:54:18 +0530 Subject: [PATCH 21/81] fixes more snippets --- .../implementing-deduplication.mdx | 18 ++++++-- .../passwordless-via-allow-list.mdx | 15 ++++--- .../passwordless-via-invite-link.mdx | 14 ++++--- .../common-customizations/get-user-info.mdx | 42 ++++++++++++++++--- .../handling-signinup-success.mdx | 10 ++--- .../backend-signup-override.mdx | 25 ++++++----- 6 files changed, 85 insertions(+), 39 deletions(-) diff --git a/v2/thirdpartypasswordless/common-customizations/deduplication/implementing-deduplication.mdx b/v2/thirdpartypasswordless/common-customizations/deduplication/implementing-deduplication.mdx index 3c8aff86d..da5a095ce 100644 --- a/v2/thirdpartypasswordless/common-customizations/deduplication/implementing-deduplication.mdx +++ b/v2/thirdpartypasswordless/common-customizations/deduplication/implementing-deduplication.mdx @@ -22,6 +22,7 @@ Add the following override logic to `ThirdPartyPasswordless.init` on the backend ```tsx import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless"; +import supertokens from "supertokens-node"; ThirdPartyPasswordless.init({ contactMethod: "EMAIL", // typecheck-only, removed from output @@ -31,12 +32,18 @@ ThirdPartyPasswordless.init({ return { ...originalImplementation, thirdPartySignInUp: async function (input) { - let existingUsers = await ThirdPartyPasswordless.getUsersByEmail(input.tenantId, input.email); + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.thirdPartySignInUp(input); } - if (existingUsers.find(i => "email" in i && "thirdParty" in i && i.thirdParty.id === input.thirdPartyId && i.thirdParty.userId === input.thirdPartyUserId)) { + if (existingUsers.find(u => + u.loginMethods.find(lM => lM.hasSameThirdPartyInfoAs({ + id: input.thirdPartyId, + userId: input.thirdPartyUserId + }) && lM.recipeId === "thirdparty") !== undefined)) { // this means we are trying to sign in with the same social login. So we allow it return originalImplementation.thirdPartySignInUp(input); } @@ -50,12 +57,15 @@ ThirdPartyPasswordless.init({ ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { - let existingUsers = await ThirdPartyPasswordless.getUsersByEmail(input.tenantId, input.email); + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.createCodePOST!(input); } - if (existingUsers.find(i => "email" in i && !("thirdParty" in i))) { + if (existingUsers.find(u => + u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined)) { // this means that the existing user is a passwordless login user. So we allow it return originalImplementation.createCodePOST!(input); } diff --git a/v2/thirdpartypasswordless/common-customizations/disable-sign-up/passwordless-via-allow-list.mdx b/v2/thirdpartypasswordless/common-customizations/disable-sign-up/passwordless-via-allow-list.mdx index fc522e123..267668932 100644 --- a/v2/thirdpartypasswordless/common-customizations/disable-sign-up/passwordless-via-allow-list.mdx +++ b/v2/thirdpartypasswordless/common-customizations/disable-sign-up/passwordless-via-allow-list.mdx @@ -193,6 +193,8 @@ After that, we override the `createCodePOST` API to check if the input email / p ```tsx import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; + declare let isEmailAllowed: (email: string) => Promise // typecheck-only, removed from output declare let isPhoneNumberAllowed: (email: string) => Promise // typecheck-only, removed from output @@ -204,8 +206,10 @@ declare let isPhoneNumberAllowed: (email: string) => Promise // typeche ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { - let existingUser = await ^{recipeNameCapitalLetters}.getUsersByEmail(input.tenantId, input.email); - if (existingUser.length === 0) { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); + if (existingUsers.length === 0) { // this is sign up attempt if (!(await isEmailAllowed(input.email))) { return { @@ -215,11 +219,10 @@ declare let isPhoneNumberAllowed: (email: string) => Promise // typeche } } } else { - let existingUser = await ^{recipeNameCapitalLetters}.getUserByPhoneNumber({ - phoneNumber: input.phoneNumber, - tenantId: input.tenantId + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + phoneNumber: input.phoneNumber }); - if (existingUser === undefined) { + if (existingUsers.length === 0) { // this is sign up attempt if (!(await isPhoneNumberAllowed(input.phoneNumber))) { return { diff --git a/v2/thirdpartypasswordless/common-customizations/disable-sign-up/passwordless-via-invite-link.mdx b/v2/thirdpartypasswordless/common-customizations/disable-sign-up/passwordless-via-invite-link.mdx index 0abbcfe30..0f2124a65 100644 --- a/v2/thirdpartypasswordless/common-customizations/disable-sign-up/passwordless-via-invite-link.mdx +++ b/v2/thirdpartypasswordless/common-customizations/disable-sign-up/passwordless-via-invite-link.mdx @@ -28,6 +28,7 @@ We start by overriding the `createCodePOST` API to check if the input email / ph ```tsx import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; ^{recipeNameCapitalLetters}.init({ ^{nodeRecipeInitDefault} @@ -37,8 +38,10 @@ import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRec ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { - let existingUser = await ^{recipeNameCapitalLetters}.getUsersByEmail(input.tenantId, input.email); - if (existingUser.length === 0) { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email + }); + if (existingUsers.length === 0) { // this is sign up attempt return { status: "GENERAL_ERROR", @@ -46,11 +49,10 @@ import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRec } } } else { - let existingUser = await ^{recipeNameCapitalLetters}.getUserByPhoneNumber({ - phoneNumber: input.phoneNumber, - tenantId: input.tenantId + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + phoneNumber: input.phoneNumber }); - if (existingUser === undefined) { + if (existingUsers.length === 0) { // this is sign up attempt return { status: "GENERAL_ERROR", diff --git a/v2/thirdpartypasswordless/common-customizations/get-user-info.mdx b/v2/thirdpartypasswordless/common-customizations/get-user-info.mdx index 9a9441fda..7b4a1ca70 100644 --- a/v2/thirdpartypasswordless/common-customizations/get-user-info.mdx +++ b/v2/thirdpartypasswordless/common-customizations/get-user-info.mdx @@ -36,14 +36,30 @@ There are several ways to fetch information about a user: -You can get a user's information on the backend using the `^{getUserByEmailNode}`, `getUserByPhoneNumber` and `getUserById` functions: +You can get a user's information on the backend using the `listUsersByAccountInfo` function: ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node"; async function handler() { - const userInfo = await ^{recipeNameCapitalLetters}.^{getUserByEmailNode}("public", "test@example.com"); + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + email: "test@example.com" + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` @@ -114,10 +130,26 @@ Notice that we pass in the `"public"` tenantId to the function call above. This ```tsx -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import supertokens from "supertokens-node" async function handler() { - const userInfo = await ^{recipeNameCapitalLetters}.getUserByPhoneNumber({tenantId: "public", phoneNumber: "+1234567891"}); + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + phoneNumber: "+1234567891" + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` diff --git a/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx b/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx index 7ca5f292f..95a56ba90 100644 --- a/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx @@ -124,13 +124,13 @@ SuperTokens.init({ let response = await originalImplementation.thirdPartySignInUp(input); if (response.status === "OK") { - let { id, email } = response.user; + let { id, emails } = response.user; // This is the response from the OAuth 2 provider that contains their tokens or user info. let providerAccessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: Post sign up logic } else { // TODO: Post sign in logic @@ -147,12 +147,12 @@ SuperTokens.init({ // Post sign up response, we check if it was successful if (response.status === "OK") { if ("phoneNumber" in response.user) { - const { id, phoneNumber } = response.user; + const { id, phoneNumbers } = response.user; } else { - const { id, email } = response.user; + const { id, emails } = response.user; } - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: post sign up logic } else { // TODO: post sign in logic diff --git a/v2/thirdpartypasswordless/supabase-intergration/backend-signup-override.mdx b/v2/thirdpartypasswordless/supabase-intergration/backend-signup-override.mdx index e14b14383..a4db5a0a1 100644 --- a/v2/thirdpartypasswordless/supabase-intergration/backend-signup-override.mdx +++ b/v2/thirdpartypasswordless/supabase-intergration/backend-signup-override.mdx @@ -13,7 +13,7 @@ In our example app there are two ways for signing up a user. Email-Password and ```ts // config/backendConfig.ts -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless"; import SessionNode from "supertokens-node/recipe/session"; import { TypeInput, AppInfo } from "supertokens-node/types"; @@ -34,7 +34,7 @@ let backendConfig = (): TypeInput => { }, appInfo, recipeList: [ - ^{recipeNameCapitalLetters}.init({ + ThirdPartyPasswordless.init({ flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", contactMethod: "EMAIL", override: { @@ -49,8 +49,8 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.consumeCodePOST(input); - if (response.status === "OK" && response.createdNewUser) { - + if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); @@ -60,7 +60,7 @@ let backendConfig = (): TypeInput => { // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") - .insert({ email: response.user.email, user_id: response.user.id }); + .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { throw error; @@ -73,12 +73,11 @@ let backendConfig = (): TypeInput => { } } }), - SessionNode.init({/*...*/}), + SessionNode.init({/*...*/ }), ], isInServerlessEnv: true, }; }; - ``` We will be changing the Passwordless flow by overriding the `consumeCodePOST` api. When a user signs up we will retrieve the `supabase_token` from the user's `accessTokenPayload`(this was added in the previous step where we changed the `createNewSession` function) and use it to query Supabase to insert the new user's information. @@ -89,7 +88,7 @@ We will be changing the Passwordless flow by overriding the `consumeCodePOST` ap ```ts // config/backendConfig.ts -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; +import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless"; import SessionNode from "supertokens-node/recipe/session"; import { TypeInput, AppInfo } from "supertokens-node/types"; @@ -110,7 +109,7 @@ let backendConfig = (): TypeInput => { }, appInfo, recipeList: [ - ^{recipeNameCapitalLetters}.init({ + ThirdPartyPasswordless.init({ contactMethod: "EMAIL", flowType: "MAGIC_LINK", providers: [/*...*/], @@ -126,7 +125,7 @@ let backendConfig = (): TypeInput => { } /*Look at the previous section*/ - + return await originalImplementation.consumeCodePOST(input) }, @@ -140,7 +139,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.thirdPartySignInUpPOST(input); // check that there is no issue with sign up and that a new user is created - if (response.status === "OK" && response.createdNewUser) { + if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); @@ -151,7 +150,7 @@ let backendConfig = (): TypeInput => { // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") - .insert({ email: response.user.email, user_id: response.user.id }); + .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { @@ -165,7 +164,7 @@ let backendConfig = (): TypeInput => { }, }, }), - SessionNode.init({/*...*/}), + SessionNode.init({/*...*/ }), ], isInServerlessEnv: true, }; From f99842cff9a05e2a4b0db10a3f36937c0e1827fa Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 26 Aug 2023 16:58:52 +0530 Subject: [PATCH 22/81] adds proper check for sign up --- .../common-customizations/handling-signup-success.mdx | 4 ++-- .../supabase-intergration/backend-signup-override.mdx | 2 +- .../common-customizations/handling-signinup-success.mdx | 4 ++-- .../supabase-intergration/backend-signup-override.mdx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/v2/emailpassword/common-customizations/handling-signup-success.mdx b/v2/emailpassword/common-customizations/handling-signup-success.mdx index 66299acbc..19b53ec9a 100644 --- a/v2/emailpassword/common-customizations/handling-signup-success.mdx +++ b/v2/emailpassword/common-customizations/handling-signup-success.mdx @@ -116,7 +116,7 @@ SuperTokens.init({ let response = await originalImplementation.signUp(input); // Post sign up response, we check if it was successful - if (response.status === "OK") { + if (response.status === "OK" && response.user.loginMethods.length === 1) { /** * * response.user contains the following info: @@ -293,7 +293,7 @@ SuperTokens.init({ let response = await originalImplementation.signUpPOST!(input); // Post sign up response, we check if it was successful - if (response.status === "OK") { + if (response.status === "OK" && response.user.loginMethods.length === 1) { /** * * response.user contains the following info: diff --git a/v2/emailpassword/supabase-intergration/backend-signup-override.mdx b/v2/emailpassword/supabase-intergration/backend-signup-override.mdx index 1440bb6d0..344a1363e 100644 --- a/v2/emailpassword/supabase-intergration/backend-signup-override.mdx +++ b/v2/emailpassword/supabase-intergration/backend-signup-override.mdx @@ -50,7 +50,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.signUpPOST(input); - if (response.status === "OK") { + if (response.status === "OK" && response.user.loginMethods.length === 1) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); diff --git a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx index 21c701a17..4570fd715 100644 --- a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx @@ -120,7 +120,7 @@ SuperTokens.init({ let response = await originalImplementation.emailPasswordSignUp(input); - if (response.status === "OK") { + if (response.status === "OK" && response.user.loginMethods.length === 1) { // TODO: some post sign up logic } @@ -414,7 +414,7 @@ SuperTokens.init({ let response = await originalImplementation.emailPasswordSignUpPOST!(input); // Post sign up response, we check if it was successful - if (response.status === "OK") { + if (response.status === "OK" && response.user.loginMethods.length === 1) { let { id, emails } = response.user; // TODO: sign up successful diff --git a/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx b/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx index ad0e759b8..4f69b308d 100644 --- a/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx +++ b/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx @@ -52,7 +52,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.emailPasswordSignUpPOST(input); - if (response.status === "OK") { + if (response.status === "OK" && response.user.loginMethods.length === 1) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); From 65a46b4663806e65382ad47bc67a38f07fa2932c Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 28 Aug 2023 11:25:09 +0530 Subject: [PATCH 23/81] adds new paghe --- v2/emailpassword/sidebars.js | 1 + v2/emailpassword/user-object.mdx | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 v2/emailpassword/user-object.mdx diff --git a/v2/emailpassword/sidebars.js b/v2/emailpassword/sidebars.js index 5b6ab10f8..c8bdc384c 100644 --- a/v2/emailpassword/sidebars.js +++ b/v2/emailpassword/sidebars.js @@ -119,6 +119,7 @@ module.exports = { }, ] }, + "user-object", { type: "category", label: "Integrations", diff --git a/v2/emailpassword/user-object.mdx b/v2/emailpassword/user-object.mdx new file mode 100644 index 000000000..12522f534 --- /dev/null +++ b/v2/emailpassword/user-object.mdx @@ -0,0 +1,10 @@ +--- +id: user-object +title: About the User Object +hide_title: true +--- + + + + +# About the User Object From 55a9bf9b1b2fd310b4dabc4d84258745132dc066 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 28 Aug 2023 12:49:44 +0530 Subject: [PATCH 24/81] more content --- v2/emailpassword/user-object.mdx | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/v2/emailpassword/user-object.mdx b/v2/emailpassword/user-object.mdx index 12522f534..d7896cd40 100644 --- a/v2/emailpassword/user-object.mdx +++ b/v2/emailpassword/user-object.mdx @@ -8,3 +8,79 @@ hide_title: true # About the User Object + +:::important +This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +::: + +The user object structure is as follows: + +```ts +type User = { + id: string; + timeJoined: number; + isPrimaryUser: boolean; + tenantIds: string[]; + emails: string[]; + phoneNumbers: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: ({ + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + tenantIds: string[]; + timeJoined: number; + recipeUserId: RecipeUserId; + verified: boolean; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + })[]; +}; + +class RecipeUserId { + private recipeUserId: string; + constructor(recipeUserId: string) { + this.recipeUserId = recipeUserId; + } + + public getAsString = () => { + return this.recipeUserId; + }; +} + +``` + +Before we dive into the meaning of each of the fields, it is important to understand the concept of a primary and a recipe user ID. + +## Primary vs recipe user ID + +In SuperTokens, each user can have multiple login methods: for example, one user may be able to login with both, email password and social login. Each of these login methods, will give the user a unique user ID - this is known as a `recipeUserId`. We call it that, cause email passwors and social login are two different recipes in SuperTokens. Now of course, when that user uses either of the login methods, we must resolve it to the same user ID. This user ID, that is common across all login methods for a user, is known as the primary user ID. The value of the primary user ID is equal to the recipe user ID of the first login method. + +Let's take an example. A user first signs up with the email password recipe. This gives them a recipe user ID or `r1`. Their primary user ID is also `r1`. Now, they sign in with Google with the same email. This will create a different recipe user ID `r2`. If you have enabled the automatic account linking, then the two recipe userIds will be linked, and `r2`'s primary user will be `r1`. Therefore, we will have the following mapping: + +```text +r1 -> [r1, r2] +``` + +If you have not enabled automatic account linking, then you will have two distinct users: + +```text +r1 -> [r1] +r2 -> [r2] +``` + +Where the email password user is `r1` and Google login user is `r2`. + +## User object fields explained + +- `id`: This will be the primary user ID of the user (See the section above for an example). This value can change if the user is linked to another user. +- `timeJoined`: This is the time (in MS since epoch), when the user first signed up. If a new login method is added for a user, this value is not updated. +- `isPrimaryUser`: This is a boolean value, which is `true` if the user can accept other login methods from other users. For example, when we link two users, `u1` and `u2`, such that post linking, the primary user ID is that of `u1`'s, then `u1` must have `isPrimaryUser` as `true`, and `u2` must have `isPrimaryUser` as `false`. \ No newline at end of file From 3a4dd0c03a3f2393e21f6b57d72fcd97bc72d2b8 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 28 Aug 2023 13:37:35 +0530 Subject: [PATCH 25/81] adds across all recipes --- v2/emailpassword/user-object.mdx | 20 +++- v2/passwordless/sidebars.js | 1 + v2/passwordless/user-object.mdx | 104 +++++++++++++++++++++ v2/thirdparty/sidebars.js | 1 + v2/thirdparty/user-object.mdx | 104 +++++++++++++++++++++ v2/thirdpartyemailpassword/sidebars.js | 1 + v2/thirdpartyemailpassword/user-object.mdx | 104 +++++++++++++++++++++ v2/thirdpartypasswordless/sidebars.js | 1 + v2/thirdpartypasswordless/user-object.mdx | 104 +++++++++++++++++++++ 9 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 v2/passwordless/user-object.mdx create mode 100644 v2/thirdparty/user-object.mdx create mode 100644 v2/thirdpartyemailpassword/user-object.mdx create mode 100644 v2/thirdpartypasswordless/user-object.mdx diff --git a/v2/emailpassword/user-object.mdx b/v2/emailpassword/user-object.mdx index d7896cd40..17cdfe522 100644 --- a/v2/emailpassword/user-object.mdx +++ b/v2/emailpassword/user-object.mdx @@ -83,4 +83,22 @@ Where the email password user is `r1` and Google login user is `r2`. - `id`: This will be the primary user ID of the user (See the section above for an example). This value can change if the user is linked to another user. - `timeJoined`: This is the time (in MS since epoch), when the user first signed up. If a new login method is added for a user, this value is not updated. -- `isPrimaryUser`: This is a boolean value, which is `true` if the user can accept other login methods from other users. For example, when we link two users, `u1` and `u2`, such that post linking, the primary user ID is that of `u1`'s, then `u1` must have `isPrimaryUser` as `true`, and `u2` must have `isPrimaryUser` as `false`. \ No newline at end of file +- `isPrimaryUser`: This is a boolean value, which is `true` if the user can accept other login methods from other users. For example, when we link two users, `u1` and `u2`, such that post linking, the primary user ID is that of `u1`'s, then `u1` must have `isPrimaryUser` as `true`, and `u2` must have `isPrimaryUser` as `false`. Read more about this boolean in the account linking section. +- `tenantIds`: This is the list of tenantIds which this user belongs to. It's the union set of all the tenantIds of each of the login methods for this user. +- `emails`: This list contains all the emails for this user. +- `phoneNumbers`: This list contains all the phone numbers for this user. +- `thirdParty`: This list contains the third party info belonging to this user. + - `id`: This is a unique identifier for the third party provider. For example, for Google login, the value will be `"google"` + - `userId`: This is the user ID of the user from the third party provider. For example, for Google login, this will be the user's Google user ID. +- `loginMethods`: This is a list that represents all the different ways this user can login to your app. Each login method has the following fields: + - `recipeId`: This is the ID representing the login method. For example, for email password, this will be `"emailpassword"`. + - `tenantIds`: This is the list of tenantIds for this login method. + - `timeJoined`: This is the time (in MS since epoch), when the user signed up with this login method. + - `recipeUserId`: This is the recipe user ID of this login method. + - `verified`: This is a boolean value, which is `true` if the user has verified their email or phone number for this login method. + - `email`: This is the email for this login method. This will be `undefined` if the login method does not contain an email. + - `phoneNumber`: This is the phone number for this login method. This will be `undefined` if the login method is not `"passwordless"` and the user has not used their phone number to login. + - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. + - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. + - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file diff --git a/v2/passwordless/sidebars.js b/v2/passwordless/sidebars.js index 145ad4f48..ee15b29f9 100644 --- a/v2/passwordless/sidebars.js +++ b/v2/passwordless/sidebars.js @@ -118,6 +118,7 @@ module.exports = { }, ], }, + "user-object", { type: "category", label: "Integrations", diff --git a/v2/passwordless/user-object.mdx b/v2/passwordless/user-object.mdx new file mode 100644 index 000000000..17cdfe522 --- /dev/null +++ b/v2/passwordless/user-object.mdx @@ -0,0 +1,104 @@ +--- +id: user-object +title: About the User Object +hide_title: true +--- + + + + +# About the User Object + +:::important +This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +::: + +The user object structure is as follows: + +```ts +type User = { + id: string; + timeJoined: number; + isPrimaryUser: boolean; + tenantIds: string[]; + emails: string[]; + phoneNumbers: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: ({ + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + tenantIds: string[]; + timeJoined: number; + recipeUserId: RecipeUserId; + verified: boolean; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + })[]; +}; + +class RecipeUserId { + private recipeUserId: string; + constructor(recipeUserId: string) { + this.recipeUserId = recipeUserId; + } + + public getAsString = () => { + return this.recipeUserId; + }; +} + +``` + +Before we dive into the meaning of each of the fields, it is important to understand the concept of a primary and a recipe user ID. + +## Primary vs recipe user ID + +In SuperTokens, each user can have multiple login methods: for example, one user may be able to login with both, email password and social login. Each of these login methods, will give the user a unique user ID - this is known as a `recipeUserId`. We call it that, cause email passwors and social login are two different recipes in SuperTokens. Now of course, when that user uses either of the login methods, we must resolve it to the same user ID. This user ID, that is common across all login methods for a user, is known as the primary user ID. The value of the primary user ID is equal to the recipe user ID of the first login method. + +Let's take an example. A user first signs up with the email password recipe. This gives them a recipe user ID or `r1`. Their primary user ID is also `r1`. Now, they sign in with Google with the same email. This will create a different recipe user ID `r2`. If you have enabled the automatic account linking, then the two recipe userIds will be linked, and `r2`'s primary user will be `r1`. Therefore, we will have the following mapping: + +```text +r1 -> [r1, r2] +``` + +If you have not enabled automatic account linking, then you will have two distinct users: + +```text +r1 -> [r1] +r2 -> [r2] +``` + +Where the email password user is `r1` and Google login user is `r2`. + +## User object fields explained + +- `id`: This will be the primary user ID of the user (See the section above for an example). This value can change if the user is linked to another user. +- `timeJoined`: This is the time (in MS since epoch), when the user first signed up. If a new login method is added for a user, this value is not updated. +- `isPrimaryUser`: This is a boolean value, which is `true` if the user can accept other login methods from other users. For example, when we link two users, `u1` and `u2`, such that post linking, the primary user ID is that of `u1`'s, then `u1` must have `isPrimaryUser` as `true`, and `u2` must have `isPrimaryUser` as `false`. Read more about this boolean in the account linking section. +- `tenantIds`: This is the list of tenantIds which this user belongs to. It's the union set of all the tenantIds of each of the login methods for this user. +- `emails`: This list contains all the emails for this user. +- `phoneNumbers`: This list contains all the phone numbers for this user. +- `thirdParty`: This list contains the third party info belonging to this user. + - `id`: This is a unique identifier for the third party provider. For example, for Google login, the value will be `"google"` + - `userId`: This is the user ID of the user from the third party provider. For example, for Google login, this will be the user's Google user ID. +- `loginMethods`: This is a list that represents all the different ways this user can login to your app. Each login method has the following fields: + - `recipeId`: This is the ID representing the login method. For example, for email password, this will be `"emailpassword"`. + - `tenantIds`: This is the list of tenantIds for this login method. + - `timeJoined`: This is the time (in MS since epoch), when the user signed up with this login method. + - `recipeUserId`: This is the recipe user ID of this login method. + - `verified`: This is a boolean value, which is `true` if the user has verified their email or phone number for this login method. + - `email`: This is the email for this login method. This will be `undefined` if the login method does not contain an email. + - `phoneNumber`: This is the phone number for this login method. This will be `undefined` if the login method is not `"passwordless"` and the user has not used their phone number to login. + - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. + - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. + - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file diff --git a/v2/thirdparty/sidebars.js b/v2/thirdparty/sidebars.js index cf3b59c90..004f20be7 100644 --- a/v2/thirdparty/sidebars.js +++ b/v2/thirdparty/sidebars.js @@ -117,6 +117,7 @@ module.exports = { }, ] }, + "user-object", { type: "category", label: "Integrations", diff --git a/v2/thirdparty/user-object.mdx b/v2/thirdparty/user-object.mdx new file mode 100644 index 000000000..17cdfe522 --- /dev/null +++ b/v2/thirdparty/user-object.mdx @@ -0,0 +1,104 @@ +--- +id: user-object +title: About the User Object +hide_title: true +--- + + + + +# About the User Object + +:::important +This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +::: + +The user object structure is as follows: + +```ts +type User = { + id: string; + timeJoined: number; + isPrimaryUser: boolean; + tenantIds: string[]; + emails: string[]; + phoneNumbers: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: ({ + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + tenantIds: string[]; + timeJoined: number; + recipeUserId: RecipeUserId; + verified: boolean; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + })[]; +}; + +class RecipeUserId { + private recipeUserId: string; + constructor(recipeUserId: string) { + this.recipeUserId = recipeUserId; + } + + public getAsString = () => { + return this.recipeUserId; + }; +} + +``` + +Before we dive into the meaning of each of the fields, it is important to understand the concept of a primary and a recipe user ID. + +## Primary vs recipe user ID + +In SuperTokens, each user can have multiple login methods: for example, one user may be able to login with both, email password and social login. Each of these login methods, will give the user a unique user ID - this is known as a `recipeUserId`. We call it that, cause email passwors and social login are two different recipes in SuperTokens. Now of course, when that user uses either of the login methods, we must resolve it to the same user ID. This user ID, that is common across all login methods for a user, is known as the primary user ID. The value of the primary user ID is equal to the recipe user ID of the first login method. + +Let's take an example. A user first signs up with the email password recipe. This gives them a recipe user ID or `r1`. Their primary user ID is also `r1`. Now, they sign in with Google with the same email. This will create a different recipe user ID `r2`. If you have enabled the automatic account linking, then the two recipe userIds will be linked, and `r2`'s primary user will be `r1`. Therefore, we will have the following mapping: + +```text +r1 -> [r1, r2] +``` + +If you have not enabled automatic account linking, then you will have two distinct users: + +```text +r1 -> [r1] +r2 -> [r2] +``` + +Where the email password user is `r1` and Google login user is `r2`. + +## User object fields explained + +- `id`: This will be the primary user ID of the user (See the section above for an example). This value can change if the user is linked to another user. +- `timeJoined`: This is the time (in MS since epoch), when the user first signed up. If a new login method is added for a user, this value is not updated. +- `isPrimaryUser`: This is a boolean value, which is `true` if the user can accept other login methods from other users. For example, when we link two users, `u1` and `u2`, such that post linking, the primary user ID is that of `u1`'s, then `u1` must have `isPrimaryUser` as `true`, and `u2` must have `isPrimaryUser` as `false`. Read more about this boolean in the account linking section. +- `tenantIds`: This is the list of tenantIds which this user belongs to. It's the union set of all the tenantIds of each of the login methods for this user. +- `emails`: This list contains all the emails for this user. +- `phoneNumbers`: This list contains all the phone numbers for this user. +- `thirdParty`: This list contains the third party info belonging to this user. + - `id`: This is a unique identifier for the third party provider. For example, for Google login, the value will be `"google"` + - `userId`: This is the user ID of the user from the third party provider. For example, for Google login, this will be the user's Google user ID. +- `loginMethods`: This is a list that represents all the different ways this user can login to your app. Each login method has the following fields: + - `recipeId`: This is the ID representing the login method. For example, for email password, this will be `"emailpassword"`. + - `tenantIds`: This is the list of tenantIds for this login method. + - `timeJoined`: This is the time (in MS since epoch), when the user signed up with this login method. + - `recipeUserId`: This is the recipe user ID of this login method. + - `verified`: This is a boolean value, which is `true` if the user has verified their email or phone number for this login method. + - `email`: This is the email for this login method. This will be `undefined` if the login method does not contain an email. + - `phoneNumber`: This is the phone number for this login method. This will be `undefined` if the login method is not `"passwordless"` and the user has not used their phone number to login. + - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. + - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. + - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/sidebars.js b/v2/thirdpartyemailpassword/sidebars.js index 77e8ea2b9..b54a2b45e 100644 --- a/v2/thirdpartyemailpassword/sidebars.js +++ b/v2/thirdpartyemailpassword/sidebars.js @@ -121,6 +121,7 @@ module.exports = { }, ], }, + "user-object", { type: "category", label: "Integrations", diff --git a/v2/thirdpartyemailpassword/user-object.mdx b/v2/thirdpartyemailpassword/user-object.mdx new file mode 100644 index 000000000..17cdfe522 --- /dev/null +++ b/v2/thirdpartyemailpassword/user-object.mdx @@ -0,0 +1,104 @@ +--- +id: user-object +title: About the User Object +hide_title: true +--- + + + + +# About the User Object + +:::important +This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +::: + +The user object structure is as follows: + +```ts +type User = { + id: string; + timeJoined: number; + isPrimaryUser: boolean; + tenantIds: string[]; + emails: string[]; + phoneNumbers: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: ({ + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + tenantIds: string[]; + timeJoined: number; + recipeUserId: RecipeUserId; + verified: boolean; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + })[]; +}; + +class RecipeUserId { + private recipeUserId: string; + constructor(recipeUserId: string) { + this.recipeUserId = recipeUserId; + } + + public getAsString = () => { + return this.recipeUserId; + }; +} + +``` + +Before we dive into the meaning of each of the fields, it is important to understand the concept of a primary and a recipe user ID. + +## Primary vs recipe user ID + +In SuperTokens, each user can have multiple login methods: for example, one user may be able to login with both, email password and social login. Each of these login methods, will give the user a unique user ID - this is known as a `recipeUserId`. We call it that, cause email passwors and social login are two different recipes in SuperTokens. Now of course, when that user uses either of the login methods, we must resolve it to the same user ID. This user ID, that is common across all login methods for a user, is known as the primary user ID. The value of the primary user ID is equal to the recipe user ID of the first login method. + +Let's take an example. A user first signs up with the email password recipe. This gives them a recipe user ID or `r1`. Their primary user ID is also `r1`. Now, they sign in with Google with the same email. This will create a different recipe user ID `r2`. If you have enabled the automatic account linking, then the two recipe userIds will be linked, and `r2`'s primary user will be `r1`. Therefore, we will have the following mapping: + +```text +r1 -> [r1, r2] +``` + +If you have not enabled automatic account linking, then you will have two distinct users: + +```text +r1 -> [r1] +r2 -> [r2] +``` + +Where the email password user is `r1` and Google login user is `r2`. + +## User object fields explained + +- `id`: This will be the primary user ID of the user (See the section above for an example). This value can change if the user is linked to another user. +- `timeJoined`: This is the time (in MS since epoch), when the user first signed up. If a new login method is added for a user, this value is not updated. +- `isPrimaryUser`: This is a boolean value, which is `true` if the user can accept other login methods from other users. For example, when we link two users, `u1` and `u2`, such that post linking, the primary user ID is that of `u1`'s, then `u1` must have `isPrimaryUser` as `true`, and `u2` must have `isPrimaryUser` as `false`. Read more about this boolean in the account linking section. +- `tenantIds`: This is the list of tenantIds which this user belongs to. It's the union set of all the tenantIds of each of the login methods for this user. +- `emails`: This list contains all the emails for this user. +- `phoneNumbers`: This list contains all the phone numbers for this user. +- `thirdParty`: This list contains the third party info belonging to this user. + - `id`: This is a unique identifier for the third party provider. For example, for Google login, the value will be `"google"` + - `userId`: This is the user ID of the user from the third party provider. For example, for Google login, this will be the user's Google user ID. +- `loginMethods`: This is a list that represents all the different ways this user can login to your app. Each login method has the following fields: + - `recipeId`: This is the ID representing the login method. For example, for email password, this will be `"emailpassword"`. + - `tenantIds`: This is the list of tenantIds for this login method. + - `timeJoined`: This is the time (in MS since epoch), when the user signed up with this login method. + - `recipeUserId`: This is the recipe user ID of this login method. + - `verified`: This is a boolean value, which is `true` if the user has verified their email or phone number for this login method. + - `email`: This is the email for this login method. This will be `undefined` if the login method does not contain an email. + - `phoneNumber`: This is the phone number for this login method. This will be `undefined` if the login method is not `"passwordless"` and the user has not used their phone number to login. + - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. + - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. + - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file diff --git a/v2/thirdpartypasswordless/sidebars.js b/v2/thirdpartypasswordless/sidebars.js index a380a74f6..4f096f673 100644 --- a/v2/thirdpartypasswordless/sidebars.js +++ b/v2/thirdpartypasswordless/sidebars.js @@ -120,6 +120,7 @@ module.exports = { }, ], }, + "user-object", { type: "category", label: "Integrations", diff --git a/v2/thirdpartypasswordless/user-object.mdx b/v2/thirdpartypasswordless/user-object.mdx new file mode 100644 index 000000000..17cdfe522 --- /dev/null +++ b/v2/thirdpartypasswordless/user-object.mdx @@ -0,0 +1,104 @@ +--- +id: user-object +title: About the User Object +hide_title: true +--- + + + + +# About the User Object + +:::important +This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +::: + +The user object structure is as follows: + +```ts +type User = { + id: string; + timeJoined: number; + isPrimaryUser: boolean; + tenantIds: string[]; + emails: string[]; + phoneNumbers: string[]; + thirdParty: { + id: string; + userId: string; + }[]; + loginMethods: ({ + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + tenantIds: string[]; + timeJoined: number; + recipeUserId: RecipeUserId; + verified: boolean; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + })[]; +}; + +class RecipeUserId { + private recipeUserId: string; + constructor(recipeUserId: string) { + this.recipeUserId = recipeUserId; + } + + public getAsString = () => { + return this.recipeUserId; + }; +} + +``` + +Before we dive into the meaning of each of the fields, it is important to understand the concept of a primary and a recipe user ID. + +## Primary vs recipe user ID + +In SuperTokens, each user can have multiple login methods: for example, one user may be able to login with both, email password and social login. Each of these login methods, will give the user a unique user ID - this is known as a `recipeUserId`. We call it that, cause email passwors and social login are two different recipes in SuperTokens. Now of course, when that user uses either of the login methods, we must resolve it to the same user ID. This user ID, that is common across all login methods for a user, is known as the primary user ID. The value of the primary user ID is equal to the recipe user ID of the first login method. + +Let's take an example. A user first signs up with the email password recipe. This gives them a recipe user ID or `r1`. Their primary user ID is also `r1`. Now, they sign in with Google with the same email. This will create a different recipe user ID `r2`. If you have enabled the automatic account linking, then the two recipe userIds will be linked, and `r2`'s primary user will be `r1`. Therefore, we will have the following mapping: + +```text +r1 -> [r1, r2] +``` + +If you have not enabled automatic account linking, then you will have two distinct users: + +```text +r1 -> [r1] +r2 -> [r2] +``` + +Where the email password user is `r1` and Google login user is `r2`. + +## User object fields explained + +- `id`: This will be the primary user ID of the user (See the section above for an example). This value can change if the user is linked to another user. +- `timeJoined`: This is the time (in MS since epoch), when the user first signed up. If a new login method is added for a user, this value is not updated. +- `isPrimaryUser`: This is a boolean value, which is `true` if the user can accept other login methods from other users. For example, when we link two users, `u1` and `u2`, such that post linking, the primary user ID is that of `u1`'s, then `u1` must have `isPrimaryUser` as `true`, and `u2` must have `isPrimaryUser` as `false`. Read more about this boolean in the account linking section. +- `tenantIds`: This is the list of tenantIds which this user belongs to. It's the union set of all the tenantIds of each of the login methods for this user. +- `emails`: This list contains all the emails for this user. +- `phoneNumbers`: This list contains all the phone numbers for this user. +- `thirdParty`: This list contains the third party info belonging to this user. + - `id`: This is a unique identifier for the third party provider. For example, for Google login, the value will be `"google"` + - `userId`: This is the user ID of the user from the third party provider. For example, for Google login, this will be the user's Google user ID. +- `loginMethods`: This is a list that represents all the different ways this user can login to your app. Each login method has the following fields: + - `recipeId`: This is the ID representing the login method. For example, for email password, this will be `"emailpassword"`. + - `tenantIds`: This is the list of tenantIds for this login method. + - `timeJoined`: This is the time (in MS since epoch), when the user signed up with this login method. + - `recipeUserId`: This is the recipe user ID of this login method. + - `verified`: This is a boolean value, which is `true` if the user has verified their email or phone number for this login method. + - `email`: This is the email for this login method. This will be `undefined` if the login method does not contain an email. + - `phoneNumber`: This is the phone number for this login method. This will be `undefined` if the login method is not `"passwordless"` and the user has not used their phone number to login. + - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. + - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. + - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file From bfc01625380103a7b2e62ad9427d3306a9c34512 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 28 Aug 2023 13:44:03 +0530 Subject: [PATCH 26/81] more changes --- v2/emailpassword/user-object.mdx | 2 +- v2/passwordless/user-object.mdx | 4 ++-- v2/thirdparty/user-object.mdx | 4 ++-- v2/thirdpartyemailpassword/user-object.mdx | 4 ++-- v2/thirdpartypasswordless/user-object.mdx | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/v2/emailpassword/user-object.mdx b/v2/emailpassword/user-object.mdx index 17cdfe522..4762c80cc 100644 --- a/v2/emailpassword/user-object.mdx +++ b/v2/emailpassword/user-object.mdx @@ -10,7 +10,7 @@ hide_title: true # About the User Object :::important -This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +This is only applicable for our NodeJS SDK >= 16.0. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki ::: The user object structure is as follows: diff --git a/v2/passwordless/user-object.mdx b/v2/passwordless/user-object.mdx index 17cdfe522..374973363 100644 --- a/v2/passwordless/user-object.mdx +++ b/v2/passwordless/user-object.mdx @@ -10,7 +10,7 @@ hide_title: true # About the User Object :::important -This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +This is only applicable for our NodeJS SDK >= 16.0. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki ::: The user object structure is as follows: @@ -101,4 +101,4 @@ Where the email password user is `r1` and Google login user is `r2`. - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. diff --git a/v2/thirdparty/user-object.mdx b/v2/thirdparty/user-object.mdx index 17cdfe522..374973363 100644 --- a/v2/thirdparty/user-object.mdx +++ b/v2/thirdparty/user-object.mdx @@ -10,7 +10,7 @@ hide_title: true # About the User Object :::important -This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +This is only applicable for our NodeJS SDK >= 16.0. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki ::: The user object structure is as follows: @@ -101,4 +101,4 @@ Where the email password user is `r1` and Google login user is `r2`. - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. diff --git a/v2/thirdpartyemailpassword/user-object.mdx b/v2/thirdpartyemailpassword/user-object.mdx index 17cdfe522..374973363 100644 --- a/v2/thirdpartyemailpassword/user-object.mdx +++ b/v2/thirdpartyemailpassword/user-object.mdx @@ -10,7 +10,7 @@ hide_title: true # About the User Object :::important -This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +This is only applicable for our NodeJS SDK >= 16.0. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki ::: The user object structure is as follows: @@ -101,4 +101,4 @@ Where the email password user is `r1` and Google login user is `r2`. - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. diff --git a/v2/thirdpartypasswordless/user-object.mdx b/v2/thirdpartypasswordless/user-object.mdx index 17cdfe522..374973363 100644 --- a/v2/thirdpartypasswordless/user-object.mdx +++ b/v2/thirdpartypasswordless/user-object.mdx @@ -10,7 +10,7 @@ hide_title: true # About the User Object :::important -This is only applicable for core versions >= 7.0, and for our NodeJS SDK. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki +This is only applicable for our NodeJS SDK >= 16.0. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki ::: The user object structure is as follows: @@ -101,4 +101,4 @@ Where the email password user is `r1` and Google login user is `r2`. - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. From 6e4709d77ea080d0852b1937f965715e463f5ad6 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 29 Aug 2023 10:57:08 +0530 Subject: [PATCH 27/81] adds example for user obj --- v2/emailpassword/user-object.mdx | 82 +++++++++++++++++++++- v2/passwordless/user-object.mdx | 78 ++++++++++++++++++++ v2/thirdparty/user-object.mdx | 78 ++++++++++++++++++++ v2/thirdpartyemailpassword/user-object.mdx | 78 ++++++++++++++++++++ v2/thirdpartypasswordless/user-object.mdx | 78 ++++++++++++++++++++ 5 files changed, 393 insertions(+), 1 deletion(-) diff --git a/v2/emailpassword/user-object.mdx b/v2/emailpassword/user-object.mdx index 4762c80cc..d89cbe408 100644 --- a/v2/emailpassword/user-object.mdx +++ b/v2/emailpassword/user-object.mdx @@ -101,4 +101,84 @@ Where the email password user is `r1` and Google login user is `r2`. - `thirdParty`: This is the third party info for this login method. This will be `undefined` if the login method is not third party. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. \ No newline at end of file + - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. + +## Examples + +### Example 1: Email password user without account linking +In a simple case, if we just have a user who signed up with email password (with `test@example.com`), and automatic account linking is not enabled, their user object would look like: + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: false; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: []; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `false`. This means that if this user is linked to another user, this user's primary user ID will change to the other user's primary user ID. +- This user has just one login method (email password), since it's not linked to any other user. + +### Example 2: Email password user linked with a social login user +We have a user who signed up with email password (with `test@example.com`), and then with Google (with the same email). Automatic account linking is enabled, therefore these two users get linked. + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: true; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: [{ + id: "google"; + userId: "1234567890"; + }]; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }, { + recipeId: "thirdparty"; + tenantIds: ["public"]; + timeJoined: 1693286254250; + recipeUserId: new RecipeUserId("6ffc0ac5-d840-4a5b-92e8-86965f67c2ea"); + verified: true; + email: "test@example.com"; + thirdParty: { + id: "google"; + userId: "1234567890"; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `true`. This means that if this user is linked to another user, this user's primary user ID will NOT change. +- This user has just two login methods (email password and third party). The top level `timeJoined` value is the min of the two `timeJoined` values in the `loginMethods`. +- In this example, the top level `emails` array just has one item since both the login methods emails as the same, but if they were different, there would be two items in this array. + + diff --git a/v2/passwordless/user-object.mdx b/v2/passwordless/user-object.mdx index 374973363..85b21fd86 100644 --- a/v2/passwordless/user-object.mdx +++ b/v2/passwordless/user-object.mdx @@ -102,3 +102,81 @@ Where the email password user is `r1` and Google login user is `r2`. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. + +## Examples + +### Example 1: Email password user without account linking +In a simple case, if we just have a user who signed up with email password (with `test@example.com`), and automatic account linking is not enabled, their user object would look like: + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: false; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: []; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `false`. This means that if this user is linked to another user, this user's primary user ID will change to the other user's primary user ID. +- This user has just one login method (email password), since it's not linked to any other user. + +### Example 2: Email password user linked with a social login user +We have a user who signed up with email password (with `test@example.com`), and then with Google (with the same email). Automatic account linking is enabled, therefore these two users get linked. + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: true; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: [{ + id: "google"; + userId: "1234567890"; + }]; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }, { + recipeId: "thirdparty"; + tenantIds: ["public"]; + timeJoined: 1693286254250; + recipeUserId: new RecipeUserId("6ffc0ac5-d840-4a5b-92e8-86965f67c2ea"); + verified: true; + email: "test@example.com"; + thirdParty: { + id: "google"; + userId: "1234567890"; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `true`. This means that if this user is linked to another user, this user's primary user ID will NOT change. +- This user has just two login methods (email password and third party). The top level `timeJoined` value is the min of the two `timeJoined` values in the `loginMethods`. +- In this example, the top level `emails` array just has one item since both the login methods emails as the same, but if they were different, there would be two items in this array. diff --git a/v2/thirdparty/user-object.mdx b/v2/thirdparty/user-object.mdx index 374973363..85b21fd86 100644 --- a/v2/thirdparty/user-object.mdx +++ b/v2/thirdparty/user-object.mdx @@ -102,3 +102,81 @@ Where the email password user is `r1` and Google login user is `r2`. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. + +## Examples + +### Example 1: Email password user without account linking +In a simple case, if we just have a user who signed up with email password (with `test@example.com`), and automatic account linking is not enabled, their user object would look like: + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: false; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: []; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `false`. This means that if this user is linked to another user, this user's primary user ID will change to the other user's primary user ID. +- This user has just one login method (email password), since it's not linked to any other user. + +### Example 2: Email password user linked with a social login user +We have a user who signed up with email password (with `test@example.com`), and then with Google (with the same email). Automatic account linking is enabled, therefore these two users get linked. + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: true; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: [{ + id: "google"; + userId: "1234567890"; + }]; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }, { + recipeId: "thirdparty"; + tenantIds: ["public"]; + timeJoined: 1693286254250; + recipeUserId: new RecipeUserId("6ffc0ac5-d840-4a5b-92e8-86965f67c2ea"); + verified: true; + email: "test@example.com"; + thirdParty: { + id: "google"; + userId: "1234567890"; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `true`. This means that if this user is linked to another user, this user's primary user ID will NOT change. +- This user has just two login methods (email password and third party). The top level `timeJoined` value is the min of the two `timeJoined` values in the `loginMethods`. +- In this example, the top level `emails` array just has one item since both the login methods emails as the same, but if they were different, there would be two items in this array. diff --git a/v2/thirdpartyemailpassword/user-object.mdx b/v2/thirdpartyemailpassword/user-object.mdx index 374973363..85b21fd86 100644 --- a/v2/thirdpartyemailpassword/user-object.mdx +++ b/v2/thirdpartyemailpassword/user-object.mdx @@ -102,3 +102,81 @@ Where the email password user is `r1` and Google login user is `r2`. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. + +## Examples + +### Example 1: Email password user without account linking +In a simple case, if we just have a user who signed up with email password (with `test@example.com`), and automatic account linking is not enabled, their user object would look like: + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: false; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: []; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `false`. This means that if this user is linked to another user, this user's primary user ID will change to the other user's primary user ID. +- This user has just one login method (email password), since it's not linked to any other user. + +### Example 2: Email password user linked with a social login user +We have a user who signed up with email password (with `test@example.com`), and then with Google (with the same email). Automatic account linking is enabled, therefore these two users get linked. + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: true; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: [{ + id: "google"; + userId: "1234567890"; + }]; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }, { + recipeId: "thirdparty"; + tenantIds: ["public"]; + timeJoined: 1693286254250; + recipeUserId: new RecipeUserId("6ffc0ac5-d840-4a5b-92e8-86965f67c2ea"); + verified: true; + email: "test@example.com"; + thirdParty: { + id: "google"; + userId: "1234567890"; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `true`. This means that if this user is linked to another user, this user's primary user ID will NOT change. +- This user has just two login methods (email password and third party). The top level `timeJoined` value is the min of the two `timeJoined` values in the `loginMethods`. +- In this example, the top level `emails` array just has one item since both the login methods emails as the same, but if they were different, there would be two items in this array. diff --git a/v2/thirdpartypasswordless/user-object.mdx b/v2/thirdpartypasswordless/user-object.mdx index 374973363..85b21fd86 100644 --- a/v2/thirdpartypasswordless/user-object.mdx +++ b/v2/thirdpartypasswordless/user-object.mdx @@ -102,3 +102,81 @@ Where the email password user is `r1` and Google login user is `r2`. - `hasSameEmailAs`: This is a helper function which takes an email as a parameter, and returns `true` if the email is the same as the email for this login method. It normalises the input email before comparing so that you get accurate results. - `hasSamePhoneNumberAs`: This is a function which takes a phone number as a parameter, and returns `true` if the phone number is the same as the phone number for this login method. It normalises the input phone number before comparing so that you get accurate results. - `hasSameThirdPartyInfoAs`: This is a function which takes a third party info as a parameter, and returns `true` if the third party info is the same as the third party info for this login method. + +## Examples + +### Example 1: Email password user without account linking +In a simple case, if we just have a user who signed up with email password (with `test@example.com`), and automatic account linking is not enabled, their user object would look like: + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: false; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: []; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `false`. This means that if this user is linked to another user, this user's primary user ID will change to the other user's primary user ID. +- This user has just one login method (email password), since it's not linked to any other user. + +### Example 2: Email password user linked with a social login user +We have a user who signed up with email password (with `test@example.com`), and then with Google (with the same email). Automatic account linking is enabled, therefore these two users get linked. + +```text +{ + id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; + timeJoined: 1693286254150; + isPrimaryUser: true; + tenantIds: ["public"]; + emails: ["test@example.com"]; + phoneNumbers: []; + thirdParty: [{ + id: "google"; + userId: "1234567890"; + }]; + loginMethods: [{ + recipeId: "emailpassword"; + tenantIds: ["public"]; + timeJoined: 1693286254150; + recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); + verified: false; + email: "test@example.com"; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }, { + recipeId: "thirdparty"; + tenantIds: ["public"]; + timeJoined: 1693286254250; + recipeUserId: new RecipeUserId("6ffc0ac5-d840-4a5b-92e8-86965f67c2ea"); + verified: true; + email: "test@example.com"; + thirdParty: { + id: "google"; + userId: "1234567890"; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }]; +}; +``` + +- Notice that the value of `isPrimaryUser` is `true`. This means that if this user is linked to another user, this user's primary user ID will NOT change. +- This user has just two login methods (email password and third party). The top level `timeJoined` value is the min of the two `timeJoined` values in the `loginMethods`. +- In this example, the top level `emails` array just has one item since both the login methods emails as the same, but if they were different, there would be two items in this array. From 3c08f9ee6810648c045c70a04c4807ecaec492e4 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 29 Aug 2023 11:26:49 +0530 Subject: [PATCH 28/81] small changes --- .../common-customizations/account-linking.mdx | 20 ++----------------- .../account-linking/overview.mdx | 11 ++++++++++ v2/thirdparty/sidebars.js | 8 +++++++- .../common-customizations/account-linking.mdx | 20 ++----------------- .../account-linking/overview.mdx | 11 ++++++++++ v2/thirdpartyemailpassword/sidebars.js | 8 +++++++- .../common-customizations/account-linking.mdx | 20 ++----------------- .../account-linking/overview.mdx | 11 ++++++++++ v2/thirdpartypasswordless/sidebars.js | 8 +++++++- 9 files changed, 60 insertions(+), 57 deletions(-) create mode 100644 v2/thirdparty/common-customizations/account-linking/overview.mdx create mode 100644 v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx create mode 100644 v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx diff --git a/v2/thirdparty/common-customizations/account-linking.mdx b/v2/thirdparty/common-customizations/account-linking.mdx index aa5f42159..0155ca8d1 100644 --- a/v2/thirdparty/common-customizations/account-linking.mdx +++ b/v2/thirdparty/common-customizations/account-linking.mdx @@ -7,22 +7,6 @@ hide_title: true -# Automatic account linking +import Redirector from '/src/components/Redirector'; -## What is account linking? -Automatic account linking is a feature that allows multiple login methods to resolve to the same user account as long as the email being used is the same. - -For example, if a user signs up with gmail using `user@gmail.com` and then signs up again with the same email, but using email & password, we will not create a new user. Instead, we will use the same `userId` across all login methods that use the same email. - -:::caution -Automatic account linking is not advisable from a security point of view. Let's take an example: -- We have an app that has sign in with google and with github. -- A user signs up with google to use this app. -- They also have their personal github account that uses their gmail ID. -- If their github account is somehow compromised, then the attacker can then sign up to our app with their github account and then access this user's account. - -Hence, by doing automatic account linking, we are increasing the attack surface for account takeover. Instead, we recommend that if a user is signing up with another provider but with the same email, we can ask them to login with their original provider instead, or then to proceed with new account creation. -::: - -## How to enable this feature? -SuperTokens does not support Account Linking yet but we are actively working on this feature. + diff --git a/v2/thirdparty/common-customizations/account-linking/overview.mdx b/v2/thirdparty/common-customizations/account-linking/overview.mdx new file mode 100644 index 000000000..cff52a49a --- /dev/null +++ b/v2/thirdparty/common-customizations/account-linking/overview.mdx @@ -0,0 +1,11 @@ +--- +id: overview +title: About account linking +hide_title: true +--- + + + + +# About account linking + diff --git a/v2/thirdparty/sidebars.js b/v2/thirdparty/sidebars.js index 004f20be7..b52e5221e 100644 --- a/v2/thirdparty/sidebars.js +++ b/v2/thirdparty/sidebars.js @@ -411,7 +411,13 @@ module.exports = { "common-customizations/get-user-info", "common-customizations/user-pagination", "common-customizations/delete-user", - "common-customizations/account-linking", + { + type: "category", + label: "Account Linking", + items: [ + "common-customizations/account-linking/overview" + ] + }, { type: "category", label: "Email Verification", diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking.mdx index ae8a82a7a..57f0fa805 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking.mdx @@ -7,22 +7,6 @@ hide_title: true -# Automatic account linking +import Redirector from '/src/components/Redirector'; -## What is account linking? -Automatic account linking is a feature that allows multiple login methods to resolve to the same user account as long as the email being used is the same. - -For example, if a user signs up with gmail using `user@gmail.com` and then signs up again with the same email, but using email & password, we will not create a new user. Instead, we will use the same `userId` across all login methods that use the same email. - -:::caution -Automatic account linking is not advisable from a security point of view. Let's take an example: -- We have an app that has sign in with google and with github. -- A user signs up with google to use this app. -- They also have their personal github account that uses their gmail ID. -- If their github account is somehow compromised, then the attacker can then sign up to our app with their github account and then access this user's account. - -Hence, by doing automatic account linking, we are increasing the attack surface for account takeover. Instead, we recommend that if a user is signing up with another provider but with the same email, we can ask them to login with their original provider instead, or then to proceed with new account creation. -::: - -## How to enable this feature? -SuperTokens does not support Account Linking yet but we are actively working on this feature. \ No newline at end of file + \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx new file mode 100644 index 000000000..cff52a49a --- /dev/null +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx @@ -0,0 +1,11 @@ +--- +id: overview +title: About account linking +hide_title: true +--- + + + + +# About account linking + diff --git a/v2/thirdpartyemailpassword/sidebars.js b/v2/thirdpartyemailpassword/sidebars.js index b54a2b45e..d13bbadcc 100644 --- a/v2/thirdpartyemailpassword/sidebars.js +++ b/v2/thirdpartyemailpassword/sidebars.js @@ -434,7 +434,13 @@ module.exports = { "common-customizations/get-user-info", "common-customizations/user-pagination", "common-customizations/delete-user", - "common-customizations/account-linking", + { + type: "category", + label: "Account Linking", + items: [ + "common-customizations/account-linking/overview" + ] + }, "common-customizations/change-password", "common-customizations/change-email-post-login", { diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking.mdx index aa5f42159..0155ca8d1 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking.mdx @@ -7,22 +7,6 @@ hide_title: true -# Automatic account linking +import Redirector from '/src/components/Redirector'; -## What is account linking? -Automatic account linking is a feature that allows multiple login methods to resolve to the same user account as long as the email being used is the same. - -For example, if a user signs up with gmail using `user@gmail.com` and then signs up again with the same email, but using email & password, we will not create a new user. Instead, we will use the same `userId` across all login methods that use the same email. - -:::caution -Automatic account linking is not advisable from a security point of view. Let's take an example: -- We have an app that has sign in with google and with github. -- A user signs up with google to use this app. -- They also have their personal github account that uses their gmail ID. -- If their github account is somehow compromised, then the attacker can then sign up to our app with their github account and then access this user's account. - -Hence, by doing automatic account linking, we are increasing the attack surface for account takeover. Instead, we recommend that if a user is signing up with another provider but with the same email, we can ask them to login with their original provider instead, or then to proceed with new account creation. -::: - -## How to enable this feature? -SuperTokens does not support Account Linking yet but we are actively working on this feature. + diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx new file mode 100644 index 000000000..cff52a49a --- /dev/null +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx @@ -0,0 +1,11 @@ +--- +id: overview +title: About account linking +hide_title: true +--- + + + + +# About account linking + diff --git a/v2/thirdpartypasswordless/sidebars.js b/v2/thirdpartypasswordless/sidebars.js index 4f096f673..96b87dce7 100644 --- a/v2/thirdpartypasswordless/sidebars.js +++ b/v2/thirdpartypasswordless/sidebars.js @@ -429,7 +429,13 @@ module.exports = { "common-customizations/change-magic-link-url", "common-customizations/change-code-lifetime", "common-customizations/change-email", - "common-customizations/account-linking", + { + type: "category", + label: "Account Linking", + items: [ + "common-customizations/account-linking/overview" + ] + }, { type: "category", label: "Email Verification", From 75179be281597dc0d606e0650771f6d86cd93e62 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 29 Aug 2023 11:53:53 +0530 Subject: [PATCH 29/81] adds more content --- .../common-customizations/account-linking/overview.mdx | 5 +++++ .../common-customizations/account-linking/overview.mdx | 5 +++++ .../common-customizations/account-linking/overview.mdx | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/v2/thirdparty/common-customizations/account-linking/overview.mdx b/v2/thirdparty/common-customizations/account-linking/overview.mdx index cff52a49a..5e21cbb9c 100644 --- a/v2/thirdparty/common-customizations/account-linking/overview.mdx +++ b/v2/thirdparty/common-customizations/account-linking/overview.mdx @@ -9,3 +9,8 @@ hide_title: true # About account linking +Account linking is the process of associating multiple authentication methods with the same account. For example, a user may have a password-based account and a Google account, and they may want to link both of these accounts to the same user account in your application. + +Account linking can be done automatically during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. + +The other method is to do manual account linking. Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx index cff52a49a..c266318f4 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx @@ -9,3 +9,8 @@ hide_title: true # About account linking +Account linking is the process of associating multiple authentication methods with the same account. For example, a user may have a password-based account and a Google account, and they may want to link both of these accounts to the same user account in your application. + +Account linking can be done automatically during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. + +The other method is to do manual account linking. Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. \ No newline at end of file diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx index cff52a49a..5e21cbb9c 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx @@ -9,3 +9,8 @@ hide_title: true # About account linking +Account linking is the process of associating multiple authentication methods with the same account. For example, a user may have a password-based account and a Google account, and they may want to link both of these accounts to the same user account in your application. + +Account linking can be done automatically during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. + +The other method is to do manual account linking. Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. From 31dc591cf6805770ad105342a53f84c3f45c72ea Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 29 Aug 2023 20:14:12 +0530 Subject: [PATCH 30/81] adds more docs --- .../account-linking/overview.mdx | 10 ++- .../automatic-account-linking.mdx | 75 +++++++++++++++++++ .../account-linking/overview.mdx | 10 ++- v2/thirdpartyemailpassword/sidebars.js | 3 +- .../account-linking/overview.mdx | 10 ++- 5 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx diff --git a/v2/thirdparty/common-customizations/account-linking/overview.mdx b/v2/thirdparty/common-customizations/account-linking/overview.mdx index 5e21cbb9c..acc5838a9 100644 --- a/v2/thirdparty/common-customizations/account-linking/overview.mdx +++ b/v2/thirdparty/common-customizations/account-linking/overview.mdx @@ -7,10 +7,16 @@ hide_title: true +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' + + + # About account linking Account linking is the process of associating multiple authentication methods with the same account. For example, a user may have a password-based account and a Google account, and they may want to link both of these accounts to the same user account in your application. -Account linking can be done automatically during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. +[Account linking can be done automatically](./automatic-account-linking) during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. + +The other method is to do [manual account linking](./manual-account-linking). Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. -The other method is to do manual account linking. Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. +We will explore both these methods in the next pages. diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx new file mode 100644 index 000000000..d25e8ade8 --- /dev/null +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -0,0 +1,75 @@ +--- +id: automatic-account-linking +title: Automatic account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Automatic account linking + +Automatic account linking is a feature that allows users to automatically sign in to their existing account using more than one login method. On a high level, the accounts for the different login methods are linked automatically by SuperTokens provided that: +- Their emails or phone numbers are the same. +- Their emails or phone numbers are verified. + +SuperTokens ensures that accounts are automatically linked only if there is [no risk of account takeover](./security-considerations). + +## Enabling automatic account linking + +You can enable this feature by providing the following callback implementation on the backend SDK: + + + + +```tsx +import supertokens, { User } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; + +supertokens.init({ + supertokens: { + connectionURI: "", + apiKey: "" + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId, user: User | undefined, tenantId: string, userContext: any) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx index c266318f4..fd63b095e 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx @@ -7,10 +7,16 @@ hide_title: true +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' + + + # About account linking Account linking is the process of associating multiple authentication methods with the same account. For example, a user may have a password-based account and a Google account, and they may want to link both of these accounts to the same user account in your application. -Account linking can be done automatically during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. +[Account linking can be done automatically](./automatic-account-linking) during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. + +The other method is to do [manual account linking](./manual-account-linking). Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. -The other method is to do manual account linking. Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. \ No newline at end of file +We will explore both these methods in the next pages. \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/sidebars.js b/v2/thirdpartyemailpassword/sidebars.js index d13bbadcc..ad535413f 100644 --- a/v2/thirdpartyemailpassword/sidebars.js +++ b/v2/thirdpartyemailpassword/sidebars.js @@ -438,7 +438,8 @@ module.exports = { type: "category", label: "Account Linking", items: [ - "common-customizations/account-linking/overview" + "common-customizations/account-linking/overview", + "common-customizations/account-linking/automatic-account-linking" ] }, "common-customizations/change-password", diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx index 5e21cbb9c..acc5838a9 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx @@ -7,10 +7,16 @@ hide_title: true +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' + + + # About account linking Account linking is the process of associating multiple authentication methods with the same account. For example, a user may have a password-based account and a Google account, and they may want to link both of these accounts to the same user account in your application. -Account linking can be done automatically during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. +[Account linking can be done automatically](./automatic-account-linking) during user sign up. In this case, if a user signs up with a second login method with the same email or phone, the two accounts will be automatically linked. There is no need for you (the developer) to perform any additional steps. This approach, of course has a lot of security considerations, which we discuss in the [security considerations](./security-considerations) section. + +The other method is to do [manual account linking](./manual-account-linking). Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. -The other method is to do manual account linking. Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. +We will explore both these methods in the next pages. From bfafa5e94ea643b848670f1a1adfdb4328134e24 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 30 Aug 2023 12:39:52 +0530 Subject: [PATCH 31/81] adds more docs --- .../account-linking/overview.mdx | 46 ++++++++++++++++++ .../automatic-account-linking.mdx | 17 ++++++- .../account-linking/overview.mdx | 48 ++++++++++++++++++- .../account-linking/overview.mdx | 46 ++++++++++++++++++ 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/v2/thirdparty/common-customizations/account-linking/overview.mdx b/v2/thirdparty/common-customizations/account-linking/overview.mdx index acc5838a9..2251b7298 100644 --- a/v2/thirdparty/common-customizations/account-linking/overview.mdx +++ b/v2/thirdparty/common-customizations/account-linking/overview.mdx @@ -20,3 +20,49 @@ Account linking is the process of associating multiple authentication methods wi The other method is to do [manual account linking](./manual-account-linking). Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. We will explore both these methods in the next pages. + +## Important concepts + +### User object +Please make sure you are familiar with the [user object](../../user-object) structure before proceeding. + +### Primary user vs non primary user +The `isPrimaryUser` boolean in the user object dictates if the user is a primary user or not. + +- 1) A primary user is one whose primary user ID does not change when accounts are linked to it. A user 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. + + For example, you **cannot** have the following scenarios: + - User A is a primary user with email `test@example.com` using email passoword login and User B is a primary user with email `test@example.com` using social login. This is not allowed because we have two primary users with the same email. + - User A is a primary user with email `test@example.com` and belongs to tenant `t1` and `t2`. User B is a primary user with email `test@example.com` and belongs to tenant `t2`. This is not allowed because we have two primary users with the same email in tenant `t2` However, if User B was in tenant `t3`, or if User A was not a part of tenant `t2`, then this would be allowed. + + You **can** have the following scenarios: + - User A is not a primary user, with email `test@example.com`, and User B is a primary user with email `test@example.com`. This is allowed because both these users aren't a primary user. + - User A is not a primary user, with email `test@example.com`, and User B is also not a primary user with email `test@example.com`. This is allowed because both these users aren't a primary user. + - User A is a primary user with email `test@exmaple.com` and is linked to User B who is not a primary user with the same email. + + +- 2) For accounts to be linked, **exactly one** of the users that are being linked need to be a primary user. The resulting user ID of the linked accounts will be the primary user's ID. + + For example, if we have User A who is a primary user with user ID `u1`, and we link it to User B (which is not a primary user) with user ID `u2`, then the resulting user's primary ID will be `u1`. The recipe ID (in the login methods of the user object) will continue to be `u1` for User A, and `u2` for User B. + +- 3) Accounts can be linked only if the resulting primary user's email / phone number / third party info is not the same as another primary user's email / phone number / third party info. For example, these accounts **cannot** be linked: + - User A is a primary user with email `e1`. There exists another primary user (User B) with email `e2`. Now we want to link another non primary user (User C) with email `e2`. We cannot linkg User A and User C because it would then lead to two primary users (User A & B) with the same email (`e1`). + +- 4) When making a user a primary user, we check for the primary user condition (point 1) across all the tenants that this user belongs to. + +- 5) When linking two accounts, we check that the primary user condition (point 1) and account linking condition (point 3) are satisfied across the **union** of all the tenants that the primary and the non primary user belongs to. For example, if we are linking User A (tenant `t1`, `t2`) to User B (tenant `t3`), then we will check for the conditions across `t1`, `t2`, and `t3`. + +## Using primary vs recipe user ID +For most purposes, the user's primary user ID is what you would care about. For example, when a user with two login methods, email password and social login, logs in, you will get back the same primary user ID when you get their user ID from the session (or read the `sub` claim in the JWT). + +However, if you want to identify what the login method used for the current session is, you can use the session's `recipeUserId` (via `session.getRecipeUserId()`) and compare its value to the `recipeUserId` in each of the `loginMethods` in the user object. + +There are also some functions from the backend SDK that accept a `recipeUserId` as a parameter. For example, the `updateEmailOrPassword` function from the emailpassword recipe takes in a recipeUserId so that it knows for which login method it needs to make the update for. If it took just a `string` user ID instead, and if you passed it a user's primary user ID, then it may unintentionally lead to updating the wrong login method's email or password, or may throw an error if the primary user is not an email password user. + +## User unlinking +User unlinking is the process of removing a login method from a user. For example, if a user has both email password and social login, and they want to remove their social login, then you can use the unlinking function from our backend SDK. + +There are a few possibilities here: +- 1) If we are unlinking a login method that **is not** associated with the primary user, then it results in two users, one would be the primary user and the other would be the non primary user. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User B, this will result in two separate users: User A (primary user), with one login method (email password) and User B (non primary user) with social login. The primary user ID of user B would be changed to be equal to their recipe user ID. +- 2) If we are unlinking a login method that **is** associated with the primary user, then it results in the login method of the primary user ID being deleted. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User A, this will result in the email password user to be deleted. Now there will only be User B, which is a social login user, and it's primary user ID will be equal to User A's primary user ID (even though the login method for A was deleted). Any metadata, role, sessions info will continue to exist. +- 3) If we are unlinking a User A which is a primary user ID, but it has not linked users, then it will simply result in this user to become a non primary user. diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index d25e8ade8..82ee7fef9 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -72,4 +72,19 @@ Coming Soon ::: - \ No newline at end of file + + +#### Input args meaning: +- `newAccountInfo: AccountInfoWithRecipeId`: This object contains information about the user whose account is going to be linked, or will become a primary user. The object contains the user's email, social login info and phone number (whichever they used to sign in / up with). It also contains the login method (`emailpassword`, `thirdparty`, or `passwordless`). +- `user: User | undefined`: If this is not `undefined`, it means that `newAccountInfo` user is about to linked to this `user`. If this is `undefined`, it means that `newAccountInfo` user is about to become a primary user. +- `tenant: string`: The ID of the tenant that the user is signing in / up to. +- `userContext: any`: User defined userContext. + +#### Output args meaning: +- `shouldAutomaticallyLink`: If this is `true`, it means that the `newAccountInfo` will be linked or will become a primary user during this API call (assuming a set of security checks pass). If this is `false`, it means that there will be no account linking related operation during this API call. +- `shouldRequireVerification`: If this is `true`, that account linking operations will only happen if the `newAccountInfo` is verified. **We strongly recommend keeping it set to `true` for security reasons.** + +You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. + +## Different scenarios of automatic account linking +TODO \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx index fd63b095e..3d3999e18 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx @@ -19,4 +19,50 @@ Account linking is the process of associating multiple authentication methods wi The other method is to do [manual account linking](./manual-account-linking). Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. -We will explore both these methods in the next pages. \ No newline at end of file +We will explore both these methods in the next pages. + +## Important concepts + +### User object +Please make sure you are familiar with the [user object](../../user-object) structure before proceeding. + +### Primary user vs non primary user +The `isPrimaryUser` boolean in the user object dictates if the user is a primary user or not. + +- 1) A primary user is one whose primary user ID does not change when accounts are linked to it. A user 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. + + For example, you **cannot** have the following scenarios: + - User A is a primary user with email `test@example.com` using email passoword login and User B is a primary user with email `test@example.com` using social login. This is not allowed because we have two primary users with the same email. + - User A is a primary user with email `test@example.com` and belongs to tenant `t1` and `t2`. User B is a primary user with email `test@example.com` and belongs to tenant `t2`. This is not allowed because we have two primary users with the same email in tenant `t2` However, if User B was in tenant `t3`, or if User A was not a part of tenant `t2`, then this would be allowed. + + You **can** have the following scenarios: + - User A is not a primary user, with email `test@example.com`, and User B is a primary user with email `test@example.com`. This is allowed because both these users aren't a primary user. + - User A is not a primary user, with email `test@example.com`, and User B is also not a primary user with email `test@example.com`. This is allowed because both these users aren't a primary user. + - User A is a primary user with email `test@exmaple.com` and is linked to User B who is not a primary user with the same email. + + +- 2) For accounts to be linked, **exactly one** of the users that are being linked need to be a primary user. The resulting user ID of the linked accounts will be the primary user's ID. + + For example, if we have User A who is a primary user with user ID `u1`, and we link it to User B (which is not a primary user) with user ID `u2`, then the resulting user's primary ID will be `u1`. The recipe ID (in the login methods of the user object) will continue to be `u1` for User A, and `u2` for User B. + +- 3) Accounts can be linked only if the resulting primary user's email / phone number / third party info is not the same as another primary user's email / phone number / third party info. For example, these accounts **cannot** be linked: + - User A is a primary user with email `e1`. There exists another primary user (User B) with email `e2`. Now we want to link another non primary user (User C) with email `e2`. We cannot linkg User A and User C because it would then lead to two primary users (User A & B) with the same email (`e1`). + +- 4) When making a user a primary user, we check for the primary user condition (point 1) across all the tenants that this user belongs to. + +- 5) When linking two accounts, we check that the primary user condition (point 1) and account linking condition (point 3) are satisfied across the **union** of all the tenants that the primary and the non primary user belongs to. For example, if we are linking User A (tenant `t1`, `t2`) to User B (tenant `t3`), then we will check for the conditions across `t1`, `t2`, and `t3`. + +## Using primary vs recipe user ID +For most purposes, the user's primary user ID is what you would care about. For example, when a user with two login methods, email password and social login, logs in, you will get back the same primary user ID when you get their user ID from the session (or read the `sub` claim in the JWT). + +However, if you want to identify what the login method used for the current session is, you can use the session's `recipeUserId` (via `session.getRecipeUserId()`) and compare its value to the `recipeUserId` in each of the `loginMethods` in the user object. + +There are also some functions from the backend SDK that accept a `recipeUserId` as a parameter. For example, the `updateEmailOrPassword` function from the emailpassword recipe takes in a recipeUserId so that it knows for which login method it needs to make the update for. If it took just a `string` user ID instead, and if you passed it a user's primary user ID, then it may unintentionally lead to updating the wrong login method's email or password, or may throw an error if the primary user is not an email password user. + +## User unlinking +User unlinking is the process of removing a login method from a user. For example, if a user has both email password and social login, and they want to remove their social login, then you can use the unlinking function from our backend SDK. + +There are a few possibilities here: +- 1) If we are unlinking a login method that **is not** associated with the primary user, then it results in two users, one would be the primary user and the other would be the non primary user. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User B, this will result in two separate users: User A (primary user), with one login method (email password) and User B (non primary user) with social login. The primary user ID of user B would be changed to be equal to their recipe user ID. +- 2) If we are unlinking a login method that **is** associated with the primary user, then it results in the login method of the primary user ID being deleted. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User A, this will result in the email password user to be deleted. Now there will only be User B, which is a social login user, and it's primary user ID will be equal to User A's primary user ID (even though the login method for A was deleted). Any metadata, role, sessions info will continue to exist. +- 3) If we are unlinking a User A which is a primary user ID, but it has not linked users, then it will simply result in this user to become a non primary user. \ No newline at end of file diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx index acc5838a9..2251b7298 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx @@ -20,3 +20,49 @@ Account linking is the process of associating multiple authentication methods wi The other method is to do [manual account linking](./manual-account-linking). Here, you (the developer) can make your own API which uses the account linking functions provided by our backend SDK to link two accounts. This API can then be called, for example, by your support team whenever needed. We will explore both these methods in the next pages. + +## Important concepts + +### User object +Please make sure you are familiar with the [user object](../../user-object) structure before proceeding. + +### Primary user vs non primary user +The `isPrimaryUser` boolean in the user object dictates if the user is a primary user or not. + +- 1) A primary user is one whose primary user ID does not change when accounts are linked to it. A user 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. + + For example, you **cannot** have the following scenarios: + - User A is a primary user with email `test@example.com` using email passoword login and User B is a primary user with email `test@example.com` using social login. This is not allowed because we have two primary users with the same email. + - User A is a primary user with email `test@example.com` and belongs to tenant `t1` and `t2`. User B is a primary user with email `test@example.com` and belongs to tenant `t2`. This is not allowed because we have two primary users with the same email in tenant `t2` However, if User B was in tenant `t3`, or if User A was not a part of tenant `t2`, then this would be allowed. + + You **can** have the following scenarios: + - User A is not a primary user, with email `test@example.com`, and User B is a primary user with email `test@example.com`. This is allowed because both these users aren't a primary user. + - User A is not a primary user, with email `test@example.com`, and User B is also not a primary user with email `test@example.com`. This is allowed because both these users aren't a primary user. + - User A is a primary user with email `test@exmaple.com` and is linked to User B who is not a primary user with the same email. + + +- 2) For accounts to be linked, **exactly one** of the users that are being linked need to be a primary user. The resulting user ID of the linked accounts will be the primary user's ID. + + For example, if we have User A who is a primary user with user ID `u1`, and we link it to User B (which is not a primary user) with user ID `u2`, then the resulting user's primary ID will be `u1`. The recipe ID (in the login methods of the user object) will continue to be `u1` for User A, and `u2` for User B. + +- 3) Accounts can be linked only if the resulting primary user's email / phone number / third party info is not the same as another primary user's email / phone number / third party info. For example, these accounts **cannot** be linked: + - User A is a primary user with email `e1`. There exists another primary user (User B) with email `e2`. Now we want to link another non primary user (User C) with email `e2`. We cannot linkg User A and User C because it would then lead to two primary users (User A & B) with the same email (`e1`). + +- 4) When making a user a primary user, we check for the primary user condition (point 1) across all the tenants that this user belongs to. + +- 5) When linking two accounts, we check that the primary user condition (point 1) and account linking condition (point 3) are satisfied across the **union** of all the tenants that the primary and the non primary user belongs to. For example, if we are linking User A (tenant `t1`, `t2`) to User B (tenant `t3`), then we will check for the conditions across `t1`, `t2`, and `t3`. + +## Using primary vs recipe user ID +For most purposes, the user's primary user ID is what you would care about. For example, when a user with two login methods, email password and social login, logs in, you will get back the same primary user ID when you get their user ID from the session (or read the `sub` claim in the JWT). + +However, if you want to identify what the login method used for the current session is, you can use the session's `recipeUserId` (via `session.getRecipeUserId()`) and compare its value to the `recipeUserId` in each of the `loginMethods` in the user object. + +There are also some functions from the backend SDK that accept a `recipeUserId` as a parameter. For example, the `updateEmailOrPassword` function from the emailpassword recipe takes in a recipeUserId so that it knows for which login method it needs to make the update for. If it took just a `string` user ID instead, and if you passed it a user's primary user ID, then it may unintentionally lead to updating the wrong login method's email or password, or may throw an error if the primary user is not an email password user. + +## User unlinking +User unlinking is the process of removing a login method from a user. For example, if a user has both email password and social login, and they want to remove their social login, then you can use the unlinking function from our backend SDK. + +There are a few possibilities here: +- 1) If we are unlinking a login method that **is not** associated with the primary user, then it results in two users, one would be the primary user and the other would be the non primary user. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User B, this will result in two separate users: User A (primary user), with one login method (email password) and User B (non primary user) with social login. The primary user ID of user B would be changed to be equal to their recipe user ID. +- 2) If we are unlinking a login method that **is** associated with the primary user, then it results in the login method of the primary user ID being deleted. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User A, this will result in the email password user to be deleted. Now there will only be User B, which is a social login user, and it's primary user ID will be equal to User A's primary user ID (even though the login method for A was deleted). Any metadata, role, sessions info will continue to exist. +- 3) If we are unlinking a User A which is a primary user ID, but it has not linked users, then it will simply result in this user to become a non primary user. From b04a54479f9780b971744b8268d52704b1814d95 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 30 Aug 2023 12:42:18 +0530 Subject: [PATCH 32/81] adds more docs --- .../common-customizations/account-linking/overview.mdx | 4 ++++ .../account-linking/automatic-account-linking.mdx | 9 ++++++++- .../common-customizations/account-linking/overview.mdx | 6 +++++- .../common-customizations/account-linking/overview.mdx | 4 ++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/v2/thirdparty/common-customizations/account-linking/overview.mdx b/v2/thirdparty/common-customizations/account-linking/overview.mdx index 2251b7298..2647d3f18 100644 --- a/v2/thirdparty/common-customizations/account-linking/overview.mdx +++ b/v2/thirdparty/common-customizations/account-linking/overview.mdx @@ -66,3 +66,7 @@ There are a few possibilities here: - 1) If we are unlinking a login method that **is not** associated with the primary user, then it results in two users, one would be the primary user and the other would be the non primary user. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User B, this will result in two separate users: User A (primary user), with one login method (email password) and User B (non primary user) with social login. The primary user ID of user B would be changed to be equal to their recipe user ID. - 2) If we are unlinking a login method that **is** associated with the primary user, then it results in the login method of the primary user ID being deleted. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User A, this will result in the email password user to be deleted. Now there will only be User B, which is a social login user, and it's primary user ID will be equal to User A's primary user ID (even though the login method for A was deleted). Any metadata, role, sessions info will continue to exist. - 3) If we are unlinking a User A which is a primary user ID, but it has not linked users, then it will simply result in this user to become a non primary user. + +:::important +All of the above checks happen automatically, so you don't need to worry about them. But it is important to understand what's happening. +::: diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 82ee7fef9..e74eb888b 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -87,4 +87,11 @@ Coming Soon You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. ## Different scenarios of automatic account linking -TODO \ No newline at end of file +TODO + +## Migration of user data when accounts are linked +TODO + +## Support status codes +TODO + diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx index 3d3999e18..29abb139e 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/overview.mdx @@ -65,4 +65,8 @@ User unlinking is the process of removing a login method from a user. For exampl There are a few possibilities here: - 1) If we are unlinking a login method that **is not** associated with the primary user, then it results in two users, one would be the primary user and the other would be the non primary user. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User B, this will result in two separate users: User A (primary user), with one login method (email password) and User B (non primary user) with social login. The primary user ID of user B would be changed to be equal to their recipe user ID. - 2) If we are unlinking a login method that **is** associated with the primary user, then it results in the login method of the primary user ID being deleted. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User A, this will result in the email password user to be deleted. Now there will only be User B, which is a social login user, and it's primary user ID will be equal to User A's primary user ID (even though the login method for A was deleted). Any metadata, role, sessions info will continue to exist. -- 3) If we are unlinking a User A which is a primary user ID, but it has not linked users, then it will simply result in this user to become a non primary user. \ No newline at end of file +- 3) If we are unlinking a User A which is a primary user ID, but it has not linked users, then it will simply result in this user to become a non primary user. + +:::important +All of the above checks happen automatically, so you don't need to worry about them. But it is important to understand what's happening. +::: \ No newline at end of file diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx index 2251b7298..2647d3f18 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/overview.mdx @@ -66,3 +66,7 @@ There are a few possibilities here: - 1) If we are unlinking a login method that **is not** associated with the primary user, then it results in two users, one would be the primary user and the other would be the non primary user. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User B, this will result in two separate users: User A (primary user), with one login method (email password) and User B (non primary user) with social login. The primary user ID of user B would be changed to be equal to their recipe user ID. - 2) If we are unlinking a login method that **is** associated with the primary user, then it results in the login method of the primary user ID being deleted. For example, if User A (primary user, with email password login) is linked with User B (social login), and then we decide to unlink User A, this will result in the email password user to be deleted. Now there will only be User B, which is a social login user, and it's primary user ID will be equal to User A's primary user ID (even though the login method for A was deleted). Any metadata, role, sessions info will continue to exist. - 3) If we are unlinking a User A which is a primary user ID, but it has not linked users, then it will simply result in this user to become a non primary user. + +:::important +All of the above checks happen automatically, so you don't need to worry about them. But it is important to understand what's happening. +::: From 8cc77aa9f9c77a1db70c45bb02261fba87824a90 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 31 Aug 2023 13:08:44 +0530 Subject: [PATCH 33/81] more changes --- .../automatic-account-linking.mdx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index e74eb888b..2076fcb80 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -87,7 +87,22 @@ Coming Soon You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. ## Different scenarios of automatic account linking -TODO + +- **During sign up**: If there exists another account with the same email or phone number within the current tenant, the new account will be linked to the existing account if: + - The existing account is a primary user + - If `shouldRequireVerification` is `true`, the new account needs to be created via a method that has the email as verified (for example via passwordless or google login). If the new method doesn't inherently verify the email (like in email password login), the the accounts will be linked post email verification. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for `shouldAutomaticallyLink`. + +- **During sign in**: If the current user is not already linked and if there exists another user with the same email or phone number within the current tenant, the accounts will be linked if: + - The user being signed into is NOT a primary user, and the other user with the same email / phone number IS a primary user + - If `shouldRequireVerification` is `true`, the current account (that's being signed into) has its email as verified. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for `shouldAutomaticallyLink`. + +- **After email verification**: If the current user whose email got verified is not a primary user, and there exists another primary user in the same tenant with the same email, then we link the two accounts if: + - TODO + +- **During password reset flow**: If there already exists a user with the same email in a non email password recipe (social login for example), and the user is doing a password reset flow, a new email password user is created and linked to the existing account if: + - TODO ## Migration of user data when accounts are linked TODO From 3fb62e899f525d2d56fc2fef167de31f99f2485d Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 31 Aug 2023 14:33:22 +0530 Subject: [PATCH 34/81] more changes --- .../account-linking/automatic-account-linking.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 2076fcb80..5ca883be0 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -91,15 +91,15 @@ You can use the input of the function to dynamically decide if you want to do ac - **During sign up**: If there exists another account with the same email or phone number within the current tenant, the new account will be linked to the existing account if: - The existing account is a primary user - If `shouldRequireVerification` is `true`, the new account needs to be created via a method that has the email as verified (for example via passwordless or google login). If the new method doesn't inherently verify the email (like in email password login), the the accounts will be linked post email verification. - - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for `shouldAutomaticallyLink`. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. - **During sign in**: If the current user is not already linked and if there exists another user with the same email or phone number within the current tenant, the accounts will be linked if: - The user being signed into is NOT a primary user, and the other user with the same email / phone number IS a primary user - If `shouldRequireVerification` is `true`, the current account (that's being signed into) has its email as verified. - - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for `shouldAutomaticallyLink`. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. - **After email verification**: If the current user whose email got verified is not a primary user, and there exists another primary user in the same tenant with the same email, then we link the two accounts if: - - TODO + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. - **During password reset flow**: If there already exists a user with the same email in a non email password recipe (social login for example), and the user is doing a password reset flow, a new email password user is created and linked to the existing account if: - TODO From fa002b09ff5763a9b0db27e00426b33d584e84d7 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 1 Sep 2023 12:24:06 +0530 Subject: [PATCH 35/81] more docs --- .../automatic-account-linking.mdx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 5ca883be0..5b8b82c70 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -102,7 +102,27 @@ You can use the input of the function to dynamically decide if you want to do ac - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. - **During password reset flow**: If there already exists a user with the same email in a non email password recipe (social login for example), and the user is doing a password reset flow, a new email password user is created and linked to the existing account if: - - TODO + - The non email password user is a primary user. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +## Affect on email verification +For a primary user, if there exists two login method (L1 & L2), and they both have the same email, but the email for L1 is verified and not for L2, SupeTokens will auto verify the email for L2 when: +- The user next logs in with L2. +- If you call `updateEmailOrPassword` from the email password or `updateUser` from the passwordless recipe, updating L2's email to be equal to L1's email. + +## Affect on email update +When updating the email of a login method for a user, SuperTokens needs to make sure that the account linking conditions mentioned above remain intact. This means that you cannot update the email of a primary user to a value that matches the email of another primary user. + +For example, if User A has login method `AL1` (email `e1`) and `AL2` (email `e1`), and User B has login method `BL1` (email `e2`) and `BL2` (email `e3`), then we cannot update `AL1` email to `e2` or `e3` because that would lead to two primary users having the same email. + +Now email updates can happen in different scenarios: +- 1) Calling the `updateEmailOrPassword` from the email password recipe +- 2) Calling the `updateUser` function from the passwordless recipe +- 3) Logging in via social login can also update emails if the email has changed from the provider's side. + +In each of these cases, the operation will fail and an appropriate status code will be returned: +- For function calls (1) and (2), you will get back a response with a status indicating that email update was not possible. +- For social login API call (3), the client will get a response with a status indicating to contact support, with a support status code (see below). ## Migration of user data when accounts are linked TODO From cad2021faa4706d0757132e2a9481e04e5683314 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 1 Sep 2023 14:37:26 +0530 Subject: [PATCH 36/81] more changes --- .../codeTypeChecking/jsEnv/package.json | 2 +- .../automatic-account-linking.mdx | 104 +++++++++++++++++- 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/v2/src/plugins/codeTypeChecking/jsEnv/package.json b/v2/src/plugins/codeTypeChecking/jsEnv/package.json index 3302eef6b..198c6c081 100644 --- a/v2/src/plugins/codeTypeChecking/jsEnv/package.json +++ b/v2/src/plugins/codeTypeChecking/jsEnv/package.json @@ -53,7 +53,7 @@ "socket.io": "^4.6.1", "socketio": "^1.0.0", "supertokens-auth-react": "^0.34.0", - "supertokens-node": "github:supertokens/supertokens-node#feat/account_linking/optimizations", + "supertokens-node": "github:supertokens/supertokens-node#account-linking", "supertokens-node7": "npm:supertokens-node@7.3", "supertokens-react-native": "^4.0.0", "supertokens-web-js": "^0.7.0", diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 5b8b82c70..b906786e2 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -28,7 +28,7 @@ You can enable this feature by providing the following callback implementation o ```tsx -import supertokens, { User } from "supertokens-node"; +import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; @@ -45,7 +45,16 @@ supertokens.init({ recipeList: [ // highlight-start AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId, user: User | undefined, tenantId: string, userContext: any) => { + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + if (newAccountInfo.recipeUserId !== undefined && user !== undefined) { + let userId = newAccountInfo.recipeUserId.getAsString(); + let hasInfoAssociatedWithUserId = false // TODO: add your own implementation here. + if (hasInfoAssociatedWithUserId) { + return { + shouldAutomaticallyLink: false + } + } + } return { shouldAutomaticallyLink: true, shouldRequireVerification: true @@ -75,7 +84,9 @@ Coming Soon #### Input args meaning: -- `newAccountInfo: AccountInfoWithRecipeId`: This object contains information about the user whose account is going to be linked, or will become a primary user. The object contains the user's email, social login info and phone number (whichever they used to sign in / up with). It also contains the login method (`emailpassword`, `thirdparty`, or `passwordless`). +- `newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }`: This object contains information about the user whose account is going to be linked, or will become a primary user. The object contains the user's email, social login info and phone number (whichever they used to sign in / up with). It also contains the login method (`emailpassword`, `thirdparty`, or `passwordless`). It may also contain the `recipeUserId` of the user that is going to be linked in case SuperTokens is attempting account linking during sign in. + + Notice that in case of `newAccountInfo.recipeUserId !== undefined && user !== undefined` being `true`, we add some extra logic to check if the user ID has any info associated with them in your application db. This is to prevent data loss for this user ID (see the [migtation section below](#migration-of-user-data-when-accounts-are-linked)). - `user: User | undefined`: If this is not `undefined`, it means that `newAccountInfo` user is about to linked to this `user`. If this is `undefined`, it means that `newAccountInfo` user is about to become a primary user. - `tenant: string`: The ID of the tenant that the user is signing in / up to. - `userContext: any`: User defined userContext. @@ -125,8 +136,91 @@ In each of these cases, the operation will fail and an appropriate status code w - For social login API call (3), the client will get a response with a status indicating to contact support, with a support status code (see below). ## Migration of user data when accounts are linked -TODO +When two accounts are linked the primary user ID of the non primary user changes. + +For example, if we have User A with with primary user ID `p1` and user B, which is a non primary user, and has a user ID of `p2`, and we link them, then the primary user ID of User B will be changed to `p1`. + +This has an effect that if the user logs in with login method from User B, the `session.getUserId()` will return `p1`. If there was any older data associated with User B (against user ID `p2`), in your database, that data will essentially be "lost". + +To prevent this scenario, you should: +- Make sure that you return `false` for `shouldAutomaticallyLink` boolean in the `shouldDoAutomaticAccountLinking` function implementation if there exists a `recipeUserId` in the `newAccountInfo` object, and if you have some information related to that user ID in your own database. This can be seen in the [code snippet above](#enabling-automatic-account-linking). + +- If you do not want to return `false` in this case, and want the accounts to be linked, then make sure to implement the `onAccountLinked` callback: + + ```tsx + import supertokens, { User, RecipeUserId } from "supertokens-node"; + import AccountLinking from "supertokens-node/recipe/accountlinking"; + import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; + + supertokens.init({ + supertokens: { + connectionURI: "", + apiKey: "" + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + }, + // highlight-start + onAccountLinked: async (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => { + let olderUserId = newAccountInfo.recipeUserId.getAsString() + let newUserId = user.id; + + // TODO: migrate data from olderUserId to newUserId in your database... + } + // highlight-end + }) + ] + }); + ``` + +:::caution +If your logic in `onAccountLinked` throws an error, then it will not be called again, and will still have resulted in the accounts being linked. +::: ## Support status codes -TODO +The following is a list of support status codes that the end user might see during their interaction with the login UI (as a general error message in the pre built UI). + +#### HAS_OTHER_EMAIL_OR_PHONE +- This can happen when creating a password reset code in the email password flow. +- TODO.. explain why and how to fix + +#### IS_SIGN_UP_ALLOWED_FALSE +- Passwordless +- API call: consumeCodePOST & createCodePOST + +#### IS_SIGN_IN_ALLOWED_FALSE +- Passwordless +- API call: consumeCodePOST & createCodePOST + +- Third party +- API call: signInUpPOST +Signing in case + +#### ANOTHER_PRIM_USER_HAS_EMAIL +- Third party +- API call: signInUpPOST +Signing in, when the email has changed and another primary user has the same email. + +#### EMAIL_ALREADY_USED_IN_ANOTHER_ACCOUNT +- Third party +- API call: signInUpPOST + +Signing up. + +#### User is seeing incorrect email password combination / `WRONG_CREDENTIALS_ERROR` +- Email password +- API call: sign in +#### User is seeing email already exists / EMAIL_ALREADY_EXISTS_ERROR +- Email password +- Sign up \ No newline at end of file From dc1e509eff6ac172dfb1fe925593813281388816 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 1 Sep 2023 17:17:52 +0530 Subject: [PATCH 37/81] more docs --- .../account-linking/automatic-account-linking.mdx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index b906786e2..f52a90a97 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -191,8 +191,14 @@ If your logic in `onAccountLinked` throws an error, then it will not be called a The following is a list of support status codes that the end user might see during their interaction with the login UI (as a general error message in the pre built UI). #### HAS_OTHER_EMAIL_OR_PHONE -- This can happen when creating a password reset code in the email password flow. -- TODO.. explain why and how to fix +- This can happen during creating a password reset code in the email password flow. +- Below is the scenario for when this status is returned: + + A malicious, User A, which is a primary user, has login methods with email `e1` (social login) and email `e2` (email password login). If user A changes their emailpassword email to `e2` (which is now in unverified state), and the real user of `e2` tries to sign up via email password, they will see a message saying that the email already exists. The victim may then try to do a reset password (thinking they had previously signed up). If we allow this to happen, then the victim will be able to login to the account, and the attacker can spy on what the user is doing via their social login, login method. + + To prevent this method of account takeover, we check that the password reset flow + account linking can only happen if the primary user has a login method with the input email as already verified, or if not, then we check that the primary user has no other login method with a different email. + +- To solve this state, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user which their new email password account would be linked to. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. #### IS_SIGN_UP_ALLOWED_FALSE - Passwordless From 191f09f85ad01c8297a2feb5ba3a5e46c0b07be1 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 1 Sep 2023 19:28:38 +0530 Subject: [PATCH 38/81] more docs --- .../account-linking/automatic-account-linking.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index f52a90a97..e9d286ba2 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -190,7 +190,7 @@ If your logic in `onAccountLinked` throws an error, then it will not be called a ## Support status codes The following is a list of support status codes that the end user might see during their interaction with the login UI (as a general error message in the pre built UI). -#### HAS_OTHER_EMAIL_OR_PHONE +#### ERR_CODE_001 - This can happen during creating a password reset code in the email password flow. - Below is the scenario for when this status is returned: @@ -200,24 +200,25 @@ The following is a list of support status codes that the end user might see duri - To solve this state, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user which their new email password account would be linked to. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. -#### IS_SIGN_UP_ALLOWED_FALSE +#### ERR_CODE_002 - Passwordless - API call: consumeCodePOST & createCodePOST -#### IS_SIGN_IN_ALLOWED_FALSE +#### ERR_CODE_003 - Passwordless - API call: consumeCodePOST & createCodePOST +#### ERR_CODE_004 - Third party - API call: signInUpPOST Signing in case -#### ANOTHER_PRIM_USER_HAS_EMAIL +#### ERR_CODE_005 - Third party - API call: signInUpPOST Signing in, when the email has changed and another primary user has the same email. -#### EMAIL_ALREADY_USED_IN_ANOTHER_ACCOUNT +#### ERR_CODE_006 - Third party - API call: signInUpPOST From 1ec4801d70309d9bc5c800384fcc3f90b91588df Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 1 Sep 2023 22:42:18 +0530 Subject: [PATCH 39/81] more docs --- .../account-linking/automatic-account-linking.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index e9d286ba2..fdc416b86 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -194,11 +194,11 @@ The following is a list of support status codes that the end user might see duri - This can happen during creating a password reset code in the email password flow. - Below is the scenario for when this status is returned: - A malicious, User A, which is a primary user, has login methods with email `e1` (social login) and email `e2` (email password login). If user A changes their emailpassword email to `e2` (which is now in unverified state), and the real user of `e2` tries to sign up via email password, they will see a message saying that the email already exists. The victim may then try to do a reset password (thinking they had previously signed up). If we allow this to happen, then the victim will be able to login to the account, and the attacker can spy on what the user is doing via their social login, login method. + A malicious user, User A, which is a primary user, has login methods with email `e1` (social login) and email `e1` (email password login). If user A changes their emailpassword email to `e2` (which is now in unverified state), and the real user of `e2` (the victim) tries to sign up via email password, they will see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If we allow this to happen, then the victim will be able to login to the account, and the attacker can spy on what the user is doing via their social login, login method. - To prevent this method of account takeover, we check that the password reset flow + account linking can only happen if the primary user has a login method with the input email as already verified, or if not, then we check that the primary user has no other login method with a different email. + To prevent this method of account takeover, we check that the password reset flow can only happen if the primary user has at least one login method with the input email which is already verified, or if not, then we check that the primary user has no other login method with a different email, or phone number. -- To solve this state, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user which their new email password account would be linked to. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. +- To solve this state, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. #### ERR_CODE_002 - Passwordless From 96c2bd37d08ca6211bef8bceb832989ff79d2e43 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 2 Sep 2023 11:30:01 +0530 Subject: [PATCH 40/81] more docs --- .../automatic-account-linking.mdx | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index fdc416b86..2a77413d5 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -190,44 +190,56 @@ If your logic in `onAccountLinked` throws an error, then it will not be called a ## Support status codes The following is a list of support status codes that the end user might see during their interaction with the login UI (as a general error message in the pre built UI). -#### ERR_CODE_001 -- This can happen during creating a password reset code in the email password flow. +### ERR_CODE_001 +- This can happen during creating a password reset code in the email password flow: + - API path and method: `/user/password/reset/token POST` + - Output JSON: + ```json + { + "status": "PASSWORD_RESET_NOT_ALLOWED", + "reason": + "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)", + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + - Below is the scenario for when this status is returned: - A malicious user, User A, which is a primary user, has login methods with email `e1` (social login) and email `e1` (email password login). If user A changes their emailpassword email to `e2` (which is now in unverified state), and the real user of `e2` (the victim) tries to sign up via email password, they will see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If we allow this to happen, then the victim will be able to login to the account, and the attacker can spy on what the user is doing via their social login, login method. + A malicious user, User A, which is a primary user, has login methods with email `e1` (social login) and email `e1` (email password login). If user A changes their emailpassword email to `e2` (which is now in unverified state), and the real user of `e2` (the victim) tries to sign up via email password, they will see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If we allow this to happen, and the victim resets the password (since they are the real owner of the email), then they will be able to login to the account, and the attacker can spy on what the user is doing via their third party login method. - To prevent this method of account takeover, we check that the password reset flow can only happen if the primary user has at least one login method with the input email which is already verified, or if not, then we check that the primary user has no other login method with a different email, or phone number. + To prevent this scenario, we enforce that the password link is only generated if the primary user has at least one login method that has the input email ID and is verified, or if not, we check that the primary user has no other login method with a different email, or phone number. If these cases are not satisfied, then we return the error code `ERR_CODE_001`. -- To solve this state, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. +- To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **These actions can be taken via our user management dashboard.** -#### ERR_CODE_002 +### ERR_CODE_002 - Passwordless - API call: consumeCodePOST & createCodePOST -#### ERR_CODE_003 +### ERR_CODE_003 - Passwordless - API call: consumeCodePOST & createCodePOST -#### ERR_CODE_004 +### ERR_CODE_004 - Third party - API call: signInUpPOST Signing in case -#### ERR_CODE_005 +### ERR_CODE_005 - Third party - API call: signInUpPOST Signing in, when the email has changed and another primary user has the same email. -#### ERR_CODE_006 +### ERR_CODE_006 - Third party - API call: signInUpPOST Signing up. -#### User is seeing incorrect email password combination / `WRONG_CREDENTIALS_ERROR` +### User is seeing incorrect email password combination / `WRONG_CREDENTIALS_ERROR` - Email password - API call: sign in -#### User is seeing email already exists / EMAIL_ALREADY_EXISTS_ERROR +### User is seeing email already exists / EMAIL_ALREADY_EXISTS_ERROR - Email password - Sign up \ No newline at end of file From af006483cefa1aee2a6468734e06cb68b2fc5b11 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 2 Sep 2023 12:07:19 +0530 Subject: [PATCH 41/81] more docs --- .../account-linking/automatic-account-linking.mdx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 2a77413d5..4e00f7152 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -236,10 +236,14 @@ Signing in, when the email has changed and another primary user has the same ema Signing up. -### User is seeing incorrect email password combination / `WRONG_CREDENTIALS_ERROR` +### Other edge cases +#### User is seeing incorrect email password combination / `WRONG_CREDENTIALS_ERROR` - Email password - API call: sign in -### User is seeing email already exists / EMAIL_ALREADY_EXISTS_ERROR +#### User is seeing email already exists / EMAIL_ALREADY_EXISTS_ERROR - Email password -- Sign up \ No newline at end of file +- Sign up + +### Changing the error message on the frontend +TODO \ No newline at end of file From c1cb86e133f90779bfadbdb4e09664dfb2e4af81 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 4 Sep 2023 16:45:48 +0530 Subject: [PATCH 42/81] more docs --- .../automatic-account-linking.mdx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 4e00f7152..97b82a40e 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -213,8 +213,28 @@ The following is a list of support status codes that the end user might see duri - To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **These actions can be taken via our user management dashboard.** ### ERR_CODE_002 -- Passwordless -- API call: consumeCodePOST & createCodePOST +- This can happen during the passwordless recipe's create or consume code API: + - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` + - Output JSON: + ```json + { + "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)"; + } + ``` + - The pre build UI on the frontend displays this error in the following way: + - For create code: + Pre built UI screenshot for showing error message. + - For consume code: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + A user is trying to sign up using passwordless login method with email `e1`. There exists an email password login method with `e1`, which is unverified (owned by an attacker). If we allow the passwordless sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the passwordless login method. This way, the attacker now has access to the user's account. + + To prevent this, we do not allow sign up with passwordless login in case there exists another account with the same email and is unverified. + +- To resolve this issue, you should ask the user to try another login method (which already has their email), or then ask then manually mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can manually mark email for the other account as verified using the dashboard.** + ### ERR_CODE_003 - Passwordless From a51ea54846dea3dd12b564ceacea286b988c01e9 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 6 Sep 2023 13:44:36 +0530 Subject: [PATCH 43/81] adds images for error codes --- v2/static/img/account-linking/err_001.png | Bin 0 -> 40257 bytes v2/static/img/account-linking/err_002.png | Bin 0 -> 43426 bytes v2/static/img/account-linking/err_003.png | Bin 0 -> 43322 bytes v2/static/img/account-linking/err_004.png | Bin 0 -> 40998 bytes v2/static/img/account-linking/err_005.png | Bin 0 -> 41997 bytes v2/static/img/account-linking/err_006.png | Bin 0 -> 41107 bytes .../automatic-account-linking.mdx | 11 ++++++----- 7 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 v2/static/img/account-linking/err_001.png create mode 100644 v2/static/img/account-linking/err_002.png create mode 100644 v2/static/img/account-linking/err_003.png create mode 100644 v2/static/img/account-linking/err_004.png create mode 100644 v2/static/img/account-linking/err_005.png create mode 100644 v2/static/img/account-linking/err_006.png diff --git a/v2/static/img/account-linking/err_001.png b/v2/static/img/account-linking/err_001.png new file mode 100644 index 0000000000000000000000000000000000000000..599cdd0272c4ad6fe71043d0a5467feff73e122c GIT binary patch literal 40257 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%0AG`lMXPq+7lDP543v%^) z!*ox=xJPQS)1h!}p+mdmmQ(IZTD=*u{_XppNdCyTG59Jq%z>e+fKyX)Dx4AmVWu^Q z<>`QxB5u(v1dokR2J5q|um@=f(bWHfwVB*?MIrW(8nv&lB5kq0&65W0ouY;$c$>vz z(9WjzqxdjIBtwq%rcw-xpGXkk7WNEz27KzI{K+yfzFA!}M=Gf|17@>ql|CrW{Gd*raFYeK0-3cPVMk#OCb87t&s0MrqeE?)cqHxf2+ zQyQ2q?cKe*w?LNJ8x9l><(pY;VP9pO-Sz^0FUC~?L|zsf7q_`~Y0cAfE~4ifV%%ie zhNsGLz4wlHa^-J1HJ&Jl~Tvc7$-#evzk3YYw))Ajh*;3SH-Jjc{FOE59 z={Vl{9Tr82GjNB~NE%K_FBS7#sx@GbI_4xsr0=>O^G%i5{mpX&U&dE)-L7N995iV| zJ!C?|CDWQU4$FjGJBX&e>ZGyh=>kU~KlJXKm6Y)C3L@ ziFUu^!gNAQVDNVawjYn-1@kLwf3)jIctd%|b)xn$B(ATOXQ^%5hJ6KEna0Z2oST=P zWbW9$oef??lKL0}W3DPJjmIb&*{E5Bj!rhjzUP;eY`K9HIb^DZlH1*|)3$=vr(;5P zbbxOON+Eaw-zU@XxI-||DE#MxH@4c1DBLD$WEV#=)Gryw~iRgL4DnXvhd|d z#NY03a%J--3mWt-Edxh%My<|}vEWB`639K+y>ftgMZv_N{Y|A6GpAA6#`*4=K+kDgXi zQYxv`iCO~&d~4oZ1eso1+4?Q#@zxV5S5k>>7Zr|vMr6y_W*z|nfdZI9Y>KF_L?uEv zXywk|mf^zMs)BRSNLNulbXMIZOPCWx_JtbR6u0|Mx9}NkXUZyF*F5E5sbvi8h47`x z`Z_kR(?{<6`0;h5OWR?blRlcRm3bGANqzQ(cL%>pPszle3;zgDl)J)|*wFYSB#u^JqDcnNWfdd5DH*SEOwApOoDzaoYzic&7q4+X+@JC2ki+6=-G=G)vzSQiWG`* zpQRGk$;tT1I$E}qQEH8!j4{YOBqU@8@rc;BM6oJK@Sh}7N|f95(FMapLw(LgOP>#V zGRoLl!disoJTKx8c zrFGGSMEr#qE}1mUZsuLPYUMVTE^y(2IOr%FjxTcf7{R#ej=;Qid(I=lw1T_xJo50^ z^1|*S)Rj*1Tuzo3gBcei(Yj)RdjY$|@fmMB)W^%d~|TwdZ4G zqY$j0Mn>vREW>PP7BCg#Rtn_gKyCO#CpB}|&L#f~H1D0pe!0-rrCm?aOdpDL_)7K* z+oUnc0cQ7l>k1c>ck%%^3w%FC>w%I;d#k z-o2|6V|k^!)VfLZ!VqeLnnbm?Z1Au`5zyb~e{kR4y@M}SUP#(Ro6wYtH^@{VXuIj! z>EE8nB@$>FZrMmI5qm@$tu*zPg`+jFYfNij>qKS@kmm4F?yVFBh+D!Y&1rhKbu+_S z$bqOO8v38aQ)&Y_05&%mZWc}U3N}r}NU8}`?ayS_E<8PU`kV0gbH3fQ@+i`&SsP*h z9@0iwb~XS!ID>`r8~8{@9cDXHoP&Q6QI$~M{#_c5aYHaxR8mTkpmJN8$mbUp9)uOx z5Ncb#;?t~Y>|^NPQht>DIs-%J?n4K0>!95>8+#UbU>7kbsY7tX18E3&<~B(>+{{?Tf?LrA!;12idL z0*3Kx#ucA=A;bz+X(>V}zwz2)_x*cqgZqt8j~*NS5>RhSgg#T0f$n%tVt`O=2f!_= z5oPS#$+yT1>!OcKAMHVWQ7Jj{`LZ^$OT0|39?unFYF3c}EOR3)nqmxKl@rGxO5_y1 z^*Zr}i#j^&k!3?KE&g`&tX~;!j-FLWMS&GMh$N6(nIc0ZE*70vK1kGQh85CIAFx4WwmaH~C3lFDyaW7s)S((5m zg{TyCJFu>~MLp&jn*_e#9!-YNId>u{SX>+0M$^ z()ymj_<(tqdy|nT>(M`b`cxj(2I|vctf|+;jbdPsK^$S@{y8bmzli4}yrQO&Ej9wk zt$h#V8)Zb_(VkXKYz?t_bC=q!xk(w%!yM-kf)OtnHWgQJ0e zQP?!yDy~us{`1GN9k6KRxU)1o7(-3=1KPd^07{?=*^@-A-g5y#xjXBaU_eQ(S3lid z)fw0^QQ1F3F${FcI=`sqOG=k@`pl(=4Bmc@CnE&&}C(CEzB? zq;oGs@4%>UOHm#f+*J`Zg}e?T$wXU1<96Mk-Sex+olqsP`|| zXaNL|F@dw-5sa8D3_m}8`oyqv=a+QPc|puOd4PiClysjaoU>s=S*(BX?b*OBhVbqr z(j=sqF90dS$E*gxtq}Qhi}tGTf2Mk30N&j}0{Nrlwu|TT@BQ$dz4<$Zei~?8BhVE9 zhTiq2##p(BQQ~NMSq1iAC){)a;=zuPl-^Q{ot{MGZle6z9TP+;KtPSaMZd!_(X=7D z9zS6g4*?7kgnkyco#-m}Z5_yo+KGfuy9|F!|F$4JQ#bEFcta2i2H z%5j7NSgQoFxnZXcT}~?;VuwR)-6{IxpNK(gPB&=ynRMx1*V7tc>i!6c(?}oZfJyev zxDsVg!N?o`WKM5cmq}e1b((WjWO=MO6YkIG{G3IYY-?*<#jTfPazr=U%fx)SM=x7V zz{d-1X4so)Ju6|f8fxu}8#-cBJhO$Um4}kIzW5(p0PNN>GoP4=g@9qPF6i9?P`;cP znx6HAd;wt|12gj}bOd%Hfr5wMraH1BB56bLkAL+Fa+8;K*ao*u{k;_7@pB+Gzxx{OdI7Q! z$t(i%iz5~+25^`HlFM^g{#Aq~1tXgHS;QM?|O~C>0=579ced>M|!# zH>AfGpzxH4AiOngn=~}-8qZ5=R|=w;at|3+AdJC{^E-Y6cPIm@mSV-J1HW!Grw${X z=G(pd4rcCm`{4wlekNuBVebtPP^5VJB=Wvq^B6Wy;J8d!+yEr6m~g(h*Mr1V3A5t_ z08Y4tzdjqtg$`+2&0-wVW`H3=srv_s?H6Umy*YhwlaTbGZ8!}KFuwh-fXnyfaex(O zzkd)5*Sob<3)uN5?v1}^Rm9*mXia3BNXO1X&w?GJA4%jcdYNew-D3#Ct<<-R{d{5hBqtS^o2z$gwOt3m8H8CCVTPOSV9nS;bQi z5AZTE{R0+He){cbkMbGSrQ~791H8Pn|FFpFoqY|9h* zQ?aI?z%E|aV=zW}-mf}I$*UYlU0CgdmNBalQ3ZvQm1FbR%>Nt3%S7Y_VoZD^=qa{> zz5DjX1MmAU0MH-3LhF17^lg3U&~}NH#!jW05Iiz6Lj3(dP@f1tKUGBi8Bb4HWCQHX zyu45RfFr(p_qe!N24Mhm;O59vh1h^U9DpKu`1v28p(-iF>bFif>x!_V zY%*VBxVZR~1XvpazW(0_dH>J3P`~8}Ng>wLRnq zX2005AKSy2#21z18(Ar1A^WklEDG5h5UUf>W8dc-0F{6}2>$o$` z1vLV+c6uew`oCC4|1ZG)(Ot(#K;}0`kRJ=Yn-_NSq(w&CYeQDc82XfMGJ|k!5eeE` zj~}qC@d)fJ{%mU~yt*nfu76q0pmyE%OYP9unxN5w)sC?!n-kr!wYMEK1HM7pG#&c}8>@B1gxK zhNxV0V*E_VV18?++A(yhn?-SyRI%S%aKUyb_x-Af{o}$bnvvt&N!l)o+$8BTIkzeX zKM$OUa`ObXuIi+;VXGZqsw%ZJ1~d-TKNuPMwjQ=4m3w&O%Q@NN&!!gTY1};fXhl}3 z_qg13n0oMT>Xof#lgA$}O%MAFQ}%Z&=}X6w_#XewU%EBPZP?Pw;_uCEJ}2|?<$@Y( z%o_Vbdh!eUzoYq{7%dlea?qBNE#y9~O4^9Kuf&GCleSOY8w`GBE4e2}jG00-*e9yz z9;0aRlg$k;YOPdnIH)x(J4njgpS^Jea%n!2XQ0&P;vKc*rGA4x8?9BgIUYav^V!;u zv7$9Wv=ZzEsQrCL;)lkLtB}22wx2Q6D3;Brd@o|=ae9^^M$M@sj=9e3b`MqSD3eva zYO}0JgkbQET_eU)y(GU-zL0$LGui zH9cJo>ZaRT*a~j_dmMo|mQ!|lZQGtTQ;G*A4k@f&yz+VMxyaBL&%%KKL%CH4gN*Fu zHK8byS(HuQ#a7iHxk+y)_#E;*IK8I}QtOO38?gJoZh8>dP7FqSDf|V)6?|CFl~|sC#C=#E+SSXmxB& z-5-^QEEeb7>nC*>S_REt#nfuWuMMSz%nGdj3$k&uMv_73SrgR}{wf znQ*L(QnqN9=ii4?A3CpY@=RYE4>U=Sa4FxEy?LO2;~uT}D_1!Nk_*|m4u!$vhfS9P zOJi0iGM46oZwsC6s0ux3*v-9r#cZzmBUxFSiRI^r>H4?(B}Ev~f{QTvX+98Bee<&@ zP3`ur6zGAK^UKPXzCQJi)vY0S?;(-b|C&`v#dqDArc&Znx^Eu9$qEULk1BdI&Y`;& zQEwWqz)qd}f2?o!ukCf!mBxqh#(#q8R*H`<-~LH8Hg8^0O(DsrNCDD|YUeS?W_LRYy~dDpS+ILRfg?vjtNF^|#2 zL93CA3bAz910sjsh}F6mA?A^egj9?o{ z%jxa=Q}5?s`Y-L{oQr-g{9ikKdWDG}A`%D-v7249_;~xtzISugK6GalM(@;%(!70r zk0Hs%$4D#wzZ#~Rsu2MG%uZEDb8Ye_U3~%xeclP@S)n8FTpsBrt$ZuUR$kwMP0y#vd{58d64~@ zoFT|3I6C?(+VQNBi?>ROS7&R9-WM%fh%ZO2fk>+Babwznu#c%8$)UIZsN4vqNY~TI z6iVN2y++b6Vbb9$Wx3S%AYrJyD&Mw}vAvy3CIc0^tI2 z1GxC3Cr<{mYDFrh773&q3941hW#NMkfq4Qjd#A1h21BH9fC=XsNMjBHLnlr(sp{Oww3ObOvRKhy@vfGculJGB zbT!8d`_w|qF3%B{&?8GE#q^~Mx(WmL2^{}bXQ%F}rI=rj;%X<@#7A>J$SpZU;(z(Mccj}^~!_ceIR;>hKq6YXg zqm0rziYlUAJWWvu%o#@XdtB9TANDcIfCYY9$%2lh#4daUa7zckm@NwvNte0+BY*Cn zfuE2IY3TMEdM2VkvHIIJ%RJWz##xx2Gx}!$z?Cjsp#Pd~P!@fUl736kt{ppe9C8}{ z`Kxpcom5fxzqaVVk!*{)2t*0JK+2*njw@Q|En)uS(w%oIdOx7!I#b0>*%Ed&!;lQG zS3C%W4-+&(;&2WRm`9{vb4xG8Ot$5BIbWSFx*Q>P_E@`1oJ7y((Z~_!vtl&&Fz+4e zkF@->kcJlXjLO}`fs zR_y4w44~U09j34IJ--4(3Gg5+?~u%$?&g9#MUcB{zdWxU+_sMg6u^B)jvR6A+k$am zATTJE{@E%n`bZVFb!g{q>-3!MA6x0F!+KLWg#`d*_O@I$(f%Unq$V3TBZp zmSwpKE@TQMvAm&U(EdB%IRPhhi0YrYd$)0y%4Cvkk45E}H~9f};Lr?jPo8keEX_;# z8q^cBoVAx#EZAH>hJ%*UufB!<`}Fh!+l_?j{Pg-698uDfV0s{7 zRLN$Tqjv7=>r4KrdHM2X6S@6wC(#Ac(s~hh&fI*IY9bvNHV9J>a8Sakr`+k&r$brz z%X%)jx)zv3fX13-(<9rSXLbAJ$~#Mdb?mvRDoa+-KaF28I<Y%W2s`Bg$ zc>ISS6cGV(WlW)2L+UE* zb6_hSnM~PtHu~KzpA%@42=(&?U@j;4jdqixU^i)dv`0Aidpwop*9Ri?DF!i@uI1h3 z;FsX=aXFlDHU30Q=u;NW+(_T*MiJ#PRp^n1PIMJ5-XHaQ@+2-NXFE_* zyy)NG-##=c!K$2az8X5wS=cD1xS|l~gcC)G_P<#4aCMpSGPbYeAZ*9j!wj=3rYFqDv1#;bOg zBm!&#n7etHnwq+;!0v6_O4B1Q3rkBOWI5$a_;gKv2XJUa(Jc7YjAq^>@GLa+ z%H9Bk%vzTu36tbYT`q$PW&XKsA~@^snd{QpIx{<;{Tm(0J6tzKmwffOgzlUj{@BQ> zDbx`}UmIA>NL^!S#Nue~2j&(*(h}EN;`u}LPyURzHHuB|stf*#{ar`y6>$h5AV+|= zC5HZf5U7DSUoLH_hyATwnh&fIF)^``?vfQd?g|2N#ukRRPmmVNuck!%0_4j8Ymhjg z3|o+ZK#e_h)BB?!U$i0YB98Kki9Lh5li&!c_+P@Z0y?i@ee-G*4rn3{>>;T68rm(d zPf>#GTT8&!*nTk8mBZpNr3v;7=t^^3855i`O$)!%L(yQHIF=519PK58UEl+R*Yl0P;ndPcSubY&w-NB zc4#c^V4yz5hW@6q^{v-*hxF3i$y{$z z_^^){Lgd>)m;9pdi({4Wu+#;)g#HoNPB~N~)$@Sb-s+J416S6i_)Xah1ANKI7TbL85p)k)srXMKO zkw1Q@r@bE)!0Om_EVi*3v2zKH8{Ys48|i`hTZJI3STuDdXJH1+0736_JhoHHet_Ri zpoyStGrHb8##TQq27S1y1@#qD-g`S9B>S&c^)TOGf;k zlI@d*z^YwKx`Rt&^20thtf($qt5Pwul)vHJF=zH86D8!=KYJV*gD!pJmc6rfnt_{a zi){wQqzvm8GcJ#!?1pq1vYafV8EIs4Cu5raq{GdK}b+ZU11x*H-S4o{^YQcisbou$_SxUd&B)924 z-*)c)5VMNsF#DPL>se54k z1I_fD=0nC$!pHSmN*3(=eXC~(7U?EDD!L@+!4M%6f8+|7{RTSTwmUyy&K33tNaJt( zB!W##bT0sRmy6*<%mmCG$H~r}-e{F&A%Qwpr8WA%SGlYm>nYv9%DFK)pwT)nbo}65 z(?dIIy*kMjX~hhRJZzmsQPw4M$ZK9XqodAbul`nt7wIc8 zo2j#Z6=w@9%uL!BWLC{^8hjvi^e)dS*DL&9+f_eM%k6!r@wJM>SSClkL77k89@iy5 zKaqnx@@pIm2bpi8l)mcRdA)tvEAt4?Efu5DaB{uH8~8B#8OJ z^xh!!W??5&2}f(kaF*;F`hJ`4$Vc_n*4!(Mc;!;-XHp^E6zTrFs8qVR?9DY#4+%%} zD~Gt5^@H5K!%3Zdx7ctVcIr3cI?v(oc6(@~v~#7Sq1D zpEB7Yl)vO^e|3|Y&XWDgsNkloh~>60819R*Juf+yRiHf8qG481l_EGqzdd4%&+(=z z;G-W6?|iF`5R3-U{`&w(-S+4K0RdhKiDvb9&?rKefU2VrcX#gd=g*+`XeLPu3r`gE zyWP@1eYe;CHzUkPfT z-pn)0wo$Q`72K9=-`~4dTvR+Nds-oO{og`TR#Ab>WFK~UPBpcm15;>;5j-vxsfhY3 zCRZy&h0;?59eBA)Gp;e*a*E8gtgw!t*hMeDvb{Budg7U2GpE2kIS;CXy4C@?B!|c^ zbw{dL$3n~^x^mK{=%$1o^u&0LTiwdP&l(_h=F`#2)l7~tGQ_?{UpIUJNC7P zthANg)IPZ>in1gY%k0ocv=3K*yq)R~i~7~*H1ujAl1cGN%GEkf^RX zruJ8K{HxMBe^PgAo94Cuv9w#Y1J`+mKoB>5`uy1rN#8Gb@EQ>M_g^N`a@0JqbJi_n zDk|yqw3e1Qa-qA&Ky7|fwf-6g*M2Fk*-dmxc78y{A@WS2w~^}LM~s58iI&^b;n{80$&!>8Ddg$m>xq7!9Ny*X|u49RD3H?}KGRl$-f}EZ)I5OaM7LZH=dfl<%6>#GUJZ7KcGh(&bQqD=K8ud_WO|cU;voJjB2{CGr%9Z) zv~#OVgEw24RO?EBgVA!bpBVYo@JAp1hVL-G*eUzB!`4CcR$;{L^1(}nW8_YOl82Z) zrMFt&>fxZgG$KJ+B?*>C-r)LQYFCdSsjr`IN;1a}+?aFz91;~lMxD=X(1k_XXL)Au zYc21S`$lY`{0v*cY`%BN-Q6ACsvjef)!A~Yo4HdZf`ircb{%7qV#Xo2{f@`LP!IiL zoe$2dP_Ux$YZeBCi|WM=qhA)ZoS$b7t(&i}d~^{_<8B>&y?M{}$7L@cRXt3plYXob zuo5EpYjTcVM1nD>@|-Zwj|1Qn>~ZYIfqi+WrdWntgBm|ynltP;kzLz1Ba^KVD~q3h zRZy0XRfA9ixFdM!_WOS5We^zt|8aKy{}d4a?+YnF3egv~zVSb}0RK?G|4+hx1U|-? zWTPL$9n%vi$MAtb@y-qupw!}UmR5auPc-4}KXrsAa);ROkf%I%EB;Fka_i|>Rho(q z!;;K5(;HOp3(^$nMX@Nv+I>q8_dU!Wrz{)GILod2?lkaTywV>BTs=HD;hRCTl$%^V z)(K!XRv*}F(3$eF#UqYedH08u_VW_0UVLl!FS%?TIf;o@y@Yx7rq0u~FN$uo@pp9n zKDQF!uh<`VVgwibxuCBpC~3W0LSP#yz2}1ENiyCP0>WFpb8qM18~FPF zT3vppx94_ofvyoPTT_yifhyNnwzE-oo1*WNN>Lg=pOjDCYw`0epp@knp{A9mukFR8QSQ*M+zJ4_6%lW%2 zojJymL5#hwGWxt9^!u-B?wT|V zAHVldtI&?x?z>uKM=l@9|4M2?M(q7)PH`nwU$e2v63e#2u=w+RnMVDFCw8Z0h#t#R z&L4MaxTBl3V0uYsXkX`!GqXI!X=%{Qq4B0m{n3``B1a;>?fhC>{o3?eSE-rY z%=hnq#(zo_bWM^=jkn(Z@J_N$sd3+r1ZNKl)4* z9S{=PYsjW>*})yX@u*7TH47TDaR?pYU7n^I$m?1;n&Nh;J?C6pg|={8goCK7S?c`) zty%gVWVVTk_X|c2Z1YngRSO>AotbWO$&OyT9vlC(O)RAD&*e`2bQ9eZo<@h1X6$g& z?BnaX9u1!DW;uJZO#~*y>u{{H@f7iQ9`WNLKc6eu)9v7xZ^h*leIN1STMK5nhBlKY zHa+C7VdLDPWOp+uZngaGM{}fK)wXkMZH00VX|;=5BODlF8dzm!((0UlCZ21fr0?D4 z@I!__sPBi^70b@MRTiakMcEEo*?Xze8>+OE>TO#&Evwr<2b8pFjJ7?`^=}jq&vkXve2}yPo;v-K1By+ zUhEHYd+-2*sO))y5wotKq*+9F-8*JF!*A*Neb-sK9Zzo^ZS(cpx?ikwhpvIHy0WF? z_B>^?DK%g&ZZZl5y4%A-8JTxHn6SMwEy{XGjq&VpRm~UTt$k$Q_Q(blpL(!o?D8e;93Pu>mfmU-~;hx)?sfHu<|3E@j$-u;ZC{g8Ni%QH6CQ&r77N!vr82N`H7 zER>(hu?(#X@Sr5!2#Y>re^Ta=-60{p{73IF!a7vPa;%kV<8#M0O=eX2|No({Xuqs-zhl4 z%Xj?VQmCB*a`9-+jz0TiQkS((8Yxko{v4<9py)1@Re|8+zauxqUM4UM{q*{5CWdtG zW|3n58-wFFa{wQsaWpi4>fqS`GJZ3*EqQ!tJr9dM9uPGau5$zdKJ zvIXUiojxbjly-J3jAsk^2P7)8>7I?W4R!O_zI{h_>JO_c$Mw0%%G^xK>iYMt`W_N6 zI;+G+(mnf*p58%SOZWTecOm|u)S?AW*?`Wsbe~*WCmic7PgiCCzztWg-+S2Vz!Qca zKYQ63+aAh5dh6qPjWNw{K#=$2<_EuIJ5OGAE{HRn4;8AFFyW$KmnWT#OSmKX$Uo)K z5MM~kF3z1)FO*)@n>z0mbhs@PqjWPo0-Nek$ou*b8-vU0H7zQd+M^;OPFIKcbdAcF zBUQ!qH+ru;!)k1{Y%3q)ynT1@YQm8Ot1~*Ofv4HiN7rM1Y+U`ph8kTwz_Tvy7>3u| zsH8q=#g}>GMYSRpV}|B3-Hz>HS~~0)#YdTka#SrmpGRq%Ra8Xo`CRo~Mk=$tk1-9-&$O@h;55DiZFgz4a zUnT8+Z6YIra!}emMXOBCa3`O-wwlsubxrr9<$_)@d>{3;hdlow!S|#f(QmQoLq_ow z$@%ugZN{*ctGZW|Pg)+{YhqoKenH`~`IAj^0ou16?6u!piw`?mU(k9XuC?!&*j+la z+??lfVp1~gHs>%gS^rEbE8X1r&`(9H#7!D6`0e>y<{|yT;txfhehCMU>9G0Pyt}>} zPr6L2Cg!(XsC1a6z7g}Cw?KBcgpY91;g8&dw>nSOR*KeLVasnZmU@&PP&M{8L*4#)gn@j~AC^$D^TABoMOKtaNA}1L zX3f!OI@m}cTww+u;CJY!r?7qSfYITv$fZNy8A0t*R<6~`ccMHUXY$v=)%rAMyGo1I zwo>QTiNog)_cZ<);(yoi{+`~7y`qZJM^Tz>D$1fXa|3G%agr)VU3r|Aouv`0cdyM- z*WXWbv%eO?l9abIiH&W%R;oISHsW_<(s3m!JB2g%FX`%3Mn~$t;p3ZjEymLOR(O8G zW~#`&%QRACzgll^c_yD~!lf@&JI8N4diad#<~zVRJ72G!EnXD}Hey>g3KP)Dzwx!X z>$Y9%_}|AQ#9<-Lwzs?=r7mCGeomRm*!W9(u-@P{*IU`~*I`m5kzvGV$d+^C!XI_BPb=>4LSJ~dj168iI^_1?^wDv;XMO!QB7)e*nRajuy*ZWQm^az3 zg8%I0{{4OPEBqq-sps ze16wNe3J2E!kLJZWBh{qbF(U6#t(fzH}{NPd*7*TSDb@IbEra!wr<+gC-u;wx42C1 zMM-;v10Czez+YamHE@__J0Pl{Iu@8Q_VVY@%=xm2gsek(=VhLm5+eC5$`G5y0+ z9lD8do{7#_IZZ~rYN3xE6^?h97(>vI#s891QlQ5pjw0ug{p)rj>5tuO6{l?J$fIgH zKG7Suzb)%F>Q{7>sF*3t|2C>QeDLtZ$x|G=BB4z}C_?I()gzkf-#HCK!G?s)%c+xMONT~)r-GEiy>bN}R>+4fk6 zVoh(4|L0Bl9Z#B5mujnjifIdlzUO|n|BZvH!Y{{T9(m5946msAMIaA0eBIqg)@`C&=t@;9O0dKn@dS5kJ+JtW6%BzJ@?v|P(fEF)b#GxD**;A8^J z2``2gx>1oZm6C`U@Qj44^LIgA{cXA2`yN=pJdWkFi^i(Us0+R~Vuv3vGcraAr;ep}O!zeZR&a94 z-6p$we4~6=e&l48nf%Dz1TW6?_1xm+mbvDyjWstb#;U*s7ziwj&|eOvTkC0eqdA$l z*N}Q^?$%uTKAi9j(&4E0)Ql^Qmjxv^jSTG94w<_oC39Z?yHfYzp3WxXX6F`bhw-cW z!kjwV#0k2N67HLMmMgJs^vbdL|rEMoim(MxACO$;x)O&3^R=A3Z_X;NdqNino=g+2#UQf%rV8_n>w)Df@ zJCwQCaZ=Ojue1{i3g_>XZwX@TxqEtm@&X%C{+>Vhs?h8|G#4E@;$HA|o&Ep+|3FQ; zIT`+sgZU=(j`06=?)(4jk#+1Q`eO>8o+62WKKVZM5@gWS-kj9Fj4sIizkgjnRaF^- z&rZv}@95Fi49ljbrVO3b>e|{<*RJuh2x)mkkr3T4wch~jY@1dIw7VAs9H7u-uK_dT z@NXIXJJAmAQ-^;SG<<>vEosOMJy`)SX!VjO>38A`%z}>}Px@{J4WmA?E4$A|`}g=i zho->5D&`9^c31kfp<{2CMQ{K7VHbs;o%bL8b|)Ii!SCH6L#USx)bV@#`0`vXI3rtV znK?Lghi7pdwUoUA2m~)ZmY|1{^DJ)3ODy4Zp6a&xE4zF5Yo%d$1f6&y4U>YlmM{N( zQ()CkX)ZyYF~gpgk#SK$0XMblaMaVM3_A~9yHCR=`r#=z3(G#VQ9`S$t4*(7iO*I+ zvw)XZT}#XLf840)Pf9dQ9b)dvJ951Q^&etNLe*>tyC9M`#7sM2Uvv@{R^km^ZF8+lRjs{%R-AJb?)2mFk1 zCn9~-wy1<1&J*+?ac5j4Z|H~nAd#wqWIhub$omf-kddH#e*zIwWUw2lPP;iY76bs> zqa5|ecPThVTPz&#eMBXi=M7Py?qEWEeDW$I-x)R^sNjRZ0IHtpuloGkQ5o_f!q9^- z3piCMJy~!H<0*{p{k7~iP?r?pvr`P|itLm`Rb^!jKuNSX_o5jU_v)2A=Ey$u8bct= zTed3KdheLjGuW8#4Wdki3wnR*7cagl^@1RUrf1{#1+Qlii|`8yGAA_hpAFy$0`Ewp zJ=Y?k29Wr!$-eca2Cq<#O`^p^ zFjBbehDD~f1WkR$JKaL{ZPe7Ct@Bmv?Xw6iyM-*feQ{FB*U6Ga{iTWg9w@$QRSuR6 z*?nM_gyqxOS$j|nLh;ELvAIZ{ak4^Y*Pt z{mUcI!ov3BNznhm^S0TlT6De771AxZNXK!MMbF_?QS{KZSD zBP%qbhp2wZk(MCyy1WjdXWFa{>>_}0^Qt?X@EUJDgj5B8oJov&N~(r(T|t3ZL6GGm zc}uo4g-J9`_hOsx5R z&=4=_vZkhrs%ilKFJ%>uS(idu3`Dh3uSmJg#y$@XO)mcV^YYE6XIy2(z~eg>X3_WQk$EfV3YsLMV^NZK7Al|1-Ed|p$!iFw;6avWKRck5Zr{H7 zs-mK=s|;8Al*eXn-ZIKv%kqRP#iiO&LDHkgkDn(e3otU;9gBJv5s}ht;O17?@P0&) zgu%wG;S+362FI}+_*f)l#Sr{yoTJca|Z zk_s6OEMfHi**%KCr)NXFTp=d8hXk$tn*v>t7!zHQF!X_zIv4r)_)Ne@NRH-^c0LEy zT+7JXX}PaZ|N7Ssiq$nh9eBBIr?@Fw&CuXr5@AA^y7(80+M^ou77=n@rKG25TPk7R z_soYzM!pNMIdAvQ?AkGU_3Q=`b1@?UR!; zfWB<6_YAKgyrq&?vDrg}b4G~8pWPGO81q~04^Dt`_wx@nwsR2@=b<@02#<;|aD0?t zZ$WW%#Y`R4Rk|*SfxwKhXSwCI>u+m_^41V>BhicolXRv8VUJVyF zi&pDuq~x(FU5Y1`oPq*O)d-Pwa`f+EZOpYCPXwQxBF)g>?S*AX5Mer!F_GgGE1ZqH zfIAlz>G!wS_dqIrq*#ACN46j-3VP31cV|K4H1O^@XrGdjqS52-;X&x$iZSVA$ls*C z1<$7%l+?@J2s?2#&`e%tS1xK@Z28eExB=0%n%nX%D0~eF_lut!m`l98aPxWk;e&jI zo_1SqcB49vc zOigm@d$j-ZI=HLfM*5$4Mm_pTF^N4j2wWIl&i!l0$(goqj}?hL&J~Epp9Z)If{0>j zjdIT8FDN!|cAHoMBk?)@ffo;!a&P$4%JGBCcJN!!#540C_G)x=4%#QN-1Ax+GvMXL zzLub4M@~*Y*lv*?g5Gme;d%UR@4TGBt9j=LM-hTTHIBP`(ht=?E6Hb)X}KsRx3O@mJlv1cN?SHDtgS-fcu}|?ZX1`^ z@gSQ;qo4oL6DPbwLN*ZGQrsu#uHjip4E~?r+|9=>aLOeKwvH+PG^b7?W|&cf;6sIg zU`JS}?TL`EdItIYd0H=M8F5+|;j|NI#IbDkwkxX1KopL6Iw72}iK#IFb9ZM`Njn*I z?FHtA-*vR3Jr=YpoLeY)Xj2WKHxszfvtzS2kz;x!Z06C?%R`}gEUoO-&LifRqiPr6 zasxI7oXIvP{x&zs)ft4UlQrmNnzjzBcwp6Ak&6uyBoG^$bXF^elB!ue4OTMX3V^hx=bm0Nf~`MR=%)pe!(roO)Z z+d#=>r_k{5q`W}cC2D!X*}B3181+sd0VAWf^z~Tp?k8J;i=ugNP4jFxu8)G_CpB^#$Z20^8H(}LE_hTpZ ztZfnL*sdCR^bRPCMkt4HKr%D5hJ=@CO#0nCEg{-8gXN{#y1K*9kz1Zbr1-gkvPrFx znvSmWykysaLuYATnLgy3u>JCbT@($+Q3jMSa0xhxZz6FEgmL3Fq(vle@Sks4`HO3) ztKUT>MDq6iV4csmXOBXYB&oH`OAeXH3|um6CUxu@6m6&}Z2dlNbNcTn|6^3b9q z-Wly=AN_uo`!iS--`<|3C)_~<%94|}!64uhc#3Xi$3wQ|!Z8iZ*k~Hrf@1rg9i&G&t{P{~O0IV_7q^zH51& z&-4C#zVG+zYl*tqR-EB5!S!#-Dy-pN5%WAp=51~U+xMvw#F|cT@1hAIG*-Nia-Ysy z#KJAYUZ#H9)(`J}M^L@e6*+yvq-wPx5QnD#Kltrf>zcFcU#DB=r{--{;VvZyGrd-C z+LR71M8%SjGxCAqNVZYSPayQ%k}7eIKN%0(W=84gSbSK7pgUp18}EN?xE=ONuh^Q& zWjq9b&6R|yN+`Veb^YhxWiV7CBZy!$a{sc)Fd=pZ~NzKXL@jUquu= zw&u#}YP#Z(V|-K6-4{y%)xJ&kWJx6?Q29ggxC|bxiySfzON!cfEebN-bkCmk)cK*a zLc+dag)E#38|GIi#)YYExe# zm?t}uR}z?6{b?zUY6F!g16|7Quw%cS{xdNckhXyjGLH}k4*&JT9o-Z1%NuXfd~NLfXBc^lk~=@6q7 zLmNJyP)*9y%ge*vouZlnwbSS4OR%rA#A+=#`?M*^iAlWm&Dadv`_B&|*YF5w%F9*@vsMUAdOY^BoU+3bFoI#fT3!S5!2JPt@I$yZDvXHj8^9n&%r z@lGniY39LVrK8M&Aa%d@lmUtZTzEtQ!#^BD8h`3qZmxV_UV}o--Ul7`6iiS%7;?=W z>2!}+2L!6&{6Smt=eijgSO=#G_vk-J#1N*<=bh5*`CEB;H8eBQ@MUsQW(dpY=k9n+ zOsMdQNOZZaJ;MOKVY3{J4a@>MgGVpYVgkjd1P_$mC3!=i*OEgALS7GHnNFN6sQ?;y$JU;` z170lh?2>cBO5f|~6e=@wfKGaanF|wC#l9A9D8Z&yFqlh{QuuZ4pv(V>V>1W@7FN&x z$ilGMIJT>6!jh0Z{#gmFEm$0Y21Iu!`~s-KPy32eXFc7Y;c&C6-_c zv)EzQDSCrA-K!%|;O1rVHQ#`qHOW`?&Ro`kP-i;dMrR@@FLz%$&G8T{gzr0haUtMA zqxBjM@f=4w_oU|IPCwqyqq-Ijds)7|Uje>y9Zqyf>)_yZkwH{koal+QnJQX0Q*=aF zIvY^e7-|l`^9jyy<$_+S_F#;G$4{bH@2@yKq_ARo&wREB=(5*NRs1Se;T@HaF{ zUvJ;;9yWPVPFgA-#I!Cxh;L%vT=bOkcg~U6$lq;NT zKWhI8%Oc=yl7f1JLgV?Y>G>I?sq{Go{e!}|2YI%H1THZU&XF_d!(D4ap8KL5!|@|ddN`26n)!K)qLpaIgx6NJG-{Z z%6`y_&fo{>>g3LSv3m6sI{9VgEe&^vkNxAofi@(;lF;!L7Zgyi&obF;Q%nUCkVEns zZmOxOdIWN9A~H1vI7*3e65We>vF=z*j2&_=buf~nw+5fBu&&M!$WB4w?HI%pmFOKJ z@M`5xQw55ZfHYzHauOgg;MYC)b_~FK9)WHRf?O)nYQTpW;f4rdLS^na7SznWa3AfI z1(f7NRK#C;dsBKwokBvk!jzwebKn)Wb`4kgQS^BghFOo>leCS%ea6e{{3Z1^88)-+ z^rJ})R5W7eR zO(ZFq!{T`gxYl4yYXN{wF&9RTdy-ru+8NAC5d~<+-O!2LE;1)4a$15Y%sPx+cQ;z@ z9-SIPLWCf+VlNb{!5oZOP)LJ;JKIj90kXI+B7zP;(G}oc*ZUiXgC~@lmPR18)Js8G zUI(ymch1BM=!17r*m@ufwW(uRSgM(P$6+DX)Lg~R?vtR21emdjx}Wdff8SxVT= z{EsYjx*FwsR*}kx+)D?hK8fgxynTGO*xPS>VmGbVnZ9voN0)UGSMa literal 0 HcmV?d00001 diff --git a/v2/static/img/account-linking/err_003.png b/v2/static/img/account-linking/err_003.png new file mode 100644 index 0000000000000000000000000000000000000000..2730bd84c71845e9a488e0f2f857a52d3a54d12d GIT binary patch literal 43322 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

TP$-?qRvUY7Bcnq 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 Gp8o@;aQL?X literal 0 HcmV?d00001 diff --git a/v2/static/img/account-linking/err_005.png b/v2/static/img/account-linking/err_005.png new file mode 100644 index 0000000000000000000000000000000000000000..be505d3ddcaf5ebf6480225d103e75e53d2d7ffa GIT binary patch literal 41997 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 literal 0 HcmV?d00001 diff --git a/v2/static/img/account-linking/err_006.png b/v2/static/img/account-linking/err_006.png new file mode 100644 index 0000000000000000000000000000000000000000..bcd9a224d997e14bfa61fc7c8f651a7de4f974c4 GIT binary patch literal 41107 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 Dc#jR) literal 0 HcmV?d00001 diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 97b82a40e..9340be211 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -202,7 +202,9 @@ The following is a list of support status codes that the end user might see duri } ``` - The pre build UI on the frontend displays this error in the following way: - Pre built UI screenshot for showing error message. + Pre built UI screenshot for showing error message. - Below is the scenario for when this status is returned: @@ -223,10 +225,9 @@ The following is a list of support status codes that the end user might see duri } ``` - The pre build UI on the frontend displays this error in the following way: - - For create code: - Pre built UI screenshot for showing error message. - - For consume code: - Pre built UI screenshot for showing error message. + Pre built UI screenshot for showing error message. - Below is as example scenario for when this status is returned (one amongst many): A user is trying to sign up using passwordless login method with email `e1`. There exists an email password login method with `e1`, which is unverified (owned by an attacker). If we allow the passwordless sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the passwordless login method. This way, the attacker now has access to the user's account. From 86722486adbb8911ad4414244ffee3bb8e3f2579 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 6 Sep 2023 16:38:33 +0530 Subject: [PATCH 44/81] more changes --- .../automatic-account-linking.mdx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 9340be211..015ab775d 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -215,7 +215,7 @@ The following is a list of support status codes that the end user might see duri - To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **These actions can be taken via our user management dashboard.** ### ERR_CODE_002 -- This can happen during the passwordless recipe's create or consume code API: +- This can happen during the passwordless recipe's create or consume code API (during sign up): - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` - Output JSON: ```json @@ -238,8 +238,24 @@ The following is a list of support status codes that the end user might see duri ### ERR_CODE_003 -- Passwordless -- API call: consumeCodePOST & createCodePOST +- This can happen during the passwordless recipe's create or consume code API (during sign in): + - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` + - Output JSON: + ```json + { + "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)"; + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + Let's say that automatic account linking is switched off in your application. There exists a passwordless user with email `e1` (owned by the victim). There exists an email password login method with `e2`, which is verified (owned by an attacker). Then you switch on automatic account linking, and the email password user becomes a primary user (this happens if they login). Now the attacker somehow changes their email to `e1` (via a support ticket), but is in an unverified state. In this case, when the passwordless user tries to sign in, they will see this error code. We prevent the sign in because if we allowed it, it may link thier account to the email password one (since the email password one is a primary account), thereby compromising the victim's account. + +- To resolve this issue, you either mark the unverified account as verified, or then delete that particular login method / account. **You can manually mark email for the other account as verified using the dashboard.** ### ERR_CODE_004 - Third party From 66f450db1e628b0d9d4237489a616a6e4e14c279 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 7 Sep 2023 12:23:02 +0530 Subject: [PATCH 45/81] more changes --- .../automatic-account-linking.mdx | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 015ab775d..dbd94fa71 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -198,7 +198,7 @@ The following is a list of support status codes that the end user might see duri { "status": "PASSWORD_RESET_NOT_ALLOWED", "reason": - "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)", + "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)" } ``` - The pre build UI on the frontend displays this error in the following way: @@ -212,7 +212,7 @@ The following is a list of support status codes that the end user might see duri To prevent this scenario, we enforce that the password link is only generated if the primary user has at least one login method that has the input email ID and is verified, or if not, we check that the primary user has no other login method with a different email, or phone number. If these cases are not satisfied, then we return the error code `ERR_CODE_001`. -- To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **These actions can be taken via our user management dashboard.** +- To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **You can do these actions using our user management dashboard.** ### ERR_CODE_002 - This can happen during the passwordless recipe's create or consume code API (during sign up): @@ -220,8 +220,8 @@ The following is a list of support status codes that the end user might see duri - Output JSON: ```json { - "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)"; + "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)" } ``` - The pre build UI on the frontend displays this error in the following way: @@ -234,7 +234,7 @@ The following is a list of support status codes that the end user might see duri To prevent this, we do not allow sign up with passwordless login in case there exists another account with the same email and is unverified. -- To resolve this issue, you should ask the user to try another login method (which already has their email), or then ask then manually mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can manually mark email for the other account as verified using the dashboard.** +- To resolve this issue, you should ask the user to try another login method (which already has their email), or then ask then manually mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can do these actions using our user management dashboard.** ### ERR_CODE_003 @@ -243,8 +243,8 @@ The following is a list of support status codes that the end user might see duri - Output JSON: ```json { - "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)"; + "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)" } ``` - The pre build UI on the frontend displays this error in the following way: @@ -255,12 +255,27 @@ The following is a list of support status codes that the end user might see duri - Below is as example scenario for when this status is returned (one amongst many): Let's say that automatic account linking is switched off in your application. There exists a passwordless user with email `e1` (owned by the victim). There exists an email password login method with `e2`, which is verified (owned by an attacker). Then you switch on automatic account linking, and the email password user becomes a primary user (this happens if they login). Now the attacker somehow changes their email to `e1` (via a support ticket), but is in an unverified state. In this case, when the passwordless user tries to sign in, they will see this error code. We prevent the sign in because if we allowed it, it may link thier account to the email password one (since the email password one is a primary account), thereby compromising the victim's account. -- To resolve this issue, you either mark the unverified account as verified, or then delete that particular login method / account. **You can manually mark email for the other account as verified using the dashboard.** +- To resolve this issue, you either mark the unverified account as verified, or then delete that particular login method / account. **You can do these actions using our user management dashboard.** ### ERR_CODE_004 -- Third party -- API call: signInUpPOST -Signing in case +- This can happen during the thirdparty recipe's signinup API (during sign in): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a thirdparty user with email `e1`, sign in with Google (owned by the victim, and the email is verified). There exists another third party login method with email, `e2` (owned by an attacker), let's say it's login with Github. The attacker then goes to their Github and changes their email to `e1` (which is in unverified state). The next time the attacker tries to login, via github, they will see this error code. We prevent login, because if we didn't, then the attacker might send an email verification link to `e1`, and if the victim clicks on it, then the attacker's account will be linked to the victim's account. + +- To resolve this issue, you can delete the login method that has the unverified email, or if manually mark the unverified account as verified (if you confirm the identity of its owner). **You can do these actions using our user management dashboard.** ### ERR_CODE_005 - Third party From 935a05e545cbcbc21aeaa6ee8df82cc04e80b8b7 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 7 Sep 2023 12:38:47 +0530 Subject: [PATCH 46/81] more changes --- .../automatic-account-linking.mdx | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index dbd94fa71..93ec784cf 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -234,7 +234,7 @@ The following is a list of support status codes that the end user might see duri To prevent this, we do not allow sign up with passwordless login in case there exists another account with the same email and is unverified. -- To resolve this issue, you should ask the user to try another login method (which already has their email), or then ask then manually mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can do these actions using our user management dashboard.** +- To resolve this issue, you should ask the user to try another login method (which already has their email), or then mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can do these actions using our user management dashboard.** ### ERR_CODE_003 @@ -278,24 +278,46 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you can delete the login method that has the unverified email, or if manually mark the unverified account as verified (if you confirm the identity of its owner). **You can do these actions using our user management dashboard.** ### ERR_CODE_005 -- Third party -- API call: signInUpPOST -Signing in, when the email has changed and another primary user has the same email. +- This can happen during the thirdparty recipe's signinup API (during sign in): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, thirdparty user with email `e1`, sign in with Google. There exists another email password user with email `e2`, which is a primary user. Now, if the user changes their email on Google to `e2`, and then try logging in via Google, they will see this error code. We do this because if we didn't, then it would result in two primary users having the same email, which voilates one of the account linking rules. + +- To resolve this issue, you can make one of the primary users as non primary (this can be done by using the unlink button against the login methon on our user management dashboard). Once the user is not a primary user, you can ask the user to relogin with that method, and it should auto link that account with the existing primary user. ### ERR_CODE_006 -- Third party -- API call: signInUpPOST +- This can happen during the thirdparty recipe's signinup API (during sign up): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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_006)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. -Signing up. +- Below is as example scenario for when this status is returned (one amongst many): + A user is trying to sign up using third party login method with email `e1`. There exists an email password login method with `e1`, which is unverified (owned by an attacker). If we allow the third party sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the third party login method. This way, the attacker now has access to the user's account. -### Other edge cases -#### User is seeing incorrect email password combination / `WRONG_CREDENTIALS_ERROR` -- Email password -- API call: sign in + To prevent this, we do not allow sign up with third party login in case there exists another account with the same email and is unverified. -#### User is seeing email already exists / EMAIL_ALREADY_EXISTS_ERROR -- Email password -- Sign up +- To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using our user management dashboard.** ### Changing the error message on the frontend -TODO \ No newline at end of file +If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). \ No newline at end of file From 2727932382ddef721a742445d2bd01171df75570 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 7 Sep 2023 12:42:07 +0530 Subject: [PATCH 47/81] copy docs --- .../automatic-account-linking.mdx | 323 ++++++++++++++++++ v2/thirdparty/sidebars.js | 3 +- .../automatic-account-linking.mdx | 323 ++++++++++++++++++ v2/thirdpartypasswordless/sidebars.js | 3 +- 4 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx create mode 100644 v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx diff --git a/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx new file mode 100644 index 000000000..93ec784cf --- /dev/null +++ b/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx @@ -0,0 +1,323 @@ +--- +id: automatic-account-linking +title: Automatic account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Automatic account linking + +Automatic account linking is a feature that allows users to automatically sign in to their existing account using more than one login method. On a high level, the accounts for the different login methods are linked automatically by SuperTokens provided that: +- Their emails or phone numbers are the same. +- Their emails or phone numbers are verified. + +SuperTokens ensures that accounts are automatically linked only if there is [no risk of account takeover](./security-considerations). + +## Enabling automatic account linking + +You can enable this feature by providing the following callback implementation on the backend SDK: + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; + +supertokens.init({ + supertokens: { + connectionURI: "", + apiKey: "" + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + if (newAccountInfo.recipeUserId !== undefined && user !== undefined) { + let userId = newAccountInfo.recipeUserId.getAsString(); + let hasInfoAssociatedWithUserId = false // TODO: add your own implementation here. + if (hasInfoAssociatedWithUserId) { + return { + shouldAutomaticallyLink: false + } + } + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +#### Input args meaning: +- `newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }`: This object contains information about the user whose account is going to be linked, or will become a primary user. The object contains the user's email, social login info and phone number (whichever they used to sign in / up with). It also contains the login method (`emailpassword`, `thirdparty`, or `passwordless`). It may also contain the `recipeUserId` of the user that is going to be linked in case SuperTokens is attempting account linking during sign in. + + Notice that in case of `newAccountInfo.recipeUserId !== undefined && user !== undefined` being `true`, we add some extra logic to check if the user ID has any info associated with them in your application db. This is to prevent data loss for this user ID (see the [migtation section below](#migration-of-user-data-when-accounts-are-linked)). +- `user: User | undefined`: If this is not `undefined`, it means that `newAccountInfo` user is about to linked to this `user`. If this is `undefined`, it means that `newAccountInfo` user is about to become a primary user. +- `tenant: string`: The ID of the tenant that the user is signing in / up to. +- `userContext: any`: User defined userContext. + +#### Output args meaning: +- `shouldAutomaticallyLink`: If this is `true`, it means that the `newAccountInfo` will be linked or will become a primary user during this API call (assuming a set of security checks pass). If this is `false`, it means that there will be no account linking related operation during this API call. +- `shouldRequireVerification`: If this is `true`, that account linking operations will only happen if the `newAccountInfo` is verified. **We strongly recommend keeping it set to `true` for security reasons.** + +You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. + +## Different scenarios of automatic account linking + +- **During sign up**: If there exists another account with the same email or phone number within the current tenant, the new account will be linked to the existing account if: + - The existing account is a primary user + - If `shouldRequireVerification` is `true`, the new account needs to be created via a method that has the email as verified (for example via passwordless or google login). If the new method doesn't inherently verify the email (like in email password login), the the accounts will be linked post email verification. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +- **During sign in**: If the current user is not already linked and if there exists another user with the same email or phone number within the current tenant, the accounts will be linked if: + - The user being signed into is NOT a primary user, and the other user with the same email / phone number IS a primary user + - If `shouldRequireVerification` is `true`, the current account (that's being signed into) has its email as verified. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +- **After email verification**: If the current user whose email got verified is not a primary user, and there exists another primary user in the same tenant with the same email, then we link the two accounts if: + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +- **During password reset flow**: If there already exists a user with the same email in a non email password recipe (social login for example), and the user is doing a password reset flow, a new email password user is created and linked to the existing account if: + - The non email password user is a primary user. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +## Affect on email verification +For a primary user, if there exists two login method (L1 & L2), and they both have the same email, but the email for L1 is verified and not for L2, SupeTokens will auto verify the email for L2 when: +- The user next logs in with L2. +- If you call `updateEmailOrPassword` from the email password or `updateUser` from the passwordless recipe, updating L2's email to be equal to L1's email. + +## Affect on email update +When updating the email of a login method for a user, SuperTokens needs to make sure that the account linking conditions mentioned above remain intact. This means that you cannot update the email of a primary user to a value that matches the email of another primary user. + +For example, if User A has login method `AL1` (email `e1`) and `AL2` (email `e1`), and User B has login method `BL1` (email `e2`) and `BL2` (email `e3`), then we cannot update `AL1` email to `e2` or `e3` because that would lead to two primary users having the same email. + +Now email updates can happen in different scenarios: +- 1) Calling the `updateEmailOrPassword` from the email password recipe +- 2) Calling the `updateUser` function from the passwordless recipe +- 3) Logging in via social login can also update emails if the email has changed from the provider's side. + +In each of these cases, the operation will fail and an appropriate status code will be returned: +- For function calls (1) and (2), you will get back a response with a status indicating that email update was not possible. +- For social login API call (3), the client will get a response with a status indicating to contact support, with a support status code (see below). + +## Migration of user data when accounts are linked +When two accounts are linked the primary user ID of the non primary user changes. + +For example, if we have User A with with primary user ID `p1` and user B, which is a non primary user, and has a user ID of `p2`, and we link them, then the primary user ID of User B will be changed to `p1`. + +This has an effect that if the user logs in with login method from User B, the `session.getUserId()` will return `p1`. If there was any older data associated with User B (against user ID `p2`), in your database, that data will essentially be "lost". + +To prevent this scenario, you should: +- Make sure that you return `false` for `shouldAutomaticallyLink` boolean in the `shouldDoAutomaticAccountLinking` function implementation if there exists a `recipeUserId` in the `newAccountInfo` object, and if you have some information related to that user ID in your own database. This can be seen in the [code snippet above](#enabling-automatic-account-linking). + +- If you do not want to return `false` in this case, and want the accounts to be linked, then make sure to implement the `onAccountLinked` callback: + + ```tsx + import supertokens, { User, RecipeUserId } from "supertokens-node"; + import AccountLinking from "supertokens-node/recipe/accountlinking"; + import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; + + supertokens.init({ + supertokens: { + connectionURI: "", + apiKey: "" + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + }, + // highlight-start + onAccountLinked: async (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => { + let olderUserId = newAccountInfo.recipeUserId.getAsString() + let newUserId = user.id; + + // TODO: migrate data from olderUserId to newUserId in your database... + } + // highlight-end + }) + ] + }); + ``` + +:::caution +If your logic in `onAccountLinked` throws an error, then it will not be called again, and will still have resulted in the accounts being linked. +::: + +## Support status codes +The following is a list of support status codes that the end user might see during their interaction with the login UI (as a general error message in the pre built UI). + +### ERR_CODE_001 +- This can happen during creating a password reset code in the email password flow: + - API path and method: `/user/password/reset/token POST` + - Output JSON: + ```json + { + "status": "PASSWORD_RESET_NOT_ALLOWED", + "reason": + "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is the scenario for when this status is returned: + + A malicious user, User A, which is a primary user, has login methods with email `e1` (social login) and email `e1` (email password login). If user A changes their emailpassword email to `e2` (which is now in unverified state), and the real user of `e2` (the victim) tries to sign up via email password, they will see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If we allow this to happen, and the victim resets the password (since they are the real owner of the email), then they will be able to login to the account, and the attacker can spy on what the user is doing via their third party login method. + + To prevent this scenario, we enforce that the password link is only generated if the primary user has at least one login method that has the input email ID and is verified, or if not, we check that the primary user has no other login method with a different email, or phone number. If these cases are not satisfied, then we return the error code `ERR_CODE_001`. + +- To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **You can do these actions using our user management dashboard.** + +### ERR_CODE_002 +- This can happen during the passwordless recipe's create or consume code API (during sign up): + - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + A user is trying to sign up using passwordless login method with email `e1`. There exists an email password login method with `e1`, which is unverified (owned by an attacker). If we allow the passwordless sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the passwordless login method. This way, the attacker now has access to the user's account. + + To prevent this, we do not allow sign up with passwordless login in case there exists another account with the same email and is unverified. + +- To resolve this issue, you should ask the user to try another login method (which already has their email), or then mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can do these actions using our user management dashboard.** + + +### ERR_CODE_003 +- This can happen during the passwordless recipe's create or consume code API (during sign in): + - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + Let's say that automatic account linking is switched off in your application. There exists a passwordless user with email `e1` (owned by the victim). There exists an email password login method with `e2`, which is verified (owned by an attacker). Then you switch on automatic account linking, and the email password user becomes a primary user (this happens if they login). Now the attacker somehow changes their email to `e1` (via a support ticket), but is in an unverified state. In this case, when the passwordless user tries to sign in, they will see this error code. We prevent the sign in because if we allowed it, it may link thier account to the email password one (since the email password one is a primary account), thereby compromising the victim's account. + +- To resolve this issue, you either mark the unverified account as verified, or then delete that particular login method / account. **You can do these actions using our user management dashboard.** + +### ERR_CODE_004 +- This can happen during the thirdparty recipe's signinup API (during sign in): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a thirdparty user with email `e1`, sign in with Google (owned by the victim, and the email is verified). There exists another third party login method with email, `e2` (owned by an attacker), let's say it's login with Github. The attacker then goes to their Github and changes their email to `e1` (which is in unverified state). The next time the attacker tries to login, via github, they will see this error code. We prevent login, because if we didn't, then the attacker might send an email verification link to `e1`, and if the victim clicks on it, then the attacker's account will be linked to the victim's account. + +- To resolve this issue, you can delete the login method that has the unverified email, or if manually mark the unverified account as verified (if you confirm the identity of its owner). **You can do these actions using our user management dashboard.** + +### ERR_CODE_005 +- This can happen during the thirdparty recipe's signinup API (during sign in): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, thirdparty user with email `e1`, sign in with Google. There exists another email password user with email `e2`, which is a primary user. Now, if the user changes their email on Google to `e2`, and then try logging in via Google, they will see this error code. We do this because if we didn't, then it would result in two primary users having the same email, which voilates one of the account linking rules. + +- To resolve this issue, you can make one of the primary users as non primary (this can be done by using the unlink button against the login methon on our user management dashboard). Once the user is not a primary user, you can ask the user to relogin with that method, and it should auto link that account with the existing primary user. + +### ERR_CODE_006 +- This can happen during the thirdparty recipe's signinup API (during sign up): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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_006)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + A user is trying to sign up using third party login method with email `e1`. There exists an email password login method with `e1`, which is unverified (owned by an attacker). If we allow the third party sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the third party login method. This way, the attacker now has access to the user's account. + + To prevent this, we do not allow sign up with third party login in case there exists another account with the same email and is unverified. + +- To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using our user management dashboard.** + +### Changing the error message on the frontend +If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). \ No newline at end of file diff --git a/v2/thirdparty/sidebars.js b/v2/thirdparty/sidebars.js index b52e5221e..ced9ca922 100644 --- a/v2/thirdparty/sidebars.js +++ b/v2/thirdparty/sidebars.js @@ -415,7 +415,8 @@ module.exports = { type: "category", label: "Account Linking", items: [ - "common-customizations/account-linking/overview" + "common-customizations/account-linking/overview", + "common-customizations/account-linking/automatic-account-linking" ] }, { diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx new file mode 100644 index 000000000..93ec784cf --- /dev/null +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx @@ -0,0 +1,323 @@ +--- +id: automatic-account-linking +title: Automatic account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Automatic account linking + +Automatic account linking is a feature that allows users to automatically sign in to their existing account using more than one login method. On a high level, the accounts for the different login methods are linked automatically by SuperTokens provided that: +- Their emails or phone numbers are the same. +- Their emails or phone numbers are verified. + +SuperTokens ensures that accounts are automatically linked only if there is [no risk of account takeover](./security-considerations). + +## Enabling automatic account linking + +You can enable this feature by providing the following callback implementation on the backend SDK: + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; + +supertokens.init({ + supertokens: { + connectionURI: "", + apiKey: "" + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + if (newAccountInfo.recipeUserId !== undefined && user !== undefined) { + let userId = newAccountInfo.recipeUserId.getAsString(); + let hasInfoAssociatedWithUserId = false // TODO: add your own implementation here. + if (hasInfoAssociatedWithUserId) { + return { + shouldAutomaticallyLink: false + } + } + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +#### Input args meaning: +- `newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }`: This object contains information about the user whose account is going to be linked, or will become a primary user. The object contains the user's email, social login info and phone number (whichever they used to sign in / up with). It also contains the login method (`emailpassword`, `thirdparty`, or `passwordless`). It may also contain the `recipeUserId` of the user that is going to be linked in case SuperTokens is attempting account linking during sign in. + + Notice that in case of `newAccountInfo.recipeUserId !== undefined && user !== undefined` being `true`, we add some extra logic to check if the user ID has any info associated with them in your application db. This is to prevent data loss for this user ID (see the [migtation section below](#migration-of-user-data-when-accounts-are-linked)). +- `user: User | undefined`: If this is not `undefined`, it means that `newAccountInfo` user is about to linked to this `user`. If this is `undefined`, it means that `newAccountInfo` user is about to become a primary user. +- `tenant: string`: The ID of the tenant that the user is signing in / up to. +- `userContext: any`: User defined userContext. + +#### Output args meaning: +- `shouldAutomaticallyLink`: If this is `true`, it means that the `newAccountInfo` will be linked or will become a primary user during this API call (assuming a set of security checks pass). If this is `false`, it means that there will be no account linking related operation during this API call. +- `shouldRequireVerification`: If this is `true`, that account linking operations will only happen if the `newAccountInfo` is verified. **We strongly recommend keeping it set to `true` for security reasons.** + +You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. + +## Different scenarios of automatic account linking + +- **During sign up**: If there exists another account with the same email or phone number within the current tenant, the new account will be linked to the existing account if: + - The existing account is a primary user + - If `shouldRequireVerification` is `true`, the new account needs to be created via a method that has the email as verified (for example via passwordless or google login). If the new method doesn't inherently verify the email (like in email password login), the the accounts will be linked post email verification. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +- **During sign in**: If the current user is not already linked and if there exists another user with the same email or phone number within the current tenant, the accounts will be linked if: + - The user being signed into is NOT a primary user, and the other user with the same email / phone number IS a primary user + - If `shouldRequireVerification` is `true`, the current account (that's being signed into) has its email as verified. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +- **After email verification**: If the current user whose email got verified is not a primary user, and there exists another primary user in the same tenant with the same email, then we link the two accounts if: + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +- **During password reset flow**: If there already exists a user with the same email in a non email password recipe (social login for example), and the user is doing a password reset flow, a new email password user is created and linked to the existing account if: + - The non email password user is a primary user. + - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. + +## Affect on email verification +For a primary user, if there exists two login method (L1 & L2), and they both have the same email, but the email for L1 is verified and not for L2, SupeTokens will auto verify the email for L2 when: +- The user next logs in with L2. +- If you call `updateEmailOrPassword` from the email password or `updateUser` from the passwordless recipe, updating L2's email to be equal to L1's email. + +## Affect on email update +When updating the email of a login method for a user, SuperTokens needs to make sure that the account linking conditions mentioned above remain intact. This means that you cannot update the email of a primary user to a value that matches the email of another primary user. + +For example, if User A has login method `AL1` (email `e1`) and `AL2` (email `e1`), and User B has login method `BL1` (email `e2`) and `BL2` (email `e3`), then we cannot update `AL1` email to `e2` or `e3` because that would lead to two primary users having the same email. + +Now email updates can happen in different scenarios: +- 1) Calling the `updateEmailOrPassword` from the email password recipe +- 2) Calling the `updateUser` function from the passwordless recipe +- 3) Logging in via social login can also update emails if the email has changed from the provider's side. + +In each of these cases, the operation will fail and an appropriate status code will be returned: +- For function calls (1) and (2), you will get back a response with a status indicating that email update was not possible. +- For social login API call (3), the client will get a response with a status indicating to contact support, with a support status code (see below). + +## Migration of user data when accounts are linked +When two accounts are linked the primary user ID of the non primary user changes. + +For example, if we have User A with with primary user ID `p1` and user B, which is a non primary user, and has a user ID of `p2`, and we link them, then the primary user ID of User B will be changed to `p1`. + +This has an effect that if the user logs in with login method from User B, the `session.getUserId()` will return `p1`. If there was any older data associated with User B (against user ID `p2`), in your database, that data will essentially be "lost". + +To prevent this scenario, you should: +- Make sure that you return `false` for `shouldAutomaticallyLink` boolean in the `shouldDoAutomaticAccountLinking` function implementation if there exists a `recipeUserId` in the `newAccountInfo` object, and if you have some information related to that user ID in your own database. This can be seen in the [code snippet above](#enabling-automatic-account-linking). + +- If you do not want to return `false` in this case, and want the accounts to be linked, then make sure to implement the `onAccountLinked` callback: + + ```tsx + import supertokens, { User, RecipeUserId } from "supertokens-node"; + import AccountLinking from "supertokens-node/recipe/accountlinking"; + import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; + + supertokens.init({ + supertokens: { + connectionURI: "", + apiKey: "" + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + }, + // highlight-start + onAccountLinked: async (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => { + let olderUserId = newAccountInfo.recipeUserId.getAsString() + let newUserId = user.id; + + // TODO: migrate data from olderUserId to newUserId in your database... + } + // highlight-end + }) + ] + }); + ``` + +:::caution +If your logic in `onAccountLinked` throws an error, then it will not be called again, and will still have resulted in the accounts being linked. +::: + +## Support status codes +The following is a list of support status codes that the end user might see during their interaction with the login UI (as a general error message in the pre built UI). + +### ERR_CODE_001 +- This can happen during creating a password reset code in the email password flow: + - API path and method: `/user/password/reset/token POST` + - Output JSON: + ```json + { + "status": "PASSWORD_RESET_NOT_ALLOWED", + "reason": + "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is the scenario for when this status is returned: + + A malicious user, User A, which is a primary user, has login methods with email `e1` (social login) and email `e1` (email password login). If user A changes their emailpassword email to `e2` (which is now in unverified state), and the real user of `e2` (the victim) tries to sign up via email password, they will see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If we allow this to happen, and the victim resets the password (since they are the real owner of the email), then they will be able to login to the account, and the attacker can spy on what the user is doing via their third party login method. + + To prevent this scenario, we enforce that the password link is only generated if the primary user has at least one login method that has the input email ID and is verified, or if not, we check that the primary user has no other login method with a different email, or phone number. If these cases are not satisfied, then we return the error code `ERR_CODE_001`. + +- To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **You can do these actions using our user management dashboard.** + +### ERR_CODE_002 +- This can happen during the passwordless recipe's create or consume code API (during sign up): + - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + A user is trying to sign up using passwordless login method with email `e1`. There exists an email password login method with `e1`, which is unverified (owned by an attacker). If we allow the passwordless sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the passwordless login method. This way, the attacker now has access to the user's account. + + To prevent this, we do not allow sign up with passwordless login in case there exists another account with the same email and is unverified. + +- To resolve this issue, you should ask the user to try another login method (which already has their email), or then mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can do these actions using our user management dashboard.** + + +### ERR_CODE_003 +- This can happen during the passwordless recipe's create or consume code API (during sign in): + - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + Let's say that automatic account linking is switched off in your application. There exists a passwordless user with email `e1` (owned by the victim). There exists an email password login method with `e2`, which is verified (owned by an attacker). Then you switch on automatic account linking, and the email password user becomes a primary user (this happens if they login). Now the attacker somehow changes their email to `e1` (via a support ticket), but is in an unverified state. In this case, when the passwordless user tries to sign in, they will see this error code. We prevent the sign in because if we allowed it, it may link thier account to the email password one (since the email password one is a primary account), thereby compromising the victim's account. + +- To resolve this issue, you either mark the unverified account as verified, or then delete that particular login method / account. **You can do these actions using our user management dashboard.** + +### ERR_CODE_004 +- This can happen during the thirdparty recipe's signinup API (during sign in): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a thirdparty user with email `e1`, sign in with Google (owned by the victim, and the email is verified). There exists another third party login method with email, `e2` (owned by an attacker), let's say it's login with Github. The attacker then goes to their Github and changes their email to `e1` (which is in unverified state). The next time the attacker tries to login, via github, they will see this error code. We prevent login, because if we didn't, then the attacker might send an email verification link to `e1`, and if the victim clicks on it, then the attacker's account will be linked to the victim's account. + +- To resolve this issue, you can delete the login method that has the unverified email, or if manually mark the unverified account as verified (if you confirm the identity of its owner). **You can do these actions using our user management dashboard.** + +### ERR_CODE_005 +- This can happen during the thirdparty recipe's signinup API (during sign in): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, thirdparty user with email `e1`, sign in with Google. There exists another email password user with email `e2`, which is a primary user. Now, if the user changes their email on Google to `e2`, and then try logging in via Google, they will see this error code. We do this because if we didn't, then it would result in two primary users having the same email, which voilates one of the account linking rules. + +- To resolve this issue, you can make one of the primary users as non primary (this can be done by using the unlink button against the login methon on our user management dashboard). Once the user is not a primary user, you can ask the user to relogin with that method, and it should auto link that account with the existing primary user. + +### ERR_CODE_006 +- This can happen during the thirdparty recipe's signinup API (during sign up): + - API path and method: `/signinup POST` + - Output JSON: + ```json + { + "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_006)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + A user is trying to sign up using third party login method with email `e1`. There exists an email password login method with `e1`, which is unverified (owned by an attacker). If we allow the third party sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the third party login method. This way, the attacker now has access to the user's account. + + To prevent this, we do not allow sign up with third party login in case there exists another account with the same email and is unverified. + +- To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using our user management dashboard.** + +### Changing the error message on the frontend +If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). \ No newline at end of file diff --git a/v2/thirdpartypasswordless/sidebars.js b/v2/thirdpartypasswordless/sidebars.js index 96b87dce7..7b83b8357 100644 --- a/v2/thirdpartypasswordless/sidebars.js +++ b/v2/thirdpartypasswordless/sidebars.js @@ -433,7 +433,8 @@ module.exports = { type: "category", label: "Account Linking", items: [ - "common-customizations/account-linking/overview" + "common-customizations/account-linking/overview", + "common-customizations/account-linking/automatic-account-linking" ] }, { From e992d3427432be4f2210617b2df4c186935fc623 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 7 Sep 2023 14:26:23 +0530 Subject: [PATCH 48/81] more changes --- .../manual-account-linking.mdx | 221 ++++++++++++++++++ v2/thirdpartyemailpassword/sidebars.js | 3 +- 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx new file mode 100644 index 000000000..2a5e948a4 --- /dev/null +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx @@ -0,0 +1,221 @@ +--- +id: manual-account-linking +title: Manual account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Manual account linking + +Manual account linking allows you to take control of when and which accounts are linked. With this, you can implement flows like: +- Connecting social login accounts to an existing account post login. +- Linking accounts which don't have the same email or phone number, or have a different identifier alltogether. + +## Creating a primary user + +In order to link two accounts, you first need to make one of them a primary user: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import {RecipeUserId} from "supertokens-node"; + +async function makeUserPrimary(recipeUserId: RecipeUserId) { + let response = await AccountLinking.createPrimaryUser(recipeUserId); + if (response.status === "OK") { + if (response.wasAlreadyAPrimaryUser) { + // The input user was already a primary user and accounts can be linked to it. + } else { + // User is now primary and accounts can be linked to it. + } + let modifiedUser = response.user; + console.log(modifiedUser.isPrimaryUser); // will print true + } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if there already exists another primary user with the same email or phone number + // in at least one of the tenants that this user belongs to. + } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { + // This happens if this user is already linked to another primary user. + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Linking accounts + +Once a user has become a primary user, you can link other accounts to this user: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { RecipeUserId } from "supertokens-node"; + +// we are linking the input recipeUserId to the primaryUserId +async function linkAccounts(primaryUserId: string, recipeUserId: RecipeUserId) { + let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId); + if (response.status === "OK") { + if (response.accountsAlreadyLinked) { + // The input users were already linked + } else { + // The two users are now linked + } + let modifiedUser = response.user; + console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well. + } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if there already exists another primary user with the same email or phone number + // as the recipeUserId's account. + } else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + // This happens if the input primaryUserId is not actually a primary user ID. + // You can call createPrimaryUserId and call linkAccountsAgain + } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if the input recipe user ID is already linked to another primary user. + // You can call unlink accounts on the recipe user ID and then try linking again. + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Unlinking accounts + +If you want to unlink an account from its primary user ID, you can use the following function: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { RecipeUserId } from "supertokens-node"; + +async function unlinkAccount(recipeUserId: RecipeUserId) { + let response = await AccountLinking.unlinkAccount(recipeUserId); + if (response.status === "OK") { + if (response.wasLinked) { + // This means that we unlinked the account from its primary user ID + } else { + // This means that the user was never linked in the first place + } + + if (response.wasRecipeUserDeleted) { + // This is true if we call unlinkAccount on the recipe user ID of the primary user ID user. + // We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens + // won't know which user info to return - the primary user, or the recipe user. + // Note that even though the recipe user is deleted, the session, metadata, roles etc for this + // primary user is still intact, and calling getUserById(primaryUserId) will still return + // the user object with the other login methods. + } else { + // There not exists a user account which is not a primary user, with the recipeUserId = to the + // input recipeUserId. + } + + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Converting a string userId into a recipeUserId +If you notice, the input to a lot of the functions above is of type `RecipeUserId`. You can convert a string userID into a `RecipeUserId` in the following way: + + + + +```tsx +import SuperTokens from "supertokens-node"; + +async function getAsRecipeUserIdType(userId: string) { + return SuperTokens.convertToRecipeUserId(userId); +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +The reason we have this type is because it prevents bugs wherein a function is expecting a recipe user id (like `createNewSession`, or `updateEmailOrPassword` from email password recipe), but you pass in the primary user ID instead. + +## Other helper functions +Our SDK also exposes several other helper functions: + +- `AccountLinking.createPrimaryUserIdOrLinkAccounts`: Given a recipe user ID, this function will attempt linking it with any primary user ID that has the same email or phone number associated with it. If no such primary user exists, this function will make the input user account a primary one. + +- `AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId`: Given a recipe user ID, this function will return a primary user ID which this user can be linked to, based on matching emails / phone numbers. If no such primary user exists, this function will return `undefined`. + +- `AccountLinking.canCreatePrimaryUser`: Given a recipe user ID, this function will return a status `OK` if the user can be made a primary user, and a different status otherwise (indicating why it can't become a primary user). A user can be made a primary user if there exists no other primary user with the same email or phone number across all the tenants that this user belongs to. + +- `AccountLinkling.canLinkAccounts`: Given a recipeUserId and a primary user ID, this function returns a status `OK` if the accounts can be linked, and if not, it returns a different status (indicating why the accounts can't be linked). Accounts can be linked if the recipe user ID is not already linked to another primary user, and if the resulting primary user does not have any email / phone number in common with another primary user across all of the tenants that it belongs to. + +- `AccountLinking.isSignUpAllowed`: Given the login info (email for example) of the new user, who is trying to sign up, this function returns `true` if it's safe to allow them to sign up, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. + +- `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. + +- `AccountLinking.isEmailChangeAllowed`: Given the recipe user id and the new email for update, this function returns `true` if it's safe to update the email, else `false`. TODO.. \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/sidebars.js b/v2/thirdpartyemailpassword/sidebars.js index ad535413f..8cf5c2e09 100644 --- a/v2/thirdpartyemailpassword/sidebars.js +++ b/v2/thirdpartyemailpassword/sidebars.js @@ -439,7 +439,8 @@ module.exports = { label: "Account Linking", items: [ "common-customizations/account-linking/overview", - "common-customizations/account-linking/automatic-account-linking" + "common-customizations/account-linking/automatic-account-linking", + "common-customizations/account-linking/manual-account-linking" ] }, "common-customizations/change-password", From 6a997edd616401bae4cf4ddea5036c3d4af0c082 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 7 Sep 2023 14:41:50 +0530 Subject: [PATCH 49/81] more docs --- .../manual-account-linking.mdx | 223 ++++++++++++++++++ v2/thirdparty/sidebars.js | 3 +- .../manual-account-linking.mdx | 4 +- .../manual-account-linking.mdx | 223 ++++++++++++++++++ v2/thirdpartypasswordless/sidebars.js | 3 +- 5 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 v2/thirdparty/common-customizations/account-linking/manual-account-linking.mdx create mode 100644 v2/thirdpartypasswordless/common-customizations/account-linking/manual-account-linking.mdx diff --git a/v2/thirdparty/common-customizations/account-linking/manual-account-linking.mdx b/v2/thirdparty/common-customizations/account-linking/manual-account-linking.mdx new file mode 100644 index 000000000..33e2c162d --- /dev/null +++ b/v2/thirdparty/common-customizations/account-linking/manual-account-linking.mdx @@ -0,0 +1,223 @@ +--- +id: manual-account-linking +title: Manual account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Manual account linking + +Manual account linking allows you to take control of when and which accounts are linked. With this, you can implement flows like: +- Connecting social login accounts to an existing account post login. +- Linking accounts which don't have the same email or phone number, or have a different identifier alltogether. + +## Creating a primary user + +In order to link two accounts, you first need to make one of them a primary user: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import {RecipeUserId} from "supertokens-node"; + +async function makeUserPrimary(recipeUserId: RecipeUserId) { + let response = await AccountLinking.createPrimaryUser(recipeUserId); + if (response.status === "OK") { + if (response.wasAlreadyAPrimaryUser) { + // The input user was already a primary user and accounts can be linked to it. + } else { + // User is now primary and accounts can be linked to it. + } + let modifiedUser = response.user; + console.log(modifiedUser.isPrimaryUser); // will print true + } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if there already exists another primary user with the same email or phone number + // in at least one of the tenants that this user belongs to. + } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { + // This happens if this user is already linked to another primary user. + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Linking accounts + +Once a user has become a primary user, you can link other accounts to this user: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { RecipeUserId } from "supertokens-node"; + +// we are linking the input recipeUserId to the primaryUserId +async function linkAccounts(primaryUserId: string, recipeUserId: RecipeUserId) { + let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId); + if (response.status === "OK") { + if (response.accountsAlreadyLinked) { + // The input users were already linked + } else { + // The two users are now linked + } + let modifiedUser = response.user; + console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well. + } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if there already exists another primary user with the same email or phone number + // as the recipeUserId's account. + } else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + // This happens if the input primaryUserId is not actually a primary user ID. + // You can call createPrimaryUserId and call linkAccountsAgain + } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if the input recipe user ID is already linked to another primary user. + // You can call unlink accounts on the recipe user ID and then try linking again. + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Unlinking accounts + +If you want to unlink an account from its primary user ID, you can use the following function: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { RecipeUserId } from "supertokens-node"; + +async function unlinkAccount(recipeUserId: RecipeUserId) { + let response = await AccountLinking.unlinkAccount(recipeUserId); + if (response.status === "OK") { + if (response.wasLinked) { + // This means that we unlinked the account from its primary user ID + } else { + // This means that the user was never linked in the first place + } + + if (response.wasRecipeUserDeleted) { + // This is true if we call unlinkAccount on the recipe user ID of the primary user ID user. + // We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens + // won't know which user info to return - the primary user, or the recipe user. + // Note that even though the recipe user is deleted, the session, metadata, roles etc for this + // primary user is still intact, and calling getUserById(primaryUserId) will still return + // the user object with the other login methods. + } else { + // There not exists a user account which is not a primary user, with the recipeUserId = to the + // input recipeUserId. + } + + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Converting a string userId into a recipeUserId +If you notice, the input to a lot of the functions above is of type `RecipeUserId`. You can convert a string userID into a `RecipeUserId` in the following way: + + + + +```tsx +import SuperTokens from "supertokens-node"; + +async function getAsRecipeUserIdType(userId: string) { + return SuperTokens.convertToRecipeUserId(userId); +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +The reason we have this type is because it prevents bugs wherein a function is expecting a recipe user id (like `createNewSession`, or `updateEmailOrPassword` from email password recipe), but you pass in the primary user ID instead. + +## Other helper functions +Our SDK also exposes several other helper functions: + +- `AccountLinking.createPrimaryUserIdOrLinkAccounts`: Given a recipe user ID, this function will attempt linking it with any primary user ID that has the same email or phone number associated with it. If no such primary user exists, this function will make the input user account a primary one. + +- `AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId`: Given a recipe user ID, this function will return a primary user ID which this user can be linked to, based on matching emails / phone numbers. If no such primary user exists, this function will return `undefined`. + +- `AccountLinking.canCreatePrimaryUser`: Given a recipe user ID, this function will return a status `OK` if the user can be made a primary user, and a different status otherwise (indicating why it can't become a primary user). A user can be made a primary user if there exists no other primary user with the same email or phone number across all the tenants that this user belongs to. + +- `AccountLinkling.canLinkAccounts`: Given a recipeUserId and a primary user ID, this function returns a status `OK` if the accounts can be linked, and if not, it returns a different status (indicating why the accounts can't be linked). Accounts can be linked if the recipe user ID is not already linked to another primary user, and if the resulting primary user does not have any email / phone number in common with another primary user across all of the tenants that it belongs to. + +- `AccountLinking.isSignUpAllowed`: Given the login info (email for example) of the new user, who is trying to sign up, this function returns `true` if it's safe to allow them to sign up, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. + +- `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. + +- `AccountLinking.isEmailChangeAllowed`: Given the recipe user id and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` is returned: + - If the input 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 email. + - If the recipe user is NOT a primary user, and if the new email is not verified, then we check if there exists a primary user with the same email, and if it exists, we disallow the email change. We disallow because 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. \ No newline at end of file diff --git a/v2/thirdparty/sidebars.js b/v2/thirdparty/sidebars.js index ced9ca922..3f4d33b9c 100644 --- a/v2/thirdparty/sidebars.js +++ b/v2/thirdparty/sidebars.js @@ -416,7 +416,8 @@ module.exports = { label: "Account Linking", items: [ "common-customizations/account-linking/overview", - "common-customizations/account-linking/automatic-account-linking" + "common-customizations/account-linking/automatic-account-linking", + "common-customizations/account-linking/manual-account-linking" ] }, { diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx index 2a5e948a4..33e2c162d 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx @@ -218,4 +218,6 @@ Our SDK also exposes several other helper functions: - `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. -- `AccountLinking.isEmailChangeAllowed`: Given the recipe user id and the new email for update, this function returns `true` if it's safe to update the email, else `false`. TODO.. \ No newline at end of file +- `AccountLinking.isEmailChangeAllowed`: Given the recipe user id and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` is returned: + - If the input 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 email. + - If the recipe user is NOT a primary user, and if the new email is not verified, then we check if there exists a primary user with the same email, and if it exists, we disallow the email change. We disallow because 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. \ No newline at end of file diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/manual-account-linking.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/manual-account-linking.mdx new file mode 100644 index 000000000..33e2c162d --- /dev/null +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/manual-account-linking.mdx @@ -0,0 +1,223 @@ +--- +id: manual-account-linking +title: Manual account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Manual account linking + +Manual account linking allows you to take control of when and which accounts are linked. With this, you can implement flows like: +- Connecting social login accounts to an existing account post login. +- Linking accounts which don't have the same email or phone number, or have a different identifier alltogether. + +## Creating a primary user + +In order to link two accounts, you first need to make one of them a primary user: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import {RecipeUserId} from "supertokens-node"; + +async function makeUserPrimary(recipeUserId: RecipeUserId) { + let response = await AccountLinking.createPrimaryUser(recipeUserId); + if (response.status === "OK") { + if (response.wasAlreadyAPrimaryUser) { + // The input user was already a primary user and accounts can be linked to it. + } else { + // User is now primary and accounts can be linked to it. + } + let modifiedUser = response.user; + console.log(modifiedUser.isPrimaryUser); // will print true + } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if there already exists another primary user with the same email or phone number + // in at least one of the tenants that this user belongs to. + } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { + // This happens if this user is already linked to another primary user. + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Linking accounts + +Once a user has become a primary user, you can link other accounts to this user: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { RecipeUserId } from "supertokens-node"; + +// we are linking the input recipeUserId to the primaryUserId +async function linkAccounts(primaryUserId: string, recipeUserId: RecipeUserId) { + let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId); + if (response.status === "OK") { + if (response.accountsAlreadyLinked) { + // The input users were already linked + } else { + // The two users are now linked + } + let modifiedUser = response.user; + console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well. + } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if there already exists another primary user with the same email or phone number + // as the recipeUserId's account. + } else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + // This happens if the input primaryUserId is not actually a primary user ID. + // You can call createPrimaryUserId and call linkAccountsAgain + } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if the input recipe user ID is already linked to another primary user. + // You can call unlink accounts on the recipe user ID and then try linking again. + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Unlinking accounts + +If you want to unlink an account from its primary user ID, you can use the following function: + + + + +```tsx +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { RecipeUserId } from "supertokens-node"; + +async function unlinkAccount(recipeUserId: RecipeUserId) { + let response = await AccountLinking.unlinkAccount(recipeUserId); + if (response.status === "OK") { + if (response.wasLinked) { + // This means that we unlinked the account from its primary user ID + } else { + // This means that the user was never linked in the first place + } + + if (response.wasRecipeUserDeleted) { + // This is true if we call unlinkAccount on the recipe user ID of the primary user ID user. + // We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens + // won't know which user info to return - the primary user, or the recipe user. + // Note that even though the recipe user is deleted, the session, metadata, roles etc for this + // primary user is still intact, and calling getUserById(primaryUserId) will still return + // the user object with the other login methods. + } else { + // There not exists a user account which is not a primary user, with the recipeUserId = to the + // input recipeUserId. + } + + } +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +## Converting a string userId into a recipeUserId +If you notice, the input to a lot of the functions above is of type `RecipeUserId`. You can convert a string userID into a `RecipeUserId` in the following way: + + + + +```tsx +import SuperTokens from "supertokens-node"; + +async function getAsRecipeUserIdType(userId: string) { + return SuperTokens.convertToRecipeUserId(userId); +} +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +The reason we have this type is because it prevents bugs wherein a function is expecting a recipe user id (like `createNewSession`, or `updateEmailOrPassword` from email password recipe), but you pass in the primary user ID instead. + +## Other helper functions +Our SDK also exposes several other helper functions: + +- `AccountLinking.createPrimaryUserIdOrLinkAccounts`: Given a recipe user ID, this function will attempt linking it with any primary user ID that has the same email or phone number associated with it. If no such primary user exists, this function will make the input user account a primary one. + +- `AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId`: Given a recipe user ID, this function will return a primary user ID which this user can be linked to, based on matching emails / phone numbers. If no such primary user exists, this function will return `undefined`. + +- `AccountLinking.canCreatePrimaryUser`: Given a recipe user ID, this function will return a status `OK` if the user can be made a primary user, and a different status otherwise (indicating why it can't become a primary user). A user can be made a primary user if there exists no other primary user with the same email or phone number across all the tenants that this user belongs to. + +- `AccountLinkling.canLinkAccounts`: Given a recipeUserId and a primary user ID, this function returns a status `OK` if the accounts can be linked, and if not, it returns a different status (indicating why the accounts can't be linked). Accounts can be linked if the recipe user ID is not already linked to another primary user, and if the resulting primary user does not have any email / phone number in common with another primary user across all of the tenants that it belongs to. + +- `AccountLinking.isSignUpAllowed`: Given the login info (email for example) of the new user, who is trying to sign up, this function returns `true` if it's safe to allow them to sign up, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. + +- `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. + +- `AccountLinking.isEmailChangeAllowed`: Given the recipe user id and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` is returned: + - If the input 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 email. + - If the recipe user is NOT a primary user, and if the new email is not verified, then we check if there exists a primary user with the same email, and if it exists, we disallow the email change. We disallow because 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. \ No newline at end of file diff --git a/v2/thirdpartypasswordless/sidebars.js b/v2/thirdpartypasswordless/sidebars.js index 7b83b8357..25e16113b 100644 --- a/v2/thirdpartypasswordless/sidebars.js +++ b/v2/thirdpartypasswordless/sidebars.js @@ -434,7 +434,8 @@ module.exports = { label: "Account Linking", items: [ "common-customizations/account-linking/overview", - "common-customizations/account-linking/automatic-account-linking" + "common-customizations/account-linking/automatic-account-linking", + "common-customizations/account-linking/manual-account-linking" ] }, { From 47d120a5b256659ed7a52a24336b50ea17036056 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 7 Sep 2023 14:52:00 +0530 Subject: [PATCH 50/81] more docs --- .../account-linking/security-considerations.mdx | 16 ++++++++++++++++ v2/thirdpartyemailpassword/sidebars.js | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx new file mode 100644 index 000000000..36061bfb7 --- /dev/null +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx @@ -0,0 +1,16 @@ +--- +id: security-considerations +title: Security considerations for automatic account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Security considerations for automatic account linking + diff --git a/v2/thirdpartyemailpassword/sidebars.js b/v2/thirdpartyemailpassword/sidebars.js index 8cf5c2e09..c16a652ae 100644 --- a/v2/thirdpartyemailpassword/sidebars.js +++ b/v2/thirdpartyemailpassword/sidebars.js @@ -440,7 +440,8 @@ module.exports = { items: [ "common-customizations/account-linking/overview", "common-customizations/account-linking/automatic-account-linking", - "common-customizations/account-linking/manual-account-linking" + "common-customizations/account-linking/manual-account-linking", + "common-customizations/account-linking/security-considerations" ] }, "common-customizations/change-password", From 06f04bb7baec02a57548300a902fd9a9152f7e97 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 8 Sep 2023 12:25:03 +0530 Subject: [PATCH 51/81] more docs --- .../security-considerations.mdx | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx index 36061bfb7..0249cc68f 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx @@ -14,3 +14,36 @@ import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; # Security considerations for automatic account linking +Below is the list of all points in time when we do account linking, and for each point, you can see the list of security checks that happen: + +## 1) During sign up: + +### Case a +- Email is `e1` +- Email is verified: `false` (is the case with email password sign up or social login with a provider that does not require email verification) + +#### Checks done: +- i) If there is no primary user with the same email, then we disallow sign up if there exists any other non-primary account with the same email and that accoun is not verified. We do this because if we didn't, there is a risk that if this user signs up and becomes a primary user, the other account (which could be malicious), might resend a verification email and the user might click on it (since they just signed up) and verify the malicious account, thereby linking it to their account. This way, the malicious user will have access to the victim's account. +- ii) If there exists a primary user with the same email, then we reject sign up of this new user. This is done cause if we allowed it, and this sign up is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. + +#### What users see: +- i) In case of email password login, users see an account already exists error, in which case, they can try logging in with another method, or go through the password reset flow, which will create a new email password account for them as well as verify it. +- ii) In case of social login, users will see that they should try a different login method for security reasons. + +### Case b + +- Email is `e1` +- Email is verified: `true` (is the case with social login with a provider that requires email verification, like google, or it could be a passwordless sign up) + +#### Checks done: +- i) Same as in Case 1a, point (i) above. +- ii) If there exists a primary user with the same email, then we allow sign up only if there exists at least one login method in the primary user with email `e1` which is verified. This is done because if we didn't, then the following account takeover is possible: + - Malicious user signs up with email password, with email `e2`, verifies it, and becomes a primary user. + - They then change their email to `e1`, and keep it in an unverified state. + - Now the actual user (victim), does a google sign with email `e1`. + - If we don't stop this sign up, then the new sign up will be linked to the primary user, and the malicious user will have access to the victim's account. + +#### What users see: +- Users will see that they should try a different login method for security reasons. + +## 2) During sign in: \ No newline at end of file From 7b48adda647ad126827a45dc2428bd867b7575b4 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 8 Sep 2023 15:26:29 +0530 Subject: [PATCH 52/81] more changes --- .../automatic-account-linking.mdx | 4 ++-- .../automatic-account-linking.mdx | 2 +- .../security-considerations.mdx | 18 ++++++++++++++++-- .../automatic-account-linking.mdx | 4 ++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx index 93ec784cf..440f8adc8 100644 --- a/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx @@ -253,7 +253,7 @@ The following is a list of support status codes that the end user might see duri }}/> - Below is as example scenario for when this status is returned (one amongst many): - Let's say that automatic account linking is switched off in your application. There exists a passwordless user with email `e1` (owned by the victim). There exists an email password login method with `e2`, which is verified (owned by an attacker). Then you switch on automatic account linking, and the email password user becomes a primary user (this happens if they login). Now the attacker somehow changes their email to `e1` (via a support ticket), but is in an unverified state. In this case, when the passwordless user tries to sign in, they will see this error code. We prevent the sign in because if we allowed it, it may link thier account to the email password one (since the email password one is a primary account), thereby compromising the victim's account. + A malicious user has a passwordless account with email `e1`. The victim has an email password login method with `e2`, which is verified. Both of these are non primary users since you have account linking switched off. Then you switch on automatic account linking. Now the attacker somehow changes their email to `e2` (via a support ticket perhaps), but it's in an unverified state. In this case, when the attacker enters email `e2` in the passwordless login box, this error code will show up. We do this because if we didn't, then the attacker might send a magic link to `e2`, and if the victim clicks on it, then the attacker's account will be linked to the victim's account. Even though the attacker won't be able to login to that account again, linking a potentially malicious account to a victim's account is not a good idea. - To resolve this issue, you either mark the unverified account as verified, or then delete that particular login method / account. **You can do these actions using our user management dashboard.** @@ -320,4 +320,4 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using our user management dashboard.** ### Changing the error message on the frontend -If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). \ No newline at end of file +If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 93ec784cf..94696e62f 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -253,7 +253,7 @@ The following is a list of support status codes that the end user might see duri }}/> - Below is as example scenario for when this status is returned (one amongst many): - Let's say that automatic account linking is switched off in your application. There exists a passwordless user with email `e1` (owned by the victim). There exists an email password login method with `e2`, which is verified (owned by an attacker). Then you switch on automatic account linking, and the email password user becomes a primary user (this happens if they login). Now the attacker somehow changes their email to `e1` (via a support ticket), but is in an unverified state. In this case, when the passwordless user tries to sign in, they will see this error code. We prevent the sign in because if we allowed it, it may link thier account to the email password one (since the email password one is a primary account), thereby compromising the victim's account. + A malicious user has a passwordless account with email `e1`. The victim has an email password login method with `e2`, which is verified. Both of these are non primary users since you have account linking switched off. Then you switch on automatic account linking. Now the attacker somehow changes their email to `e2` (via a support ticket perhaps), but it's in an unverified state. In this case, when the attacker enters email `e2` in the passwordless login box, this error code will show up. We do this because if we didn't, then the attacker might send a magic link to `e2`, and if the victim clicks on it, then the attacker's account will be linked to the victim's account. Even though the attacker won't be able to login to that account again, linking a potentially malicious account to a victim's account is not a good idea. - To resolve this issue, you either mark the unverified account as verified, or then delete that particular login method / account. **You can do these actions using our user management dashboard.** diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx index 0249cc68f..c8737980e 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx @@ -23,7 +23,7 @@ Below is the list of all points in time when we do account linking, and for each - Email is verified: `false` (is the case with email password sign up or social login with a provider that does not require email verification) #### Checks done: -- i) If there is no primary user with the same email, then we disallow sign up if there exists any other non-primary account with the same email and that accoun is not verified. We do this because if we didn't, there is a risk that if this user signs up and becomes a primary user, the other account (which could be malicious), might resend a verification email and the user might click on it (since they just signed up) and verify the malicious account, thereby linking it to their account. This way, the malicious user will have access to the victim's account. +- i) If there is no primary user with the same email, then we disallow sign up if there exists any other non-primary account with the same email and that account is not verified. We do this because if we didn't, there is a risk that if this user signs up and becomes a primary user, the other account (which could be malicious), might resend a verification email and the user might click on it (since they just signed up) and verify the malicious account, thereby linking it to their account. This way, the malicious user will have access to the victim's account. - ii) If there exists a primary user with the same email, then we reject sign up of this new user. This is done cause if we allowed it, and this sign up is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. #### What users see: @@ -46,4 +46,18 @@ Below is the list of all points in time when we do account linking, and for each #### What users see: - Users will see that they should try a different login method for security reasons. -## 2) During sign in: \ No newline at end of file +## 2) During sign in: +### Case a +- Email is `e1` +- Email is verified: `false` +- User is not a primary user + +#### Checks done: + - If there exists another user with the same email, and they are not a primary user, but their email is unverified, we disallow this sign in because if we did, and this user verifies their email, it will result in this user becoming a primary user. If the other account then sends an email verification email, this user may click on it (they may not get too suspicious), and verify the other account, thereby linking it to their account. This way, the other account, which may belong to a malicious user, will have access to the victim's account. + - If there exists another user with the same email, and they are not a primary user, we disallow signing in. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. + +#### What users see: + - For email password sign in, users will see a wrong credentials error message. This will prompt them to go through the password reset flow, which will also mark the email as verified, thereby allowing them to sign in. This will also block sign ins from malicious users, since the new password is only known to the actual owner of the email. + - For passwordless or social login, users will see that they should try a different login method for security reasons. + +### Case b diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx index 93ec784cf..440f8adc8 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx @@ -253,7 +253,7 @@ The following is a list of support status codes that the end user might see duri }}/> - Below is as example scenario for when this status is returned (one amongst many): - Let's say that automatic account linking is switched off in your application. There exists a passwordless user with email `e1` (owned by the victim). There exists an email password login method with `e2`, which is verified (owned by an attacker). Then you switch on automatic account linking, and the email password user becomes a primary user (this happens if they login). Now the attacker somehow changes their email to `e1` (via a support ticket), but is in an unverified state. In this case, when the passwordless user tries to sign in, they will see this error code. We prevent the sign in because if we allowed it, it may link thier account to the email password one (since the email password one is a primary account), thereby compromising the victim's account. + A malicious user has a passwordless account with email `e1`. The victim has an email password login method with `e2`, which is verified. Both of these are non primary users since you have account linking switched off. Then you switch on automatic account linking. Now the attacker somehow changes their email to `e2` (via a support ticket perhaps), but it's in an unverified state. In this case, when the attacker enters email `e2` in the passwordless login box, this error code will show up. We do this because if we didn't, then the attacker might send a magic link to `e2`, and if the victim clicks on it, then the attacker's account will be linked to the victim's account. Even though the attacker won't be able to login to that account again, linking a potentially malicious account to a victim's account is not a good idea. - To resolve this issue, you either mark the unverified account as verified, or then delete that particular login method / account. **You can do these actions using our user management dashboard.** @@ -320,4 +320,4 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using our user management dashboard.** ### Changing the error message on the frontend -If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). \ No newline at end of file +If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). From b474a839f4af5a743fc95a8c202c2904fa99fd37 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 8 Sep 2023 15:40:46 +0530 Subject: [PATCH 53/81] more changes --- .../security-considerations.mdx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx index c8737980e..c16337622 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx @@ -53,11 +53,22 @@ Below is the list of all points in time when we do account linking, and for each - User is not a primary user #### Checks done: - - If there exists another user with the same email, and they are not a primary user, but their email is unverified, we disallow this sign in because if we did, and this user verifies their email, it will result in this user becoming a primary user. If the other account then sends an email verification email, this user may click on it (they may not get too suspicious), and verify the other account, thereby linking it to their account. This way, the other account, which may belong to a malicious user, will have access to the victim's account. - - If there exists another user with the same email, and they are not a primary user, we disallow signing in. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. +- If there exists another user with the same email, and they are not a primary user, but their email is unverified, we disallow this sign in because if we did, and this user verifies their email, it will result in this user becoming a primary user. If the other account then sends an email verification email, this user may click on it (they may not get too suspicious), and verify the other account, thereby linking it to their account. This way, the other account, which may belong to a malicious user, will have access to the victim's account. +- If there exists another user with the same email, and they are not a primary user, we disallow signing in. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. #### What users see: - - For email password sign in, users will see a wrong credentials error message. This will prompt them to go through the password reset flow, which will also mark the email as verified, thereby allowing them to sign in. This will also block sign ins from malicious users, since the new password is only known to the actual owner of the email. - - For passwordless or social login, users will see that they should try a different login method for security reasons. +- For email password sign in, users will see a wrong credentials error message. This will prompt them to go through the password reset flow, which will also mark the email as verified, thereby allowing them to sign in. This will also block sign ins from malicious users, since the new password is only known to the actual owner of the email. +- For passwordless or social login, users will see that they should try a different login method for security reasons. ### Case b +- The user first does a social login sign up with email `e1`. +- They then change their email to `e2` on the provider and tries signing in again. +- The third party provider does not verify emails, so `e2` is unverified. + +#### Checks done: +- If the social login user is not a primary user, and there exists another primary user with email `e2`, then we disallow sign in here. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. +- If this user is a primary user, and there exists another primary user with email `e2`, we disallow sign in because there can't be two primary users with the same email. The email update which happens during sign in for social login, is rejected. + +#### What users see: +- For the first point, users will see a message asking them to try a different login method, or contact support for security reasons. +- For the second case, users will see that email update is not allowed and to contact support. \ No newline at end of file From 254781450c11547cf29d10b84ba91f905e9e7718 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 8 Sep 2023 17:22:01 +0530 Subject: [PATCH 54/81] more changes --- .../account-linking/security-considerations.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx index c16337622..7897a518f 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx @@ -71,4 +71,10 @@ Below is the list of all points in time when we do account linking, and for each #### What users see: - For the first point, users will see a message asking them to try a different login method, or contact support for security reasons. -- For the second case, users will see that email update is not allowed and to contact support. \ No newline at end of file +- For the second case, users will see that email update is not allowed and to contact support. + +## 3) During password reset flow: +TODO + +## 4) During email update flow: +TODO \ No newline at end of file From 865378b5b91a0cbf301dc89665c89ad91a3f050d Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 8 Sep 2023 19:07:56 +0530 Subject: [PATCH 55/81] more changes --- .../account-linking/security-considerations.mdx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx index 7897a518f..bff82c28a 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx @@ -74,7 +74,15 @@ Below is the list of all points in time when we do account linking, and for each - For the second case, users will see that email update is not allowed and to contact support. ## 3) During password reset flow: -TODO +- Malicious user has email password and a social login account with email `e1`, and they both are linked. +- They then change their email to `e2` for the email passowrd login, which belongs to the victim. +- Actual owner of `e2` tries to sign up, but sees that their account already exists (Case 1), they then try to sign in, but can't cause they don't know the password. So they try the password reset flow. + +#### Checks done: +- In this case, we deny generating the password reset token because if we did, then the real user would change the password of the email password account, and also mark it as verified. They would have access to the account, however, the malicious user could also then login using social login (with email `e1`) to access the same account. + + More generally, during password reset, we do not generate a token if the email password account for that email, is associated with a primary account that also has other emails / phone numbers, and the email whose password is being reset is not verified for any of the login methods. + ## 4) During email update flow: TODO \ No newline at end of file From a806e6a79e6b028a1be3bae9373d9874efdcfdd6 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 8 Sep 2023 19:08:58 +0530 Subject: [PATCH 56/81] more changes --- .../account-linking/security-considerations.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx index bff82c28a..9182504ee 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx @@ -83,6 +83,8 @@ Below is the list of all points in time when we do account linking, and for each More generally, during password reset, we do not generate a token if the email password account for that email, is associated with a primary account that also has other emails / phone numbers, and the email whose password is being reset is not verified for any of the login methods. +#### What users see: +- They will see a message telling them that the reset password link was not generated becasue of account takeover risk, and to contact support. ## 4) During email update flow: TODO \ No newline at end of file From cb6662674e50c8ba40f278d0cf94ceda3e574cbb Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 8 Sep 2023 19:26:54 +0530 Subject: [PATCH 57/81] more changes --- .../manual-account-linking.mdx | 3 +- .../security-considerations.mdx | 98 +++++++++++++++++++ v2/thirdparty/sidebars.js | 3 +- .../manual-account-linking.mdx | 1 + .../security-considerations.mdx | 14 ++- .../manual-account-linking.mdx | 3 +- .../security-considerations.mdx | 98 +++++++++++++++++++ v2/thirdpartypasswordless/sidebars.js | 3 +- 8 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 v2/thirdparty/common-customizations/account-linking/security-considerations.mdx create mode 100644 v2/thirdpartypasswordless/common-customizations/account-linking/security-considerations.mdx diff --git a/v2/thirdparty/common-customizations/account-linking/manual-account-linking.mdx b/v2/thirdparty/common-customizations/account-linking/manual-account-linking.mdx index 33e2c162d..9d11dc169 100644 --- a/v2/thirdparty/common-customizations/account-linking/manual-account-linking.mdx +++ b/v2/thirdparty/common-customizations/account-linking/manual-account-linking.mdx @@ -16,6 +16,7 @@ import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; Manual account linking allows you to take control of when and which accounts are linked. With this, you can implement flows like: - Connecting social login accounts to an existing account post login. +- Adding a password to an account that was created with a social or passwordless login. - Linking accounts which don't have the same email or phone number, or have a different identifier alltogether. ## Creating a primary user @@ -220,4 +221,4 @@ Our SDK also exposes several other helper functions: - `AccountLinking.isEmailChangeAllowed`: Given the recipe user id and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` is returned: - If the input 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 email. - - If the recipe user is NOT a primary user, and if the new email is not verified, then we check if there exists a primary user with the same email, and if it exists, we disallow the email change. We disallow because 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. \ No newline at end of file + - If the recipe user is NOT a primary user, and if the new email is not verified, then we check if there exists a primary user with the same email, and if it exists, we disallow the email change. We disallow because 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. diff --git a/v2/thirdparty/common-customizations/account-linking/security-considerations.mdx b/v2/thirdparty/common-customizations/account-linking/security-considerations.mdx new file mode 100644 index 000000000..c59f68aea --- /dev/null +++ b/v2/thirdparty/common-customizations/account-linking/security-considerations.mdx @@ -0,0 +1,98 @@ +--- +id: security-considerations +title: Security considerations for automatic account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Security considerations for automatic account linking + +Below is the list of all points in time when we do account linking, and for each point, you can see the list of security checks that happen: + +## 1) During sign up: + +### Case a +- Email is `e1` +- Email is verified: `false` (is the case with email password sign up or social login with a provider that does not require email verification) + +#### Checks done: +- i) If there is no primary user with the same email, then we disallow sign up if there exists any other non-primary account with the same email and that account is not verified. We do this because if we didn't, there is a risk that if this user signs up and becomes a primary user, the other account (which could be malicious), might resend a verification email and the user might click on it (since they just signed up) and verify the malicious account, thereby linking it to their account. This way, the malicious user will have access to the victim's account. +- ii) If there exists a primary user with the same email, then we reject sign up of this new user. This is done cause if we allowed it, and this sign up is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. + +#### What users see: +- i) In case of email password login, users see an account already exists error, in which case, they can try logging in with another method, or go through the password reset flow, which will create a new email password account for them as well as verify it. +- ii) In case of social login, users will see that they should try a different login method for security reasons. + +### Case b + +- Email is `e1` +- Email is verified: `true` (is the case with social login with a provider that requires email verification, like google, or it could be a passwordless sign up) + +#### Checks done: +- i) Same as in Case 1a, point (i) above. +- ii) If there exists a primary user with the same email, then we allow sign up only if there exists at least one login method in the primary user with email `e1` which is verified. This is done because if we didn't, then the following account takeover is possible: + - Malicious user signs up with email password, with email `e2`, verifies it, and becomes a primary user. + - They then change their email to `e1`, and keep it in an unverified state. + - Now the actual user (victim), does a google sign with email `e1`. + - If we don't stop this sign up, then the new sign up will be linked to the primary user, and the malicious user will have access to the victim's account. + +#### What users see: +- Users will see that they should try a different login method for security reasons. + +## 2) During sign in: +### Case a +- Email is `e1` +- Email is verified: `false` +- User is not a primary user + +#### Checks done: +- If there exists another user with the same email, and they are not a primary user, but their email is unverified, we disallow this sign in because if we did, and this user verifies their email, it will result in this user becoming a primary user. If the other account then sends an email verification email, this user may click on it (they may not get too suspicious), and verify the other account, thereby linking it to their account. This way, the other account, which may belong to a malicious user, will have access to the victim's account. +- If there exists another user with the same email, and they are not a primary user, we disallow signing in. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. + +#### What users see: +- For email password sign in, users will see a wrong credentials error message. This will prompt them to go through the password reset flow, which will also mark the email as verified, thereby allowing them to sign in. This will also block sign ins from malicious users, since the new password is only known to the actual owner of the email. +- For passwordless or social login, users will see that they should try a different login method for security reasons. + +### Case b +- The user first does a social login sign up with email `e1`. +- They then change their email to `e2` on the provider and tries signing in again. +- The third party provider does not verify emails, so `e2` is unverified. + +#### Checks done: +- If the social login user is **not** a primary user, and there exists another primary user with email `e2`, then we disallow sign in here. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. +- If this user is a primary user, and there exists another primary user with email `e2`, we disallow sign in because there can't be two primary users with the same email. The email update which happens during sign in for social login, is rejected. + +#### What users see: +- For the first point, users will see a message asking them to try a different login method, or contact support for security reasons. +- For the second case, users will see that email update is not allowed and to contact support. + +## 3) During password reset flow: +- Malicious user has email password and a social login account with email `e1`, and they both are linked. +- They then change their email to `e2` for the email passowrd login, which belongs to the victim. +- Actual owner of `e2` tries to sign up, but sees that their account already exists (Case 1), they then try to sign in, but can't cause they don't know the password. So they try the password reset flow. + +#### Checks done: +- In this case, we deny generating the password reset token because if we did, then the real user would change the password of the email password account, and also mark it as verified. They would have access to the account, however, the malicious user could also then login using social login (with email `e1`) to access the same account. + + More generally, during password reset, we do not generate a token if the email password account for that email is associated with a primary account that also has other emails / phone numbers, and the email for which the password is being reset is not verified for any of the login methods in that primary user. + +#### What users see: +- They will see a message telling them that the reset password link was not generated becasue of account takeover risk, and to contact support. + +## 4) During email update flow: +- A user has email `e1`, and they want to change it to email `e2` + +#### Checks done: +- If the user's account is not a primary user, and there exists another primary user with email `e2`, then we disallow email update here. This is done cause if we allowed it, and this email update is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. +- If this user is a primary user, and there exists another primary user with email `e2`, we disallow email update because there can't be two primary users with the same email. + +#### What users see: +- If the email update is happening during sign in of social login, users will see a message that email update is not allowed and to contact support. +- If this is happening post login (from a settings page), then you can send any message you want to the user, since this would be your custom API. \ No newline at end of file diff --git a/v2/thirdparty/sidebars.js b/v2/thirdparty/sidebars.js index 3f4d33b9c..e7dae2f4b 100644 --- a/v2/thirdparty/sidebars.js +++ b/v2/thirdparty/sidebars.js @@ -417,7 +417,8 @@ module.exports = { items: [ "common-customizations/account-linking/overview", "common-customizations/account-linking/automatic-account-linking", - "common-customizations/account-linking/manual-account-linking" + "common-customizations/account-linking/manual-account-linking", + "common-customizations/account-linking/security-considerations" ] }, { diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx index 33e2c162d..f417df747 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/manual-account-linking.mdx @@ -16,6 +16,7 @@ import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; Manual account linking allows you to take control of when and which accounts are linked. With this, you can implement flows like: - Connecting social login accounts to an existing account post login. +- Adding a password to an account that was created with a social or passwordless login. - Linking accounts which don't have the same email or phone number, or have a different identifier alltogether. ## Creating a primary user diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx index 9182504ee..c59f68aea 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/security-considerations.mdx @@ -66,7 +66,7 @@ Below is the list of all points in time when we do account linking, and for each - The third party provider does not verify emails, so `e2` is unverified. #### Checks done: -- If the social login user is not a primary user, and there exists another primary user with email `e2`, then we disallow sign in here. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. +- If the social login user is **not** a primary user, and there exists another primary user with email `e2`, then we disallow sign in here. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. - If this user is a primary user, and there exists another primary user with email `e2`, we disallow sign in because there can't be two primary users with the same email. The email update which happens during sign in for social login, is rejected. #### What users see: @@ -81,10 +81,18 @@ Below is the list of all points in time when we do account linking, and for each #### Checks done: - In this case, we deny generating the password reset token because if we did, then the real user would change the password of the email password account, and also mark it as verified. They would have access to the account, however, the malicious user could also then login using social login (with email `e1`) to access the same account. - More generally, during password reset, we do not generate a token if the email password account for that email, is associated with a primary account that also has other emails / phone numbers, and the email whose password is being reset is not verified for any of the login methods. + More generally, during password reset, we do not generate a token if the email password account for that email is associated with a primary account that also has other emails / phone numbers, and the email for which the password is being reset is not verified for any of the login methods in that primary user. #### What users see: - They will see a message telling them that the reset password link was not generated becasue of account takeover risk, and to contact support. ## 4) During email update flow: -TODO \ No newline at end of file +- A user has email `e1`, and they want to change it to email `e2` + +#### Checks done: +- If the user's account is not a primary user, and there exists another primary user with email `e2`, then we disallow email update here. This is done cause if we allowed it, and this email update is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. +- If this user is a primary user, and there exists another primary user with email `e2`, we disallow email update because there can't be two primary users with the same email. + +#### What users see: +- If the email update is happening during sign in of social login, users will see a message that email update is not allowed and to contact support. +- If this is happening post login (from a settings page), then you can send any message you want to the user, since this would be your custom API. \ No newline at end of file diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/manual-account-linking.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/manual-account-linking.mdx index 33e2c162d..9d11dc169 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking/manual-account-linking.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/manual-account-linking.mdx @@ -16,6 +16,7 @@ import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; Manual account linking allows you to take control of when and which accounts are linked. With this, you can implement flows like: - Connecting social login accounts to an existing account post login. +- Adding a password to an account that was created with a social or passwordless login. - Linking accounts which don't have the same email or phone number, or have a different identifier alltogether. ## Creating a primary user @@ -220,4 +221,4 @@ Our SDK also exposes several other helper functions: - `AccountLinking.isEmailChangeAllowed`: Given the recipe user id and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` is returned: - If the input 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 email. - - If the recipe user is NOT a primary user, and if the new email is not verified, then we check if there exists a primary user with the same email, and if it exists, we disallow the email change. We disallow because 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. \ No newline at end of file + - If the recipe user is NOT a primary user, and if the new email is not verified, then we check if there exists a primary user with the same email, and if it exists, we disallow the email change. We disallow because 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. diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/security-considerations.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/security-considerations.mdx new file mode 100644 index 000000000..c59f68aea --- /dev/null +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/security-considerations.mdx @@ -0,0 +1,98 @@ +--- +id: security-considerations +title: Security considerations for automatic account linking +hide_title: true +--- + + + + +import MultiTenancyPaidBanner from '../../../community/reusableMD/multitenancy/MultiTenancyPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Security considerations for automatic account linking + +Below is the list of all points in time when we do account linking, and for each point, you can see the list of security checks that happen: + +## 1) During sign up: + +### Case a +- Email is `e1` +- Email is verified: `false` (is the case with email password sign up or social login with a provider that does not require email verification) + +#### Checks done: +- i) If there is no primary user with the same email, then we disallow sign up if there exists any other non-primary account with the same email and that account is not verified. We do this because if we didn't, there is a risk that if this user signs up and becomes a primary user, the other account (which could be malicious), might resend a verification email and the user might click on it (since they just signed up) and verify the malicious account, thereby linking it to their account. This way, the malicious user will have access to the victim's account. +- ii) If there exists a primary user with the same email, then we reject sign up of this new user. This is done cause if we allowed it, and this sign up is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. + +#### What users see: +- i) In case of email password login, users see an account already exists error, in which case, they can try logging in with another method, or go through the password reset flow, which will create a new email password account for them as well as verify it. +- ii) In case of social login, users will see that they should try a different login method for security reasons. + +### Case b + +- Email is `e1` +- Email is verified: `true` (is the case with social login with a provider that requires email verification, like google, or it could be a passwordless sign up) + +#### Checks done: +- i) Same as in Case 1a, point (i) above. +- ii) If there exists a primary user with the same email, then we allow sign up only if there exists at least one login method in the primary user with email `e1` which is verified. This is done because if we didn't, then the following account takeover is possible: + - Malicious user signs up with email password, with email `e2`, verifies it, and becomes a primary user. + - They then change their email to `e1`, and keep it in an unverified state. + - Now the actual user (victim), does a google sign with email `e1`. + - If we don't stop this sign up, then the new sign up will be linked to the primary user, and the malicious user will have access to the victim's account. + +#### What users see: +- Users will see that they should try a different login method for security reasons. + +## 2) During sign in: +### Case a +- Email is `e1` +- Email is verified: `false` +- User is not a primary user + +#### Checks done: +- If there exists another user with the same email, and they are not a primary user, but their email is unverified, we disallow this sign in because if we did, and this user verifies their email, it will result in this user becoming a primary user. If the other account then sends an email verification email, this user may click on it (they may not get too suspicious), and verify the other account, thereby linking it to their account. This way, the other account, which may belong to a malicious user, will have access to the victim's account. +- If there exists another user with the same email, and they are not a primary user, we disallow signing in. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. + +#### What users see: +- For email password sign in, users will see a wrong credentials error message. This will prompt them to go through the password reset flow, which will also mark the email as verified, thereby allowing them to sign in. This will also block sign ins from malicious users, since the new password is only known to the actual owner of the email. +- For passwordless or social login, users will see that they should try a different login method for security reasons. + +### Case b +- The user first does a social login sign up with email `e1`. +- They then change their email to `e2` on the provider and tries signing in again. +- The third party provider does not verify emails, so `e2` is unverified. + +#### Checks done: +- If the social login user is **not** a primary user, and there exists another primary user with email `e2`, then we disallow sign in here. This is done cause if we allowed it, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. +- If this user is a primary user, and there exists another primary user with email `e2`, we disallow sign in because there can't be two primary users with the same email. The email update which happens during sign in for social login, is rejected. + +#### What users see: +- For the first point, users will see a message asking them to try a different login method, or contact support for security reasons. +- For the second case, users will see that email update is not allowed and to contact support. + +## 3) During password reset flow: +- Malicious user has email password and a social login account with email `e1`, and they both are linked. +- They then change their email to `e2` for the email passowrd login, which belongs to the victim. +- Actual owner of `e2` tries to sign up, but sees that their account already exists (Case 1), they then try to sign in, but can't cause they don't know the password. So they try the password reset flow. + +#### Checks done: +- In this case, we deny generating the password reset token because if we did, then the real user would change the password of the email password account, and also mark it as verified. They would have access to the account, however, the malicious user could also then login using social login (with email `e1`) to access the same account. + + More generally, during password reset, we do not generate a token if the email password account for that email is associated with a primary account that also has other emails / phone numbers, and the email for which the password is being reset is not verified for any of the login methods in that primary user. + +#### What users see: +- They will see a message telling them that the reset password link was not generated becasue of account takeover risk, and to contact support. + +## 4) During email update flow: +- A user has email `e1`, and they want to change it to email `e2` + +#### Checks done: +- If the user's account is not a primary user, and there exists another primary user with email `e2`, then we disallow email update here. This is done cause if we allowed it, and this email update is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This will cause the new, malicious account to be linked, thereby giving the malicious user access to the victim's account. +- If this user is a primary user, and there exists another primary user with email `e2`, we disallow email update because there can't be two primary users with the same email. + +#### What users see: +- If the email update is happening during sign in of social login, users will see a message that email update is not allowed and to contact support. +- If this is happening post login (from a settings page), then you can send any message you want to the user, since this would be your custom API. \ No newline at end of file diff --git a/v2/thirdpartypasswordless/sidebars.js b/v2/thirdpartypasswordless/sidebars.js index 25e16113b..2032217b4 100644 --- a/v2/thirdpartypasswordless/sidebars.js +++ b/v2/thirdpartypasswordless/sidebars.js @@ -435,7 +435,8 @@ module.exports = { items: [ "common-customizations/account-linking/overview", "common-customizations/account-linking/automatic-account-linking", - "common-customizations/account-linking/manual-account-linking" + "common-customizations/account-linking/manual-account-linking", + "common-customizations/account-linking/security-considerations" ] }, { From 07a401775432762a5fec485585689a7f69364a29 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 9 Sep 2023 16:47:18 +0530 Subject: [PATCH 58/81] fixes code snippets --- .../frontend-hooks/handle-event.mdx | 2 +- .../frontend-hooks/redirection-callback.mdx | 2 +- .../handling-signin-success.mdx | 2 +- .../handling-signup-success.mdx | 2 +- v2/multitenancy/users-and-tenants.mdx | 20 ++++++++++++++----- .../frontend-hooks/handle-event.mdx | 2 +- .../frontend-hooks/redirection-callback.mdx | 2 +- .../handling-signinup-success.mdx | 2 +- .../codeTypeChecking/jsEnv/package.json | 6 +++--- .../frontend-hooks/handle-event.mdx | 2 +- .../frontend-hooks/redirection-callback.mdx | 2 +- .../handling-signinup-success.mdx | 2 +- .../frontend-hooks/handle-event.mdx | 2 +- .../frontend-hooks/redirection-callback.mdx | 2 +- .../handling-signinup-success.mdx | 2 +- .../frontend-hooks/handle-event.mdx | 2 +- .../frontend-hooks/redirection-callback.mdx | 2 +- .../handling-signinup-success.mdx | 2 +- 18 files changed, 34 insertions(+), 24 deletions(-) diff --git a/v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx index fbdbfc39a..84dba4210 100644 --- a/v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx @@ -28,7 +28,7 @@ EmailPassword.init({ // in this case, they are usually redirected to the main app } else if (context.action === "SUCCESS") { let user = context.user; - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success diff --git a/v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx index 3603bcde0..41d3dde59 100644 --- a/v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -28,7 +28,7 @@ EmailPassword.init({ // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // user signed up return "/onboarding" } else { diff --git a/v2/emailpassword/common-customizations/handling-signin-success.mdx b/v2/emailpassword/common-customizations/handling-signin-success.mdx index f469b02a9..d3779fcc3 100644 --- a/v2/emailpassword/common-customizations/handling-signin-success.mdx +++ b/v2/emailpassword/common-customizations/handling-signin-success.mdx @@ -44,7 +44,7 @@ SuperTokens.init({ // TODO: } else { if (context.action === "SUCCESS") { - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in diff --git a/v2/emailpassword/common-customizations/handling-signup-success.mdx b/v2/emailpassword/common-customizations/handling-signup-success.mdx index 19b53ec9a..f01699989 100644 --- a/v2/emailpassword/common-customizations/handling-signup-success.mdx +++ b/v2/emailpassword/common-customizations/handling-signup-success.mdx @@ -44,7 +44,7 @@ SuperTokens.init({ // TODO: } else { if (context.action === "SUCCESS") { - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in diff --git a/v2/multitenancy/users-and-tenants.mdx b/v2/multitenancy/users-and-tenants.mdx index ec6734635..b8ec4bfd9 100644 --- a/v2/multitenancy/users-and-tenants.mdx +++ b/v2/multitenancy/users-and-tenants.mdx @@ -25,9 +25,10 @@ In order to associate a user with a tenant, you can call the following API: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; +import {RecipeUserId} from "supertokens-node"; -async function addUserToTenant(userId: string, tenantId: string) { - let resp = await Multitenancy.associateUserToTenant(tenantId, userId); +async function addUserToTenant(recipeUserId: RecipeUserId, tenantId: string) { + let resp = await Multitenancy.associateUserToTenant(tenantId, recipeUserId); if (resp.status === "OK") { // User is now associated with tenant @@ -37,6 +38,10 @@ async function addUserToTenant(userId: string, tenantId: string) { // This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. } else if (resp.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { // This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. + } else if (resp.status === "ASSOCIATION_NOT_ALLOWED_ERROR") { + // This can happen if using account linking along with multi tenancy. One example of when this + // happens if if the target tenant has a primary user with the same email / phone numbers + // as the current user. } else { // status is THIRD_PARTY_USER_ALREADY_EXISTS_ERROR // This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant. @@ -172,10 +177,15 @@ import Multitenancy from "supertokens-node/recipe/multitenancy"; async function removeUserFromTeannt(userId: string, tenantId: string) { let resp = await Multitenancy.disassociateUserFromTenant(tenantId, userId); - if (resp.wasAssociated) { - // User was removed from tenant + if (resp.status === "OK") { + if (resp.wasAssociated) { + // User was removed from tenant + } else { + // User was never a part of the tenant anyway + } } else { - // User was never a part of the tenant anyway + // status is DISASSOCIATION_NOT_ALLOWED_ERROR + // this happens if you are trying to disassociate last remaining tenant from the user. } } ``` diff --git a/v2/passwordless/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/passwordless/advanced-customizations/frontend-hooks/handle-event.mdx index dec22280c..83d32e6f4 100644 --- a/v2/passwordless/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/passwordless/advanced-customizations/frontend-hooks/handle-event.mdx @@ -29,7 +29,7 @@ Passwordless.init({ // in this case, they are usually redirected to the main app } else if (context.action === "SUCCESS") { let user = context.user; - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success diff --git a/v2/passwordless/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/passwordless/advanced-customizations/frontend-hooks/redirection-callback.mdx index d4523e534..e122c405c 100644 --- a/v2/passwordless/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/passwordless/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -27,7 +27,7 @@ Passwordless.init({ // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // user signed up return "/onboarding" } else { diff --git a/v2/passwordless/common-customizations/handling-signinup-success.mdx b/v2/passwordless/common-customizations/handling-signinup-success.mdx index 182c90439..fd3e992d1 100644 --- a/v2/passwordless/common-customizations/handling-signinup-success.mdx +++ b/v2/passwordless/common-customizations/handling-signinup-success.mdx @@ -51,7 +51,7 @@ SuperTokens.init({ } else { let {id, email, phoneNumber} = context.user; if (context.action === "SUCCESS") { - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in diff --git a/v2/src/plugins/codeTypeChecking/jsEnv/package.json b/v2/src/plugins/codeTypeChecking/jsEnv/package.json index 198c6c081..305ba42a4 100644 --- a/v2/src/plugins/codeTypeChecking/jsEnv/package.json +++ b/v2/src/plugins/codeTypeChecking/jsEnv/package.json @@ -52,12 +52,12 @@ "react-router-dom5": "npm:react-router-dom@^5.3.0", "socket.io": "^4.6.1", "socketio": "^1.0.0", - "supertokens-auth-react": "^0.34.0", + "supertokens-auth-react": "github:supertokens/supertokens-auth-react#feat/account-linking", "supertokens-node": "github:supertokens/supertokens-node#account-linking", "supertokens-node7": "npm:supertokens-node@7.3", "supertokens-react-native": "^4.0.0", - "supertokens-web-js": "^0.7.0", - "supertokens-web-js-script": "github:supertokens/supertokens-web-js#0.7", + "supertokens-web-js": "github:supertokens/supertokens-web-js#feat/account-linking", + "supertokens-web-js-script": "github:supertokens/supertokens-web-js#feat/account-linking", "supertokens-website": "^17.0.0", "supertokens-website-script": "github:supertokens/supertokens-website#17.0", "typescript": "^4.9.5" diff --git a/v2/thirdparty/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/thirdparty/advanced-customizations/frontend-hooks/handle-event.mdx index ba66e7cff..9aec0a58b 100644 --- a/v2/thirdparty/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/thirdparty/advanced-customizations/frontend-hooks/handle-event.mdx @@ -24,7 +24,7 @@ ThirdParty.init({ // in this case, they are usually redirected to the main app } else if (context.action === "SUCCESS") { let user = context.user; - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success diff --git a/v2/thirdparty/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/thirdparty/advanced-customizations/frontend-hooks/redirection-callback.mdx index 155e66fb3..6993e04a6 100644 --- a/v2/thirdparty/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/thirdparty/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -26,7 +26,7 @@ ThirdParty.init({ // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // user signed up return "/onboarding" } else { diff --git a/v2/thirdparty/common-customizations/handling-signinup-success.mdx b/v2/thirdparty/common-customizations/handling-signinup-success.mdx index 7ba45833c..deef80788 100644 --- a/v2/thirdparty/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdparty/common-customizations/handling-signinup-success.mdx @@ -43,7 +43,7 @@ SuperTokens.init({ // TODO: } else { if (context.action === "SUCCESS") { - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in diff --git a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx index c37d57fde..9b2610629 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx @@ -28,7 +28,7 @@ ThirdPartyEmailPassword.init({ // in this case, they are usually redirected to the main app } else if (context.action === "SUCCESS") { let user = context.user; - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success diff --git a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx index 2b6123cc9..9e74fa4da 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -28,7 +28,7 @@ ThirdPartyEmailPassword.init({ // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // user signed up return "/onboarding" } else { diff --git a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx index 4570fd715..b55bb60b6 100644 --- a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx @@ -45,7 +45,7 @@ SuperTokens.init({ // TODO: } else if (context.action === "SUCCESS") { let { id, email } = context.user; - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in diff --git a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/handle-event.mdx index be714c5ed..047db2105 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/handle-event.mdx @@ -28,7 +28,7 @@ ThirdPartyPasswordless.init({ // TODO: } else if (context.action === "SUCCESS") { const user = context.user; - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in diff --git a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/redirection-callback.mdx index 5fff915d4..9741393bb 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -27,7 +27,7 @@ ThirdPartyPasswordless.init({ // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // user signed up return "/onboarding" } else { diff --git a/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx b/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx index 95a56ba90..33fc15437 100644 --- a/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx @@ -48,7 +48,7 @@ SuperTokens.init({ } else if (context.action === "PASSWORDLESS_CODE_SENT") { // TODO: } else if (context.action === "SUCCESS") { - if (context.isNewUser) { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { if ("phoneNumber" in context.user) { const { phoneNumber } = context.user; } else { From 56a6ef2dd2dfa94c6f96a35fe377e8e36cab7e38 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 9 Sep 2023 19:09:38 +0530 Subject: [PATCH 59/81] more changes --- .../common-customizations/handling-signinup-success.mdx | 2 +- v2/passwordless/custom-ui/login-magic-link.mdx | 4 ++-- v2/passwordless/custom-ui/login-otp.mdx | 4 ++-- v2/thirdparty/custom-ui/thirdparty-login.mdx | 4 ++-- .../advanced-customizations/frontend-hooks/handle-event.mdx | 2 -- .../common-customizations/handling-signinup-success.mdx | 2 +- v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx | 4 ++-- .../common-customizations/handling-signinup-success.mdx | 2 +- v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx | 4 ++-- v2/thirdpartypasswordless/custom-ui/login-otp.mdx | 4 ++-- v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx | 4 ++-- 11 files changed, 17 insertions(+), 19 deletions(-) diff --git a/v2/passwordless/common-customizations/handling-signinup-success.mdx b/v2/passwordless/common-customizations/handling-signinup-success.mdx index fd3e992d1..ee94fd3fe 100644 --- a/v2/passwordless/common-customizations/handling-signinup-success.mdx +++ b/v2/passwordless/common-customizations/handling-signinup-success.mdx @@ -49,7 +49,7 @@ SuperTokens.init({ } else if (context.action === "PASSWORDLESS_CODE_SENT") { // TODO: } else { - let {id, email, phoneNumber} = context.user; + let {id, emails, phoneNumbers} = context.user; if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up diff --git a/v2/passwordless/custom-ui/login-magic-link.mdx b/v2/passwordless/custom-ui/login-magic-link.mdx index 206e0a877..7c25693b6 100644 --- a/v2/passwordless/custom-ui/login-magic-link.mdx +++ b/v2/passwordless/custom-ui/login-magic-link.mdx @@ -501,7 +501,7 @@ async function handleMagicLinkClicked() { let response = await ^{webJsConsumePasswordlessCode}(); if (response.status === "OK") { - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success @@ -533,7 +533,7 @@ async function handleMagicLinkClicked() { let response = await supertokens^{recipeNameCapitalLetters}.^{webJsConsumePasswordlessCode}(); if (response.status === "OK") { - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success diff --git a/v2/passwordless/custom-ui/login-otp.mdx b/v2/passwordless/custom-ui/login-otp.mdx index 29d6898d1..da4682562 100644 --- a/v2/passwordless/custom-ui/login-otp.mdx +++ b/v2/passwordless/custom-ui/login-otp.mdx @@ -317,7 +317,7 @@ async function handleOTPInput(otp: string) { }); if (response.status === "OK") { - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success @@ -358,7 +358,7 @@ async function handleOTPInput(otp: string) { }); if (response.status === "OK") { - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success diff --git a/v2/thirdparty/custom-ui/thirdparty-login.mdx b/v2/thirdparty/custom-ui/thirdparty-login.mdx index db0539888..664066819 100644 --- a/v2/thirdparty/custom-ui/thirdparty-login.mdx +++ b/v2/thirdparty/custom-ui/thirdparty-login.mdx @@ -128,7 +128,7 @@ Once the third party provider redirects your user back to your app, you need to if (response.status === "OK") { console.log(response.user) - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful @@ -166,7 +166,7 @@ Once the third party provider redirects your user back to your app, you need to if (response.status === "OK") { console.log(response.user) - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful diff --git a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx index 9b2610629..30ef21aff 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx @@ -33,8 +33,6 @@ ThirdPartyEmailPassword.init({ } else { // sign in success } - } else if (context.action === "VERIFY_EMAIL_SENT") { - } } }) diff --git a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx index b55bb60b6..40f3b6240 100644 --- a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx @@ -44,7 +44,7 @@ SuperTokens.init({ if (context.action === "SESSION_ALREADY_EXISTS") { // TODO: } else if (context.action === "SUCCESS") { - let { id, email } = context.user; + let { id, emails } = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { diff --git a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx index 81d49c570..24ea61ac6 100644 --- a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx @@ -131,7 +131,7 @@ Once the third party provider redirects your user back to your app, you need to if (response.status === "OK") { console.log(response.user) - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful @@ -169,7 +169,7 @@ Once the third party provider redirects your user back to your app, you need to if (response.status === "OK") { console.log(response.user) - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful diff --git a/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx b/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx index 33fc15437..7822eadaa 100644 --- a/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx @@ -52,7 +52,7 @@ SuperTokens.init({ if ("phoneNumber" in context.user) { const { phoneNumber } = context.user; } else { - const { email } = context.user; + const { emails } = context.user; } // TODO: Sign up } else { diff --git a/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx b/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx index 566bd2688..fdfc931bd 100644 --- a/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx +++ b/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx @@ -501,7 +501,7 @@ async function handleMagicLinkClicked() { let response = await ^{webJsConsumePasswordlessCode}(); if (response.status === "OK") { - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success @@ -533,7 +533,7 @@ async function handleMagicLinkClicked() { let response = await supertokens^{recipeNameCapitalLetters}.^{webJsConsumePasswordlessCode}(); if (response.status === "OK") { - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success diff --git a/v2/thirdpartypasswordless/custom-ui/login-otp.mdx b/v2/thirdpartypasswordless/custom-ui/login-otp.mdx index c4eec5518..1f4efbde0 100644 --- a/v2/thirdpartypasswordless/custom-ui/login-otp.mdx +++ b/v2/thirdpartypasswordless/custom-ui/login-otp.mdx @@ -317,7 +317,7 @@ async function handleOTPInput(otp: string) { }); if (response.status === "OK") { - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success @@ -358,7 +358,7 @@ async function handleOTPInput(otp: string) { }); if (response.status === "OK") { - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success diff --git a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx index cf6d3c623..ae97cd4b4 100644 --- a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx @@ -131,7 +131,7 @@ Once the third party provider redirects your user back to your app, you need to if (response.status === "OK") { console.log(response.user) - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful @@ -169,7 +169,7 @@ Once the third party provider redirects your user back to your app, you need to if (response.status === "OK") { console.log(response.user) - if (response.createdNewUser) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful From 2a957c2cc30ddda099cce4144c615a17d673f366 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Sat, 9 Sep 2023 22:43:05 +0530 Subject: [PATCH 60/81] more changes --- .../get-session.mdx | 50 ------------------- .../get-session.mdx | 50 ------------------- .../backend/passwordless-customisation.mdx | 3 -- .../get-session.mdx | 50 ------------------- .../get-session.mdx | 50 ------------------- .../get-session.mdx | 50 ------------------- .../get-session.mdx | 50 ------------------- 7 files changed, 303 deletions(-) diff --git a/v2/emailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/emailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx index 558afc451..8b9789ba8 100644 --- a/v2/emailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/emailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -37,10 +37,6 @@ app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -66,9 +62,6 @@ server.route({ handler: async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -88,9 +81,6 @@ let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -109,9 +99,6 @@ import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -134,9 +121,6 @@ let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -159,9 +143,6 @@ class LikeComment { async handler() { let session = await Session.getSession(this.ctx, this.ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -187,9 +168,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -212,9 +190,6 @@ export class ExampleController { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... @@ -686,10 +661,6 @@ app.post("/like-comment", async (req, res, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -722,9 +693,6 @@ server.route({ ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -751,9 +719,6 @@ fastify.post("/like-comment", async (req, res) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -779,9 +744,6 @@ async function likeComment(awsEvent: SessionEvent) { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -811,9 +773,6 @@ router.post("/like-comment", async (ctx, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -843,9 +802,6 @@ class LikeComment { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -878,9 +834,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -910,9 +863,6 @@ export class ExampleController { ] }); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... diff --git a/v2/passwordless/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/passwordless/common-customizations/sessions/session-verification-in-api/get-session.mdx index 558afc451..8b9789ba8 100644 --- a/v2/passwordless/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/passwordless/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -37,10 +37,6 @@ app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -66,9 +62,6 @@ server.route({ handler: async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -88,9 +81,6 @@ let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -109,9 +99,6 @@ import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -134,9 +121,6 @@ let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -159,9 +143,6 @@ class LikeComment { async handler() { let session = await Session.getSession(this.ctx, this.ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -187,9 +168,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -212,9 +190,6 @@ export class ExampleController { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... @@ -686,10 +661,6 @@ app.post("/like-comment", async (req, res, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -722,9 +693,6 @@ server.route({ ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -751,9 +719,6 @@ fastify.post("/like-comment", async (req, res) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -779,9 +744,6 @@ async function likeComment(awsEvent: SessionEvent) { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -811,9 +773,6 @@ router.post("/like-comment", async (ctx, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -843,9 +802,6 @@ class LikeComment { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -878,9 +834,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -910,9 +863,6 @@ export class ExampleController { ] }); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... diff --git a/v2/phonepassword/backend/passwordless-customisation.mdx b/v2/phonepassword/backend/passwordless-customisation.mdx index 0636bebf2..d69b8b8d0 100644 --- a/v2/phonepassword/backend/passwordless-customisation.mdx +++ b/v2/phonepassword/backend/passwordless-customisation.mdx @@ -141,9 +141,6 @@ supertokens.init({ let session = await Session.getSession(input.options.req, input.options.res, { overrideGlobalClaimValidators: () => [], }); - if (session === undefined) { - throw new Error("Should never come here"); - } // we add the session to the user context so that the createNewSession // function doesn't create a new session diff --git a/v2/session/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/session/common-customizations/sessions/session-verification-in-api/get-session.mdx index 4d217e495..f823de5ca 100644 --- a/v2/session/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/session/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -37,10 +37,6 @@ app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -66,9 +62,6 @@ server.route({ handler: async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -88,9 +81,6 @@ let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -109,9 +99,6 @@ import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -134,9 +121,6 @@ let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -159,9 +143,6 @@ class LikeComment { async handler() { let session = await Session.getSession(this.ctx, this.ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -187,9 +168,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -212,9 +190,6 @@ export class ExampleController { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... @@ -686,10 +661,6 @@ app.post("/like-comment", async (req, res, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -722,9 +693,6 @@ server.route({ ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -751,9 +719,6 @@ fastify.post("/like-comment", async (req, res) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -779,9 +744,6 @@ async function likeComment(awsEvent: SessionEvent) { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -811,9 +773,6 @@ router.post("/like-comment", async (ctx, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -843,9 +802,6 @@ class LikeComment { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -878,9 +834,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -910,9 +863,6 @@ export class ExampleController { ] }); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... diff --git a/v2/thirdparty/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/thirdparty/common-customizations/sessions/session-verification-in-api/get-session.mdx index 558afc451..8b9789ba8 100644 --- a/v2/thirdparty/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/thirdparty/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -37,10 +37,6 @@ app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -66,9 +62,6 @@ server.route({ handler: async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -88,9 +81,6 @@ let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -109,9 +99,6 @@ import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -134,9 +121,6 @@ let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -159,9 +143,6 @@ class LikeComment { async handler() { let session = await Session.getSession(this.ctx, this.ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -187,9 +168,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -212,9 +190,6 @@ export class ExampleController { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... @@ -686,10 +661,6 @@ app.post("/like-comment", async (req, res, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -722,9 +693,6 @@ server.route({ ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -751,9 +719,6 @@ fastify.post("/like-comment", async (req, res) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -779,9 +744,6 @@ async function likeComment(awsEvent: SessionEvent) { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -811,9 +773,6 @@ router.post("/like-comment", async (ctx, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -843,9 +802,6 @@ class LikeComment { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -878,9 +834,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -910,9 +863,6 @@ export class ExampleController { ] }); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx index 558afc451..8b9789ba8 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -37,10 +37,6 @@ app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -66,9 +62,6 @@ server.route({ handler: async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -88,9 +81,6 @@ let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -109,9 +99,6 @@ import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -134,9 +121,6 @@ let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -159,9 +143,6 @@ class LikeComment { async handler() { let session = await Session.getSession(this.ctx, this.ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -187,9 +168,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -212,9 +190,6 @@ export class ExampleController { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... @@ -686,10 +661,6 @@ app.post("/like-comment", async (req, res, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -722,9 +693,6 @@ server.route({ ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -751,9 +719,6 @@ fastify.post("/like-comment", async (req, res) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -779,9 +744,6 @@ async function likeComment(awsEvent: SessionEvent) { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -811,9 +773,6 @@ router.post("/like-comment", async (ctx, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -843,9 +802,6 @@ class LikeComment { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -878,9 +834,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -910,9 +863,6 @@ export class ExampleController { ] }); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/get-session.mdx index 558afc451..8b9789ba8 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -37,10 +37,6 @@ app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -66,9 +62,6 @@ server.route({ handler: async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -88,9 +81,6 @@ let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -109,9 +99,6 @@ import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -134,9 +121,6 @@ let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -159,9 +143,6 @@ class LikeComment { async handler() { let session = await Session.getSession(this.ctx, this.ctx); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -187,9 +168,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -212,9 +190,6 @@ export class ExampleController { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... @@ -686,10 +661,6 @@ app.post("/like-comment", async (req, res, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } - let userId = session.getUserId(); // highlight-end //.... @@ -722,9 +693,6 @@ server.route({ ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //... @@ -751,9 +719,6 @@ fastify.post("/like-comment", async (req, res) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -779,9 +744,6 @@ async function likeComment(awsEvent: SessionEvent) { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -811,9 +773,6 @@ router.post("/like-comment", async (ctx, next) => { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -843,9 +802,6 @@ class LikeComment { ] }); - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); //highlight-end //.... @@ -878,9 +834,6 @@ export default async function likeComment(req: SessionRequest, res: any) { res ) - if (session === undefined) { - throw Error("Should never come here") - } let userId = session.getUserId(); // highlight-end //.... @@ -910,9 +863,6 @@ export class ExampleController { ] }); - if (session === undefined) { - throw Error("Should never come here") - } const userId = session.getUserId(); //highlight-end //.... From c28ddbb5e50b405463e87572958771d70a1fc077 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 11 Sep 2023 12:40:50 +0530 Subject: [PATCH 61/81] more changes --- .../custom-ui/email-password-login.mdx | 14 +++++++++++++ .../custom-ui/forgot-password.mdx | 5 +++++ v2/multitenancy/users-and-tenants.mdx | 5 +++-- .../custom-ui/login-magic-link.mdx | 20 +++++++++++++++---- v2/passwordless/custom-ui/login-otp.mdx | 20 +++++++++++++++---- v2/thirdparty/custom-ui/thirdparty-login.mdx | 11 +++++++++- .../custom-ui/email-password-login.mdx | 14 +++++++++++++ .../custom-ui/forgot-password.mdx | 5 +++++ .../custom-ui/thirdparty-login.mdx | 11 +++++++++- .../custom-ui/login-magic-link.mdx | 20 +++++++++++++++---- .../custom-ui/login-otp.mdx | 20 +++++++++++++++---- .../custom-ui/thirdparty-login.mdx | 11 +++++++++- 12 files changed, 135 insertions(+), 21 deletions(-) diff --git a/v2/emailpassword/custom-ui/email-password-login.mdx b/v2/emailpassword/custom-ui/email-password-login.mdx index 31fb1d589..de9928743 100644 --- a/v2/emailpassword/custom-ui/email-password-login.mdx +++ b/v2/emailpassword/custom-ui/email-password-login.mdx @@ -53,6 +53,9 @@ async function signUpClicked(email: string, password: string) { window.alert(formField.error) } }) + } else if (response.status === "SIGN_UP_NOT_ALLOWED") { + // this can happen during automatic account linking. Tell the user to use another + // login method, or go through the password reset flow. } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. @@ -99,6 +102,9 @@ async function signUpClicked(email: string, password: string) { window.alert(formField.error) } }) + } else if (response.status === "SIGN_UP_NOT_ALLOWED") { + // this can happen during automatic account linking. Tell the user to use another + // login method, or go through the password reset flow. } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. @@ -151,6 +157,7 @@ The response body from the API call has a `status` property in it: Either way, you want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking. Tell the user to use another login method, or go through the password reset flow. @@ -295,6 +302,9 @@ async function signInClicked(email: string, password: string) { }) } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") + } else if (response.status === "SIGN_IN_NOT_ALLOWED") { + // this can happen due to automatic account linking. Tell the user that their + // input credentials is wrong (so that they do through the password reset flow) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. @@ -338,6 +348,9 @@ async function signInClicked(email: string, password: string) { }) } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") + } else if (response.status === "SIGN_IN_NOT_ALLOWED") { + // this can happen due to automatic account linking. Tell the user that their + // input credentials is wrong (so that they do through the password reset flow) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. @@ -387,6 +400,7 @@ The response body from the API call has a `status` property in it: - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking. Tell the user that their input credentials is wrong (so that they do through the password reset flow). diff --git a/v2/emailpassword/custom-ui/forgot-password.mdx b/v2/emailpassword/custom-ui/forgot-password.mdx index 0cc0c2265..fe9d8ab9a 100644 --- a/v2/emailpassword/custom-ui/forgot-password.mdx +++ b/v2/emailpassword/custom-ui/forgot-password.mdx @@ -52,6 +52,8 @@ async function sendEmailClicked(email: string) { window.alert(formField.error) } }) + } else if (response.status === "PASSWORD_RESET_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please read our account linking docs } else { // reset password email sent. window.alert("Please check your email for the password reset link") @@ -89,6 +91,8 @@ async function signUpClicked(email: string) { window.alert(formField.error) } }) + } else if (response.status === "PASSWORD_RESET_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please read our account linking docs } else { // reset password email sent. window.alert("Please check your email for the password reset link") @@ -135,6 +139,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: If the user exists in SuperTokens, an email has been sent to them. - `status: "FIELD_ERROR"`: The input email failed the backend validation logic (i.e. the email is not a valid email from a syntax point of view). You want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "PASSWORD_RESET_NOT_ALLOWED"`: This can happen due to automatic account linking. Please read our account linking docs. diff --git a/v2/multitenancy/users-and-tenants.mdx b/v2/multitenancy/users-and-tenants.mdx index b8ec4bfd9..8d34b57dc 100644 --- a/v2/multitenancy/users-and-tenants.mdx +++ b/v2/multitenancy/users-and-tenants.mdx @@ -173,9 +173,10 @@ You can even remove a user's access from a tenant using the API call shown below ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; +import {RecipeUserId} from "supertokens-node"; -async function removeUserFromTeannt(userId: string, tenantId: string) { - let resp = await Multitenancy.disassociateUserFromTenant(tenantId, userId); +async function removeUserFromTeannt(recipeUserId: RecipeUserId, tenantId: string) { + let resp = await Multitenancy.disassociateUserFromTenant(tenantId, recipeUserId); if (resp.status === "OK") { if (resp.wasAssociated) { diff --git a/v2/passwordless/custom-ui/login-magic-link.mdx b/v2/passwordless/custom-ui/login-magic-link.mdx index 7c25693b6..69244d6ca 100644 --- a/v2/passwordless/custom-ui/login-magic-link.mdx +++ b/v2/passwordless/custom-ui/login-magic-link.mdx @@ -53,8 +53,12 @@ async function sendMagicLink(email: string) { */ - // Magic link sent successfully. - window.alert("Please check your email for the magic link"); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. See that section in our docs. + } else { + // Magic link sent successfully. + window.alert("Please check your email for the magic link"); + } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, @@ -86,8 +90,12 @@ async function sendMagicLink(email: string) { */ - // Magic link sent successfully. - window.alert("Please check your email for the magic link"); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. See that section in our docs. + } else { + // Magic link sent successfully. + window.alert("Please check your email for the magic link"); + } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, @@ -135,6 +143,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup/co The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the magic link was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. The response from the API call is the following object (in case of `status: "OK"`): ```json @@ -509,6 +518,7 @@ async function handleMagicLinkClicked() { window.location.assign("/home") } else { // this can happen if the magic link has expired or is invalid + // or if it was denied due to security reasons in case of automatic account linking window.alert("Login failed. Please try again"); window.location.assign("/auth") } @@ -541,6 +551,7 @@ async function handleMagicLinkClicked() { window.location.assign("/home") } else { // this can happen if the magic link has expired or is invalid + // or if it was denied due to security reasons in case of automatic account linking window.alert("Login failed. Please try again"); window.location.assign("/auth") } @@ -596,6 +607,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. diff --git a/v2/passwordless/custom-ui/login-otp.mdx b/v2/passwordless/custom-ui/login-otp.mdx index da4682562..8fcd80341 100644 --- a/v2/passwordless/custom-ui/login-otp.mdx +++ b/v2/passwordless/custom-ui/login-otp.mdx @@ -52,8 +52,12 @@ async function sendOTP(email: string) { */ - // OTP sent successfully. - window.alert("Please check your email for an OTP"); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. See that section in our docs. + } else { + // OTP sent successfully. + window.alert("Please check your email for an OTP"); + } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, @@ -85,8 +89,12 @@ async function sendOTP(email: string) { */ - // OTP sent successfully. - window.alert("Please check your email for an OTP"); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. See that section in our docs. + } else { + // OTP sent successfully. + window.alert("Please check your email for an OTP"); + } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, @@ -134,6 +142,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup/co The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the OTP was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. The response from the API call is the following object (in case of `status: "OK"`): ```json @@ -332,6 +341,7 @@ async function handleOTPInput(otp: string) { window.alert("Old OTP entered. Please regenerate a new one and try again"); } else { // this can happen if the user tried an incorrect OTP too many times. + // or if it was denied due to security reasons in case of automatic account linking window.alert("Login failed. Please try again"); window.location.assign("/auth") } @@ -373,6 +383,7 @@ async function handleOTPInput(otp: string) { window.alert("Old OTP entered. Please regenerate a new one and try again"); } else { // this can happen if the user tried an incorrect OTP too many times. + // or if it was denied due to security reasons in case of automatic account linking window.alert("Login failed. Please try again"); window.location.assign("/auth") } @@ -415,6 +426,7 @@ The response body from the API call has a `status` property in it: - `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again. - `status: "RESTART_FLOW_ERROR"`: These responses that the user tried invalid OTPs too many times. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. diff --git a/v2/thirdparty/custom-ui/thirdparty-login.mdx b/v2/thirdparty/custom-ui/thirdparty-login.mdx index 664066819..278275eb0 100644 --- a/v2/thirdparty/custom-ui/thirdparty-login.mdx +++ b/v2/thirdparty/custom-ui/thirdparty-login.mdx @@ -134,6 +134,8 @@ Once the third party provider redirects your user back to your app, you need to // sign in successful } window.location.assign("/home"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please see our account linking docs } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -142,7 +144,7 @@ Once the third party provider redirects your user back to your app, you need to // As a hack to solve this, you can override the backend functions to create a fake email for the user. window.alert("No email provided by social login. Please use another form of login"); - window.location.assign("/auth"); // redirect back to login page + window.location.assig("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { @@ -172,6 +174,8 @@ Once the third party provider redirects your user back to your app, you need to // sign in successful } window.location.assign("/home"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please see our account linking docs } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -427,6 +431,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -509,6 +514,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -560,6 +566,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -640,6 +647,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -713,6 +721,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. diff --git a/v2/thirdpartyemailpassword/custom-ui/email-password-login.mdx b/v2/thirdpartyemailpassword/custom-ui/email-password-login.mdx index 32d70b3b7..6fdc91f11 100644 --- a/v2/thirdpartyemailpassword/custom-ui/email-password-login.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/email-password-login.mdx @@ -56,6 +56,9 @@ async function signUpClicked(email: string, password: string) { window.alert(formField.error) } }) + } else if (response.status === "SIGN_UP_NOT_ALLOWED") { + // this can happen during automatic account linking. Tell the user to use another + // login method, or go through the password reset flow. } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. @@ -102,6 +105,9 @@ async function signUpClicked(email: string, password: string) { window.alert(formField.error) } }) + } else if (response.status === "SIGN_UP_NOT_ALLOWED") { + // this can happen during automatic account linking. Tell the user to use another + // login method, or go through the password reset flow. } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. @@ -154,6 +160,7 @@ The response body from the API call has a `status` property in it: Either way, you want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking. Tell the user to use another login method, or go through the password reset flow. @@ -298,6 +305,9 @@ async function signInClicked(email: string, password: string) { }) } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") + } else if (response.status === "SIGN_IN_NOT_ALLOWED") { + // this can happen due to automatic account linking. Tell the user that their + // input credentials is wrong (so that they do through the password reset flow) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. @@ -341,6 +351,9 @@ async function signInClicked(email: string, password: string) { }) } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") + } else if (response.status === "SIGN_IN_NOT_ALLOWED") { + // this can happen due to automatic account linking. Tell the user that their + // input credentials is wrong (so that they do through the password reset flow) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. @@ -390,6 +403,7 @@ The response body from the API call has a `status` property in it: - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking. Tell the user that their input credentials is wrong (so that they do through the password reset flow). diff --git a/v2/thirdpartyemailpassword/custom-ui/forgot-password.mdx b/v2/thirdpartyemailpassword/custom-ui/forgot-password.mdx index 0cc0c2265..fe9d8ab9a 100644 --- a/v2/thirdpartyemailpassword/custom-ui/forgot-password.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/forgot-password.mdx @@ -52,6 +52,8 @@ async function sendEmailClicked(email: string) { window.alert(formField.error) } }) + } else if (response.status === "PASSWORD_RESET_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please read our account linking docs } else { // reset password email sent. window.alert("Please check your email for the password reset link") @@ -89,6 +91,8 @@ async function signUpClicked(email: string) { window.alert(formField.error) } }) + } else if (response.status === "PASSWORD_RESET_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please read our account linking docs } else { // reset password email sent. window.alert("Please check your email for the password reset link") @@ -135,6 +139,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: If the user exists in SuperTokens, an email has been sent to them. - `status: "FIELD_ERROR"`: The input email failed the backend validation logic (i.e. the email is not a valid email from a syntax point of view). You want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "PASSWORD_RESET_NOT_ALLOWED"`: This can happen due to automatic account linking. Please read our account linking docs. diff --git a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx index 24ea61ac6..237974c71 100644 --- a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx @@ -137,6 +137,8 @@ Once the third party provider redirects your user back to your app, you need to // sign in successful } window.location.assign("/home"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please see our account linking docs } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -145,7 +147,7 @@ Once the third party provider redirects your user back to your app, you need to // As a hack to solve this, you can override the backend functions to create a fake email for the user. window.alert("No email provided by social login. Please use another form of login"); - window.location.assign("/auth"); // redirect back to login page + window.location.assig("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { @@ -175,6 +177,8 @@ Once the third party provider redirects your user back to your app, you need to // sign in successful } window.location.assign("/home"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please see our account linking docs } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -430,6 +434,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -512,6 +517,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -563,6 +569,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -643,6 +650,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -716,6 +724,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. diff --git a/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx b/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx index fdfc931bd..4ad1f8f9f 100644 --- a/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx +++ b/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx @@ -53,8 +53,12 @@ async function sendMagicLink(email: string) { */ - // Magic link sent successfully. - window.alert("Please check your email for the magic link"); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. See that section in our docs. + } else { + // Magic link sent successfully. + window.alert("Please check your email for the magic link"); + } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, @@ -86,8 +90,12 @@ async function sendMagicLink(email: string) { */ - // Magic link sent successfully. - window.alert("Please check your email for the magic link"); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. See that section in our docs. + } else { + // Magic link sent successfully. + window.alert("Please check your email for the magic link"); + } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, @@ -135,6 +143,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup/co The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the magic link was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. The response from the API call is the following object (in case of `status: "OK"`): ```json @@ -509,6 +518,7 @@ async function handleMagicLinkClicked() { window.location.assign("/home") } else { // this can happen if the magic link has expired or is invalid + // or if it was denied due to security reasons in case of automatic account linking window.alert("Login failed. Please try again"); window.location.assign("/auth") } @@ -541,6 +551,7 @@ async function handleMagicLinkClicked() { window.location.assign("/home") } else { // this can happen if the magic link has expired or is invalid + // or if it was denied due to security reasons in case of automatic account linking window.alert("Login failed. Please try again"); window.location.assign("/auth") } @@ -596,6 +607,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. diff --git a/v2/thirdpartypasswordless/custom-ui/login-otp.mdx b/v2/thirdpartypasswordless/custom-ui/login-otp.mdx index 1f4efbde0..148cf19b3 100644 --- a/v2/thirdpartypasswordless/custom-ui/login-otp.mdx +++ b/v2/thirdpartypasswordless/custom-ui/login-otp.mdx @@ -52,8 +52,12 @@ async function sendOTP(email: string) { */ - // OTP sent successfully. - window.alert("Please check your email for an OTP"); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. See that section in our docs. + } else { + // OTP sent successfully. + window.alert("Please check your email for an OTP"); + } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, @@ -85,8 +89,12 @@ async function sendOTP(email: string) { */ - // OTP sent successfully. - window.alert("Please check your email for an OTP"); + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. See that section in our docs. + } else { + // OTP sent successfully. + window.alert("Please check your email for an OTP"); + } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, @@ -134,6 +142,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup/co The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the OTP was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. The response from the API call is the following object (in case of `status: "OK"`): ```json @@ -332,6 +341,7 @@ async function handleOTPInput(otp: string) { window.alert("Old OTP entered. Please regenerate a new one and try again"); } else { // this can happen if the user tried an incorrect OTP too many times. + // or if it was denied due to security reasons in case of automatic account linking window.alert("Login failed. Please try again"); window.location.assign("/auth") } @@ -373,6 +383,7 @@ async function handleOTPInput(otp: string) { window.alert("Old OTP entered. Please regenerate a new one and try again"); } else { // this can happen if the user tried an incorrect OTP too many times. + // or if it was denied due to security reasons in case of automatic account linking window.alert("Login failed. Please try again"); window.location.assign("/auth") } @@ -415,6 +426,7 @@ The response body from the API call has a `status` property in it: - `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again. - `status: "RESTART_FLOW_ERROR"`: These responses that the user tried invalid OTPs too many times. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. diff --git a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx index ae97cd4b4..41b4db0fb 100644 --- a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx @@ -137,6 +137,8 @@ Once the third party provider redirects your user back to your app, you need to // sign in successful } window.location.assign("/home"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please see our account linking docs } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -145,7 +147,7 @@ Once the third party provider redirects your user back to your app, you need to // As a hack to solve this, you can override the backend functions to create a fake email for the user. window.alert("No email provided by social login. Please use another form of login"); - window.location.assign("/auth"); // redirect back to login page + window.location.assig("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { @@ -175,6 +177,8 @@ Once the third party provider redirects your user back to your app, you need to // sign in successful } window.location.assign("/home"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // this can happen due to automatic account linking. Please see our account linking docs } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -430,6 +434,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -512,6 +517,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -563,6 +569,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -643,6 +650,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -716,6 +724,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. From 0d952efb78835d9bc3783b3b7fce5cf0f9fcbc00 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 11 Sep 2023 13:39:07 +0530 Subject: [PATCH 62/81] more docs --- .../change-email-post-login.mdx | 25 +++++++++++++++++++ .../get-session.mdx | 3 ++- .../verify-session.mdx | 3 ++- .../common-customizations/change-email.mdx | 23 +++++++++++++++++ .../get-session.mdx | 3 ++- .../verify-session.mdx | 3 ++- .../get-session.mdx | 3 ++- .../verify-session.mdx | 3 ++- .../get-session.mdx | 3 ++- .../verify-session.mdx | 3 ++- .../change-email-post-login.mdx | 23 +++++++++++++++++ .../get-session.mdx | 3 ++- .../verify-session.mdx | 3 ++- .../common-customizations/change-email.mdx | 23 +++++++++++++++++ .../get-session.mdx | 3 ++- .../verify-session.mdx | 3 ++- 16 files changed, 118 insertions(+), 12 deletions(-) diff --git a/v2/emailpassword/common-customizations/change-email-post-login.mdx b/v2/emailpassword/common-customizations/change-email-post-login.mdx index 24b549c6f..ffb45e0d6 100644 --- a/v2/emailpassword/common-customizations/change-email-post-login.mdx +++ b/v2/emailpassword/common-customizations/change-email-post-login.mdx @@ -109,6 +109,7 @@ import EmailPassword from "supertokens-node/recipe/emailpassword"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" let app = express(); @@ -123,6 +124,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // TODO: handle invalid email error return } + + 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 EmailPassword.updateEmailOrPassword({ @@ -138,6 +144,12 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // TODO: handle error that email exists with another account. return } + if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + // This is possible if you have enabled account linking. + // See our docs for account linking to know more about this. + // TODO: tell the user to contact support. + } + throw new Error("Should never come here"); // highlight-end @@ -390,6 +402,7 @@ import { verifySession } from "supertokens-node/recipe/session/framework/express import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; import supertokens from "supertokens-node"; +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" let app = express(); @@ -410,6 +423,12 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { + 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. + return res.status(400).send("Email change not allowed. Please contact support"); + } + // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. let user = (await supertokens.getUser(session.getUserId()))!; @@ -435,6 +454,12 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr return } + if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, true))) { + // 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. + return res.status(400).send("Email change not allowed. Please contact support"); + } + // Since the email is verified, we try and do an update let resp = await EmailPassword.updateEmailOrPassword({ recipeUserId: session.getRecipeUserId(), diff --git a/v2/emailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/emailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx index 8b9789ba8..b2b17c09e 100644 --- a/v2/emailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/emailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -305,7 +305,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/emailpassword/common-customizations/sessions/session-verification-in-api/verify-session.mdx b/v2/emailpassword/common-customizations/sessions/session-verification-in-api/verify-session.mdx index bafa1e75e..0ef69eb3c 100644 --- a/v2/emailpassword/common-customizations/sessions/session-verification-in-api/verify-session.mdx +++ b/v2/emailpassword/common-customizations/sessions/session-verification-in-api/verify-session.mdx @@ -395,7 +395,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/passwordless/common-customizations/change-email.mdx b/v2/passwordless/common-customizations/change-email.mdx index 71bd51442..bb530940d 100644 --- a/v2/passwordless/common-customizations/change-email.mdx +++ b/v2/passwordless/common-customizations/change-email.mdx @@ -109,6 +109,7 @@ import Passwordless from "supertokens-node/recipe/passwordless"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" let app = express(); @@ -123,6 +124,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // TODO: handle invalid email error return } + + 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 Passwordless.updateUser({ @@ -138,6 +144,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // TODO: handle error that email exists with another account. return } + if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + // This is possible if you have enabled account linking. + // See our docs for account linking to know more about this. + // TODO: tell the user to contact support. + } throw new Error("Should never come here"); // highlight-end @@ -389,6 +400,7 @@ import { verifySession } from "supertokens-node/recipe/session/framework/express import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; import supertokens from "supertokens-node"; +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" let app = express(); @@ -409,6 +421,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { + 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. + return res.status(400).send("Email change not allowed. Please contact support"); + } // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. let user = (await supertokens.getUser(session.getUserId()))!; @@ -433,6 +450,12 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr return } + if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, true))) { + // 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. + return res.status(400).send("Email change not allowed. Please contact support"); + } + // Since the email is verified, we try and do an update let resp = await Passwordless.updateUser({ recipeUserId: session.getRecipeUserId(), diff --git a/v2/passwordless/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/passwordless/common-customizations/sessions/session-verification-in-api/get-session.mdx index 8b9789ba8..b2b17c09e 100644 --- a/v2/passwordless/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/passwordless/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -305,7 +305,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/passwordless/common-customizations/sessions/session-verification-in-api/verify-session.mdx b/v2/passwordless/common-customizations/sessions/session-verification-in-api/verify-session.mdx index bafa1e75e..0ef69eb3c 100644 --- a/v2/passwordless/common-customizations/sessions/session-verification-in-api/verify-session.mdx +++ b/v2/passwordless/common-customizations/sessions/session-verification-in-api/verify-session.mdx @@ -395,7 +395,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/session/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/session/common-customizations/sessions/session-verification-in-api/get-session.mdx index f823de5ca..20f214f9a 100644 --- a/v2/session/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/session/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -305,7 +305,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/session/common-customizations/sessions/session-verification-in-api/verify-session.mdx b/v2/session/common-customizations/sessions/session-verification-in-api/verify-session.mdx index 768de8458..fb9793b0c 100644 --- a/v2/session/common-customizations/sessions/session-verification-in-api/verify-session.mdx +++ b/v2/session/common-customizations/sessions/session-verification-in-api/verify-session.mdx @@ -395,7 +395,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/thirdparty/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/thirdparty/common-customizations/sessions/session-verification-in-api/get-session.mdx index 8b9789ba8..b2b17c09e 100644 --- a/v2/thirdparty/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/thirdparty/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -305,7 +305,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/thirdparty/common-customizations/sessions/session-verification-in-api/verify-session.mdx b/v2/thirdparty/common-customizations/sessions/session-verification-in-api/verify-session.mdx index bafa1e75e..0ef69eb3c 100644 --- a/v2/thirdparty/common-customizations/sessions/session-verification-in-api/verify-session.mdx +++ b/v2/thirdparty/common-customizations/sessions/session-verification-in-api/verify-session.mdx @@ -395,7 +395,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx b/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx index 9425b3f5a..49d57d5d9 100644 --- a/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx @@ -111,6 +111,7 @@ import { verifySession } from "supertokens-node/recipe/session/framework/express import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; import supertokens from "supertokens-node"; +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" let app = express(); @@ -139,6 +140,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr } } + 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(), @@ -153,6 +159,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // TODO: handle error that email exists with another account. return } + if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + // This is possible if you have enabled account linking. + // See our docs for account linking to know more about this. + // TODO: tell the user to contact support. + } throw new Error("Should never come here"); // highlight-end @@ -430,6 +441,7 @@ import { verifySession } from "supertokens-node/recipe/session/framework/express import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; import supertokens from "supertokens-node"; +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" let app = express(); @@ -464,6 +476,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { + 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. + return res.status(400).send("Email change not allowed. Please contact support"); + } // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. let user = (await supertokens.getUser(session.getUserId()))!; @@ -488,6 +505,12 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr return } + if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, true))) { + // 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. + return res.status(400).send("Email change not allowed. Please contact support"); + } + // Since the email is verified, we try and do an update let resp = await ThirdPartyEmailPassword.updateEmailOrPassword({ recipeUserId: session.getRecipeUserId(), diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx index 8b9789ba8..b2b17c09e 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -305,7 +305,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/verify-session.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/verify-session.mdx index bafa1e75e..0ef69eb3c 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/verify-session.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/session-verification-in-api/verify-session.mdx @@ -395,7 +395,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/thirdpartypasswordless/common-customizations/change-email.mdx b/v2/thirdpartypasswordless/common-customizations/change-email.mdx index 469f4812a..631411ace 100644 --- a/v2/thirdpartypasswordless/common-customizations/change-email.mdx +++ b/v2/thirdpartypasswordless/common-customizations/change-email.mdx @@ -111,6 +111,7 @@ import { verifySession } from "supertokens-node/recipe/session/framework/express import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; import supertokens from "supertokens-node"; +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" let app = express(); @@ -142,6 +143,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr } } + 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 ThirdPartyPasswordless.updatePasswordlessUser({ recipeUserId: session.getRecipeUserId(), @@ -156,6 +162,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr // TODO: handle error that email exists with another account. return } + if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + // This is possible if you have enabled account linking. + // See our docs for account linking to know more about this. + // TODO: tell the user to contact support. + } throw new Error("Should never come here"); // highlight-end @@ -431,6 +442,7 @@ import { verifySession } from "supertokens-node/recipe/session/framework/express import { SessionRequest } from "supertokens-node/framework/express" import express from "express"; import supertokens from "supertokens-node"; +import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" let app = express(); @@ -465,6 +477,11 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { + 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. + return res.status(400).send("Email change not allowed. Please contact support"); + } // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. let user = (await supertokens.getUser(session.getUserId()))!; @@ -489,6 +506,12 @@ app.post("/change-email", verifySession(), async (req: SessionRequest, res: expr return } + if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, true))) { + // 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. + return res.status(400).send("Email change not allowed. Please contact support"); + } + // Since the email is verified, we try and do an update let resp = await ThirdPartyPasswordless.updatePasswordlessUser({ recipeUserId: session.getRecipeUserId(), diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/get-session.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/get-session.mdx index 8b9789ba8..b2b17c09e 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/get-session.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/get-session.mdx @@ -305,7 +305,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/verify-session.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/verify-session.mdx index bafa1e75e..0ef69eb3c 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/verify-session.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/session-verification-in-api/verify-session.mdx @@ -395,7 +395,8 @@ async def like_comment(request: HttpRequest): ### The `session` object This object exposes the following functions: - `getHandle`: Returns the `sessionHandle` for this session. This is a constant, unique string per session that never changes for its session. -- `getUserId`: Returns the userId of logged in user +- `getUserId`: Returns the userId of logged in user. +- `getRecipeUserId`: Returns the `RecipeUserId` object for the session. If there is only one login method for this user, then the `getRecipeUserId().getAsString()` will be equal to the `getUserId()`. Otherwise, this will point to the user ID of the specific login method for this user. - `getSessionDataFromDatabase`: Returns the session data (stored in the database) that is associated with the session. - `updateSessionDataInDatabase`: Set a new JSON object to the session data (stored in the database) - `getAccessTokenPayload`: Returns the access token's payload for this session. This includes claims defined by you (e.g.: in `createNewSession`), standard claims (`sub`, `iat`, `exp`) and supertokens specific ones (`sessionHandle`, `parentRefreshTokenHash1`, etc.) From a7862a3cd8fe6ca2f89745f629988ca5bdac169c Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 11 Sep 2023 14:30:30 +0530 Subject: [PATCH 63/81] rps docs change --- v2/emailpassword/rate-limits.mdx | 33 ++++++++++++---------- v2/passwordless/rate-limits.mdx | 33 ++++++++++++---------- v2/session/rate-limits.mdx | 33 ++++++++++++---------- v2/thirdparty/rate-limits.mdx | 33 ++++++++++++---------- v2/thirdpartyemailpassword/rate-limits.mdx | 33 ++++++++++++---------- v2/thirdpartypasswordless/rate-limits.mdx | 33 ++++++++++++---------- 6 files changed, 108 insertions(+), 90 deletions(-) diff --git a/v2/emailpassword/rate-limits.mdx b/v2/emailpassword/rate-limits.mdx index 5f0d621b8..ad9801649 100644 --- a/v2/emailpassword/rate-limits.mdx +++ b/v2/emailpassword/rate-limits.mdx @@ -7,34 +7,37 @@ hide_title: true +import CustomAdmonition from "/src/components/customAdmonition" + # Rate limit policy ## For managed service The SuperTokens core is rate limited on a per app and per IP address basis. This means that if query the core for app1 using the same IP address very quickly, the rate limit will kick in and you will get a `429` status code back from the core. However, if you query the core using different IP addresses or for a different app, the rate limit of that won't interfere with the previous requests (that had another IP or was for another app). -### Development env -The free tier of the managed service has a rate limit of 3 requests per second with a burst of 20 requests per second (with nodelay). - -### Production env -The free tier of the managed service has a rate limit of 6 requests per second with a burst of 20 requests per second (with nodelay). - :::note - -To know more about this behaviour, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). - +To know more about rps and bursts, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). ::: -### Applicable to all envs -The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. +## Free tier +The free tier of the managed service has a rate limit of 10 requests per second with a burst of 20 requests per second (with nodelay). This should be enough for 2 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). :::important +Our backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error). +::: + +## Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit is set dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **very significant** spike in requests. -If you are not a paying user, and would still like higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + +If you want higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + + +## Special case +The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. -If you would like a different rate limit policy than the one described above, you can also [email us](mailto:team@supertokens.com), describing what you want. -::: ## For self hosted @@ -51,7 +54,7 @@ http { default $binary_remote_addr; } - limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=5r/s; + limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=10r/s; limit_req_status 429; # other configs.. diff --git a/v2/passwordless/rate-limits.mdx b/v2/passwordless/rate-limits.mdx index 70598fcea..3b1db5865 100644 --- a/v2/passwordless/rate-limits.mdx +++ b/v2/passwordless/rate-limits.mdx @@ -7,34 +7,37 @@ hide_title: true +import CustomAdmonition from "/src/components/customAdmonition" + # Rate limit policy ## For managed service The SuperTokens core is rate limited on a per app and per IP address basis. This means that if query the core for app1 using the same IP address very quickly, the rate limit will kick in and you will get a `429` status code back from the core. However, if you query the core using different IP addresses or for a different app, the rate limit of that won't interfere with the previous requests (that had another IP or was for another app). -### Development env -The free tier of the managed service has a rate limit of 3 requests per second with a burst of 20 requests per second (with nodelay). - -### Production env -The free tier of the managed service has a rate limit of 6 requests per second with a burst of 20 requests per second (with nodelay). - :::note - -To know more about this behaviour, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). - +To know more about rps and bursts, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). ::: -### Applicable to all envs -The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. +## Free tier +The free tier of the managed service has a rate limit of 10 requests per second with a burst of 20 requests per second (with nodelay). This should be enough for 2 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). :::important +Our backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error). +::: + +## Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit is set dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **very significant** spike in requests. -If you are not a paying user, and would still like higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + +If you want higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + + +## Special case +The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. -If you would like a different rate limit policy than the one described above, you can also [email us](mailto:team@supertokens.com), describing what you want. -::: ## For self hosted @@ -51,7 +54,7 @@ http { default $binary_remote_addr; } - limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=5r/s; + limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=10r/s; limit_req_status 429; # other configs.. diff --git a/v2/session/rate-limits.mdx b/v2/session/rate-limits.mdx index 70598fcea..3b1db5865 100644 --- a/v2/session/rate-limits.mdx +++ b/v2/session/rate-limits.mdx @@ -7,34 +7,37 @@ hide_title: true +import CustomAdmonition from "/src/components/customAdmonition" + # Rate limit policy ## For managed service The SuperTokens core is rate limited on a per app and per IP address basis. This means that if query the core for app1 using the same IP address very quickly, the rate limit will kick in and you will get a `429` status code back from the core. However, if you query the core using different IP addresses or for a different app, the rate limit of that won't interfere with the previous requests (that had another IP or was for another app). -### Development env -The free tier of the managed service has a rate limit of 3 requests per second with a burst of 20 requests per second (with nodelay). - -### Production env -The free tier of the managed service has a rate limit of 6 requests per second with a burst of 20 requests per second (with nodelay). - :::note - -To know more about this behaviour, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). - +To know more about rps and bursts, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). ::: -### Applicable to all envs -The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. +## Free tier +The free tier of the managed service has a rate limit of 10 requests per second with a burst of 20 requests per second (with nodelay). This should be enough for 2 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). :::important +Our backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error). +::: + +## Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit is set dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **very significant** spike in requests. -If you are not a paying user, and would still like higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + +If you want higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + + +## Special case +The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. -If you would like a different rate limit policy than the one described above, you can also [email us](mailto:team@supertokens.com), describing what you want. -::: ## For self hosted @@ -51,7 +54,7 @@ http { default $binary_remote_addr; } - limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=5r/s; + limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=10r/s; limit_req_status 429; # other configs.. diff --git a/v2/thirdparty/rate-limits.mdx b/v2/thirdparty/rate-limits.mdx index 70598fcea..3b1db5865 100644 --- a/v2/thirdparty/rate-limits.mdx +++ b/v2/thirdparty/rate-limits.mdx @@ -7,34 +7,37 @@ hide_title: true +import CustomAdmonition from "/src/components/customAdmonition" + # Rate limit policy ## For managed service The SuperTokens core is rate limited on a per app and per IP address basis. This means that if query the core for app1 using the same IP address very quickly, the rate limit will kick in and you will get a `429` status code back from the core. However, if you query the core using different IP addresses or for a different app, the rate limit of that won't interfere with the previous requests (that had another IP or was for another app). -### Development env -The free tier of the managed service has a rate limit of 3 requests per second with a burst of 20 requests per second (with nodelay). - -### Production env -The free tier of the managed service has a rate limit of 6 requests per second with a burst of 20 requests per second (with nodelay). - :::note - -To know more about this behaviour, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). - +To know more about rps and bursts, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). ::: -### Applicable to all envs -The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. +## Free tier +The free tier of the managed service has a rate limit of 10 requests per second with a burst of 20 requests per second (with nodelay). This should be enough for 2 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). :::important +Our backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error). +::: + +## Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit is set dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **very significant** spike in requests. -If you are not a paying user, and would still like higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + +If you want higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + + +## Special case +The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. -If you would like a different rate limit policy than the one described above, you can also [email us](mailto:team@supertokens.com), describing what you want. -::: ## For self hosted @@ -51,7 +54,7 @@ http { default $binary_remote_addr; } - limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=5r/s; + limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=10r/s; limit_req_status 429; # other configs.. diff --git a/v2/thirdpartyemailpassword/rate-limits.mdx b/v2/thirdpartyemailpassword/rate-limits.mdx index 70598fcea..3b1db5865 100644 --- a/v2/thirdpartyemailpassword/rate-limits.mdx +++ b/v2/thirdpartyemailpassword/rate-limits.mdx @@ -7,34 +7,37 @@ hide_title: true +import CustomAdmonition from "/src/components/customAdmonition" + # Rate limit policy ## For managed service The SuperTokens core is rate limited on a per app and per IP address basis. This means that if query the core for app1 using the same IP address very quickly, the rate limit will kick in and you will get a `429` status code back from the core. However, if you query the core using different IP addresses or for a different app, the rate limit of that won't interfere with the previous requests (that had another IP or was for another app). -### Development env -The free tier of the managed service has a rate limit of 3 requests per second with a burst of 20 requests per second (with nodelay). - -### Production env -The free tier of the managed service has a rate limit of 6 requests per second with a burst of 20 requests per second (with nodelay). - :::note - -To know more about this behaviour, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). - +To know more about rps and bursts, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). ::: -### Applicable to all envs -The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. +## Free tier +The free tier of the managed service has a rate limit of 10 requests per second with a burst of 20 requests per second (with nodelay). This should be enough for 2 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). :::important +Our backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error). +::: + +## Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit is set dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **very significant** spike in requests. -If you are not a paying user, and would still like higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + +If you want higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + + +## Special case +The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. -If you would like a different rate limit policy than the one described above, you can also [email us](mailto:team@supertokens.com), describing what you want. -::: ## For self hosted @@ -51,7 +54,7 @@ http { default $binary_remote_addr; } - limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=5r/s; + limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=10r/s; limit_req_status 429; # other configs.. diff --git a/v2/thirdpartypasswordless/rate-limits.mdx b/v2/thirdpartypasswordless/rate-limits.mdx index 70598fcea..3b1db5865 100644 --- a/v2/thirdpartypasswordless/rate-limits.mdx +++ b/v2/thirdpartypasswordless/rate-limits.mdx @@ -7,34 +7,37 @@ hide_title: true +import CustomAdmonition from "/src/components/customAdmonition" + # Rate limit policy ## For managed service The SuperTokens core is rate limited on a per app and per IP address basis. This means that if query the core for app1 using the same IP address very quickly, the rate limit will kick in and you will get a `429` status code back from the core. However, if you query the core using different IP addresses or for a different app, the rate limit of that won't interfere with the previous requests (that had another IP or was for another app). -### Development env -The free tier of the managed service has a rate limit of 3 requests per second with a burst of 20 requests per second (with nodelay). - -### Production env -The free tier of the managed service has a rate limit of 6 requests per second with a burst of 20 requests per second (with nodelay). - :::note - -To know more about this behaviour, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). - +To know more about rps and bursts, please [this nginx document](https://www.nginx.com/blog/rate-limiting-nginx/). ::: -### Applicable to all envs -The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. +## Free tier +The free tier of the managed service has a rate limit of 10 requests per second with a burst of 20 requests per second (with nodelay). This should be enough for 2 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). :::important +Our backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error). +::: + +## Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit is set dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **very significant** spike in requests. -If you are not a paying user, and would still like higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + +If you want higher rate limits, please [email us](mailto:team@supertokens.com), requesting a higher rate limit. + + + +## Special case +The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has it's own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. -If you would like a different rate limit policy than the one described above, you can also [email us](mailto:team@supertokens.com), describing what you want. -::: ## For self hosted @@ -51,7 +54,7 @@ http { default $binary_remote_addr; } - limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=5r/s; + limit_req_zone $limit_req_zone_key zone=mylimit:10m rate=10r/s; limit_req_status 429; # other configs.. From a7b91b373c03338a2083a01ed126d3d823b97d73 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 12 Sep 2023 19:21:49 +0530 Subject: [PATCH 64/81] more docs --- v2/multitenancy/users-and-tenants.mdx | 15 +++------------ v2/static/img/account-linking/err_007.png | Bin 0 -> 57818 bytes v2/static/img/account-linking/err_008.png | Bin 0 -> 37144 bytes 3 files changed, 3 insertions(+), 12 deletions(-) create mode 100644 v2/static/img/account-linking/err_007.png create mode 100644 v2/static/img/account-linking/err_008.png diff --git a/v2/multitenancy/users-and-tenants.mdx b/v2/multitenancy/users-and-tenants.mdx index 8d34b57dc..d1e137cfd 100644 --- a/v2/multitenancy/users-and-tenants.mdx +++ b/v2/multitenancy/users-and-tenants.mdx @@ -178,15 +178,10 @@ import {RecipeUserId} from "supertokens-node"; async function removeUserFromTeannt(recipeUserId: RecipeUserId, tenantId: string) { let resp = await Multitenancy.disassociateUserFromTenant(tenantId, recipeUserId); - if (resp.status === "OK") { - if (resp.wasAssociated) { - // User was removed from tenant - } else { - // User was never a part of the tenant anyway - } + if (resp.wasAssociated) { + // User was removed from tenant } else { - // status is DISASSOCIATION_NOT_ALLOWED_ERROR - // this happens if you are trying to disassociate last remaining tenant from the user. + // User was never a part of the tenant anyway } } ``` @@ -230,10 +225,6 @@ from supertokens_python.recipe.multitenancy.asyncio import dissociate_user_from_ async def some_func(): response = await dissociate_user_from_tenant("customer1", "user1") - if response.status != "OK": - print("Handle error") - return - if response.was_associated: print("User was removed from tenant") else: diff --git a/v2/static/img/account-linking/err_007.png b/v2/static/img/account-linking/err_007.png new file mode 100644 index 0000000000000000000000000000000000000000..a4c7fd9c5d2de99aed0754688e6f288cd407cef2 GIT binary patch literal 57818 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 fzRsu%zFLRFySi5pfv4T15zEIUTZq>g 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 Rh|@?Xl(m$ykDa^ozW|n0ASeI; literal 0 HcmV?d00001 diff --git a/v2/static/img/account-linking/err_008.png b/v2/static/img/account-linking/err_008.png new file mode 100644 index 0000000000000000000000000000000000000000..7fad2de878acf1c5a26fe9db1b66bf56070f27ed GIT binary patch literal 37144 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 Date: Wed, 13 Sep 2023 12:03:15 +0530 Subject: [PATCH 65/81] more docs --- .../automatic-account-linking.mdx | 40 +++++++++++++++++++ v2/thirdparty/custom-ui/thirdparty-login.mdx | 2 +- .../automatic-account-linking.mdx | 40 +++++++++++++++++++ .../custom-ui/thirdparty-login.mdx | 2 +- .../automatic-account-linking.mdx | 40 +++++++++++++++++++ .../custom-ui/thirdparty-login.mdx | 2 +- 6 files changed, 123 insertions(+), 3 deletions(-) diff --git a/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx index 440f8adc8..0dcafb8dd 100644 --- a/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx @@ -319,5 +319,45 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using our user management dashboard.** +### ERR_CODE_007 +- This can happen during the email password signup API: + - API path and method: `/signup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, social login account with email `e1`, sign in with Google. Now an attacker tries to sign up with email password with email `e1`. If we allow this, an email verificaiton email will be sent to the victim, and they may click it since they had previously signed up with Google. This will result in the attacker's account being linked to the victim's account. + +- To resolve this issue, you can ask the user to try and login, or go through the reset password flow. + +### ERR_CODE_008 +- This can happen during the email password signin API: + - API path and method: `/signin POST` + - Output JSON: + ```json + { + "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 pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, social login account with email `e1`, sign in with Google. There also exists an email password account (owned by the attacker) that is unverified with the same email `e1` (this is not a primary user). Now if the attacker tries to sign in with email password, they will see this error. We do this because if we didn't, then the attacker might send an email verification email on sign in, and the actual user may click on it (since they had previously signed up). Upon verifying that account, the attacker's account will be linked to the victim's account. + +- To resolve this issue, you can ask the user to try the reset password flow. + ### Changing the error message on the frontend If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). diff --git a/v2/thirdparty/custom-ui/thirdparty-login.mdx b/v2/thirdparty/custom-ui/thirdparty-login.mdx index 278275eb0..2b9bd91f8 100644 --- a/v2/thirdparty/custom-ui/thirdparty-login.mdx +++ b/v2/thirdparty/custom-ui/thirdparty-login.mdx @@ -144,7 +144,7 @@ Once the third party provider redirects your user back to your app, you need to // As a hack to solve this, you can override the backend functions to create a fake email for the user. window.alert("No email provided by social login. Please use another form of login"); - window.location.assig("/auth"); // redirect back to login page + window.location.assign("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 94696e62f..a5417adab 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -319,5 +319,45 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using our user management dashboard.** +### ERR_CODE_007 +- This can happen during the email password signup API: + - API path and method: `/signup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, social login account with email `e1`, sign in with Google. Now an attacker tries to sign up with email password with email `e1`. If we allow this, an email verificaiton email will be sent to the victim, and they may click it since they had previously signed up with Google. This will result in the attacker's account being linked to the victim's account. + +- To resolve this issue, you can ask the user to try and login, or go through the reset password flow. + +### ERR_CODE_008 +- This can happen during the email password signin API: + - API path and method: `/signin POST` + - Output JSON: + ```json + { + "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 pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, social login account with email `e1`, sign in with Google. There also exists an email password account (owned by the attacker) that is unverified with the same email `e1` (this is not a primary user). Now if the attacker tries to sign in with email password, they will see this error. We do this because if we didn't, then the attacker might send an email verification email on sign in, and the actual user may click on it (since they had previously signed up). Upon verifying that account, the attacker's account will be linked to the victim's account. + +- To resolve this issue, you can ask the user to try the reset password flow. + ### Changing the error message on the frontend If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx index 237974c71..221269a61 100644 --- a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx @@ -147,7 +147,7 @@ Once the third party provider redirects your user back to your app, you need to // As a hack to solve this, you can override the backend functions to create a fake email for the user. window.alert("No email provided by social login. Please use another form of login"); - window.location.assig("/auth"); // redirect back to login page + window.location.assign("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx index 440f8adc8..0dcafb8dd 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx @@ -319,5 +319,45 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using our user management dashboard.** +### ERR_CODE_007 +- This can happen during the email password signup API: + - API path and method: `/signup POST` + - Output JSON: + ```json + { + "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)" + } + ``` + - The pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, social login account with email `e1`, sign in with Google. Now an attacker tries to sign up with email password with email `e1`. If we allow this, an email verificaiton email will be sent to the victim, and they may click it since they had previously signed up with Google. This will result in the attacker's account being linked to the victim's account. + +- To resolve this issue, you can ask the user to try and login, or go through the reset password flow. + +### ERR_CODE_008 +- This can happen during the email password signin API: + - API path and method: `/signin POST` + - Output JSON: + ```json + { + "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 pre build UI on the frontend displays this error in the following way: + Pre built UI screenshot for showing error message. + +- Below is as example scenario for when this status is returned (one amongst many): + There exists a primary, social login account with email `e1`, sign in with Google. There also exists an email password account (owned by the attacker) that is unverified with the same email `e1` (this is not a primary user). Now if the attacker tries to sign in with email password, they will see this error. We do this because if we didn't, then the attacker might send an email verification email on sign in, and the actual user may click on it (since they had previously signed up). Upon verifying that account, the attacker's account will be linked to the victim's account. + +- To resolve this issue, you can ask the user to try the reset password flow. + ### Changing the error message on the frontend If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). diff --git a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx index 41b4db0fb..35bfa3aac 100644 --- a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx @@ -147,7 +147,7 @@ Once the third party provider redirects your user back to your app, you need to // As a hack to solve this, you can override the backend functions to create a fake email for the user. window.alert("No email provided by social login. Please use another form of login"); - window.location.assig("/auth"); // redirect back to login page + window.location.assign("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { From ed1279865bd8fb9a114cf829277bd12039e9b026 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 13 Sep 2023 17:22:07 +0530 Subject: [PATCH 66/81] fixes https://github.com/supertokens/supertokens-core/issues/447 --- v2/emailpassword/common-customizations/change-password.mdx | 2 +- .../common-customizations/change-password.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/v2/emailpassword/common-customizations/change-password.mdx b/v2/emailpassword/common-customizations/change-password.mdx index b8cd54428..f676928f8 100644 --- a/v2/emailpassword/common-customizations/change-password.mdx +++ b/v2/emailpassword/common-customizations/change-password.mdx @@ -221,7 +221,7 @@ func changePasswordAPI(w http.ResponseWriter, r *http.Request) { } // highlight-start - if isPasswordValid.OK != nil { + if isPasswordValid.WrongCredentialsError != nil { // TODO: Handle error return } diff --git a/v2/thirdpartyemailpassword/common-customizations/change-password.mdx b/v2/thirdpartyemailpassword/common-customizations/change-password.mdx index d5cfc7c1a..69be61625 100644 --- a/v2/thirdpartyemailpassword/common-customizations/change-password.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/change-password.mdx @@ -221,7 +221,7 @@ func changePasswordAPI(w http.ResponseWriter, r *http.Request) { } // highlight-start - if isPasswordValid.OK != nil { + if isPasswordValid.WrongCredentialsError != nil { // TODO: Handle error return } From 8e34953445bff1fc747e6b42175075e1ad18d70a Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 13 Sep 2023 17:30:05 +0530 Subject: [PATCH 67/81] fixes https://github.com/supertokens/docs/issues/704 --- .../change-email-post-login.mdx | 18 +++++++++--------- .../common-customizations/change-email.mdx | 16 ++++++++-------- .../change-email-post-login.mdx | 18 +++++++++--------- .../common-customizations/change-email.mdx | 16 ++++++++-------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/v2/emailpassword/common-customizations/change-email-post-login.mdx b/v2/emailpassword/common-customizations/change-email-post-login.mdx index ffb45e0d6..a9eb1e00c 100644 --- a/v2/emailpassword/common-customizations/change-email-post-login.mdx +++ b/v2/emailpassword/common-customizations/change-email-post-login.mdx @@ -181,7 +181,7 @@ import ( ) type RequestBody struct { - email string + Email string } // the following example uses net/http @@ -203,14 +203,14 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // validate the input email - if !isValidEmail(requestBody.email) { + if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } // update the email userId := sessionContainer.GetUserID() - updateResponse, err := emailpassword.UpdateEmailOrPassword(userId, &requestBody.email, nil, nil, nil) + updateResponse, err := emailpassword.UpdateEmailOrPassword(userId, &requestBody.Email, nil, nil, nil) if err != nil { // TODO: handle error @@ -507,7 +507,7 @@ import ( ) type RequestBody struct { - email string + Email string } // the following example uses net/http @@ -529,7 +529,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // validate the input email - if !isValidEmail(requestBody.email) { + if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } @@ -542,7 +542,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. - isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.email) + isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.Email) if err != nil { // TODO: handle error @@ -561,7 +561,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { for i := 0; i < len(user.TenantIds); i++ { // Since once user can be shared across many tenants, we need to check if // the email already exists in any of the tenants. - userWithEmail, err := emailpassword.GetUserByEmail(user.TenantIds[i], requestBody.email) + userWithEmail, err := emailpassword.GetUserByEmail(user.TenantIds[i], requestBody.Email) if err != nil { // TODO: handle error @@ -575,7 +575,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // Now we create and send the email verification link to the user for the new email. - _, err = emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.email) + _, err = emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.Email) if err != nil { // TODO: handle error @@ -587,7 +587,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // Since the email is verified, we try and do an update - updateResponse, err := emailpassword.UpdateEmailOrPassword(userId, &requestBody.email, nil, nil, nil) + updateResponse, err := emailpassword.UpdateEmailOrPassword(userId, &requestBody.Email, nil, nil, nil) if err != nil { // TODO: handle error diff --git a/v2/passwordless/common-customizations/change-email.mdx b/v2/passwordless/common-customizations/change-email.mdx index bb530940d..f33a3a5db 100644 --- a/v2/passwordless/common-customizations/change-email.mdx +++ b/v2/passwordless/common-customizations/change-email.mdx @@ -179,7 +179,7 @@ import ( ) type RequestBody struct { - email string + Email string } // the following example uses net/http @@ -201,14 +201,14 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // validate the input email - if !isValidEmail(requestBody.email) { + if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } // update the email userId := sessionContainer.GetUserID() - updateResponse, err := passwordless.UpdateUser(userId, &requestBody.email, nil) + updateResponse, err := passwordless.UpdateUser(userId, &requestBody.Email, nil) if err != nil { // TODO: handle error @@ -503,7 +503,7 @@ import ( ) type RequestBody struct { - email string + Email string } // the following example uses net/http @@ -525,7 +525,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // validate the input email - if !isValidEmail(requestBody.email) { + if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } @@ -538,7 +538,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. - isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.email) + isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.Email) if err != nil { // TODO: handle error @@ -546,7 +546,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { if !isVerified { // Now we create and send the email verification link to the user for the new email. - _, err := emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.email) + _, err := emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.Email) if err != nil { // TODO: handle error @@ -556,7 +556,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // Since the email is verified, we try and do an update - updateResponse, err := passwordless.UpdateUser(userId, &requestBody.email, nil) + updateResponse, err := passwordless.UpdateUser(userId, &requestBody.Email, nil) if err != nil { // TODO: handle error diff --git a/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx b/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx index 49d57d5d9..3390bc0e7 100644 --- a/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/change-email-post-login.mdx @@ -195,7 +195,7 @@ import ( ) type RequestBody struct { - email string + Email string } // the following example uses net/http @@ -217,7 +217,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // validate the input email - if !isValidEmail(requestBody.email) { + if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } @@ -237,7 +237,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // update the email tenantId := "public" - updateResponse, err := thirdpartyemailpassword.UpdateEmailOrPassword(userId, &requestBody.email, nil, nil, &tenantId) + updateResponse, err := thirdpartyemailpassword.UpdateEmailOrPassword(userId, &requestBody.Email, nil, nil, &tenantId) if err != nil { // TODO: handle error @@ -556,7 +556,7 @@ import ( ) type RequestBody struct { - email string + Email string } // the following example uses net/http @@ -578,7 +578,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // validate the input email - if !isValidEmail(requestBody.email) { + if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } @@ -599,7 +599,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. - isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.email) + isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.Email) if err != nil { // TODO: handle error @@ -618,7 +618,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { for i := 0; i < len(user.TenantIds); i++ { // Since once user can be shared across many tenants, we need to check if // the email already exists in any of the tenants. - userWithEmail, err := thirdpartyemailpassword.GetUsersByEmail(user.TenantIds[i], requestBody.email) + userWithEmail, err := thirdpartyemailpassword.GetUsersByEmail(user.TenantIds[i], requestBody.Email) if err != nil { // TODO: handle error @@ -634,7 +634,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // Now we create and send the email verification link to the user for the new email. - _, err = emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.email) + _, err = emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.Email) if err != nil { // TODO: handle error @@ -645,7 +645,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // Since the email is verified, we try and do an update tenantId := "public" - updateResponse, err := thirdpartyemailpassword.UpdateEmailOrPassword(userId, &requestBody.email, nil, nil, &tenantId) + updateResponse, err := thirdpartyemailpassword.UpdateEmailOrPassword(userId, &requestBody.Email, nil, nil, &tenantId) if err != nil { // TODO: handle error diff --git a/v2/thirdpartypasswordless/common-customizations/change-email.mdx b/v2/thirdpartypasswordless/common-customizations/change-email.mdx index 631411ace..414da34da 100644 --- a/v2/thirdpartypasswordless/common-customizations/change-email.mdx +++ b/v2/thirdpartypasswordless/common-customizations/change-email.mdx @@ -197,7 +197,7 @@ import ( ) type RequestBody struct { - email string + Email string } // the following example uses net/http @@ -219,7 +219,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // validate the input email - if !isValidEmail(requestBody.email) { + if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } @@ -238,7 +238,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // update the email - updateResponse, err := thirdpartypasswordless.UpdatePasswordlessUser(userId, &requestBody.email, nil) + updateResponse, err := thirdpartypasswordless.UpdatePasswordlessUser(userId, &requestBody.Email, nil) if err != nil { // TODO: handle error @@ -557,7 +557,7 @@ import ( ) type RequestBody struct { - email string + Email string } // the following example uses net/http @@ -579,7 +579,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // validate the input email - if !isValidEmail(requestBody.email) { + if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } @@ -600,7 +600,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. - isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.email) + isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.Email) if err != nil { // TODO: handle error @@ -608,7 +608,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { if !isVerified { // Now we create and send the email verification link to the user for the new email. - _, err := emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.email) + _, err := emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.Email) if err != nil { // TODO: handle error @@ -618,7 +618,7 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } // Since the email is verified, we try and do an update - updateResponse, err := thirdpartypasswordless.UpdatePasswordlessUser(userId, &requestBody.email, nil) + updateResponse, err := thirdpartypasswordless.UpdatePasswordlessUser(userId, &requestBody.Email, nil) if err != nil { // TODO: handle error From 58a379994e7d9a2d4899312d75404d15ddb2fda9 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 14 Sep 2023 11:25:13 +0530 Subject: [PATCH 68/81] fixes in docs --- .../react-component-override/usage.mdx | 7 +++---- .../react-component-override/usage.mdx | 7 +++---- .../react-component-override/usage.mdx | 7 +++---- .../react-component-override/usage.mdx | 7 +++---- .../react-component-override/usage.mdx | 7 +++---- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/v2/emailpassword/advanced-customizations/react-component-override/usage.mdx b/v2/emailpassword/advanced-customizations/react-component-override/usage.mdx index e1aff1fa4..e2e2a4ff5 100644 --- a/v2/emailpassword/advanced-customizations/react-component-override/usage.mdx +++ b/v2/emailpassword/advanced-customizations/react-component-override/usage.mdx @@ -11,10 +11,9 @@ import {Question}from "/src/components/question" # How to use -1. You will need to modify the `EmailPassword.init(...)` function call. -2. Component overrides can be configured at `override.components` config object. -3. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. -4. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. +1. You will have to use the `EmailPasswordComponentsOverrideProvider` component as shown below. make sure that it render the SuperTokens components inside this component. +2. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. +3. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. ## Example diff --git a/v2/passwordless/advanced-customizations/react-component-override/usage.mdx b/v2/passwordless/advanced-customizations/react-component-override/usage.mdx index a33336af6..addc64e2d 100644 --- a/v2/passwordless/advanced-customizations/react-component-override/usage.mdx +++ b/v2/passwordless/advanced-customizations/react-component-override/usage.mdx @@ -11,10 +11,9 @@ import {Question}from "/src/components/question" # How to use -1. You will need to modify the `Passwordless.init(...)` function call. -2. Component overrides can be configured at `override.components` config object. -3. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. -4. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. +1. You will have to use the `PasswordlessComponentsOverrideProvider` component as shown below. make sure that it render the SuperTokens components inside this component. +2. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. +3. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. ## Example diff --git a/v2/thirdparty/advanced-customizations/react-component-override/usage.mdx b/v2/thirdparty/advanced-customizations/react-component-override/usage.mdx index 74ecfa555..eafc55594 100644 --- a/v2/thirdparty/advanced-customizations/react-component-override/usage.mdx +++ b/v2/thirdparty/advanced-customizations/react-component-override/usage.mdx @@ -12,10 +12,9 @@ import {Question}from "/src/components/question" # How to use -1. You will need to modify the `ThirdParty.init(...)` function call. -2. Component overrides can be configured at `override.components` config object. -3. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. -4. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. +1. You will have to use the `ThirdpartyComponentsOverrideProvider` component as shown below. make sure that it render the SuperTokens components inside this component. +2. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. +3. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. ### Example diff --git a/v2/thirdpartyemailpassword/advanced-customizations/react-component-override/usage.mdx b/v2/thirdpartyemailpassword/advanced-customizations/react-component-override/usage.mdx index 5d84dcd0e..1d532600d 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/react-component-override/usage.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/react-component-override/usage.mdx @@ -11,10 +11,9 @@ import {Question}from "/src/components/question" # How to use -1. You will need to modify the `ThirdPartyEmailPassword.init(...)` function call. -2. Component overrides can be configured at `override.components` config object. -3. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. -4. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. +1. You will have to use the `ThirdpartyEmailPasswordComponentsOverrideProvider` component as shown below. make sure that it render the SuperTokens components inside this component. +2. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. +3. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. ## Example diff --git a/v2/thirdpartypasswordless/advanced-customizations/react-component-override/usage.mdx b/v2/thirdpartypasswordless/advanced-customizations/react-component-override/usage.mdx index f012a61c4..773242435 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/react-component-override/usage.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/react-component-override/usage.mdx @@ -11,10 +11,9 @@ import {Question}from "/src/components/question" # How to use -1. You will need to modify the `Passwordless.init(...)` function call. -2. Component overrides can be configured at `override.components` config object. -3. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. -4. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. +1. You will have to use the `ThirdpartyPasswordlessComponentsOverrideProvider` component as shown below. make sure that it render the SuperTokens components inside this component. +2. [Pick a component](#finding-which-component-will-be-overridden) that you'd like to override by its key. +3. Supply a React component against the key you have picked. Your custom component will get the original component as a `prop`. ## Example From 45664dbda2bfe3777595c2941b506378121db132 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 14 Sep 2023 15:00:22 +0530 Subject: [PATCH 69/81] fixes docs --- .../frontend-hooks/pre-api.mdx | 34 ++++++++++++++----- .../frontend-hooks/pre-api.mdx | 18 ++++++++-- .../frontend-hooks/pre-api.mdx | 30 +++++++++++++--- .../frontend-hooks/pre-api.mdx | 30 ++++++++++++---- .../frontend-hooks/pre-api.mdx | 26 ++++++++------ 5 files changed, 107 insertions(+), 31 deletions(-) diff --git a/v2/emailpassword/advanced-customizations/frontend-hooks/pre-api.mdx b/v2/emailpassword/advanced-customizations/frontend-hooks/pre-api.mdx index 7acc2c391..9055e68d9 100644 --- a/v2/emailpassword/advanced-customizations/frontend-hooks/pre-api.mdx +++ b/v2/emailpassword/advanced-customizations/frontend-hooks/pre-api.mdx @@ -39,7 +39,7 @@ EmailPassword.init({ } else if (action === "SEND_RESET_PASSWORD_EMAIL") { - }else if (action === "EMAIL_PASSWORD_SIGN_IN") { + } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { @@ -81,23 +81,32 @@ Also checkout the [session recipe pre API hook](/docs/session/advanced-customiza import EmailPassword from "supertokens-web-js/recipe/emailpassword" EmailPassword.init({ - postAPIHook: async (context) => { + preAPIHook: async (context) => { + let url = context.url; + + // is the fetch config object that contains the header, body etc.. + let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { + } else if (action === "SEND_RESET_PASSWORD_EMAIL") { + } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { - } else if (action === "SEND_RESET_PASSWORD_EMAIL") { - } else if (action === "SUBMIT_NEW_PASSWORD") { - } + } else if (action === "VERIFY_EMAIL") { + + } // events such as sign out are in the // session recipe pre API hook (See the info box below) + return { + requestInit, url + }; }, }) ``` @@ -131,23 +140,32 @@ EmailPassword.doesEmailExist({ import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; supertokensEmailPassword.init({ - postAPIHook: async (context) => { + preAPIHook: async (context) => { + let url = context.url; + + // is the fetch config object that contains the header, body etc.. + let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { + } else if (action === "SEND_RESET_PASSWORD_EMAIL") { + } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { - } else if (action === "SEND_RESET_PASSWORD_EMAIL") { - } else if (action === "SUBMIT_NEW_PASSWORD") { + } else if (action === "VERIFY_EMAIL") { + } // events such as sign out are in the // session recipe pre API hook (See the info box below) + return { + requestInit, url + }; }, }) ``` diff --git a/v2/passwordless/advanced-customizations/frontend-hooks/pre-api.mdx b/v2/passwordless/advanced-customizations/frontend-hooks/pre-api.mdx index 6ba0e577d..43a7ee38c 100644 --- a/v2/passwordless/advanced-customizations/frontend-hooks/pre-api.mdx +++ b/v2/passwordless/advanced-customizations/frontend-hooks/pre-api.mdx @@ -79,7 +79,11 @@ Also checkout the [session recipe pre API hook](/docs/session/advanced-customiza import Passwordless from "supertokens-web-js/recipe/passwordless"; Passwordless.init({ - postAPIHook: async (context) => { + preAPIHook: async (context) => { + let url = context.url; + + // is the fetch config object that contains the header, body etc.. + let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { @@ -96,6 +100,9 @@ Passwordless.init({ // events such as sign out are in the // session recipe pre API hook (See the info box below) + return { + requestInit, url + }; }, }) @@ -126,7 +133,11 @@ Passwordless.doesEmailExist({ import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; supertokensPasswordless.init({ - postAPIHook: async (context) => { + preAPIHook: async (context) => { + let url = context.url; + + // is the fetch config object that contains the header, body etc.. + let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { @@ -143,6 +154,9 @@ supertokensPasswordless.init({ // events such as sign out are in the // session recipe pre API hook (See the info box below) + return { + requestInit, url + }; }, }) diff --git a/v2/thirdparty/advanced-customizations/frontend-hooks/pre-api.mdx b/v2/thirdparty/advanced-customizations/frontend-hooks/pre-api.mdx index 98372e53e..9d46a6e2f 100644 --- a/v2/thirdparty/advanced-customizations/frontend-hooks/pre-api.mdx +++ b/v2/thirdparty/advanced-customizations/frontend-hooks/pre-api.mdx @@ -77,17 +77,27 @@ Also checkout the [session recipe pre API hook](/docs/session/advanced-customiza import ThirdParty from "supertokens-web-js/recipe/thirdparty" ThirdParty.init({ - postAPIHook: async (context) => { + preAPIHook: async (context) => { + let url = context.url; + + // is the fetch config object that contains the header, body etc.. + let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - - } + // Note: this could either be sign in or sign up. + // we don't know that at the time of the API call + // since all we have is the authorisation code from + // the social provider + } // events such as sign out are in the // session recipe pre API hook (See the info box below) + return { + requestInit, url + }; }, }) @@ -120,17 +130,27 @@ ThirdParty.signInAndUp({ import supertokensThirdParty from "supertokens-web-js-script/recipe/thirdparty"; supertokensThirdParty.init({ - postAPIHook: async (context) => { + preAPIHook: async (context) => { + let url = context.url; + + // is the fetch config object that contains the header, body etc.. + let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - + // Note: this could either be sign in or sign up. + // we don't know that at the time of the API call + // since all we have is the authorisation code from + // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) + return { + requestInit, url + }; }, }) diff --git a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/pre-api.mdx b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/pre-api.mdx index 36d67c156..74e7a8b58 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/pre-api.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/pre-api.mdx @@ -86,8 +86,10 @@ Also checkout the [session recipe pre API hook](/docs/session/advanced-customiza import ThirdPartyEmailPassword from "supertokens-web-js/recipe/thirdpartyemailpassword" ThirdPartyEmailPassword.init({ - postAPIHook: async (context) => { - + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; + let action = context.action; if (action === "EMAIL_EXISTS") { @@ -102,11 +104,18 @@ ThirdPartyEmailPassword.init({ } else if (action === "SUBMIT_NEW_PASSWORD") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - + // Note: this could either be sign in or sign up. + // we don't know that at the time of the API call + // since all we have is the authorisation code from + // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) + + return { + requestInit, url + }; }, }) ``` @@ -142,8 +151,10 @@ ThirdPartyEmailPassword.doesEmailExist({ import supertokensThirdPartyEmailPassword from "supertokens-web-js-script/recipe/thirdpartyemailpassword"; supertokensThirdPartyEmailPassword.init({ - postAPIHook: async (context) => { - + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; + let action = context.action; if (action === "EMAIL_EXISTS") { @@ -158,11 +169,18 @@ supertokensThirdPartyEmailPassword.init({ } else if (action === "SUBMIT_NEW_PASSWORD") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - + // Note: this could either be sign in or sign up. + // we don't know that at the time of the API call + // since all we have is the authorisation code from + // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) + + return { + requestInit, url + }; }, }) diff --git a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/pre-api.mdx b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/pre-api.mdx index fbf2e295e..5fe96cb94 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/pre-api.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/pre-api.mdx @@ -80,13 +80,15 @@ Also checkout the [session recipe pre API hook](/docs/session/advanced-customiza import ThirdPartyPasswordless from "supertokens-web-js/recipe/thirdpartypasswordless"; ThirdPartyPasswordless.init({ - postAPIHook: async (context) => { + preAPIHook: async (context) => { + let url = context.url; + + // is the fetch config object that contains the header, body etc.. + let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { - } else if (action === "GET_AUTHORISATION_URL") { - } else if (action === "PASSWORDLESS_CONSUME_CODE") { } else if (action === "PASSWORDLESS_CREATE_CODE") { @@ -95,12 +97,13 @@ ThirdPartyPasswordless.init({ } else if (action === "PHONE_NUMBER_EXISTS") { - } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - } // events such as sign out are in the // session recipe pre API hook (See the info box below) + return { + requestInit, url + }; }, }) @@ -131,13 +134,15 @@ ThirdPartyPasswordless.doesPasswordlessUserEmailExist({ import supertokensThirdPartyPasswordless from "supertokens-web-js-script/recipe/thirdpartypasswordless"; supertokensThirdPartyPasswordless.init({ - postAPIHook: async (context) => { + preAPIHook: async (context) => { + let url = context.url; + + // is the fetch config object that contains the header, body etc.. + let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { - } else if (action === "GET_AUTHORISATION_URL") { - } else if (action === "PASSWORDLESS_CONSUME_CODE") { } else if (action === "PASSWORDLESS_CREATE_CODE") { @@ -146,12 +151,13 @@ supertokensThirdPartyPasswordless.init({ } else if (action === "PHONE_NUMBER_EXISTS") { - } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - } // events such as sign out are in the // session recipe pre API hook (See the info box below) + return { + requestInit, url + }; }, }) From a05275913decb31381af378b08658117d507f164 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 14 Sep 2023 15:26:45 +0530 Subject: [PATCH 70/81] Add setup step for swift package manager --- v2/emailpassword/custom-ui/init/frontend.mdx | 12 ++++++++++++ v2/passwordless/custom-ui/init/frontend.mdx | 12 ++++++++++++ v2/thirdparty/custom-ui/init/frontend.mdx | 12 ++++++++++++ .../custom-ui/init/frontend.mdx | 12 ++++++++++++ .../custom-ui/init/frontend.mdx | 12 ++++++++++++ 5 files changed, 60 insertions(+) diff --git a/v2/emailpassword/custom-ui/init/frontend.mdx b/v2/emailpassword/custom-ui/init/frontend.mdx index d19731b0f..17523afdc 100644 --- a/v2/emailpassword/custom-ui/init/frontend.mdx +++ b/v2/emailpassword/custom-ui/init/frontend.mdx @@ -89,12 +89,24 @@ You can find the latest version of the SDK [here](https://github.com/supertokens +#### Using Cocoapods + Add the Cocoapod dependency to your Podfile ```bash pod 'SuperTokensIOS' ``` +#### Using Swift Package Manager + +Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. + +When adding the dependency use the `master` branch after you enter the supertokens-ios repository URL: + +```bash +https://github.com/supertokens/supertokens-ios +``` + diff --git a/v2/passwordless/custom-ui/init/frontend.mdx b/v2/passwordless/custom-ui/init/frontend.mdx index 693656320..19b92ddb9 100644 --- a/v2/passwordless/custom-ui/init/frontend.mdx +++ b/v2/passwordless/custom-ui/init/frontend.mdx @@ -90,12 +90,24 @@ You can find the latest version of the SDK [here](https://github.com/supertokens +#### Using Cocoapods + Add the Cocoapod dependency to your Podfile ```bash pod 'SuperTokensIOS' ``` +#### Using Swift Package Manager + +Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. + +When adding the dependency use the `master` branch after you enter the supertokens-ios repository URL: + +```bash +https://github.com/supertokens/supertokens-ios +``` + diff --git a/v2/thirdparty/custom-ui/init/frontend.mdx b/v2/thirdparty/custom-ui/init/frontend.mdx index 7375efb42..2c7c597a4 100644 --- a/v2/thirdparty/custom-ui/init/frontend.mdx +++ b/v2/thirdparty/custom-ui/init/frontend.mdx @@ -88,12 +88,24 @@ You can find the latest version of the SDK [here](https://github.com/supertokens +#### Using Cocoapods + Add the Cocoapod dependency to your Podfile ```bash pod 'SuperTokensIOS' ``` +#### Using Swift Package Manager + +Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. + +When adding the dependency use the `master` branch after you enter the supertokens-ios repository URL: + +```bash +https://github.com/supertokens/supertokens-ios +``` + diff --git a/v2/thirdpartyemailpassword/custom-ui/init/frontend.mdx b/v2/thirdpartyemailpassword/custom-ui/init/frontend.mdx index d6b421558..00e1c8ef2 100644 --- a/v2/thirdpartyemailpassword/custom-ui/init/frontend.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/init/frontend.mdx @@ -88,12 +88,24 @@ You can find the latest version of the SDK [here](https://github.com/supertokens +#### Using Cocoapods + Add the Cocoapod dependency to your Podfile ```bash pod 'SuperTokensIOS' ``` +#### Using Swift Package Manager + +Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. + +When adding the dependency use the `master` branch after you enter the supertokens-ios repository URL: + +```bash +https://github.com/supertokens/supertokens-ios +``` + diff --git a/v2/thirdpartypasswordless/custom-ui/init/frontend.mdx b/v2/thirdpartypasswordless/custom-ui/init/frontend.mdx index 76d9b3c48..35935740a 100644 --- a/v2/thirdpartypasswordless/custom-ui/init/frontend.mdx +++ b/v2/thirdpartypasswordless/custom-ui/init/frontend.mdx @@ -91,12 +91,24 @@ You can find the latest version of the SDK [here](https://github.com/supertokens +#### Using Cocoapods + Add the Cocoapod dependency to your Podfile ```bash pod 'SuperTokensIOS' ``` +#### Using Swift Package Manager + +Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. + +When adding the dependency use the `master` branch after you enter the supertokens-ios repository URL: + +```bash +https://github.com/supertokens/supertokens-ios +``` + From 77f54feb556d276a37e7d8c476970ad040ea7f81 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 14 Sep 2023 15:59:56 +0530 Subject: [PATCH 71/81] Update snippet for session recipe --- v2/session/quick-setup/frontend.mdx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/v2/session/quick-setup/frontend.mdx b/v2/session/quick-setup/frontend.mdx index b547a5ac9..b878890a1 100644 --- a/v2/session/quick-setup/frontend.mdx +++ b/v2/session/quick-setup/frontend.mdx @@ -85,12 +85,24 @@ You can find the latest version of the SDK [here](https://github.com/supertokens +#### Using Cocoapods + Add the Cocoapod dependency to your Podfile ```bash pod 'SuperTokensIOS' ``` +#### Using Swift Package Manager + +Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. + +When adding the dependency use the `master` branch after you enter the supertokens-ios repository URL: + +```bash +https://github.com/supertokens/supertokens-ios +``` + From 4a62d210abf7d353edc00d3b4fac2962993aa6d9 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 15 Sep 2023 11:32:50 +0530 Subject: [PATCH 72/81] fixes redirection --- .../getting-provider-access-token.mdx | 177 +----------------- .../getting-provider-access-token.mdx | 177 +----------------- .../getting-provider-access-token.mdx | 177 +----------------- .../getting-provider-access-token.mdx | 177 +----------------- 4 files changed, 8 insertions(+), 700 deletions(-) diff --git a/v2/thirdparty/post-login/getting-provider-access-token.mdx b/v2/thirdparty/post-login/getting-provider-access-token.mdx index 4ea5bf765..9c8b0830a 100644 --- a/v2/thirdparty/post-login/getting-provider-access-token.mdx +++ b/v2/thirdparty/post-login/getting-provider-access-token.mdx @@ -7,179 +7,6 @@ hide_title: true -import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs" -import TabItem from '@theme/TabItem'; +import Redirector from '/src/components/Redirector'; -# Getting provider's access token - -You can get the Third Party Provider's access token to query their APIs with the following method: - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; -import Session from "supertokens-node/recipe/session"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [/* ... */] - }, - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - - signInUpPOST: async function (input) { - - if (originalImplementation.signInUpPOST === undefined) { - throw Error("Should never come here"); - } - - let response = await originalImplementation.signInUpPOST(input) - - if (response.status === "OK") { - - // In this example we are using Google as our provider - let accessToken = response.oAuthTokens["access_token"] - - // TODO: ... - - } - - return response; - }, - } - } - }, - }), - Session.init() - ] -}); -``` - - - -```go -import ( - "fmt" - - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - Override: &tpmodels.OverrideStruct{ - APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { - // First we copy the original implementation - originalSignInUpPOST := *originalImplementation.SignInUpPOST - - (*originalImplementation.SignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext *map[string]interface{}) (tpmodels.SignInUpPOSTResponse, error) { - resp, err := originalSignInUpPOST(provider, input, tenantId, options, userContext) - if err != nil { - return tpmodels.SignInUpPOSTResponse{}, err - } - - if resp.OK != nil { - // the user logged in / signed up successfully - - // In this example we are using Google as our provider - authCodeResponse := resp.OK.OAuthTokens - - accessToken := authCodeResponse["access_token"].(string) - - fmt.Println(accessToken) - - } - - return resp, err - } - - return originalImplementation - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty.interfaces import APIInterface, APIOptions, SignInUpPostOkResult -from typing import Optional, Dict, Any -from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo - -# highlight-start -def override_thirdparty_apis(original_implementation: APIInterface): - - original_sign_in_up_post = original_implementation.sign_in_up_post - - async def sign_in_up_post( - provider: Provider, - redirect_uri_info: Optional[RedirectUriInfo], - oauth_tokens: Optional[Dict[str, Any]], - tenant_id: str, - api_options: APIOptions, - user_context: Dict[str, Any] - ): - # First we call the original implementation of signInPOST. - response = await original_sign_in_up_post(provider, redirect_uri_info, oauth_tokens, tenant_id, api_options, user_context) - - # Post sign up response, we check if it was successful - if isinstance(response, SignInUpPostOkResult): - _ = response.user.user_id - __ = response.user.email - - # In this example we are using Google as our provider - thirdparty_auth_response = response.oauth_tokens - - access_token = thirdparty_auth_response["access_token"] - print(access_token) - # TODO - - return response - - original_implementation.sign_in_up_post = sign_in_up_post - return original_implementation -# highlight-end - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - # highlight-start - override=thirdparty.InputOverrideConfig( - apis=override_thirdparty_apis - ), - # highlight-end - sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[ - #... - ]) - ) - ] -) -``` - - - \ No newline at end of file + diff --git a/v2/thirdpartyemailpassword/post-login/getting-provider-access-token.mdx b/v2/thirdpartyemailpassword/post-login/getting-provider-access-token.mdx index 3b2f2b19f..67c4f5ee0 100644 --- a/v2/thirdpartyemailpassword/post-login/getting-provider-access-token.mdx +++ b/v2/thirdpartyemailpassword/post-login/getting-provider-access-token.mdx @@ -7,179 +7,6 @@ hide_title: true -import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs" -import TabItem from '@theme/TabItem'; +import Redirector from '/src/components/Redirector'; -# Getting provider's access token - -You can get the Third Party Provider's access token to query their APIs with the following method: - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; -import Session from "supertokens-node/recipe/session"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ^{recipeNameCapitalLetters}.init({ - ^{nodeRecipeInitDefault} - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - - //highlight-start - // we override the thirdparty sign in / up API - thirdPartySignInUpPOST: async function (input) { - if (originalImplementation.thirdPartySignInUpPOST === undefined) { - throw Error("Should never come here"); - } - - let response = await originalImplementation.thirdPartySignInUpPOST(input); - - // if sign in / up was successful... - if (response.status === "OK") { - // In this example we are using Google as our provider - let accessToken = response.oAuthTokens["access_token"] - - // TODO: ... - } - - return response; - }, - //highlight-end - } - } - }, - }), - Session.init() - ] -}); -``` - - - -```go -import ( - "fmt" - - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/recipe/^{codeImportRecipeName}" - "github.com/supertokens/supertokens-golang/recipe/^{codeImportRecipeName}/^{goModelName}" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - ^{codeImportRecipeName}.Init(^{goModelNameForInit}.TypeInput{ - ^{goRecipeInitDefault} // typecheck-only, removed from output - Override: &^{goModelName}.OverrideStruct{ - APIs: func(originalImplementation ^{goModelName}.APIInterface) ^{goModelName}.APIInterface { - // First we copy the original implementation - originalThirdPartySignInUpPOST := *originalImplementation.ThirdPartySignInUpPOST - - //highlight-start - // we override the thirdparty sign in / up API - (*originalImplementation.ThirdPartySignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (^{goModelName}.ThirdPartySignInUpPOSTResponse, error) { - // first we call the original implementation - resp, err := originalThirdPartySignInUpPOST(provider, input, tenantId, options, userContext) - if err != nil { - return ^{goModelName}.ThirdPartySignInUpPOSTResponse{}, err - } - - // if sign in / up was successful... - if resp.OK != nil { - authCodeResponse := resp.OK.OAuthTokens - - accessToken := authCodeResponse["access_token"].(string) - - fmt.Println(accessToken) // TODO: - } - - return resp, err - } - //highlight-end - - return originalImplementation - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import ^{codeImportRecipeName} -from supertokens_python.recipe.^{codeImportRecipeName}.interfaces import APIInterface, ThirdPartyAPIOptions, ^{codeSignInUpPostOkResultPythonType} -from typing import Dict, Any, Optional -from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo - -# highlight-start -def apis_override(original_implementation: APIInterface): - original_thirdparty_sign_in_up_post = original_implementation.thirdparty_sign_in_up_post - - async def thirdparty_sign_in_up_post( - provider: Provider, - redirect_uri_info: Optional[RedirectUriInfo], - oauth_tokens: Optional[Dict[str, Any]], - tenant_id: str, - api_options: ThirdPartyAPIOptions, - user_context: Dict[str, Any] - ): - # First we call the original implementation of sign_in_up_post. - response = await original_thirdparty_sign_in_up_post(provider, redirect_uri_info, oauth_tokens, tenant_id, api_options, user_context) - - # Post sign up response, we check if it was successful - if isinstance(response, ^{codeSignInUpPostOkResultPythonType}): - _ = response.user.user_id - __ = response.user.email - - # In this example we are using Google as our provider - thirdparty_auth_response = response.oauth_tokens - - access_token = thirdparty_auth_response["access_token"] - - print(access_token) - - return response - - original_implementation.thirdparty_sign_in_up_post = thirdparty_sign_in_up_post - return original_implementation -# highlight-end - -^{pythonDefaultCallbackDefs} # typecheck-only, removed from output -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - ^{codeImportRecipeName}.init( - ^{pythonRecipeInitDefault} # typecheck-only, removed from output - # highlight-start - override=^{codeImportRecipeName}.InputOverrideConfig( - apis=apis_override - ) - # highlight-end - ) - ] -) -``` - - - + \ No newline at end of file diff --git a/v2/thirdpartypasswordless/common-customizations/getting-provider-access-token.mdx b/v2/thirdpartypasswordless/common-customizations/getting-provider-access-token.mdx index 3b2f2b19f..0aee8ecf6 100644 --- a/v2/thirdpartypasswordless/common-customizations/getting-provider-access-token.mdx +++ b/v2/thirdpartypasswordless/common-customizations/getting-provider-access-token.mdx @@ -7,179 +7,6 @@ hide_title: true -import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs" -import TabItem from '@theme/TabItem'; +import Redirector from '/src/components/Redirector'; -# Getting provider's access token - -You can get the Third Party Provider's access token to query their APIs with the following method: - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; -import Session from "supertokens-node/recipe/session"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ^{recipeNameCapitalLetters}.init({ - ^{nodeRecipeInitDefault} - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - - //highlight-start - // we override the thirdparty sign in / up API - thirdPartySignInUpPOST: async function (input) { - if (originalImplementation.thirdPartySignInUpPOST === undefined) { - throw Error("Should never come here"); - } - - let response = await originalImplementation.thirdPartySignInUpPOST(input); - - // if sign in / up was successful... - if (response.status === "OK") { - // In this example we are using Google as our provider - let accessToken = response.oAuthTokens["access_token"] - - // TODO: ... - } - - return response; - }, - //highlight-end - } - } - }, - }), - Session.init() - ] -}); -``` - - - -```go -import ( - "fmt" - - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/recipe/^{codeImportRecipeName}" - "github.com/supertokens/supertokens-golang/recipe/^{codeImportRecipeName}/^{goModelName}" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - ^{codeImportRecipeName}.Init(^{goModelNameForInit}.TypeInput{ - ^{goRecipeInitDefault} // typecheck-only, removed from output - Override: &^{goModelName}.OverrideStruct{ - APIs: func(originalImplementation ^{goModelName}.APIInterface) ^{goModelName}.APIInterface { - // First we copy the original implementation - originalThirdPartySignInUpPOST := *originalImplementation.ThirdPartySignInUpPOST - - //highlight-start - // we override the thirdparty sign in / up API - (*originalImplementation.ThirdPartySignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (^{goModelName}.ThirdPartySignInUpPOSTResponse, error) { - // first we call the original implementation - resp, err := originalThirdPartySignInUpPOST(provider, input, tenantId, options, userContext) - if err != nil { - return ^{goModelName}.ThirdPartySignInUpPOSTResponse{}, err - } - - // if sign in / up was successful... - if resp.OK != nil { - authCodeResponse := resp.OK.OAuthTokens - - accessToken := authCodeResponse["access_token"].(string) - - fmt.Println(accessToken) // TODO: - } - - return resp, err - } - //highlight-end - - return originalImplementation - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import ^{codeImportRecipeName} -from supertokens_python.recipe.^{codeImportRecipeName}.interfaces import APIInterface, ThirdPartyAPIOptions, ^{codeSignInUpPostOkResultPythonType} -from typing import Dict, Any, Optional -from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo - -# highlight-start -def apis_override(original_implementation: APIInterface): - original_thirdparty_sign_in_up_post = original_implementation.thirdparty_sign_in_up_post - - async def thirdparty_sign_in_up_post( - provider: Provider, - redirect_uri_info: Optional[RedirectUriInfo], - oauth_tokens: Optional[Dict[str, Any]], - tenant_id: str, - api_options: ThirdPartyAPIOptions, - user_context: Dict[str, Any] - ): - # First we call the original implementation of sign_in_up_post. - response = await original_thirdparty_sign_in_up_post(provider, redirect_uri_info, oauth_tokens, tenant_id, api_options, user_context) - - # Post sign up response, we check if it was successful - if isinstance(response, ^{codeSignInUpPostOkResultPythonType}): - _ = response.user.user_id - __ = response.user.email - - # In this example we are using Google as our provider - thirdparty_auth_response = response.oauth_tokens - - access_token = thirdparty_auth_response["access_token"] - - print(access_token) - - return response - - original_implementation.thirdparty_sign_in_up_post = thirdparty_sign_in_up_post - return original_implementation -# highlight-end - -^{pythonDefaultCallbackDefs} # typecheck-only, removed from output -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - ^{codeImportRecipeName}.init( - ^{pythonRecipeInitDefault} # typecheck-only, removed from output - # highlight-start - override=^{codeImportRecipeName}.InputOverrideConfig( - apis=apis_override - ) - # highlight-end - ) - ] -) -``` - - - + diff --git a/v2/thirdpartypasswordless/post-login/getting-provider-access-token.mdx b/v2/thirdpartypasswordless/post-login/getting-provider-access-token.mdx index 3b2f2b19f..0aee8ecf6 100644 --- a/v2/thirdpartypasswordless/post-login/getting-provider-access-token.mdx +++ b/v2/thirdpartypasswordless/post-login/getting-provider-access-token.mdx @@ -7,179 +7,6 @@ hide_title: true -import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs" -import TabItem from '@theme/TabItem'; +import Redirector from '/src/components/Redirector'; -# Getting provider's access token - -You can get the Third Party Provider's access token to query their APIs with the following method: - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ^{recipeNameCapitalLetters} from "supertokens-node/recipe/^{codeImportRecipeName}"; -import Session from "supertokens-node/recipe/session"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ^{recipeNameCapitalLetters}.init({ - ^{nodeRecipeInitDefault} - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - - //highlight-start - // we override the thirdparty sign in / up API - thirdPartySignInUpPOST: async function (input) { - if (originalImplementation.thirdPartySignInUpPOST === undefined) { - throw Error("Should never come here"); - } - - let response = await originalImplementation.thirdPartySignInUpPOST(input); - - // if sign in / up was successful... - if (response.status === "OK") { - // In this example we are using Google as our provider - let accessToken = response.oAuthTokens["access_token"] - - // TODO: ... - } - - return response; - }, - //highlight-end - } - } - }, - }), - Session.init() - ] -}); -``` - - - -```go -import ( - "fmt" - - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/recipe/^{codeImportRecipeName}" - "github.com/supertokens/supertokens-golang/recipe/^{codeImportRecipeName}/^{goModelName}" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - ^{codeImportRecipeName}.Init(^{goModelNameForInit}.TypeInput{ - ^{goRecipeInitDefault} // typecheck-only, removed from output - Override: &^{goModelName}.OverrideStruct{ - APIs: func(originalImplementation ^{goModelName}.APIInterface) ^{goModelName}.APIInterface { - // First we copy the original implementation - originalThirdPartySignInUpPOST := *originalImplementation.ThirdPartySignInUpPOST - - //highlight-start - // we override the thirdparty sign in / up API - (*originalImplementation.ThirdPartySignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (^{goModelName}.ThirdPartySignInUpPOSTResponse, error) { - // first we call the original implementation - resp, err := originalThirdPartySignInUpPOST(provider, input, tenantId, options, userContext) - if err != nil { - return ^{goModelName}.ThirdPartySignInUpPOSTResponse{}, err - } - - // if sign in / up was successful... - if resp.OK != nil { - authCodeResponse := resp.OK.OAuthTokens - - accessToken := authCodeResponse["access_token"].(string) - - fmt.Println(accessToken) // TODO: - } - - return resp, err - } - //highlight-end - - return originalImplementation - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import ^{codeImportRecipeName} -from supertokens_python.recipe.^{codeImportRecipeName}.interfaces import APIInterface, ThirdPartyAPIOptions, ^{codeSignInUpPostOkResultPythonType} -from typing import Dict, Any, Optional -from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo - -# highlight-start -def apis_override(original_implementation: APIInterface): - original_thirdparty_sign_in_up_post = original_implementation.thirdparty_sign_in_up_post - - async def thirdparty_sign_in_up_post( - provider: Provider, - redirect_uri_info: Optional[RedirectUriInfo], - oauth_tokens: Optional[Dict[str, Any]], - tenant_id: str, - api_options: ThirdPartyAPIOptions, - user_context: Dict[str, Any] - ): - # First we call the original implementation of sign_in_up_post. - response = await original_thirdparty_sign_in_up_post(provider, redirect_uri_info, oauth_tokens, tenant_id, api_options, user_context) - - # Post sign up response, we check if it was successful - if isinstance(response, ^{codeSignInUpPostOkResultPythonType}): - _ = response.user.user_id - __ = response.user.email - - # In this example we are using Google as our provider - thirdparty_auth_response = response.oauth_tokens - - access_token = thirdparty_auth_response["access_token"] - - print(access_token) - - return response - - original_implementation.thirdparty_sign_in_up_post = thirdparty_sign_in_up_post - return original_implementation -# highlight-end - -^{pythonDefaultCallbackDefs} # typecheck-only, removed from output -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - ^{codeImportRecipeName}.init( - ^{pythonRecipeInitDefault} # typecheck-only, removed from output - # highlight-start - override=^{codeImportRecipeName}.InputOverrideConfig( - apis=apis_override - ) - # highlight-end - ) - ] -) -``` - - - + From 1fe00ee1d90ccea1ec5978b74547be5b0251122e Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Mon, 18 Sep 2023 11:16:42 +0530 Subject: [PATCH 73/81] Add UI for old docs widget --- v2/src/theme/TOC/index.js | 17 ++++++++++++ v2/src/theme/TOC/styles.module.css | 44 ++++++++++++++++++++++++++++++ v2/static/img/ic-binoculars.svg | 15 ++++++++++ 3 files changed, 76 insertions(+) create mode 100644 v2/static/img/ic-binoculars.svg diff --git a/v2/src/theme/TOC/index.js b/v2/src/theme/TOC/index.js index 92051d728..d50115282 100644 --- a/v2/src/theme/TOC/index.js +++ b/v2/src/theme/TOC/index.js @@ -47,6 +47,22 @@ function Headings({ toc, isChild }) { ); } +const OldDocsDisclaimer = () => { + return ( +

+
+ + + Looking for older version of the documentation? + +
+
+ Click here! +
+
+ ); +} + function TOC({ toc, showUISwitcher }) { let [selectedUIMode, setSelectedUIMode] = useState(getUIModeFromStorage()) @@ -117,6 +133,7 @@ function TOC({ toc, showUISwitcher }) { const unselectedBorderColorString = "var(--ui-selector-inactive-border)"; return (
+
+ + + + + + + + + + + + + + From cad35d913fa30f89e6ceaea8cc7f1e7c686ca2a8 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Mon, 18 Sep 2023 11:45:09 +0530 Subject: [PATCH 74/81] Add page to explain how to view odler docs --- v2/guides/versioning.mdx | 62 ++++++++++++++++++++++++++++++ v2/src/theme/TOC/index.js | 14 ++++++- v2/src/theme/TOC/styles.module.css | 4 ++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 v2/guides/versioning.mdx diff --git a/v2/guides/versioning.mdx b/v2/guides/versioning.mdx new file mode 100644 index 000000000..d580726c1 --- /dev/null +++ b/v2/guides/versioning.mdx @@ -0,0 +1,62 @@ +--- +id: versioning +title: Viewing older versions of the documentation +hide_title: true +slug: /versioning +--- + +# Viewing older versions of the documentation + +You can use the [docs](https://github.com/supertokens/docs) repository on Github to check-out and view an older version of the documentation. + +## 1. Finding the correct version of the documentation + +The [release notes](https://github.com/supertokens/docs/releases) of the Github repo lists all the SDK versions the documentation is comptabile with. Find the correct version for you by looking at the version of the SDK you are using. + +For example if you are using `supertokens-node` version `15.2.0` you want to find the version of the docs repository that mentions that it is compatible with + +```markdown +supertokens-node: 15.X.X +``` + +The release notes only mention the major version of the SDK it supports because all snippets are compatible with patch versions for a given major version. + +## 2. Clone the repository and switch to the correct version + +Clone the repository: + +```bash +git clone https://github.com/supertokens/docs.git +``` + +Once downloaded, switch to the correct tag. The tag can be found on the releases page next to the version you are trying to use. + +```bash +git checkout +``` + +## 3. Setup the documentation project + +Navigate to the `v2` folder: + +```bash +cd v2/ +``` + +Install all dependencies: + +```bash +npm i -d +``` + +## 4. Run the website locally + +From inside the `v2` directory run: + +```bash +npm run start +``` + +This will build the project and start it on `http://localhost:3000` where you can view the full documentation. + +If you see a screen that asks you to choose between user docs and contributing docs, use the user docs. \ No newline at end of file diff --git a/v2/src/theme/TOC/index.js b/v2/src/theme/TOC/index.js index d50115282..314d6edca 100644 --- a/v2/src/theme/TOC/index.js +++ b/v2/src/theme/TOC/index.js @@ -48,6 +48,14 @@ function Headings({ toc, isChild }) { } const OldDocsDisclaimer = () => { + const goToVersioningPage = () => { + window.location.href = "/docs/guides/versioning"; + } + + if (window.location.pathname === "/docs/guides/versioning") { + return <>; + } + return (
@@ -56,9 +64,11 @@ const OldDocsDisclaimer = () => { Looking for older version of the documentation?
-
+
+
); } diff --git a/v2/src/theme/TOC/styles.module.css b/v2/src/theme/TOC/styles.module.css index 3e9da8388..9fe992d49 100644 --- a/v2/src/theme/TOC/styles.module.css +++ b/v2/src/theme/TOC/styles.module.css @@ -56,6 +56,10 @@ cursor: pointer; } +.tocOldDOcsButton:hover { + color: #000; +} + @media only screen and (max-width: 996px) { .tableOfContents { display: none; From 5398f8d57e1ef116bb54f9ea267d48f35e4acbde Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Mon, 18 Sep 2023 15:48:51 +0530 Subject: [PATCH 75/81] Changes based on review --- v2/{guides => community}/versioning.mdx | 0 v2/src/theme/TOC/index.js | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) rename v2/{guides => community}/versioning.mdx (100%) diff --git a/v2/guides/versioning.mdx b/v2/community/versioning.mdx similarity index 100% rename from v2/guides/versioning.mdx rename to v2/community/versioning.mdx diff --git a/v2/src/theme/TOC/index.js b/v2/src/theme/TOC/index.js index 314d6edca..30b380f0f 100644 --- a/v2/src/theme/TOC/index.js +++ b/v2/src/theme/TOC/index.js @@ -49,10 +49,14 @@ function Headings({ toc, isChild }) { const OldDocsDisclaimer = () => { const goToVersioningPage = () => { - window.location.href = "/docs/guides/versioning"; + window.location.href = "/docs/community/versioning"; } - if (window.location.pathname === "/docs/guides/versioning") { + if (window.location.href.includes("/docs/guides")) { + return <>; + } + + if (window.location.href.includes("/docs/community/versioning")) { return <>; } From 52d3de3088d7c6d7404dfc700650e02585312b2a Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 21 Sep 2023 10:39:43 +0530 Subject: [PATCH 76/81] Add admonition to explain bundle id for ios --- v2/thirdparty/custom-ui/init/backend.mdx | 4 ++++ v2/thirdparty/pre-built-ui/setup/backend.mdx | 4 ++++ v2/thirdpartyemailpassword/custom-ui/init/backend.mdx | 4 ++++ v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/v2/thirdparty/custom-ui/init/backend.mdx b/v2/thirdparty/custom-ui/init/backend.mdx index 9c0495654..d146d081b 100644 --- a/v2/thirdparty/custom-ui/init/backend.mdx +++ b/v2/thirdparty/custom-ui/init/backend.mdx @@ -547,6 +547,10 @@ thirdparty.init( +:::important +If you are adding sign in with Apple to an iOS application, the client id should be the same as the bundle identifier for your app +::: + **When you want to generate your own keys**, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
diff --git a/v2/thirdparty/pre-built-ui/setup/backend.mdx b/v2/thirdparty/pre-built-ui/setup/backend.mdx index 1fcaf312b..7ea760ea2 100644 --- a/v2/thirdparty/pre-built-ui/setup/backend.mdx +++ b/v2/thirdparty/pre-built-ui/setup/backend.mdx @@ -547,6 +547,10 @@ thirdparty.init( +:::important +If you are adding sign in with Apple to an iOS application, the client id should be the same as the bundle identifier for your app +::: + **When you want to generate your own keys**, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
diff --git a/v2/thirdpartyemailpassword/custom-ui/init/backend.mdx b/v2/thirdpartyemailpassword/custom-ui/init/backend.mdx index 0f35ad2fb..748f9576d 100644 --- a/v2/thirdpartyemailpassword/custom-ui/init/backend.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/init/backend.mdx @@ -541,6 +541,10 @@ thirdpartyemailpassword.init( +:::important +If you are adding sign in with Apple to an iOS application, the client id should be the same as the bundle identifier for your app +::: + **When you want to generate your own keys**, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
diff --git a/v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx b/v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx index 19e13e1ee..892207af9 100644 --- a/v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx +++ b/v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx @@ -541,6 +541,10 @@ thirdpartyemailpassword.init( +:::important +If you are adding sign in with Apple to an iOS application, the client id should be the same as the bundle identifier for your app +::: + **When you want to generate your own keys**, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
From d0c556845e5073547730d4d62be05225da9ea9b1 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 21 Sep 2023 11:11:20 +0530 Subject: [PATCH 77/81] Revert --- v2/thirdparty/custom-ui/init/backend.mdx | 4 ---- v2/thirdparty/pre-built-ui/setup/backend.mdx | 4 ---- v2/thirdpartyemailpassword/custom-ui/init/backend.mdx | 4 ---- v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx | 4 ---- 4 files changed, 16 deletions(-) diff --git a/v2/thirdparty/custom-ui/init/backend.mdx b/v2/thirdparty/custom-ui/init/backend.mdx index d146d081b..9c0495654 100644 --- a/v2/thirdparty/custom-ui/init/backend.mdx +++ b/v2/thirdparty/custom-ui/init/backend.mdx @@ -547,10 +547,6 @@ thirdparty.init( -:::important -If you are adding sign in with Apple to an iOS application, the client id should be the same as the bundle identifier for your app -::: - **When you want to generate your own keys**, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
diff --git a/v2/thirdparty/pre-built-ui/setup/backend.mdx b/v2/thirdparty/pre-built-ui/setup/backend.mdx index 7ea760ea2..1fcaf312b 100644 --- a/v2/thirdparty/pre-built-ui/setup/backend.mdx +++ b/v2/thirdparty/pre-built-ui/setup/backend.mdx @@ -547,10 +547,6 @@ thirdparty.init( -:::important -If you are adding sign in with Apple to an iOS application, the client id should be the same as the bundle identifier for your app -::: - **When you want to generate your own keys**, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
diff --git a/v2/thirdpartyemailpassword/custom-ui/init/backend.mdx b/v2/thirdpartyemailpassword/custom-ui/init/backend.mdx index 748f9576d..0f35ad2fb 100644 --- a/v2/thirdpartyemailpassword/custom-ui/init/backend.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/init/backend.mdx @@ -541,10 +541,6 @@ thirdpartyemailpassword.init( -:::important -If you are adding sign in with Apple to an iOS application, the client id should be the same as the bundle identifier for your app -::: - **When you want to generate your own keys**, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
diff --git a/v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx b/v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx index 892207af9..19e13e1ee 100644 --- a/v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx +++ b/v2/thirdpartyemailpassword/pre-built-ui/setup/backend.mdx @@ -541,10 +541,6 @@ thirdpartyemailpassword.init( -:::important -If you are adding sign in with Apple to an iOS application, the client id should be the same as the bundle identifier for your app -::: - **When you want to generate your own keys**, please refer to the corresponding documentation to get your client ids and client secrets for each of the below providers:
From 5432706809c1b9510817c31cafc6a628c798c948 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 21 Sep 2023 11:16:51 +0530 Subject: [PATCH 78/81] Add docs to explain bundle id for ios --- v2/thirdparty/custom-ui/thirdparty-login.mdx | 1 + v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx | 1 + v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx | 1 + 3 files changed, 3 insertions(+) diff --git a/v2/thirdparty/custom-ui/thirdparty-login.mdx b/v2/thirdparty/custom-ui/thirdparty-login.mdx index 2b9bd91f8..c6c9cb9df 100644 --- a/v2/thirdparty/custom-ui/thirdparty-login.mdx +++ b/v2/thirdparty/custom-ui/thirdparty-login.mdx @@ -421,6 +421,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup' \ ``` :::important +- On iOS, the client id set in the backend should be the same as the bundle identifier for your app - The `clientType` input is optional and is required only if you have initialised the same provider multiple times on the backend (See the "Social / SSO login for both, web and mobile apps" section below). - On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. - On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. diff --git a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx index 221269a61..8992c1f32 100644 --- a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx @@ -424,6 +424,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup' \ ``` :::important +- On iOS, the client id set in the backend should be the same as the bundle identifier for your app - The `clientType` input is optional and is required only if you have initialised the same provider multiple times on the backend (See the "Social / SSO login for both, web and mobile apps" section below). - On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. - On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. diff --git a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx index 35bfa3aac..d6da52bf4 100644 --- a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx @@ -424,6 +424,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup' \ ``` :::important +- On iOS, the client id set in the backend should be the same as the bundle identifier for your app - The `clientType` input is optional and is required only if you have initialised the same provider multiple times on the backend (See the "Social / SSO login for both, web and mobile apps" section below). - On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. - On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. From ca472c19b66bda85a33b2a7e4fd3d9ae2ba2f656 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 21 Sep 2023 11:21:38 +0530 Subject: [PATCH 79/81] Add docs to explain bundle id for ios --- v2/thirdparty/custom-ui/thirdparty-login.mdx | 2 +- v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx | 2 +- v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v2/thirdparty/custom-ui/thirdparty-login.mdx b/v2/thirdparty/custom-ui/thirdparty-login.mdx index c6c9cb9df..dfe1d6744 100644 --- a/v2/thirdparty/custom-ui/thirdparty-login.mdx +++ b/v2/thirdparty/custom-ui/thirdparty-login.mdx @@ -421,7 +421,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup' \ ``` :::important -- On iOS, the client id set in the backend should be the same as the bundle identifier for your app +- On iOS, the client id set in the backend should be the same as the bundle identifier for your app. - The `clientType` input is optional and is required only if you have initialised the same provider multiple times on the backend (See the "Social / SSO login for both, web and mobile apps" section below). - On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. - On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. diff --git a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx index 8992c1f32..d2ca9d5d1 100644 --- a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx @@ -424,7 +424,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup' \ ``` :::important -- On iOS, the client id set in the backend should be the same as the bundle identifier for your app +- On iOS, the client id set in the backend should be the same as the bundle identifier for your app. - The `clientType` input is optional and is required only if you have initialised the same provider multiple times on the backend (See the "Social / SSO login for both, web and mobile apps" section below). - On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. - On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. diff --git a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx index d6da52bf4..fd0ac995c 100644 --- a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx @@ -424,7 +424,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup' \ ``` :::important -- On iOS, the client id set in the backend should be the same as the bundle identifier for your app +- On iOS, the client id set in the backend should be the same as the bundle identifier for your app. - The `clientType` input is optional and is required only if you have initialised the same provider multiple times on the backend (See the "Social / SSO login for both, web and mobile apps" section below). - On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. - On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. From 8a42fc36889f8236e34b7624f2358882d0d5dca1 Mon Sep 17 00:00:00 2001 From: Alexander Pervakov Date: Thu, 21 Sep 2023 15:50:14 +0300 Subject: [PATCH 80/81] Add missing third a in "available" --- .../common-customizations/sessions/anonymous-session.mdx | 6 +++--- .../sessions/claims/access-token-payload.mdx | 6 +++--- .../sessions/claims/claim-validators.mdx | 2 +- .../sessions/with-jwt/jwt-verification.mdx | 2 +- .../common-customizations/sessions/anonymous-session.mdx | 6 +++--- .../sessions/claims/access-token-payload.mdx | 6 +++--- .../sessions/claims/claim-validators.mdx | 2 +- .../sessions/with-jwt/jwt-verification.mdx | 2 +- .../common-customizations/sessions/anonymous-session.mdx | 6 +++--- .../sessions/claims/access-token-payload.mdx | 6 +++--- .../sessions/claims/claim-validators.mdx | 2 +- .../sessions/with-jwt/jwt-verification.mdx | 2 +- .../common-customizations/sessions/anonymous-session.mdx | 6 +++--- .../sessions/claims/access-token-payload.mdx | 6 +++--- .../sessions/claims/claim-validators.mdx | 2 +- .../sessions/with-jwt/jwt-verification.mdx | 2 +- .../common-customizations/sessions/anonymous-session.mdx | 6 +++--- .../sessions/claims/access-token-payload.mdx | 6 +++--- .../sessions/claims/claim-validators.mdx | 2 +- .../sessions/with-jwt/jwt-verification.mdx | 2 +- .../common-customizations/sessions/anonymous-session.mdx | 6 +++--- .../sessions/claims/access-token-payload.mdx | 6 +++--- .../sessions/claims/claim-validators.mdx | 2 +- .../sessions/with-jwt/jwt-verification.mdx | 2 +- 24 files changed, 48 insertions(+), 48 deletions(-) diff --git a/v2/emailpassword/common-customizations/sessions/anonymous-session.mdx b/v2/emailpassword/common-customizations/sessions/anonymous-session.mdx index eb06ba7be..2f6d09e71 100644 --- a/v2/emailpassword/common-customizations/sessions/anonymous-session.mdx +++ b/v2/emailpassword/common-customizations/sessions/anonymous-session.mdx @@ -190,7 +190,7 @@ SuperTokens.init({ jwtPayload = { /* ... get from decoded jwt ... */}; } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, ...jwtPayload @@ -243,7 +243,7 @@ func main() { // from JWT verification lib } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -310,7 +310,7 @@ def override_functions(original_implementation: RecipeInterface): # from JWT verification lib } - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. if access_token_payload is None: access_token_payload = {} access_token_payload["someKey"] = jwt_payload["someKey"] diff --git a/v2/emailpassword/common-customizations/sessions/claims/access-token-payload.mdx b/v2/emailpassword/common-customizations/sessions/claims/access-token-payload.mdx index 06e893b49..8ebeb1e69 100644 --- a/v2/emailpassword/common-customizations/sessions/claims/access-token-payload.mdx +++ b/v2/emailpassword/common-customizations/sessions/claims/access-token-payload.mdx @@ -64,7 +64,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, someKey: "someValue", @@ -103,7 +103,7 @@ func main() { // Now we override the CreateNewSession function (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -143,7 +143,7 @@ def override_functions(original_implementation: RecipeInterface): if access_token_payload is None: access_token_payload = {} - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. access_token_payload["someKey"] = 'someValue' return await original_implementation_create_new_session(user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) diff --git a/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx b/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..2f2ba2639 100644 --- a/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx @@ -342,7 +342,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line diff --git a/v2/emailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/emailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx index a40e0bf49..01e6b96c5 100644 --- a/v2/emailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/emailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -28,7 +28,7 @@ There are three steps in doing session verification using JWTs: ### Method 1) Using JWKS endpoint (recommended) -Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is availble at the following URL: +Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is available at the following URL: ```bash curl --location --request GET '^{form_apiDomain}^{form_apiBasePath}/jwt/jwks.json' diff --git a/v2/passwordless/common-customizations/sessions/anonymous-session.mdx b/v2/passwordless/common-customizations/sessions/anonymous-session.mdx index eb06ba7be..2f6d09e71 100644 --- a/v2/passwordless/common-customizations/sessions/anonymous-session.mdx +++ b/v2/passwordless/common-customizations/sessions/anonymous-session.mdx @@ -190,7 +190,7 @@ SuperTokens.init({ jwtPayload = { /* ... get from decoded jwt ... */}; } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, ...jwtPayload @@ -243,7 +243,7 @@ func main() { // from JWT verification lib } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -310,7 +310,7 @@ def override_functions(original_implementation: RecipeInterface): # from JWT verification lib } - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. if access_token_payload is None: access_token_payload = {} access_token_payload["someKey"] = jwt_payload["someKey"] diff --git a/v2/passwordless/common-customizations/sessions/claims/access-token-payload.mdx b/v2/passwordless/common-customizations/sessions/claims/access-token-payload.mdx index 06e893b49..8ebeb1e69 100644 --- a/v2/passwordless/common-customizations/sessions/claims/access-token-payload.mdx +++ b/v2/passwordless/common-customizations/sessions/claims/access-token-payload.mdx @@ -64,7 +64,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, someKey: "someValue", @@ -103,7 +103,7 @@ func main() { // Now we override the CreateNewSession function (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -143,7 +143,7 @@ def override_functions(original_implementation: RecipeInterface): if access_token_payload is None: access_token_payload = {} - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. access_token_payload["someKey"] = 'someValue' return await original_implementation_create_new_session(user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) diff --git a/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx b/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..2f2ba2639 100644 --- a/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx @@ -342,7 +342,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line diff --git a/v2/passwordless/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/passwordless/common-customizations/sessions/with-jwt/jwt-verification.mdx index a40e0bf49..01e6b96c5 100644 --- a/v2/passwordless/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/passwordless/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -28,7 +28,7 @@ There are three steps in doing session verification using JWTs: ### Method 1) Using JWKS endpoint (recommended) -Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is availble at the following URL: +Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is available at the following URL: ```bash curl --location --request GET '^{form_apiDomain}^{form_apiBasePath}/jwt/jwks.json' diff --git a/v2/session/common-customizations/sessions/anonymous-session.mdx b/v2/session/common-customizations/sessions/anonymous-session.mdx index 23351ed2d..3f11d1651 100644 --- a/v2/session/common-customizations/sessions/anonymous-session.mdx +++ b/v2/session/common-customizations/sessions/anonymous-session.mdx @@ -190,7 +190,7 @@ SuperTokens.init({ jwtPayload = { /* ... get from decoded jwt ... */}; } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, ...jwtPayload @@ -243,7 +243,7 @@ func main() { // from JWT verification lib } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -310,7 +310,7 @@ def override_functions(original_implementation: RecipeInterface): # from JWT verification lib } - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. if access_token_payload is None: access_token_payload = {} access_token_payload["someKey"] = jwt_payload["someKey"] diff --git a/v2/session/common-customizations/sessions/claims/access-token-payload.mdx b/v2/session/common-customizations/sessions/claims/access-token-payload.mdx index b1517cc2c..201d79224 100644 --- a/v2/session/common-customizations/sessions/claims/access-token-payload.mdx +++ b/v2/session/common-customizations/sessions/claims/access-token-payload.mdx @@ -64,7 +64,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, someKey: "someValue", @@ -103,7 +103,7 @@ func main() { // Now we override the CreateNewSession function (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -143,7 +143,7 @@ def override_functions(original_implementation: RecipeInterface): if access_token_payload is None: access_token_payload = {} - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. access_token_payload["someKey"] = 'someValue' return await original_implementation_create_new_session(user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) diff --git a/v2/session/common-customizations/sessions/claims/claim-validators.mdx b/v2/session/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..2f2ba2639 100644 --- a/v2/session/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/session/common-customizations/sessions/claims/claim-validators.mdx @@ -342,7 +342,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line diff --git a/v2/session/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/session/common-customizations/sessions/with-jwt/jwt-verification.mdx index 0b20d7459..6a64530f1 100644 --- a/v2/session/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/session/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -28,7 +28,7 @@ There are three steps in doing session verification using JWTs: ### Method 1) Using JWKS endpoint (recommended) -Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is availble at the following URL: +Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is available at the following URL: ```bash curl --location --request GET '^{form_apiDomain}^{form_apiBasePath}/jwt/jwks.json' diff --git a/v2/thirdparty/common-customizations/sessions/anonymous-session.mdx b/v2/thirdparty/common-customizations/sessions/anonymous-session.mdx index eb06ba7be..2f6d09e71 100644 --- a/v2/thirdparty/common-customizations/sessions/anonymous-session.mdx +++ b/v2/thirdparty/common-customizations/sessions/anonymous-session.mdx @@ -190,7 +190,7 @@ SuperTokens.init({ jwtPayload = { /* ... get from decoded jwt ... */}; } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, ...jwtPayload @@ -243,7 +243,7 @@ func main() { // from JWT verification lib } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -310,7 +310,7 @@ def override_functions(original_implementation: RecipeInterface): # from JWT verification lib } - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. if access_token_payload is None: access_token_payload = {} access_token_payload["someKey"] = jwt_payload["someKey"] diff --git a/v2/thirdparty/common-customizations/sessions/claims/access-token-payload.mdx b/v2/thirdparty/common-customizations/sessions/claims/access-token-payload.mdx index 06e893b49..8ebeb1e69 100644 --- a/v2/thirdparty/common-customizations/sessions/claims/access-token-payload.mdx +++ b/v2/thirdparty/common-customizations/sessions/claims/access-token-payload.mdx @@ -64,7 +64,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, someKey: "someValue", @@ -103,7 +103,7 @@ func main() { // Now we override the CreateNewSession function (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -143,7 +143,7 @@ def override_functions(original_implementation: RecipeInterface): if access_token_payload is None: access_token_payload = {} - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. access_token_payload["someKey"] = 'someValue' return await original_implementation_create_new_session(user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) diff --git a/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..2f2ba2639 100644 --- a/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx @@ -342,7 +342,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line diff --git a/v2/thirdparty/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/thirdparty/common-customizations/sessions/with-jwt/jwt-verification.mdx index a40e0bf49..01e6b96c5 100644 --- a/v2/thirdparty/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/thirdparty/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -28,7 +28,7 @@ There are three steps in doing session verification using JWTs: ### Method 1) Using JWKS endpoint (recommended) -Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is availble at the following URL: +Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is available at the following URL: ```bash curl --location --request GET '^{form_apiDomain}^{form_apiBasePath}/jwt/jwks.json' diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/anonymous-session.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/anonymous-session.mdx index eb06ba7be..2f6d09e71 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/anonymous-session.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/anonymous-session.mdx @@ -190,7 +190,7 @@ SuperTokens.init({ jwtPayload = { /* ... get from decoded jwt ... */}; } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, ...jwtPayload @@ -243,7 +243,7 @@ func main() { // from JWT verification lib } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -310,7 +310,7 @@ def override_functions(original_implementation: RecipeInterface): # from JWT verification lib } - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. if access_token_payload is None: access_token_payload = {} access_token_payload["someKey"] = jwt_payload["someKey"] diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/claims/access-token-payload.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/claims/access-token-payload.mdx index 06e893b49..8ebeb1e69 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/claims/access-token-payload.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/claims/access-token-payload.mdx @@ -64,7 +64,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, someKey: "someValue", @@ -103,7 +103,7 @@ func main() { // Now we override the CreateNewSession function (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -143,7 +143,7 @@ def override_functions(original_implementation: RecipeInterface): if access_token_payload is None: access_token_payload = {} - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. access_token_payload["someKey"] = 'someValue' return await original_implementation_create_new_session(user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..2f2ba2639 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx @@ -342,7 +342,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx index a40e0bf49..01e6b96c5 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -28,7 +28,7 @@ There are three steps in doing session verification using JWTs: ### Method 1) Using JWKS endpoint (recommended) -Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is availble at the following URL: +Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is available at the following URL: ```bash curl --location --request GET '^{form_apiDomain}^{form_apiBasePath}/jwt/jwks.json' diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/anonymous-session.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/anonymous-session.mdx index eb06ba7be..2f6d09e71 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/anonymous-session.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/anonymous-session.mdx @@ -190,7 +190,7 @@ SuperTokens.init({ jwtPayload = { /* ... get from decoded jwt ... */}; } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, ...jwtPayload @@ -243,7 +243,7 @@ func main() { // from JWT verification lib } - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -310,7 +310,7 @@ def override_functions(original_implementation: RecipeInterface): # from JWT verification lib } - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. if access_token_payload is None: access_token_payload = {} access_token_payload["someKey"] = jwt_payload["someKey"] diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/claims/access-token-payload.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/claims/access-token-payload.mdx index 06e893b49..8ebeb1e69 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/claims/access-token-payload.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/claims/access-token-payload.mdx @@ -64,7 +64,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, someKey: "someValue", @@ -103,7 +103,7 @@ func main() { // Now we override the CreateNewSession function (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -143,7 +143,7 @@ def override_functions(original_implementation: RecipeInterface): if access_token_payload is None: access_token_payload = {} - # This goes in the access token, and is availble to read on the frontend. + # This goes in the access token, and is available to read on the frontend. access_token_payload["someKey"] = 'someValue' return await original_implementation_create_new_session(user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx index 9c566cb44..2f2ba2639 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx @@ -342,7 +342,7 @@ SuperTokens.init({ createNewSession: async function (input) { let userId = input.userId; - // This goes in the access token, and is availble to read on the frontend. + // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/with-jwt/jwt-verification.mdx index a40e0bf49..01e6b96c5 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -28,7 +28,7 @@ There are three steps in doing session verification using JWTs: ### Method 1) Using JWKS endpoint (recommended) -Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is availble at the following URL: +Some libraries let you provide a JWKS endpoint to verify a JWT. The JWKS endpoint exposed by SuperTokens is available at the following URL: ```bash curl --location --request GET '^{form_apiDomain}^{form_apiBasePath}/jwt/jwks.json' From 135b13f88087c82bcdc18519b45e49fa3595a02e Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 21 Sep 2023 20:31:53 +0530 Subject: [PATCH 81/81] updates versions --- v2/community/versioning.mdx | 2 +- v2/src/plugins/codeTypeChecking/jsEnv/package.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/v2/community/versioning.mdx b/v2/community/versioning.mdx index d580726c1..4f7f126a4 100644 --- a/v2/community/versioning.mdx +++ b/v2/community/versioning.mdx @@ -15,7 +15,7 @@ The [release notes](https://github.com/supertokens/docs/releases) of the Github For example if you are using `supertokens-node` version `15.2.0` you want to find the version of the docs repository that mentions that it is compatible with -```markdown +```text supertokens-node: 15.X.X ``` diff --git a/v2/src/plugins/codeTypeChecking/jsEnv/package.json b/v2/src/plugins/codeTypeChecking/jsEnv/package.json index 305ba42a4..d2536b9ef 100644 --- a/v2/src/plugins/codeTypeChecking/jsEnv/package.json +++ b/v2/src/plugins/codeTypeChecking/jsEnv/package.json @@ -52,12 +52,12 @@ "react-router-dom5": "npm:react-router-dom@^5.3.0", "socket.io": "^4.6.1", "socketio": "^1.0.0", - "supertokens-auth-react": "github:supertokens/supertokens-auth-react#feat/account-linking", - "supertokens-node": "github:supertokens/supertokens-node#account-linking", + "supertokens-auth-react": "^0.35.0", + "supertokens-node": "github:supertokens/supertokens-node#16.0", "supertokens-node7": "npm:supertokens-node@7.3", "supertokens-react-native": "^4.0.0", - "supertokens-web-js": "github:supertokens/supertokens-web-js#feat/account-linking", - "supertokens-web-js-script": "github:supertokens/supertokens-web-js#feat/account-linking", + "supertokens-web-js": "^0.8.0", + "supertokens-web-js-script": "github:supertokens/supertokens-web-js#0.8", "supertokens-website": "^17.0.0", "supertokens-website-script": "github:supertokens/supertokens-website#17.0", "typescript": "^4.9.5"