diff --git a/examples/with-account-linking/frontend/src/LinkingPage/index.tsx b/examples/with-account-linking/frontend/src/LinkingPage/index.tsx index 1e116dd3f..123fb3c73 100644 --- a/examples/with-account-linking/frontend/src/LinkingPage/index.tsx +++ b/examples/with-account-linking/frontend/src/LinkingPage/index.tsx @@ -34,6 +34,7 @@ export const LinkingPage: React.FC = () => { { id: "email", value: userInfo.user.emails[0] }, { id: "password", value: password }, ], + shouldTryLinkingWithSessionUser: true, }); if (resp.status !== "OK") { @@ -54,6 +55,7 @@ export const LinkingPage: React.FC = () => { try { let response = await Passwordless.createCode({ phoneNumber, + shouldTryLinkingWithSessionUser: true, }); if (cancel) { diff --git a/examples/with-multifactorauth-phone-chooser/frontend/package.json b/examples/with-multifactorauth-phone-chooser/frontend/package.json index 9bd476ef3..009f61f7b 100644 --- a/examples/with-multifactorauth-phone-chooser/frontend/package.json +++ b/examples/with-multifactorauth-phone-chooser/frontend/package.json @@ -15,7 +15,7 @@ "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/mfa_redirect", + "supertokens-auth-react": "latest", "supertokens-web-js": "latest", "typescript": "^4.8.2", "web-vitals": "^2.1.4" diff --git a/examples/with-multifactorauth-phone-chooser/frontend/src/SelectPhone/index.tsx b/examples/with-multifactorauth-phone-chooser/frontend/src/SelectPhone/index.tsx index 7feaec211..5aa656753 100644 --- a/examples/with-multifactorauth-phone-chooser/frontend/src/SelectPhone/index.tsx +++ b/examples/with-multifactorauth-phone-chooser/frontend/src/SelectPhone/index.tsx @@ -60,7 +60,7 @@ export default function SelectPhone() {
  • { - Passwordless.createCode({ phoneNumber: number }) + Passwordless.createCode({ phoneNumber: number, shouldTryLinkingWithSessionUser: true }) .then(async (info) => { if (info.status !== "OK") { setError(info.reason); diff --git a/examples/with-multiple-email-sign-in/api-server/epOverride.ts b/examples/with-multiple-email-sign-in/api-server/epOverride.ts index b37bd0db3..13c151fa1 100644 --- a/examples/with-multiple-email-sign-in/api-server/epOverride.ts +++ b/examples/with-multiple-email-sign-in/api-server/epOverride.ts @@ -7,7 +7,7 @@ export function epOverride(oI: APIInterface): APIInterface { signInPOST: async function (input) { const emailField = input.formFields.find((f) => f.id === "email")!; - let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value); + let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value as string); if (primaryEmail !== undefined) { emailField.value = primaryEmail; } @@ -16,7 +16,7 @@ export function epOverride(oI: APIInterface): APIInterface { signUpPOST: async function (input) { const emailField = input.formFields.find((f) => f.id === "email")!; - let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value); + let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value as string); if (primaryEmail !== undefined) { emailField.value = primaryEmail; } diff --git a/examples/with-phone-password-mfa/api-server/index.ts b/examples/with-phone-password-mfa/api-server/index.ts index 213df7df6..61d3aa417 100644 --- a/examples/with-phone-password-mfa/api-server/index.ts +++ b/examples/with-phone-password-mfa/api-server/index.ts @@ -77,7 +77,7 @@ supertokens.init({ // We format the phone number here to get it to a standard format const emailField = input.formFields.find((field) => field.id === "email"); if (emailField) { - const phoneNumber = parsePhoneNumber(emailField.value); + const phoneNumber = parsePhoneNumber(emailField.value as string); if (phoneNumber !== undefined && phoneNumber.isValid()) { emailField.value = phoneNumber.number; } @@ -93,7 +93,7 @@ supertokens.init({ // We format the phone number here to get it to a standard format const emailField = input.formFields.find((field) => field.id === "email"); if (emailField) { - const phoneNumber = parsePhoneNumber(emailField.value); + const phoneNumber = parsePhoneNumber(emailField.value as string); if (phoneNumber !== undefined && phoneNumber.isValid()) { emailField.value = phoneNumber.number; } diff --git a/examples/with-phone-password/api-server/index.ts b/examples/with-phone-password/api-server/index.ts index 6a697598a..e7b6c7e0b 100644 --- a/examples/with-phone-password/api-server/index.ts +++ b/examples/with-phone-password/api-server/index.ts @@ -75,7 +75,7 @@ supertokens.init({ // We format the phone number here to get it to a standard format const emailField = input.formFields.find((field) => field.id === "email"); if (emailField) { - const phoneNumber = parsePhoneNumber(emailField.value); + const phoneNumber = parsePhoneNumber(emailField.value as string); if (phoneNumber !== undefined && phoneNumber.isValid()) { emailField.value = phoneNumber.number; } @@ -91,7 +91,7 @@ supertokens.init({ // We format the phone number here to get it to a standard format const emailField = input.formFields.find((field) => field.id === "email"); if (emailField) { - const phoneNumber = parsePhoneNumber(emailField.value); + const phoneNumber = parsePhoneNumber(emailField.value as string); if (phoneNumber !== undefined && phoneNumber.isValid()) { emailField.value = phoneNumber.number; } diff --git a/examples/with-phone-password/src/PhoneVerification/index.tsx b/examples/with-phone-password/src/PhoneVerification/index.tsx index 00d045561..e8e90b82c 100644 --- a/examples/with-phone-password/src/PhoneVerification/index.tsx +++ b/examples/with-phone-password/src/PhoneVerification/index.tsx @@ -37,6 +37,7 @@ const CustomSignInUpTheme = (props: AuthPageThemeProps) => { const res = await Passwordless.createCode({ phoneNumber, + shouldTryLinkingWithSessionUser: true, userContext: { additionalAttemptInfo }, }); diff --git a/examples/with-unified-login/.gitignore b/examples/with-unified-login/.gitignore new file mode 100644 index 000000000..c6bba5913 --- /dev/null +++ b/examples/with-unified-login/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/examples/with-unified-login/auth-provider/backend/config.ts b/examples/with-unified-login/auth-provider/backend/config.ts new file mode 100644 index 000000000..b910d0bcb --- /dev/null +++ b/examples/with-unified-login/auth-provider/backend/config.ts @@ -0,0 +1,184 @@ +import Multitenancy from "supertokens-node/recipe/multitenancy"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import Session from "supertokens-node/recipe/session"; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; +import { TypeInput } from "supertokens-node/types"; +import Dashboard from "supertokens-node/recipe/dashboard"; +import UserRoles from "supertokens-node/recipe/userroles"; +import { readFile, writeFile } from "fs/promises"; + +export const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3000; +export const apiPort = process.env.REACT_APP_API_PORT || 3001; + +export function getApiDomain() { + const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`; + return apiUrl; +} + +export function getWebsiteDomain() { + 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: [ + Multitenancy.init(), + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + 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", + }, + }, + ], + }, + }, + { + config: { + thirdPartyId: "twitter", + clients: [ + { + clientId: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", + clientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", + }, + ], + }, + }, + ], + }, + }), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + getCustomUserInputCode: async (userContext) => { + return "123456"; + }, + emailDelivery: { + override: (oI) => ({ + ...oI, + sendEmail: async (input) => { + console.log(input); + }, + }), + }, + smsDelivery: { + override: (oI) => ({ + ...oI, + sendSms: async (input) => { + console.log(input); + }, + }), + }, + }), + Session.init(), + Dashboard.init(), + UserRoles.init(), + OAuth2Provider.init(), + ], +}; + +let clients: Record = {}; + +export async function setupTenants() { + try { + const data = await readFile("../clients.json", "utf8"); + clients = JSON.parse(data); + } catch (e) { + console.error(e); + } + + if (Object.keys(clients).length === 0) { + const client1 = await OAuth2Provider.createOAuth2Client({ + redirectUris: ["http://localhost:3011/auth/callback"], + tokenEndpointAuthMethod: "client_secret_post", + }); + if (client1.status !== "OK") { + throw new Error("Failed to create client1"); + } + clients["tenant1"] = { + clientId: client1.client.clientId, + clientSecret: client1.client.clientSecret, + }; + + const client2 = await OAuth2Provider.createOAuth2Client({ + redirectUris: ["http://localhost:3012/auth/callback"], + postLogoutRedirectUris: ["http://localhost:3012/loggedout"], + policyUri: "http://localhost:3012/policy", + clientName: "Tenant 2", + clientUri: "http://localhost:3012", + logoUri: "https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg", + tokenEndpointAuthMethod: "none", + }); + if (client2.status !== "OK") { + throw new Error("Failed to create client2"); + } + clients["tenant2"] = { + clientId: client2.client.clientId, + clientSecret: client2.client.clientSecret, + }; + + const client3 = await OAuth2Provider.createOAuth2Client({ + redirectUris: ["http://localhost:3013/oauth2/redirect"], + logoUri: "https://www.passportjs.org/images/logo.svg", + scope: "openid profile email phoneNumber test", + tokenEndpointAuthMethod: "client_secret_post", + }); + if (client3.status !== "OK") { + throw new Error("Failed to create client3"); + } + clients["tenant3"] = { + clientId: client3.client.clientId, + clientSecret: client3.client.clientSecret, + }; + + await writeFile("../clients.json", JSON.stringify(clients, null, 2)); + } +} diff --git a/examples/with-unified-login/auth-provider/backend/index.ts b/examples/with-unified-login/auth-provider/backend/index.ts new file mode 100644 index 000000000..5ba7459f2 --- /dev/null +++ b/examples/with-unified-login/auth-provider/backend/index.ts @@ -0,0 +1,63 @@ +import express from "express"; +import cors from "cors"; +import supertokens from "supertokens-node"; +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { middleware, errorHandler, SessionRequest } from "supertokens-node/framework/express"; +import { apiPort, getWebsiteDomain, setupTenants, SuperTokensConfig } from "./config"; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; + +supertokens.init(SuperTokensConfig); + +const app = express(); + +app.use( + cors({ + origin: [getWebsiteDomain(), "http://localhost:3011", "http://localhost:3012", "http://localhost:3013"], + 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()); + +// 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(), + }); +}); + +const testObj = { + name: "John Doe", + age: 30, + email: "john.doe@client.localhost", +}; + +app.get("/test", async (req, res) => { + let you: any = "not authorized"; + if (req.headers.authorization) { + const token = await OAuth2Provider.validateOAuth2AccessToken(req.headers.authorization.split(" ")[1]); + you = token.payload; + } + + res.send({ + resp: testObj[req.query.prop as keyof typeof testObj] ?? "no query?", + you, + }); +}); + +// In case of session related errors, this error handler +// returns 401 to the client. +app.use(errorHandler()); + +app.listen(3001, async () => { + console.log("Setting up tenants"); + await setupTenants(); + console.log("Tenants setup complete"); + console.log(`API Server listening on port ${apiPort}`); +}); diff --git a/examples/with-unified-login/auth-provider/backend/package.json b/examples/with-unified-login/auth-provider/backend/package.json new file mode 100644 index 000000000..3d8ff3319 --- /dev/null +++ b/examples/with-unified-login/auth-provider/backend/package.json @@ -0,0 +1,30 @@ +{ + "name": "supertokens-node", + "version": "0.0.1", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "start": "NODE_OPTIONS=--use-openssl-ca 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": "latest", + "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-unified-login/auth-provider/backend/tsconfig.json b/examples/with-unified-login/auth-provider/backend/tsconfig.json new file mode 100644 index 000000000..8a91acaae --- /dev/null +++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/clients.json b/examples/with-unified-login/auth-provider/clients.json new file mode 100644 index 000000000..b686efb46 --- /dev/null +++ b/examples/with-unified-login/auth-provider/clients.json @@ -0,0 +1,13 @@ +{ + "tenant1": { + "clientId": "stcl_f1412d57-29ab-43c1-bb67-29b701a8d098", + "clientSecret": "pctjhrEzpi6q46dZ~q1OaTR3.r" + }, + "tenant2": { + "clientId": "stcl_944d226e-cde4-4edf-b625-487c4b02e79e" + }, + "tenant3": { + "clientId": "stcl_2ba3b96c-07a8-4c3e-8b26-0dd94ee1f30f", + "clientSecret": "DNZIl9sULJqsf_r9HDpuutf1Vq" + } +} diff --git a/examples/with-unified-login/auth-provider/frontend/.gitignore b/examples/with-unified-login/auth-provider/frontend/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/frontend/LICENSE.md b/examples/with-unified-login/auth-provider/frontend/LICENSE.md new file mode 100644 index 000000000..588f27e68 --- /dev/null +++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/frontend/package.json b/examples/with-unified-login/auth-provider/frontend/package.json new file mode 100644 index 000000000..a1d078db2 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/package.json @@ -0,0 +1,47 @@ +{ + "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": "latest", + "supertokens-web-js": "latest", + "typescript": "^4.8.2", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "BROWSER=none PORT=3000 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-unified-login/auth-provider/frontend/public/favicon.ico b/examples/with-unified-login/auth-provider/frontend/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/with-unified-login/auth-provider/frontend/public/favicon.ico differ diff --git a/examples/with-unified-login/auth-provider/frontend/public/index.html b/examples/with-unified-login/auth-provider/frontend/public/index.html new file mode 100644 index 000000000..6f1f7cb51 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + React App + + + +
    + + diff --git a/examples/with-unified-login/auth-provider/frontend/public/manifest.json b/examples/with-unified-login/auth-provider/frontend/public/manifest.json new file mode 100644 index 000000000..f01493ff0 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/with-unified-login/auth-provider/frontend/public/robots.txt b/examples/with-unified-login/auth-provider/frontend/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/with-unified-login/auth-provider/frontend/src/App.css b/examples/with-unified-login/auth-provider/frontend/src/App.css new file mode 100644 index 000000000..8a98a2341 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/App.css @@ -0,0 +1,27 @@ +.App { + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + font-family: Rubik; +} + +.fill { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; +} + +.sessionButton { + padding-left: 13px; + padding-right: 13px; + padding-top: 8px; + padding-bottom: 8px; + background-color: black; + border-radius: 10px; + cursor: pointer; + color: white; + font-weight: bold; + font-size: 17px; +} diff --git a/examples/with-unified-login/auth-provider/frontend/src/App.tsx b/examples/with-unified-login/auth-provider/frontend/src/App.tsx new file mode 100644 index 000000000..9462ebcdf --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/App.tsx @@ -0,0 +1,40 @@ +import "./App.css"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import { Routes, BrowserRouter as Router, Route } from "react-router-dom"; +import Home from "./Home"; +import { PreBuiltUIList, SuperTokensConfig } from "./config"; + +SuperTokens.init(SuperTokensConfig); + +function App() { + return ( + +
    + +
    + + {/* This shows the login UI on "/auth" route */} + {getSuperTokensRoutesForReactRouterDom(require("react-router-dom"), PreBuiltUIList)} + + only if the user is logged in. + Else it redirects the user to "/auth" */ + + + + } + /> + +
    +
    +
    +
    + ); +} + +export default App; diff --git a/examples/with-unified-login/auth-provider/frontend/src/Home/CallAPIView.tsx b/examples/with-unified-login/auth-provider/frontend/src/Home/CallAPIView.tsx new file mode 100644 index 000000000..6a9d510d4 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/Home/CallAPIView.tsx @@ -0,0 +1,15 @@ +import axios from "axios"; +import { getApiDomain } from "../config"; + +export default function CallAPIView() { + async function callAPIClicked() { + let response = await axios.get(getApiDomain() + "/sessioninfo"); + window.alert("Session Information:\n" + JSON.stringify(response.data, null, 2)); + } + + return ( +
    + Call API +
    + ); +} diff --git a/examples/with-unified-login/auth-provider/frontend/src/Home/Home.css b/examples/with-unified-login/auth-provider/frontend/src/Home/Home.css new file mode 100644 index 000000000..a056cb2eb --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/Home/Home.css @@ -0,0 +1,189 @@ +@font-face { + font-family: Menlo; + src: url("../assets/fonts/MenloRegular.ttf"); +} + +.app-container { + font-family: Rubik, sans-serif; +} + +.app-container * { + box-sizing: border-box; +} + +.bold-400 { + font-variation-settings: "wght" 400; +} + +.bold-500 { + font-variation-settings: "wght" 500; +} + +.bold-600 { + font-variation-settings: "wght" 600; +} + +#home-container { + align-items: center; + min-height: 100vh; + background: url("../assets/images/background.png"); + background-size: cover; +} + +.bold-700 { + font-variation-settings: "wght" 700; +} + +.app-container .main-container { + box-shadow: 0px 0px 60px 0px rgba(0, 0, 0, 0.16); + width: min(635px, calc(100% - 24px)); + border-radius: 16px; + margin-block-end: 159px; + background-color: #ffffff; +} + +.main-container .success-title { + line-height: 1; + padding-block: 26px; + background-color: #e7ffed; + text-align: center; + color: #3eb655; + display: flex; + justify-content: center; + align-items: flex-end; + font-size: 20px; +} + +.success-title img.success-icon { + margin-right: 8px; +} + +.main-container .inner-content { + padding-block: 48px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.inner-content #user-id { + position: relative; + padding: 14px 17px; + border-image-slice: 1; + width: min(430px, calc(100% - 30px)); + margin-inline: auto; + margin-block: 11px 23px; + border-radius: 9px; + line-height: 1; + font-family: Menlo, serif; + cursor: text; +} + +.inner-content #user-id:before { + content: ""; + position: absolute; + inset: 0; + border-radius: 9px; + padding: 2px; + background: linear-gradient(90.31deg, #ff9933 0.11%, #ff3f33 99.82%); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask-composite: exclude; + -webkit-mask-composite: xor; +} + +.main-container > .top-band, +.main-container > .bottom-band { + border-radius: inherit; +} + +.main-container .top-band { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.main-container .bottom-band { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.main-container .sessionButton { + box-sizing: border-box; + background: #ff9933; + border: 1px solid #ff8a15; + box-shadow: 0px 3px 6px rgba(255, 153, 51, 0.16); + border-radius: 6px; + font-size: 16px; +} + +.bottom-cta-container { + display: flex; + justify-content: flex-end; + padding-inline: 21px; + background-color: #212d4f; +} + +.bottom-cta-container .view-code { + padding-block: 11px; + color: #bac9f5; + cursor: pointer; + font-size: 14px; +} + +.bottom-links-container { + display: grid; + grid-template-columns: repeat(4, auto); + margin-bottom: 22px; +} + +.bottom-links-container .link { + display: flex; + align-items: center; + margin-inline-end: 68px; + cursor: pointer; +} + +.bottom-links-container .link:last-child { + margin-right: 0; +} + +.truncate { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.separator-line { + max-width: 100%; +} + +.link .link-icon { + width: 15px; + margin-right: 5px; +} + +@media screen and (max-width: 768px) { + .bottom-links-container { + grid-template-columns: repeat(2, 1fr); + column-gap: 64px; + row-gap: 34px; + } + + .bottom-links-container .link { + margin-inline-end: 0; + } + + .separator-line { + max-width: 200px; + } +} + +@media screen and (max-width: 480px) { + #home-container { + justify-content: start; + padding-block-start: 25px; + } + + .app-container .main-container { + margin-block-end: 90px; + } +} diff --git a/examples/with-unified-login/auth-provider/frontend/src/Home/SuccessView.tsx b/examples/with-unified-login/auth-provider/frontend/src/Home/SuccessView.tsx new file mode 100644 index 000000000..bea4566ef --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/Home/SuccessView.tsx @@ -0,0 +1,72 @@ +import { useNavigate } from "react-router-dom"; +import { signOut } from "supertokens-auth-react/recipe/session"; +import { recipeDetails } from "../config"; +import CallAPIView from "./CallAPIView"; +import { BlogsIcon, CelebrateIcon, GuideIcon, SeparatorLine, SignOutIcon } from "../assets/images"; + +interface ILink { + name: string; + onClick: () => void; + icon: string; +} + +export default function SuccessView(props: { userId: string }) { + let userId = props.userId; + + const navigate = useNavigate(); + + async function logoutClicked() { + await signOut(); + navigate("/auth"); + } + + function openLink(url: string) { + window.open(url, "_blank"); + } + + const links: ILink[] = [ + { + name: "Blogs", + onClick: () => openLink("https://supertokens.com/blog"), + icon: BlogsIcon, + }, + { + name: "Documentation", + onClick: () => openLink(recipeDetails.docsLink), + icon: GuideIcon, + }, + { + name: "Sign Out", + onClick: logoutClicked, + icon: SignOutIcon, + }, + ]; + + return ( + <> +
    +
    + Login successful Login successful +
    +
    +
    Your userID is:
    +
    + {userId} +
    + +
    +
    +
    + {links.map((link) => ( +
    + {link.name} +
    + {link.name} +
    +
    + ))} +
    + separator + + ); +} diff --git a/examples/with-unified-login/auth-provider/frontend/src/Home/index.tsx b/examples/with-unified-login/auth-provider/frontend/src/Home/index.tsx new file mode 100644 index 000000000..0c3f288e8 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/Home/index.tsx @@ -0,0 +1,17 @@ +import SuccessView from "./SuccessView"; +import { useSessionContext } from "supertokens-auth-react/recipe/session"; +import "./Home.css"; + +export default function Home() { + const sessionContext = useSessionContext(); + + if (sessionContext.loading === true) { + return null; + } + + return ( +
    + +
    + ); +} diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/fonts/MenloRegular.ttf b/examples/with-unified-login/auth-provider/frontend/src/assets/fonts/MenloRegular.ttf new file mode 100644 index 000000000..033dc6d21 Binary files /dev/null and b/examples/with-unified-login/auth-provider/frontend/src/assets/fonts/MenloRegular.ttf differ diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/arrow-right-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/arrow-right-icon.svg new file mode 100644 index 000000000..95aa1fec6 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/arrow-right-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/background.png b/examples/with-unified-login/auth-provider/frontend/src/assets/images/background.png new file mode 100644 index 000000000..2147c15c2 Binary files /dev/null and b/examples/with-unified-login/auth-provider/frontend/src/assets/images/background.png differ diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/blogs-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/blogs-icon.svg new file mode 100644 index 000000000..a2fc9dd62 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/blogs-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/celebrate-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/celebrate-icon.svg new file mode 100644 index 000000000..3b40b1efa --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/celebrate-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/guide-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/guide-icon.svg new file mode 100644 index 000000000..bd85af72b --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/guide-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/index.ts b/examples/with-unified-login/auth-provider/frontend/src/assets/images/index.ts new file mode 100644 index 000000000..7adf036c4 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/index.ts @@ -0,0 +1,8 @@ +import SeparatorLine from "./separator-line.svg"; +import ArrowRight from "./arrow-right-icon.svg"; +import SignOutIcon from "./sign-out-icon.svg"; +import GuideIcon from "./guide-icon.svg"; +import BlogsIcon from "./blogs-icon.svg"; +import CelebrateIcon from "./celebrate-icon.svg"; + +export { SeparatorLine, ArrowRight, SignOutIcon, GuideIcon, BlogsIcon, CelebrateIcon }; diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/separator-line.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/separator-line.svg new file mode 100644 index 000000000..7127a00dc --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/separator-line.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/sign-out-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/sign-out-icon.svg new file mode 100644 index 000000000..6cc4f85fd --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/sign-out-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-unified-login/auth-provider/frontend/src/config.tsx b/examples/with-unified-login/auth-provider/frontend/src/config.tsx new file mode 100644 index 000000000..fe9ba012e --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/config.tsx @@ -0,0 +1,64 @@ +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; +import MultiTenancy from "supertokens-auth-react/recipe/multitenancy"; +import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; +import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; +import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; +import Session from "supertokens-auth-react/recipe/session"; +import React from "react"; + +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 = { + usesDynamicLoginMethods: true, + 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: [ + EmailPassword.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + ThirdParty.Github.init(), + ThirdParty.Google.init(), + ThirdParty.Apple.init(), + ThirdParty.Twitter.init(), + ], + }, + }), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + }), + Session.init(), + OAuth2Provider.init() as any, + MultiTenancy.init(), + ], +}; + +export const recipeDetails = { + docsLink: "https://supertokens.com/docs/thirdpartypasswordless/introduction", +}; + +export const PreBuiltUIList = [ + EmailPasswordPreBuiltUI, + ThirdPartyPreBuiltUI, + PasswordlessPreBuiltUI, + OAuth2ProviderPreBuiltUI as any, +]; diff --git a/examples/with-unified-login/auth-provider/frontend/src/index.css b/examples/with-unified-login/auth-provider/frontend/src/index.css new file mode 100644 index 000000000..04146b5e7 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} diff --git a/examples/with-unified-login/auth-provider/frontend/src/index.tsx b/examples/with-unified-login/auth-provider/frontend/src/index.tsx new file mode 100644 index 000000000..399c737cd --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/index.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); +root.render( + + + +); diff --git a/examples/with-unified-login/auth-provider/frontend/src/react-app-env.d.ts b/examples/with-unified-login/auth-provider/frontend/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/with-unified-login/auth-provider/frontend/tsconfig.json b/examples/with-unified-login/auth-provider/frontend/tsconfig.json new file mode 100644 index 000000000..c0555cbc6 --- /dev/null +++ b/examples/with-unified-login/auth-provider/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/with-unified-login/auth-provider/package.json b/examples/with-unified-login/auth-provider/package.json new file mode 100644 index 000000000..36e11d945 --- /dev/null +++ b/examples/with-unified-login/auth-provider/package.json @@ -0,0 +1,20 @@ +{ + "name": "auth-provider", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "start:frontend": "cd frontend && npm run start", + "start:frontend-live-demo-app": "cd frontend && npx serve -s build", + "start:backend": "cd backend && npm run start", + "start:backend-live-demo-app": "cd backend && ./startLiveDemoApp.sh", + "start": "npm-run-all --parallel start:frontend start:backend", + "start-live-demo-app": "npx npm-run-all --parallel start:frontend-live-demo-app start:backend-live-demo-app" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "npm-run-all": "^4.1.5" + } +} diff --git a/examples/with-unified-login/client1/.gitignore b/examples/with-unified-login/client1/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/examples/with-unified-login/client1/.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-unified-login/client1/LICENSE.md b/examples/with-unified-login/client1/LICENSE.md new file mode 100644 index 000000000..588f27e68 --- /dev/null +++ b/examples/with-unified-login/client1/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-unified-login/client1/package.json b/examples/with-unified-login/client1/package.json new file mode 100644 index 000000000..ff4a4116a --- /dev/null +++ b/examples/with-unified-login/client1/package.json @@ -0,0 +1,47 @@ +{ + "name": "supertokens-react", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "BROWSER=none PORT=3010 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" + ] + }, + "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-oidc-context": "^3.1.1", + "react-router-dom": "^6.2.1", + "react-scripts": "5.0.1", + "supertokens-auth-react": "latest", + "typescript": "^4.8.2", + "web-vitals": "^2.1.4" + } +} diff --git a/examples/with-unified-login/client1/public/favicon.ico b/examples/with-unified-login/client1/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/with-unified-login/client1/public/favicon.ico differ diff --git a/examples/with-unified-login/client1/public/index.html b/examples/with-unified-login/client1/public/index.html new file mode 100644 index 000000000..6f1f7cb51 --- /dev/null +++ b/examples/with-unified-login/client1/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + React App + + + +
    + + diff --git a/examples/with-unified-login/client1/public/manifest.json b/examples/with-unified-login/client1/public/manifest.json new file mode 100644 index 000000000..f01493ff0 --- /dev/null +++ b/examples/with-unified-login/client1/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/with-unified-login/client1/public/robots.txt b/examples/with-unified-login/client1/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/with-unified-login/client1/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/with-unified-login/client1/src/App.css b/examples/with-unified-login/client1/src/App.css new file mode 100644 index 000000000..b9ee98c15 --- /dev/null +++ b/examples/with-unified-login/client1/src/App.css @@ -0,0 +1,34 @@ +.App { + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + font-family: Rubik; +} + +button, +.sessionButton { + padding-left: 13px; + padding-right: 13px; + padding-top: 8px; + padding-bottom: 8px; + background-color: black; + border-radius: 10px; + cursor: pointer; + color: white; + font-weight: bold; + font-size: 17px; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; +} + +.fill { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; +} diff --git a/examples/with-unified-login/client1/src/App.tsx b/examples/with-unified-login/client1/src/App.tsx new file mode 100644 index 000000000..b3fec4494 --- /dev/null +++ b/examples/with-unified-login/client1/src/App.tsx @@ -0,0 +1,144 @@ +import "./App.css"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { SessionAuth, doesSessionExist } from "supertokens-auth-react/recipe/session"; +import { Routes, BrowserRouter as Router, Route, useNavigate } from "react-router-dom"; +import Home from "./Home"; +import { SuperTokensConfig, ComponentWrapper, getApiDomain, getWebsiteDomain } from "./config"; +import { useEffect, useRef } from "react"; +import { AuthProvider, AuthProviderProps, useAuth } from "react-oidc-context"; +import clients from "./clients.json"; +import "./App.css"; + +SuperTokens.init(SuperTokensConfig); + +const tenantId = "tenant1"; +const clientId = clients[tenantId].clientId; + +const redirectURI = `${getWebsiteDomain()}/auth/callback`; +const oidcConfig: AuthProviderProps = { + client_id: clientId, + authority: `${getApiDomain()}/auth`, + response_type: "code", + redirect_uri: redirectURI, + scope: "openid offline_access", + skipSigninCallback: true, + extraQueryParams: { + tenant_id: tenantId, + }, +}; + +function AuthPage() { + const navigate = useNavigate(); + const authCallbackHandled = useRef(false); + const { signinRedirect, signoutSilent, isLoading, user, error } = useAuth(); + + useEffect(() => { + // Redirect to home if supertokens session exists + doesSessionExist().then((sessionExists) => { + if (sessionExists) { + navigate("/"); + } + }); + }, []); + + useEffect(() => { + if (authCallbackHandled.current) return; + + const params = new URLSearchParams(window.location.search); + if (params.get("code") === null || params.get("state") === null) return; + + authCallbackHandled.current = true; + + async function handleSign() { + const state = params.get("state"); + const savedInfo = localStorage.getItem(`oidc.${state}`); + if (!savedInfo) return; + const { id, code_verifier } = JSON.parse(savedInfo); + if (id !== state) return; + + const res = await fetch(`${getApiDomain()}/auth/oauth/client/signin`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + clientId, + redirectURIInfo: { + redirectURI: redirectURI, + redirectURIQueryParams: Object.fromEntries(params.entries()), + pkceCodeVerifier: code_verifier, + }, + }), + }); + if (res.ok) { + const data = await res.json(); + if (data.status === "OK") { + navigate("/"); + } else { + // setIsLoading(false); + window.alert("Login Failed "); + } + } + } + + handleSign(); + }, [user, navigate, signoutSilent]); + + if (isLoading || user) { + return ( +
    +

    Loading...

    +
    + ); + } + + return ( +
    +

    OAuth2 Example With Supertokens

    + {error &&

    {error.message}

    } +
    +
    + +
    +
    +
    + ); +} + +function App() { + return ( + + +
    + +
    + + + + + } + /> + only if the user is logged in. + Else it redirects the user to "/auth" */ + + + + } + /> + +
    +
    +
    +
    +
    + ); +} + +export default App; diff --git a/examples/with-unified-login/client1/src/Home/CallAPIView.tsx b/examples/with-unified-login/client1/src/Home/CallAPIView.tsx new file mode 100644 index 000000000..6a9d510d4 --- /dev/null +++ b/examples/with-unified-login/client1/src/Home/CallAPIView.tsx @@ -0,0 +1,15 @@ +import axios from "axios"; +import { getApiDomain } from "../config"; + +export default function CallAPIView() { + async function callAPIClicked() { + let response = await axios.get(getApiDomain() + "/sessioninfo"); + window.alert("Session Information:\n" + JSON.stringify(response.data, null, 2)); + } + + return ( +
    + Call API +
    + ); +} diff --git a/examples/with-unified-login/client1/src/Home/Home.css b/examples/with-unified-login/client1/src/Home/Home.css new file mode 100644 index 000000000..a056cb2eb --- /dev/null +++ b/examples/with-unified-login/client1/src/Home/Home.css @@ -0,0 +1,189 @@ +@font-face { + font-family: Menlo; + src: url("../assets/fonts/MenloRegular.ttf"); +} + +.app-container { + font-family: Rubik, sans-serif; +} + +.app-container * { + box-sizing: border-box; +} + +.bold-400 { + font-variation-settings: "wght" 400; +} + +.bold-500 { + font-variation-settings: "wght" 500; +} + +.bold-600 { + font-variation-settings: "wght" 600; +} + +#home-container { + align-items: center; + min-height: 100vh; + background: url("../assets/images/background.png"); + background-size: cover; +} + +.bold-700 { + font-variation-settings: "wght" 700; +} + +.app-container .main-container { + box-shadow: 0px 0px 60px 0px rgba(0, 0, 0, 0.16); + width: min(635px, calc(100% - 24px)); + border-radius: 16px; + margin-block-end: 159px; + background-color: #ffffff; +} + +.main-container .success-title { + line-height: 1; + padding-block: 26px; + background-color: #e7ffed; + text-align: center; + color: #3eb655; + display: flex; + justify-content: center; + align-items: flex-end; + font-size: 20px; +} + +.success-title img.success-icon { + margin-right: 8px; +} + +.main-container .inner-content { + padding-block: 48px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.inner-content #user-id { + position: relative; + padding: 14px 17px; + border-image-slice: 1; + width: min(430px, calc(100% - 30px)); + margin-inline: auto; + margin-block: 11px 23px; + border-radius: 9px; + line-height: 1; + font-family: Menlo, serif; + cursor: text; +} + +.inner-content #user-id:before { + content: ""; + position: absolute; + inset: 0; + border-radius: 9px; + padding: 2px; + background: linear-gradient(90.31deg, #ff9933 0.11%, #ff3f33 99.82%); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask-composite: exclude; + -webkit-mask-composite: xor; +} + +.main-container > .top-band, +.main-container > .bottom-band { + border-radius: inherit; +} + +.main-container .top-band { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.main-container .bottom-band { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.main-container .sessionButton { + box-sizing: border-box; + background: #ff9933; + border: 1px solid #ff8a15; + box-shadow: 0px 3px 6px rgba(255, 153, 51, 0.16); + border-radius: 6px; + font-size: 16px; +} + +.bottom-cta-container { + display: flex; + justify-content: flex-end; + padding-inline: 21px; + background-color: #212d4f; +} + +.bottom-cta-container .view-code { + padding-block: 11px; + color: #bac9f5; + cursor: pointer; + font-size: 14px; +} + +.bottom-links-container { + display: grid; + grid-template-columns: repeat(4, auto); + margin-bottom: 22px; +} + +.bottom-links-container .link { + display: flex; + align-items: center; + margin-inline-end: 68px; + cursor: pointer; +} + +.bottom-links-container .link:last-child { + margin-right: 0; +} + +.truncate { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.separator-line { + max-width: 100%; +} + +.link .link-icon { + width: 15px; + margin-right: 5px; +} + +@media screen and (max-width: 768px) { + .bottom-links-container { + grid-template-columns: repeat(2, 1fr); + column-gap: 64px; + row-gap: 34px; + } + + .bottom-links-container .link { + margin-inline-end: 0; + } + + .separator-line { + max-width: 200px; + } +} + +@media screen and (max-width: 480px) { + #home-container { + justify-content: start; + padding-block-start: 25px; + } + + .app-container .main-container { + margin-block-end: 90px; + } +} diff --git a/examples/with-unified-login/client1/src/Home/SuccessView.tsx b/examples/with-unified-login/client1/src/Home/SuccessView.tsx new file mode 100644 index 000000000..bea4566ef --- /dev/null +++ b/examples/with-unified-login/client1/src/Home/SuccessView.tsx @@ -0,0 +1,72 @@ +import { useNavigate } from "react-router-dom"; +import { signOut } from "supertokens-auth-react/recipe/session"; +import { recipeDetails } from "../config"; +import CallAPIView from "./CallAPIView"; +import { BlogsIcon, CelebrateIcon, GuideIcon, SeparatorLine, SignOutIcon } from "../assets/images"; + +interface ILink { + name: string; + onClick: () => void; + icon: string; +} + +export default function SuccessView(props: { userId: string }) { + let userId = props.userId; + + const navigate = useNavigate(); + + async function logoutClicked() { + await signOut(); + navigate("/auth"); + } + + function openLink(url: string) { + window.open(url, "_blank"); + } + + const links: ILink[] = [ + { + name: "Blogs", + onClick: () => openLink("https://supertokens.com/blog"), + icon: BlogsIcon, + }, + { + name: "Documentation", + onClick: () => openLink(recipeDetails.docsLink), + icon: GuideIcon, + }, + { + name: "Sign Out", + onClick: logoutClicked, + icon: SignOutIcon, + }, + ]; + + return ( + <> +
    +
    + Login successful Login successful +
    +
    +
    Your userID is:
    +
    + {userId} +
    + +
    +
    +
    + {links.map((link) => ( +
    + {link.name} +
    + {link.name} +
    +
    + ))} +
    + separator + + ); +} diff --git a/examples/with-unified-login/client1/src/Home/index.tsx b/examples/with-unified-login/client1/src/Home/index.tsx new file mode 100644 index 000000000..0c3f288e8 --- /dev/null +++ b/examples/with-unified-login/client1/src/Home/index.tsx @@ -0,0 +1,17 @@ +import SuccessView from "./SuccessView"; +import { useSessionContext } from "supertokens-auth-react/recipe/session"; +import "./Home.css"; + +export default function Home() { + const sessionContext = useSessionContext(); + + if (sessionContext.loading === true) { + return null; + } + + return ( +
    + +
    + ); +} diff --git a/examples/with-unified-login/client1/src/assets/fonts/MenloRegular.ttf b/examples/with-unified-login/client1/src/assets/fonts/MenloRegular.ttf new file mode 100644 index 000000000..033dc6d21 Binary files /dev/null and b/examples/with-unified-login/client1/src/assets/fonts/MenloRegular.ttf differ diff --git a/examples/with-unified-login/client1/src/assets/images/arrow-right-icon.svg b/examples/with-unified-login/client1/src/assets/images/arrow-right-icon.svg new file mode 100644 index 000000000..95aa1fec6 --- /dev/null +++ b/examples/with-unified-login/client1/src/assets/images/arrow-right-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-unified-login/client1/src/assets/images/background.png b/examples/with-unified-login/client1/src/assets/images/background.png new file mode 100644 index 000000000..2147c15c2 Binary files /dev/null and b/examples/with-unified-login/client1/src/assets/images/background.png differ diff --git a/examples/with-unified-login/client1/src/assets/images/blogs-icon.svg b/examples/with-unified-login/client1/src/assets/images/blogs-icon.svg new file mode 100644 index 000000000..a2fc9dd62 --- /dev/null +++ b/examples/with-unified-login/client1/src/assets/images/blogs-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-unified-login/client1/src/assets/images/celebrate-icon.svg b/examples/with-unified-login/client1/src/assets/images/celebrate-icon.svg new file mode 100644 index 000000000..3b40b1efa --- /dev/null +++ b/examples/with-unified-login/client1/src/assets/images/celebrate-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/with-unified-login/client1/src/assets/images/guide-icon.svg b/examples/with-unified-login/client1/src/assets/images/guide-icon.svg new file mode 100644 index 000000000..bd85af72b --- /dev/null +++ b/examples/with-unified-login/client1/src/assets/images/guide-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-unified-login/client1/src/assets/images/index.ts b/examples/with-unified-login/client1/src/assets/images/index.ts new file mode 100644 index 000000000..7adf036c4 --- /dev/null +++ b/examples/with-unified-login/client1/src/assets/images/index.ts @@ -0,0 +1,8 @@ +import SeparatorLine from "./separator-line.svg"; +import ArrowRight from "./arrow-right-icon.svg"; +import SignOutIcon from "./sign-out-icon.svg"; +import GuideIcon from "./guide-icon.svg"; +import BlogsIcon from "./blogs-icon.svg"; +import CelebrateIcon from "./celebrate-icon.svg"; + +export { SeparatorLine, ArrowRight, SignOutIcon, GuideIcon, BlogsIcon, CelebrateIcon }; diff --git a/examples/with-unified-login/client1/src/assets/images/separator-line.svg b/examples/with-unified-login/client1/src/assets/images/separator-line.svg new file mode 100644 index 000000000..7127a00dc --- /dev/null +++ b/examples/with-unified-login/client1/src/assets/images/separator-line.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/examples/with-unified-login/client1/src/assets/images/sign-out-icon.svg b/examples/with-unified-login/client1/src/assets/images/sign-out-icon.svg new file mode 100644 index 000000000..6cc4f85fd --- /dev/null +++ b/examples/with-unified-login/client1/src/assets/images/sign-out-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-unified-login/client1/src/clients.json b/examples/with-unified-login/client1/src/clients.json new file mode 120000 index 000000000..c2ba927f0 --- /dev/null +++ b/examples/with-unified-login/client1/src/clients.json @@ -0,0 +1 @@ +../../auth-provider/clients.json \ No newline at end of file diff --git a/examples/with-unified-login/client1/src/config.tsx b/examples/with-unified-login/client1/src/config.tsx new file mode 100644 index 000000000..25d8182f5 --- /dev/null +++ b/examples/with-unified-login/client1/src/config.tsx @@ -0,0 +1,38 @@ +import Session from "supertokens-auth-react/recipe/session"; + +export function getApiDomain() { + const apiPort = 3001; + const apiUrl = `http://localhost:${apiPort}`; + return apiUrl; +} + +export function getWebsiteDomain() { + const websitePort = 3011; + const websiteUrl = `http://localhost:${websitePort}`; + return websiteUrl; +} + +export const SuperTokensConfig = { + 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: [ + Session.init({ + tokenTransferMethod: "header", + }), + ], +}; + +export const recipeDetails = { + docsLink: "https://supertokens.com/docs/thirdparty/introduction", +}; + +export const PreBuiltUIList = []; + +export const ComponentWrapper = (props: { children: JSX.Element }): JSX.Element => { + return props.children; +}; diff --git a/examples/with-unified-login/client1/src/index.css b/examples/with-unified-login/client1/src/index.css new file mode 100644 index 000000000..04146b5e7 --- /dev/null +++ b/examples/with-unified-login/client1/src/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} diff --git a/examples/with-unified-login/client1/src/index.tsx b/examples/with-unified-login/client1/src/index.tsx new file mode 100644 index 000000000..126116f9b --- /dev/null +++ b/examples/with-unified-login/client1/src/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); + +root.render( + + + +); diff --git a/examples/with-unified-login/client1/src/react-app-env.d.ts b/examples/with-unified-login/client1/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/examples/with-unified-login/client1/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/with-unified-login/client1/tsconfig.json b/examples/with-unified-login/client1/tsconfig.json new file mode 100644 index 000000000..c0555cbc6 --- /dev/null +++ b/examples/with-unified-login/client1/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/with-unified-login/client2/LICENSE.md b/examples/with-unified-login/client2/LICENSE.md new file mode 100644 index 000000000..588f27e68 --- /dev/null +++ b/examples/with-unified-login/client2/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-unified-login/client2/README.md b/examples/with-unified-login/client2/README.md new file mode 100644 index 000000000..1507029d7 --- /dev/null +++ b/examples/with-unified-login/client2/README.md @@ -0,0 +1,57 @@ +![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) + +# Example React App using a Generic OAuth2 Library and SuperTokens as OAuth2 Provider + +This examples app demonstrates how to use SuperTokens as OAuth2 Provider in a React App using a generic OAuth2 library such as [react-oidc-context](https://github.com/authts/react-oidc-context). + +## 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-oauth2-without-supertokens +npm install +``` + +## 1. Create an OAuth2 Client + +```bash +curl -X POST http://localhost:3567/recipe/oauth/clients \ + -H "Content-Type: application/json" \ + -d '{ + "scope": "offline_access openid email", + "redirectUris": ["http://localhost:3000"], + "grantTypes": ["authorization_code", "refresh_token"], + "responseTypes": ["code", "id_token"], + "tokenEndpointAuthMethod": "none" + }' +``` + +Note down the `client_id` from the response. + +## 2. Run the st-oauth2-authorization-server + +1. Open a new terminal window and navigate to `supertokens-auth-react/examples/ +st-oauth2-authorization-server` +2. Read the README.md to setup `st-oauth2-authorization-server ` and run it using `npm start` + +## 3. Update config + +Open the `App.tsx` file and update `clientId` and `authServerUrl` values as per step 1 and step 2. + +## 4. Run the demo app + +This compiles and serves the React app. + +```bash +npm run start +``` + +## 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-unified-login/client2/package.json b/examples/with-unified-login/client2/package.json new file mode 100644 index 000000000..960c16119 --- /dev/null +++ b/examples/with-unified-login/client2/package.json @@ -0,0 +1,49 @@ +{ + "name": "with-oauth2-without-supertokens", + "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", + "helmet": "^5.1.0", + "oidc-client-ts": "^3.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-oidc-context": "^3.1.0", + "react-router-dom": "^6.2.1", + "react-scripts": "5.0.1", + "ts-node-dev": "^2.0.0", + "typescript": "^4.8.2", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "PORT=3020 BROWSER=none 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-unified-login/client2/public/favicon.ico b/examples/with-unified-login/client2/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/with-unified-login/client2/public/favicon.ico differ diff --git a/examples/with-unified-login/client2/public/index.html b/examples/with-unified-login/client2/public/index.html new file mode 100644 index 000000000..6f1f7cb51 --- /dev/null +++ b/examples/with-unified-login/client2/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + React App + + + +
    + + diff --git a/examples/with-unified-login/client2/public/manifest.json b/examples/with-unified-login/client2/public/manifest.json new file mode 100644 index 000000000..f01493ff0 --- /dev/null +++ b/examples/with-unified-login/client2/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/with-unified-login/client2/public/robots.txt b/examples/with-unified-login/client2/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/with-unified-login/client2/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/with-unified-login/client2/src/App.css b/examples/with-unified-login/client2/src/App.css new file mode 100644 index 000000000..9886f003d --- /dev/null +++ b/examples/with-unified-login/client2/src/App.css @@ -0,0 +1,26 @@ +.App { + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + font-family: Rubik; +} + +button { + padding-left: 13px; + padding-right: 13px; + padding-top: 8px; + padding-bottom: 8px; + background-color: black; + border-radius: 10px; + cursor: pointer; + color: white; + font-weight: bold; + font-size: 17px; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/examples/with-unified-login/client2/src/App.tsx b/examples/with-unified-login/client2/src/App.tsx new file mode 100644 index 000000000..3734f17b4 --- /dev/null +++ b/examples/with-unified-login/client2/src/App.tsx @@ -0,0 +1,49 @@ +import { AuthProvider, AuthProviderProps, useAuth } from "react-oidc-context"; +import "./App.css"; +import clients from "./clients.json"; + +const tenantId = "tenant2"; +const clientId = clients[tenantId].clientId; + +const redirectURI = `http://localhost:3012/auth/callback`; +const logoutURI = `http://localhost:3012/loggedout`; +const oidcConfig: AuthProviderProps = { + client_id: clientId, + authority: `http://localhost:3001/auth`, + response_type: "code", + redirect_uri: redirectURI, + scope: "openid offline_access", + extraQueryParams: { + tenant_id: tenantId, + }, +}; + +function AuthPage() { + const { signinRedirect, signoutRedirect, user } = useAuth(); + + return ( +
    +

    OAuth2 Example With Generic OAuth2 Lib

    +
    + {user ? ( +
    +
    {JSON.stringify(user.profile, null, 2)}
    + +
    + ) : ( +
    + +
    + )} +
    +
    + ); +} + +export default function App() { + return ( + + + + ); +} diff --git a/examples/with-unified-login/client2/src/assets/fonts/MenloRegular.ttf b/examples/with-unified-login/client2/src/assets/fonts/MenloRegular.ttf new file mode 100644 index 000000000..033dc6d21 Binary files /dev/null and b/examples/with-unified-login/client2/src/assets/fonts/MenloRegular.ttf differ diff --git a/examples/with-unified-login/client2/src/clients.json b/examples/with-unified-login/client2/src/clients.json new file mode 120000 index 000000000..c2ba927f0 --- /dev/null +++ b/examples/with-unified-login/client2/src/clients.json @@ -0,0 +1 @@ +../../auth-provider/clients.json \ No newline at end of file diff --git a/examples/with-unified-login/client2/src/index.css b/examples/with-unified-login/client2/src/index.css new file mode 100644 index 000000000..04146b5e7 --- /dev/null +++ b/examples/with-unified-login/client2/src/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} diff --git a/examples/with-unified-login/client2/src/index.tsx b/examples/with-unified-login/client2/src/index.tsx new file mode 100644 index 000000000..399c737cd --- /dev/null +++ b/examples/with-unified-login/client2/src/index.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); +root.render( + + + +); diff --git a/examples/with-unified-login/client2/src/react-app-env.d.ts b/examples/with-unified-login/client2/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/examples/with-unified-login/client2/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/with-unified-login/client2/tsconfig.json b/examples/with-unified-login/client2/tsconfig.json new file mode 100644 index 000000000..c0555cbc6 --- /dev/null +++ b/examples/with-unified-login/client2/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/with-unified-login/client3/.gitignore b/examples/with-unified-login/client3/.gitignore new file mode 100644 index 000000000..fa85cd8da --- /dev/null +++ b/examples/with-unified-login/client3/.gitignore @@ -0,0 +1,9 @@ +.env +var + +# Node.js +node_modules/ +npm-debug.log* + +# Mac OS X +.DS_Store diff --git a/examples/with-unified-login/client3/LICENSE b/examples/with-unified-login/client3/LICENSE new file mode 100644 index 000000000..00d2e135a --- /dev/null +++ b/examples/with-unified-login/client3/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/examples/with-unified-login/client3/README.md b/examples/with-unified-login/client3/README.md new file mode 100644 index 000000000..b099d3580 --- /dev/null +++ b/examples/with-unified-login/client3/README.md @@ -0,0 +1,63 @@ +# todos-express-openidconnect + +This app illustrates how to use [Passport](https://www.passportjs.org/) with +[Express](https://expressjs.com/) to sign users in via OpenID Connect. Use this +example as a starting point for your own web applications. + +## Quick Start + +To run this app, clone the repository and install dependencies: + +```bash +$ git clone https://github.com/passport/todos-express-openidconnect.git +$ cd todos-express-openidconnect +$ npm install +``` + +This app must be configured with an OpenID Provider (OP)'s endpoints, as well as +a client ID and secret that has been issued by the OP. + +The endpoints should be set as options to `OpenIDConnectStrategy` in +[routes/auth.js](https://github.com/passport/todos-express-openidconnect/blob/master/routes/auth.js#L7-L10). + +Once the client ID and secret have been obtained, create a `.env` file and add +the following environment variables: + +``` +CLIENT_ID=__INSERT_CLIENT_ID_HERE__ +CLIENT_SECRET=__INSERT_CLIENT_SECRET_HERE__ +``` + +Start the server. + +```bash +$ npm start +``` + +Navigate to [`http://localhost:3000`](http://localhost:3000). + +## Overview + +This example illustrates how to use Passport and the [`passport-openidconnect`](https://www.passportjs.org/packages/passport-openidconnect/) +strategy within an Express application to sign users in via OpenID Connect. + +This app implements the features of a typical [TodoMVC](https://todomvc.com/) +app, and adds sign in functionality. This app is a traditional web application, +in which all application logic and data persistence is handled on the server. + +User interaction is performed via HTML pages and forms, which are rendered via +[EJS](https://ejs.co/) templates and styled with vanilla CSS. Data is stored in +and queried from a [SQLite](https://www.sqlite.org/) database. + +After users sign in, a login session is established and maintained between the +server and the browser with a cookie. As authenticated users interact with the +app, creating and editing todo items, the login state is restored by +authenticating the session. + +## License + +[The Unlicense](https://opensource.org/licenses/unlicense) + +## Credit + +Created by [Jared Hanson](https://www.jaredhanson.me/) diff --git a/examples/with-unified-login/client3/app.js b/examples/with-unified-login/client3/app.js new file mode 100644 index 000000000..11dcd8061 --- /dev/null +++ b/examples/with-unified-login/client3/app.js @@ -0,0 +1,73 @@ +require("dotenv").config(); + +var createError = require("http-errors"); +var express = require("express"); +var path = require("path"); +var cookieParser = require("cookie-parser"); +var session = require("express-session"); +var csrf = require("csurf"); +var passport = require("passport"); +var logger = require("morgan"); + +// pass the session to the connect sqlite3 module +// allowing it to inherit from session.Store +var SQLiteStore = require("connect-sqlite3")(session); + +var indexRouter = require("./routes/index"); +var authRouter = require("./routes/auth"); + +var app = express(); + +// view engine setup +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "ejs"); + +app.locals.pluralize = require("pluralize"); + +app.use(logger("dev")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, "public"))); +app.use( + session({ + secret: "keyboard cat", + resave: false, // don't save session if unmodified + saveUninitialized: false, // don't create session until something stored + store: new SQLiteStore({ db: "sessions.db", dir: "var/db" }), + }) +); +app.use(csrf()); +app.use(passport.authenticate("session")); +app.use(function (req, res, next) { + var msgs = req.session.messages || []; + res.locals.messages = msgs; + res.locals.hasMessages = !!msgs.length; + req.session.messages = []; + next(); +}); +app.use(function (req, res, next) { + res.locals.csrfToken = req.csrfToken(); + next(); +}); + +app.use("/", indexRouter); +app.use("/", authRouter); + +// catch 404 and forward to error handler +app.use(function (req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function (err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get("env") === "development" ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render("error"); +}); + +module.exports = app; diff --git a/examples/with-unified-login/client3/bin/www b/examples/with-unified-login/client3/bin/www new file mode 100755 index 000000000..4472f5b15 --- /dev/null +++ b/examples/with-unified-login/client3/bin/www @@ -0,0 +1,86 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require("../app"); +var debug = require("debug")("todos:server"); +var http = require("http"); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || "3030"); +app.set("port", port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on("error", onError); +server.on("listening", onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== "listen") { + throw error; + } + + var bind = typeof port === "string" ? "Pipe " + port : "Port " + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case "EACCES": + console.error(bind + " requires elevated privileges"); + process.exit(1); + break; + case "EADDRINUSE": + console.error(bind + " is already in use"); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; + debug("Listening on " + bind); +} diff --git a/examples/with-unified-login/client3/clients.json b/examples/with-unified-login/client3/clients.json new file mode 120000 index 000000000..b024041e2 --- /dev/null +++ b/examples/with-unified-login/client3/clients.json @@ -0,0 +1 @@ +../auth-provider/clients.json \ No newline at end of file diff --git a/examples/with-unified-login/client3/db.js b/examples/with-unified-login/client3/db.js new file mode 100644 index 000000000..b91e0589a --- /dev/null +++ b/examples/with-unified-login/client3/db.js @@ -0,0 +1,18 @@ +var sqlite3 = require("sqlite3"); +var mkdirp = require("mkdirp"); + +mkdirp.sync("var/db"); + +var db = new sqlite3.Database("var/db/todos.db"); + +db.serialize(function () { + db.run( + "CREATE TABLE IF NOT EXISTS todos ( \ + owner_id INTEGER NOT NULL, \ + title TEXT NOT NULL, \ + completed INTEGER \ + )" + ); +}); + +module.exports = db; diff --git a/examples/with-unified-login/client3/package.json b/examples/with-unified-login/client3/package.json new file mode 100644 index 000000000..e056d1ac7 --- /dev/null +++ b/examples/with-unified-login/client3/package.json @@ -0,0 +1,51 @@ +{ + "name": "todos-express-openidconnect", + "version": "0.0.0", + "private": true, + "description": "Todo app using Express, Passport, and SQLite for sign in via OpenID Connect.", + "keywords": [ + "example", + "express", + "passport", + "sqlite" + ], + "author": { + "name": "Jared Hanson", + "email": "jaredhanson@gmail.com", + "url": "https://www.jaredhanson.me/" + }, + "homepage": "https://github.com/passport/todos-express-openidconnect", + "repository": { + "type": "git", + "url": "git://github.com/passport/todos-express-openidconnect.git" + }, + "bugs": { + "url": "https://github.com/passport/todos-express-openidconnect/issues" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + }, + "license": "Unlicense", + "scripts": { + "start": "NODE_OPTIONS=--use-openssl-ca node ./bin/www" + }, + "dependencies": { + "connect-ensure-login": "^0.1.1", + "connect-sqlite3": "^0.9.13", + "cookie-parser": "~1.4.4", + "csurf": "^1.11.0", + "debug": "~2.6.9", + "dotenv": "^8.6.0", + "ejs": "~2.6.1", + "express": "~4.16.1", + "express-session": "^1.17.2", + "http-errors": "~1.6.3", + "mkdirp": "^1.0.4", + "morgan": "~1.9.1", + "passport": "^0.5.2", + "passport-openidconnect": "^0.1.1", + "pluralize": "^8.0.0", + "sqlite3": "^5.0.2" + } +} diff --git a/examples/with-unified-login/client3/public/css/app.css b/examples/with-unified-login/client3/public/css/app.css new file mode 100644 index 000000000..ac19f01a6 --- /dev/null +++ b/examples/with-unified-login/client3/public/css/app.css @@ -0,0 +1,55 @@ +.nav { + position: absolute; + top: -130px; + right: 0; +} + +.nav ul { + margin: 0; + list-style: none; + text-align: center; +} + +.nav li { + display: inline-block; + height: 40px; + margin-left: 12px; + font-size: 14px; + font-weight: 400; + line-height: 40px; +} + +.nav a { + display: block; + color: inherit; + text-decoration: none; +} + +.nav a:hover { + border-bottom: 1px solid #db7676; +} + +.nav button { + height: 40px; +} + +.nav button:hover { + border-bottom: 1px solid #db7676; + cursor: pointer; +} + +/* background image by Cole Bemis */ +.nav .user { + padding-left: 20px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} + +/* background image by Cole Bemis */ +.nav .logout { + padding-left: 20px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-log-out'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'%3E%3C/path%3E%3Cpolyline points='16 17 21 12 16 7'%3E%3C/polyline%3E%3Cline x1='21' y1='12' x2='9' y2='12'%3E%3C/line%3E%3C/svg%3E%0A"); + background-repeat: no-repeat; + background-position: center left; +} diff --git a/examples/with-unified-login/client3/public/css/base.css b/examples/with-unified-login/client3/public/css/base.css new file mode 100644 index 000000000..d3938100c --- /dev/null +++ b/examples/with-unified-login/client3/public/css/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: "“"; + font-size: 50px; + opacity: 0.15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: "”"; + font-size: 50px; + opacity: 0.15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, 0.04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ""; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, 0.04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, 0.6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/examples/with-unified-login/client3/public/css/home.css b/examples/with-unified-login/client3/public/css/home.css new file mode 100644 index 000000000..51c0b5973 --- /dev/null +++ b/examples/with-unified-login/client3/public/css/home.css @@ -0,0 +1,41 @@ +.todohome { + margin: 130px 0 40px 0; + position: relative; +} + +.todohome h1 { + position: absolute; + top: -140px; + width: 100%; + font-size: 80px; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.todohome section { + padding-top: 1px; + text-align: center; +} + +.todohome h2 { + padding-bottom: 48px; + font-size: 28px; + font-weight: 300; +} + +.todohome .button { + padding: 13px 45px; + font-size: 16px; + font-weight: 500; + color: white; + border-radius: 5px; + background: #d83f45; +} + +.todohome a.button { + text-decoration: none; +} diff --git a/examples/with-unified-login/client3/public/css/index.css b/examples/with-unified-login/client3/public/css/index.css new file mode 100644 index 000000000..be48f991e --- /dev/null +++ b/examples/with-unified-login/client3/public/css/index.css @@ -0,0 +1,387 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111111; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp h1 { + position: absolute; + top: -140px; + width: 100%; + font-size: 80px; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + height: 65px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 65px; + font-size: 0; + position: absolute; + top: -65px; + left: -0; +} + +.toggle-all + label:before { + content: "❯"; + display: inline-block; + font-size: 22px; + color: #949494; + padding: 10px 27px 10px 27px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all:checked + label:before { + color: #484848; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E"); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + font-weight: 400; + color: #484848; +} + +.todo-list li.completed label { + color: #949494; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #949494; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover, +.todo-list li .destroy:focus { + color: #c18585; +} + +.todo-list li .destroy:after { + content: "×"; + display: block; + height: 100%; + line-height: 1.1; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + padding: 10px 15px; + height: 20px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ""; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: #db7676; +} + +.filters li a.selected { + border-color: #ce4646; +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 19px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #4d4d4d; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} + +:focus, +.toggle:focus + label, +.toggle-all:focus + label { + box-shadow: 0 0 2px 2px #cf7d7d; + outline: 0; +} diff --git a/examples/with-unified-login/client3/routes/auth.js b/examples/with-unified-login/client3/routes/auth.js new file mode 100644 index 000000000..dc6de7272 --- /dev/null +++ b/examples/with-unified-login/client3/routes/auth.js @@ -0,0 +1,57 @@ +var express = require("express"); +var clients = require("../clients.json"); +var passport = require("passport"); +var OpenIDConnectStrategy = require("passport-openidconnect"); + +const tenantId = "tenant3"; +const clientId = clients[tenantId].clientId; +const clientSecret = clients[tenantId].clientSecret; + +passport.use( + new OpenIDConnectStrategy( + { + issuer: `http://localhost:3001/auth`, + authorizationURL: "http://localhost:3001/auth/oauth/auth", + tokenURL: "http://localhost:3001/auth/oauth/token", + userInfoURL: "http://localhost:3001/auth/oauth/userinfo", + clientID: clientId, + clientSecret: clientSecret, + callbackURL: "http://localhost:3013/oauth2/redirect", + scope: ["profile"], + }, + function verify(issuer, profile, cb) { + return cb(null, profile); + } + ) +); + +passport.serializeUser(function (user, cb) { + process.nextTick(function () { + cb(null, { id: user.id, username: user.username, name: user.displayName }); + }); +}); + +passport.deserializeUser(function (user, cb) { + process.nextTick(function () { + return cb(null, user); + }); +}); + +var router = express.Router(); + +router.get("/login", passport.authenticate("openidconnect")); + +router.get( + "/oauth2/redirect", + passport.authenticate("openidconnect", { + successReturnToOrRedirect: "/", + failureRedirect: "/login", + }) +); + +router.post("/logout", function (req, res, next) { + req.logout(); + res.redirect("/"); +}); + +module.exports = router; diff --git a/examples/with-unified-login/client3/routes/index.js b/examples/with-unified-login/client3/routes/index.js new file mode 100644 index 000000000..1b4302c5e --- /dev/null +++ b/examples/with-unified-login/client3/routes/index.js @@ -0,0 +1,154 @@ +var express = require("express"); +var ensureLogIn = require("connect-ensure-login").ensureLoggedIn; +var db = require("../db"); + +var ensureLoggedIn = ensureLogIn(); + +function fetchTodos(req, res, next) { + db.all("SELECT rowid AS id, * FROM todos WHERE owner_id = ?", [req.user.id], function (err, rows) { + if (err) { + return next(err); + } + + var todos = rows.map(function (row) { + return { + id: row.id, + title: row.title, + completed: row.completed == 1 ? true : false, + url: "/" + row.id, + }; + }); + res.locals.todos = todos; + res.locals.activeCount = todos.filter(function (todo) { + return !todo.completed; + }).length; + res.locals.completedCount = todos.length - res.locals.activeCount; + next(); + }); +} + +var router = express.Router(); + +/* GET home page. */ +router.get( + "/", + function (req, res, next) { + if (!req.user) { + return res.render("home"); + } + next(); + }, + fetchTodos, + function (req, res, next) { + res.locals.filter = null; + res.render("index", { user: req.user }); + } +); + +router.get("/active", ensureLoggedIn, fetchTodos, function (req, res, next) { + res.locals.todos = res.locals.todos.filter(function (todo) { + return !todo.completed; + }); + res.locals.filter = "active"; + res.render("index", { user: req.user }); +}); + +router.get("/completed", ensureLoggedIn, fetchTodos, function (req, res, next) { + res.locals.todos = res.locals.todos.filter(function (todo) { + return todo.completed; + }); + res.locals.filter = "completed"; + res.render("index", { user: req.user }); +}); + +router.post( + "/", + ensureLoggedIn, + function (req, res, next) { + req.body.title = req.body.title.trim(); + next(); + }, + function (req, res, next) { + if (req.body.title !== "") { + return next(); + } + return res.redirect("/" + (req.body.filter || "")); + }, + function (req, res, next) { + db.run( + "INSERT INTO todos (owner_id, title, completed) VALUES (?, ?, ?)", + [req.user.id, req.body.title, req.body.completed == true ? 1 : null], + function (err) { + if (err) { + return next(err); + } + return res.redirect("/" + (req.body.filter || "")); + } + ); + } +); + +router.post( + "/:id(\\d+)", + ensureLoggedIn, + function (req, res, next) { + req.body.title = req.body.title.trim(); + next(); + }, + function (req, res, next) { + if (req.body.title !== "") { + return next(); + } + db.run("DELETE FROM todos WHERE rowid = ? AND owner_id = ?", [req.params.id, req.user.id], function (err) { + if (err) { + return next(err); + } + return res.redirect("/" + (req.body.filter || "")); + }); + }, + function (req, res, next) { + db.run( + "UPDATE todos SET title = ?, completed = ? WHERE rowid = ? AND owner_id = ?", + [req.body.title, req.body.completed !== undefined ? 1 : null, req.params.id, req.user.id], + function (err) { + if (err) { + return next(err); + } + return res.redirect("/" + (req.body.filter || "")); + } + ); + } +); + +router.post("/:id(\\d+)/delete", ensureLoggedIn, function (req, res, next) { + db.run("DELETE FROM todos WHERE rowid = ? AND owner_id = ?", [req.params.id, req.user.id], function (err) { + if (err) { + return next(err); + } + return res.redirect("/" + (req.body.filter || "")); + }); +}); + +router.post("/toggle-all", ensureLoggedIn, function (req, res, next) { + db.run( + "UPDATE todos SET completed = ? WHERE owner_id = ?", + [req.body.completed !== undefined ? 1 : null, req.user.id], + function (err) { + if (err) { + return next(err); + } + return res.redirect("/" + (req.body.filter || "")); + } + ); +}); + +router.post("/clear-completed", ensureLoggedIn, function (req, res, next) { + db.run("DELETE FROM todos WHERE owner_id = ? AND completed = ?", [req.user.id, 1], function (err) { + if (err) { + return next(err); + } + return res.redirect("/" + (req.body.filter || "")); + }); +}); + +module.exports = router; diff --git a/examples/with-unified-login/client3/views/error.ejs b/examples/with-unified-login/client3/views/error.ejs new file mode 100644 index 000000000..7cf94edf1 --- /dev/null +++ b/examples/with-unified-login/client3/views/error.ejs @@ -0,0 +1,3 @@ +

    <%= message %>

    +

    <%= error.status %>

    +
    <%= error.stack %>
    diff --git a/examples/with-unified-login/client3/views/home.ejs b/examples/with-unified-login/client3/views/home.ejs new file mode 100644 index 000000000..6eb7d50ad --- /dev/null +++ b/examples/with-unified-login/client3/views/home.ejs @@ -0,0 +1,27 @@ + + + + + + Express • TodoMVC + + + + + +
    +
    +

    todos

    +
    +
    +

    todos helps you get things done

    + Sign in +
    +
    + + + diff --git a/examples/with-unified-login/client3/views/index.ejs b/examples/with-unified-login/client3/views/index.ejs new file mode 100644 index 000000000..6a0ffe278 --- /dev/null +++ b/examples/with-unified-login/client3/views/index.ejs @@ -0,0 +1,100 @@ + + + + + + Express • TodoMVC + + + + + +
    + +
    +

    todos

    +
    + + <% if (filter) { %> + + <% } %> + +
    +
    + <% if (activeCount + completedCount > 0) { %> +
    +
    + onchange="this.form.submit();"> + + +
    +
      + <% todos.forEach(function(todo) { %> +
    • > +
      +
      + onchange="this.form.submit();"> + + +
      + + <% if (filter) { %> + + <% } %> + +
      +
      + <% if (filter) { %> + + <% } %> + +
      +
    • + <% }); %> +
    +
    + <% } %> + <% if (activeCount + completedCount > 0) { %> + + <% } %> +
    + + +