diff --git a/examples/with-account-linking/README.md b/examples/with-account-linking/README.md index 06201884d..5a477f2e7 100644 --- a/examples/with-account-linking/README.md +++ b/examples/with-account-linking/README.md @@ -1 +1,55 @@ -# Account linking is not supported yet, we are actively working on the feature. +![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) + +# SuperTokens Google one tap Demo app + +This demo app demonstrates the following use cases: + +- Thirdparty Login / Sign-up +- Email Password Login / Sign-up +- Logout +- Session management & Calling APIs +- Account linking + +## Project setup + +Clone the repo, enter the directory, and use `npm` to install the project dependencies: + +```bash +git clone https://github.com/supertokens/supertokens-auth-react +cd supertokens-auth-react/examples/with-thirdparty-google-onetap +npm install +cd frontend && npm install && cd ../ +cd backend && npm install && cd ../ +``` + +## Run the demo app + +This compiles and serves the React app and starts the backend API server on port 3001. + +```bash +npm run start +``` + +The app will start on `http://localhost:3000` + +## How it works + +TODO + +### On the frontend + +The demo uses the pre-built UI, but you can always build your own UI instead. + +TODO + +### On the backend + +TODO + +## Author + +Created with :heart: by the folks at supertokens.com. + +## License + +This project is licensed under the Apache 2.0 license. diff --git a/examples/with-account-linking/backend/config.ts b/examples/with-account-linking/backend/config.ts new file mode 100644 index 000000000..1201af2ee --- /dev/null +++ b/examples/with-account-linking/backend/config.ts @@ -0,0 +1,109 @@ +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; +import Session from "supertokens-node/recipe/session"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import EmailVerification from "supertokens-node/recipe/emailverification"; +import { TypeInput } from "supertokens-node/types"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; +import Dashboard from "supertokens-node/recipe/dashboard"; +import { getUser } from "supertokens-node"; + +export function getApiDomain() { + const apiPort = process.env.REACT_APP_API_PORT || 3001; + const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`; + return apiUrl; +} + +export function getWebsiteDomain() { + const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3000; + const websiteUrl = process.env.REACT_APP_WEBSITE_URL || `http://localhost:${websitePort}`; + return websiteUrl; +} + +export const SuperTokensConfig: TypeInput = { + supertokens: { + // this is the location of the SuperTokens core. + connectionURI: "http://localhost:3567", + }, + appInfo: { + appName: "SuperTokens Demo App", + apiDomain: getApiDomain(), + websiteDomain: getWebsiteDomain(), + }, + // recipeList contains all the modules that you want to + // use from SuperTokens. See the full list here: https://supertokens.com/docs/guides + recipeList: [ + EmailVerification.init({ + mode: "REQUIRED", + }), + UserMetadata.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (_newInfo, _user, _tenantId, context) => { + if (context.doNotLink === true) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ThirdPartyEmailPassword.init({ + providers: [ + // We have provided you with development keys which you can use for testing. + // IMPORTANT: Please replace them with your own OAuth keys for production use. + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }, + ], + }, + }, + { + config: { + thirdPartyId: "github", + clients: [ + { + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }, + ], + }, + }, + { + config: { + thirdPartyId: "apple", + clients: [ + { + clientId: "4398792-io.supertokens.example.service", + additionalConfig: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }, + ], + }, + }, + ], + override: { + apis: (oI) => ({ + ...oI, + }), + }, + }), + Passwordless.init({ + contactMethod: "PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + Session.init(), + Dashboard.init(), + ], +}; diff --git a/examples/with-account-linking/backend/index.ts b/examples/with-account-linking/backend/index.ts new file mode 100644 index 000000000..1810c15ec --- /dev/null +++ b/examples/with-account-linking/backend/index.ts @@ -0,0 +1,304 @@ +import express from "express"; +import cors from "cors"; +import supertokens, { getUser, listUsersByAccountInfo } from "supertokens-node"; +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { middleware, errorHandler, SessionRequest } from "supertokens-node/framework/express"; +import { getWebsiteDomain, SuperTokensConfig } from "./config"; +import EmailVerification from "supertokens-node/recipe/emailverification"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import Session from "supertokens-node/recipe/session"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; +import Passwordless from "supertokens-node/recipe/passwordless"; + +supertokens.init(SuperTokensConfig); + +const app = express(); + +app.use( + cors({ + origin: getWebsiteDomain(), + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + methods: ["GET", "PUT", "POST", "DELETE"], + credentials: true, + }) +); + +// This exposes all the APIs from SuperTokens to the client. +app.use(middleware()); +app.use(express.json()); + +// An example API that requires session verification +app.get("/sessioninfo", verifySession(), async (req: SessionRequest, res) => { + let session = req.session; + res.send({ + sessionHandle: session!.getHandle(), + userId: session!.getUserId(), + accessTokenPayload: session!.getAccessTokenPayload(), + }); +}); + +app.get("/userInfo", verifySession(), async (req: SessionRequest, res) => { + const session = req.session!; + const user = await getUser(session.getRecipeUserId().getAsString()); + if (!user) { + throw new Session.Error({ type: Session.Error.UNAUTHORISED, message: "user removed" }); + } + + res.json({ + user: user.toJson(), + }); +}); + +app.post("/addPassword", verifySession(), async (req: SessionRequest, res) => { + const session = req.session!; + const user = await getUser(session.getRecipeUserId().getAsString()); + if (!user) { + throw new Session.Error({ type: Session.Error.UNAUTHORISED, message: "user removed" }); + } + const loginMethod = user.loginMethods.find( + (m) => m.recipeUserId.getAsString() === session.getRecipeUserId().getAsString() + ); + if (!loginMethod) { + throw new Error("This should never happen"); + } + + if (loginMethod.recipeId === "emailpassword") { + return res.json({ + status: "GENERAL_ERROR", + message: "This user already has a password associated to it", + }); + } + + if (!loginMethod.verified) { + return res.json({ + status: "GENERAL_ERROR", + message: "You can only add a password when logged in using a verified account", + }); + } + + // Technically we do not need this limitation + if (loginMethod.email === undefined) { + return res.json({ + status: "GENERAL_ERROR", + message: "You can only add a password when to accounts associated with email addresses", + }); + } + + let password: string = req.body.password; + + const signUpResp = await ThirdPartyEmailPassword.emailPasswordSignUp( + session.getTenantId(), + loginMethod.email, + password + ); + + if (signUpResp.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // This is an edge-case where the current third-party user has an email that has already signed up but not linked for some reason. + return res.json({ + status: "GENERAL_ERROR", + message: "This user has already signed up. Please delete it first.", + }); + } + + if (signUpResp.status !== "OK") { + return res.json(signUpResp); + } + // Here we can assume the user in signUpResp is not a primary user since it was just created + // Plus the linkAccounts core impl checks anyway + const newRecipeUserId = signUpResp.user.loginMethods[0].recipeUserId; + + const tokenResp = await EmailVerification.createEmailVerificationToken( + session.getTenantId(), + newRecipeUserId, + loginMethod.email + ); + if (tokenResp.status === "OK") { + await EmailVerification.verifyEmailUsingToken(session.getTenantId(), tokenResp.token, false); + } + + const linkResp = await AccountLinking.linkAccounts(newRecipeUserId, session.getUserId()); + if (linkResp.status !== "OK") { + return res.json({ + status: "GENERAL_ERROR", + message: linkResp.status, // TODO: proper string + }); + } + // if the access token payload contains any information that'd change based on the new account, we'd want to update it here. + + return res.json({ + status: "OK", + user: linkResp.user, + }); +}); + +app.post("/addThirdPartyUser", verifySession(), async (req: SessionRequest, res) => { + // We need this because several functions below require it + const userContext = {}; + const session = req.session!; + const user = await getUser(session.getRecipeUserId().getAsString()); + if (!user) { + throw new Session.Error({ type: Session.Error.UNAUTHORISED, message: "user removed" }); + } + const loginMethod = user.loginMethods.find( + (m) => m.recipeUserId.getAsString() === session.getRecipeUserId().getAsString() + ); + if (!loginMethod) { + throw new Error("This should never happen"); + } + + if (!loginMethod.verified) { + return res.json({ + status: "GENERAL_ERROR", + message: "You can only add a password when logged in using a verified account", + }); + } + + const provider = await ThirdPartyEmailPassword.thirdPartyGetProvider( + session.getTenantId(), + req.body.thirdPartyId, + req.body.clientType + ); + if (provider === undefined) { + return res.json({ + status: "GENERAL_ERROR", + message: "Unknown thirdparty provider id/client type", + }); + } + + let oAuthTokensToUse; + if ("redirectURIInfo" in req.body && req.body.redirectURIInfo !== undefined) { + oAuthTokensToUse = await provider.exchangeAuthCodeForOAuthTokens({ + redirectURIInfo: req.body.redirectURIInfo, + userContext, + }); + } else if ("oAuthTokens" in req.body && req.body.oAuthTokens !== undefined) { + oAuthTokensToUse = req.body.oAuthTokens; + } else { + throw Error("should never come here"); + } + const tpUserInfo = await provider.getUserInfo({ oAuthTokens: oAuthTokensToUse, userContext }); + let emailInfo = tpUserInfo.email; + if (emailInfo === undefined) { + return res.json({ + status: "NO_EMAIL_GIVEN_BY_PROVIDER", + }); + } + + if (!user.emails.includes(emailInfo.id) && !emailInfo.isVerified) { + return res.json({ + status: "GENERAL_ERROR", + message: "The email of the third-party account doesn't match the current user and is not verified", + }); + } + const signUpResp = await ThirdPartyEmailPassword.thirdPartyManuallyCreateOrUpdateUser( + session.getTenantId(), + req.body.thirdPartyId, + tpUserInfo.thirdPartyUserId, + emailInfo.id, + emailInfo.isVerified, + { doNotLink: true } + ); + + if (signUpResp.status !== "OK") { + return res.json(signUpResp); + } + + if (!signUpResp.createdNewRecipeUser) { + return res.json({ + status: "GENERAL_ERROR", + message: "This user has already signed up. Please delete it first.", + }); + } + // Here we can assume the user in signUpResp is not a primary user since it was just created + // Plus the linkAccounts core impl checks anyway + const newRecipeUserId = signUpResp.user.loginMethods[0].recipeUserId; + + const tokenResp = await EmailVerification.createEmailVerificationToken( + session.getTenantId(), + newRecipeUserId, + loginMethod.email + ); + if (tokenResp.status === "OK") { + await EmailVerification.verifyEmailUsingToken(session.getTenantId(), tokenResp.token, false); + } + + const linkResp = await AccountLinking.linkAccounts(newRecipeUserId, session.getUserId()); + if (linkResp.status !== "OK") { + return res.json({ + status: "GENERAL_ERROR", + message: linkResp.status, // TODO: proper string + }); + } + // if the access token payload contains any information that'd change based on the new account, we'd want to update it here. + + return res.json({ + status: "OK", + user: linkResp.user, + }); +}); + +app.post("/addPhoneNumber", verifySession(), async (req: SessionRequest, res) => { + const session = req.session!; + const user = await getUser(session.getRecipeUserId().getAsString()); + if (!user) { + throw new Session.Error({ type: Session.Error.UNAUTHORISED, message: "user removed" }); + } + const loginMethod = user.loginMethods.find( + (m) => m.recipeUserId.getAsString() === session.getRecipeUserId().getAsString() + ); + if (!loginMethod) { + throw new Error("This should never happen"); + } + + if (!loginMethod.verified) { + return res.json({ + status: "GENERAL_ERROR", + message: "You can only add a phone number when logged in using a verified account", + }); + } + + const phoneNumber = req.body.phoneNumber; + + const otherUsers = await listUsersByAccountInfo("public", { phoneNumber }); + if (otherUsers.length > 0) { + return res.json({ + status: "GENERAL_ERROR", + message: "You can only add a phone number to a single user", + }); + } + + const signUpResp = await Passwordless.signInUp({ + tenantId: session.getTenantId(), + phoneNumber, + userContext: { doNotLink: true }, + }); + + if (signUpResp.createdNewRecipeUser === false) { + // This is possible only in a race-condition where 2 users are adding the same phone number. + return res.json({ + status: "GENERAL_ERROR", + message: "You can only add a phone number to a single user", + }); + } + const newRecipeUserId = signUpResp.user.loginMethods[0].recipeUserId; + + const linkResp = await AccountLinking.linkAccounts(newRecipeUserId, session.getUserId()); + if (linkResp.status !== "OK") { + return res.json({ + status: "GENERAL_ERROR", + message: linkResp.status, // TODO: proper string + }); + } + // if the access token payload contains any information that'd change based on the new account, we'd want to update it here. + + return res.json({ + status: "OK", + user: linkResp.user, + }); +}); + +// In case of session related errors, this error handler +// returns 401 to the client. +app.use(errorHandler()); + +app.listen(3001, () => console.log(`API Server listening on port 3001`)); diff --git a/examples/with-account-linking/backend/package.json b/examples/with-account-linking/backend/package.json new file mode 100644 index 000000000..59a4adeee --- /dev/null +++ b/examples/with-account-linking/backend/package.json @@ -0,0 +1,30 @@ +{ + "name": "supertokens-node", + "version": "0.0.1", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "start": "npx ts-node-dev --project ./tsconfig.json ./index.ts" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.1", + "helmet": "^5.1.0", + "morgan": "^1.10.0", + "npm-run-all": "^4.1.5", + "supertokens-node": "github:supertokens/supertokens-node#account-linking", + "ts-node-dev": "^2.0.0", + "typescript": "^4.7.2" + }, + "devDependencies": { + "@types/cors": "^2.8.12", + "@types/express": "^4.17.17", + "@types/morgan": "^1.9.3", + "@types/node": "^16.11.38", + "nodemon": "^2.0.16" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/examples/with-account-linking/backend/tsconfig.json b/examples/with-account-linking/backend/tsconfig.json new file mode 100644 index 000000000..8a91acaae --- /dev/null +++ b/examples/with-account-linking/backend/tsconfig.json @@ -0,0 +1,62 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/examples/with-account-linking/frontend/.env b/examples/with-account-linking/frontend/.env new file mode 100644 index 000000000..7d910f148 --- /dev/null +++ b/examples/with-account-linking/frontend/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true \ No newline at end of file diff --git a/examples/with-account-linking/frontend/.gitignore b/examples/with-account-linking/frontend/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/examples/with-account-linking/frontend/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/with-account-linking/frontend/LICENSE.md b/examples/with-account-linking/frontend/LICENSE.md new file mode 100644 index 000000000..588f27e68 --- /dev/null +++ b/examples/with-account-linking/frontend/LICENSE.md @@ -0,0 +1,192 @@ +Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved. + +This software is licensed under the Apache License, Version 2.0 (the +"License") as published by the Apache Software Foundation. + +You may not use this software except in compliance with the License. A copy +of the License is available below the line. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/examples/with-account-linking/frontend/package.json b/examples/with-account-linking/frontend/package.json new file mode 100644 index 000000000..cd6e6e145 --- /dev/null +++ b/examples/with-account-linking/frontend/package.json @@ -0,0 +1,46 @@ +{ + "name": "supertokens-react", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.11.56", + "@types/react": "^18.0.18", + "@types/react-dom": "^18.0.6", + "axios": "^0.21.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.2.1", + "react-scripts": "5.0.1", + "supertokens-auth-react": "github:supertokens/supertokens-auth-react#feat/account-linking", + "typescript": "^4.8.2", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/with-account-linking/frontend/public/favicon.ico b/examples/with-account-linking/frontend/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/with-account-linking/frontend/public/favicon.ico differ diff --git a/examples/with-account-linking/frontend/public/index.html b/examples/with-account-linking/frontend/public/index.html new file mode 100644 index 000000000..6f1f7cb51 --- /dev/null +++ b/examples/with-account-linking/frontend/public/index.html @@ -0,0 +1,40 @@ + + +
+ + + + + + + + + + +