diff --git a/examples/astro/with-thirdpartyemailpassword/README.md b/examples/astro/with-thirdpartyemailpassword/README.md new file mode 100644 index 00000000..058bb769 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/README.md @@ -0,0 +1,34 @@ +![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) + +# SuperTokens ThirdPartyEmailPassword Demo app for Astro + +This demo app demonstrates the following use cases: + +- Social Login / Sign up +- Email & Password login +- Logout +- Session management & Calling APIs + +## Project setup + +Use `npm` to install the project dependencies: + +```bash +npm install +``` + +## Run the demo app + +```bash +npm run dev +``` + +The app will start on `http://localhost:4321` + +## 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/astro/with-thirdpartyemailpassword/astro.config.mjs b/examples/astro/with-thirdpartyemailpassword/astro.config.mjs new file mode 100644 index 00000000..9153aa09 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/astro.config.mjs @@ -0,0 +1,11 @@ +import { defineConfig } from "astro/config"; + +import node from "@astrojs/node"; + +// https://astro.build/config +export default defineConfig({ + output: "server", + adapter: node({ + mode: "standalone", + }), +}); diff --git a/examples/astro/with-thirdpartyemailpassword/package.json b/examples/astro/with-thirdpartyemailpassword/package.json new file mode 100644 index 00000000..92369741 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/package.json @@ -0,0 +1,28 @@ +{ + "name": "astro-supertokens", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/check": "^0.9.2", + "@astrojs/node": "^8.3.3", + "astro": "^4.14.2", + "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.1.0", + "micromatch": "^4.0.7", + "supertokens-node": "^20.0.2", + "supertokens-web-js": "^0.13.0", + "typescript": "^5.5.4" + }, + "devDependencies": { + "@types/micromatch": "^4.0.9", + "prettier": "^3.3.3", + "prettier-plugin-astro": "^0.14.1" + } +} diff --git a/examples/astro/with-thirdpartyemailpassword/public/favicon.svg b/examples/astro/with-thirdpartyemailpassword/public/favicon.svg new file mode 100644 index 00000000..f157bd1c --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/examples/astro/with-thirdpartyemailpassword/src/auth/Auth.ts b/examples/astro/with-thirdpartyemailpassword/src/auth/Auth.ts new file mode 100644 index 00000000..568a68a2 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/auth/Auth.ts @@ -0,0 +1,241 @@ +import { signOut, signUp } from "supertokens-web-js/recipe/emailpassword"; +import { doesEmailExist } from "supertokens-web-js/recipe/emailpassword"; +import { signIn } from "supertokens-web-js/recipe/emailpassword"; +import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty"; +import { signInAndUp } from "supertokens-web-js/recipe/thirdparty"; + +export async function handleCallback() { + try { + const response = await signInAndUp(); + + if (response.status === "OK") { + console.log(response.user); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // sign up successful + } else { + // sign in successful + } + window.location.assign("/dashboard"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // SuperTokens requires that the third party provider + // gives an email for the user. If that's not the case, sign up / in + // will fail. + + // As a hack to solve this, you can override the backend functions to create a fake email for the user. + + window.alert("No email provided by social login. Please use another form of login"); + window.location.assign("/"); // redirect back to login page + } + } catch (err: any) { + console.log(err); + + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +export async function googleSignInClicked() { + try { + const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ + thirdPartyId: "google", + + // This is where Google should redirect the user back after login or error. + // This URL goes on the Google's dashboard as well. + frontendRedirectURI: "http://localhost:4321/auth/callback/google", + }); + + /* + Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow + */ + + // we redirect the user to google for auth. + window.location.assign(authUrl); + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +export async function githubSignInClicked() { + try { + const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ + thirdPartyId: "github", + + // This is where Google should redirect the user back after login or error. + // This URL goes on the Google's dashboard as well. + frontendRedirectURI: "http://localhost:4321/auth/callback/github", + }); + + /* + Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow + */ + + // we redirect the user to google for auth. + window.location.assign(authUrl); + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +export async function signInClicked(email: string, password: string) { + try { + let response = await signIn({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + }); + + if (response.status === "FIELD_ERROR") { + response.formFields.forEach((formField) => { + if (formField.id === "email") { + // Email validation failed (for example incorrect email syntax). + window.alert(formField.error); + } + }); + } else if (response.status === "WRONG_CREDENTIALS_ERROR") { + window.alert("Email password combination is incorrect."); + } else if (response.status === "SIGN_IN_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason); + } else { + // sign in successful. The session tokens are automatically handled by + // the frontend SDK. + window.location.href = "/dashboard"; + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +export async function checkEmail(email: string) { + try { + let response = await doesEmailExist({ + email, + }); + + if (response.doesExist) { + window.alert("Email already exists. Please sign in instead"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +export async function signUpClicked(email: string, password: string) { + try { + let response = await signUp({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + }); + + if (response.status === "FIELD_ERROR") { + // one of the input formFields failed validation + response.formFields.forEach((formField) => { + if (formField.id === "email") { + // Email validation failed (for example incorrect email syntax), + // or the email is not unique. + window.alert(formField.error); + } else if (formField.id === "password") { + // Password validation failed. + // Maybe it didn't match the password strength + window.alert(formField.error); + } + }); + } else if (response.status === "SIGN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign up was not allowed. + window.alert(response.reason); + } else { + // sign up successful. The session tokens are automatically handled by + // the frontend SDK. + window.location.href = "/dashboard"; + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +export async function signOutClicked() { + try { + await signOut(); + window.location.href = "/"; + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +export async function getSessionInfo() { + try { + let response = await fetch("/auth/sessioninfo", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.status === 200) { + let data = await response.json(); + return data; + } else { + window.alert("Oops! Something went wrong."); + } + } catch (err: any) { + window.alert("Oops! Something went wrong."); + } +} diff --git a/examples/astro/with-thirdpartyemailpassword/src/auth/STBEConfig.ts b/examples/astro/with-thirdpartyemailpassword/src/auth/STBEConfig.ts new file mode 100644 index 00000000..6aa82926 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/auth/STBEConfig.ts @@ -0,0 +1,54 @@ +import ThirdPartyNode from "supertokens-node/recipe/thirdparty"; +import EmailPasswordNode from "supertokens-node/recipe/emailpassword"; + +import SessionNode from "supertokens-node/recipe/session"; +import appInfo from "./appInfo.json"; +import { type TypeInput } from "supertokens-node/types"; + +export const initBE = (): TypeInput => { + return { + framework: "custom", + supertokens: { + // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. + connectionURI: "https://try.supertokens.com", + // apiKey: , + }, + appInfo, + recipeList: [ + EmailPasswordNode.init(), + ThirdPartyNode.init({ + // 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. + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: + "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }, + ], + }, + }, + { + config: { + thirdPartyId: "github", + clients: [ + { + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }, + ], + }, + }, + ], + }, + }), + SessionNode.init(), + ], + isInServerlessEnv: true, + }; +}; diff --git a/examples/astro/with-thirdpartyemailpassword/src/auth/STFEConfig.ts b/examples/astro/with-thirdpartyemailpassword/src/auth/STFEConfig.ts new file mode 100644 index 00000000..56326bdc --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/auth/STFEConfig.ts @@ -0,0 +1,12 @@ +import ThirdPartyWebJs from "supertokens-web-js/recipe/thirdparty"; +import EmailPasswordWebJs from "supertokens-web-js/recipe/emailpassword"; +import SessionWebJs from "supertokens-web-js/recipe/session"; +import appInfo from "./appInfo.json"; +import { type SuperTokensConfig } from "supertokens-web-js/types"; + +export const initFE = (): SuperTokensConfig => { + return { + appInfo, + recipeList: [ThirdPartyWebJs.init(), EmailPasswordWebJs.init(), SessionWebJs.init()], + }; +}; diff --git a/examples/astro/with-thirdpartyemailpassword/src/auth/appInfo.json b/examples/astro/with-thirdpartyemailpassword/src/auth/appInfo.json new file mode 100644 index 00000000..97c72116 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/auth/appInfo.json @@ -0,0 +1,6 @@ +{ + "appName": "Astro-ST-demo", + "apiDomain": "http://localhost:4321", + "apiBasePath": "/auth", + "websiteDomain": "http://localhost:4321" +} diff --git a/examples/astro/with-thirdpartyemailpassword/src/auth/superTokensHelper.ts b/examples/astro/with-thirdpartyemailpassword/src/auth/superTokensHelper.ts new file mode 100644 index 00000000..1b34bba2 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/auth/superTokensHelper.ts @@ -0,0 +1,278 @@ +import { + PreParsedRequest, + CollectingResponse, + middleware, + errorHandler, +} from "supertokens-node/framework/custom/index.js"; +import Session, { type SessionContainer, type VerifySessionOptions } from "supertokens-node/recipe/session/index.js"; +import SessionRecipe from "supertokens-node/lib/build/recipe/session/recipe.js"; +import { availableTokenTransferMethods } from "supertokens-node/lib/build/recipe/session/constants.js"; +import { getToken } from "supertokens-node/lib/build/recipe/session/cookieAndHeaders.js"; +import { parseJWTWithoutSignatureVerification } from "supertokens-node/lib/build/recipe/session/jwt.js"; +import { serialize } from "cookie"; +import JsonWebToken from "jsonwebtoken"; +import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken"; +import jwksClient from "jwks-rsa"; +import appInfo from "./appInfo.json"; + +type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; + +const client = jwksClient({ + jwksUri: `${appInfo.apiDomain}${appInfo.apiBasePath}/jwt/jwks.json`, +}); + +function getAccessToken(request: Request): string | undefined { + return getCookieFromRequest(request)["sAccessToken"]; +} + +function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) { + client.getSigningKey(header.kid, (err, key) => { + if (err) { + callback(err); + } else { + const signingKey = key?.getPublicKey(); + callback(null, signingKey); + } + }); +} + +async function verifyToken(token: string): Promise { + return new Promise((resolve, reject) => { + JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => { + if (err) { + reject(err); + } else { + resolve(decoded as JwtPayload); + } + }); + }); +} + +export function handleAuthAPIRequest(AstroResponse: typeof Response) { + const stMiddleware = middleware((req) => { + return createPreParsedRequest(req); + }); + + return async function handleCall(req: Request) { + const baseResponse = new CollectingResponse(); + + const { handled, error } = await stMiddleware(req, baseResponse); + + if (error) { + throw error; + } + if (!handled) { + return new AstroResponse("Not found", { status: 404 }); + } + + for (const respCookie of baseResponse.cookies) { + baseResponse.headers.append( + "Set-Cookie", + serialize(respCookie.key, respCookie.value, { + domain: respCookie.domain, + expires: new Date(respCookie.expires), + httpOnly: respCookie.httpOnly, + path: respCookie.path, + sameSite: respCookie.sameSite, + secure: respCookie.secure, + }) + ); + } + + return new AstroResponse(baseResponse.body, { + headers: baseResponse.headers, + status: baseResponse.statusCode, + }); + }; +} + +function getCookieFromRequest(request: Request) { + const cookies: Record = {}; + const cookieHeader = request.headers.get("Cookie"); + if (cookieHeader) { + const cookieStrings = cookieHeader.split(";"); + for (const cookieString of cookieStrings) { + const [name, value] = cookieString.trim().split("="); + cookies[name] = decodeURIComponent(value); + } + } + return cookies; +} + +function getQueryFromRequest(request: Request) { + const query: Record = {}; + const url = new URL(request.url); + const searchParams = url.searchParams; + searchParams.forEach((value, key) => { + query[key] = value; + }); + return query; +} + +function createPreParsedRequest(request: Request): PreParsedRequest { + return new PreParsedRequest({ + cookies: getCookieFromRequest(request), + url: request.url as string, + method: request.method as HTTPMethod, + query: getQueryFromRequest(request), + headers: request.headers, + getFormBody: async () => { + return await request.formData(); + }, + getJSONBody: async () => { + return await request.json(); + }, + }); +} + +async function getSessionDetails( + preParsedRequest: PreParsedRequest, + options?: VerifySessionOptions, + userContext?: Record +): Promise<{ + session: SessionContainer | undefined; + hasToken: boolean; + hasInvalidClaims: boolean; + baseResponse: CollectingResponse; + AstroResponse?: Response; +}> { + const baseResponse = new CollectingResponse(); + const recipe = (SessionRecipe as any).default.instance; + const tokenTransferMethod = recipe.config.getTokenTransferMethod({ + req: preParsedRequest, + forCreateNewSession: false, + userContext, + }); + const transferMethods = tokenTransferMethod === "any" ? availableTokenTransferMethods : [tokenTransferMethod]; + const hasToken = transferMethods.some((transferMethod) => { + const token = getToken(preParsedRequest, "access", transferMethod); + if (!token) { + return false; + } + try { + parseJWTWithoutSignatureVerification(token); + return true; + } catch { + return false; + } + }); + + try { + const session = await Session.getSession(preParsedRequest, baseResponse, options, userContext); + return { + session, + hasInvalidClaims: false, + hasToken, + baseResponse, + }; + } catch (err) { + if (Session.Error.isErrorFromSuperTokens(err)) { + return { + hasToken, + hasInvalidClaims: err.type === Session.Error.INVALID_CLAIMS, + session: undefined, + baseResponse, + AstroResponse: new Response("Authentication required", { + status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401, + }), + }; + } else { + throw err; + } + } +} + +/** + * A helper function to retrieve session details on the server side. + * + * NOTE: This function does not use the getSession function from the supertokens-node SDK + * because getSession can update the access token. These updated tokens would not be + * propagated to the client side, as request interceptors do not run on the server side. + */ +export async function getSessionForSSR(astroRequest: Request): Promise<{ + accessTokenPayload: JwtPayload | undefined; + hasToken: boolean; + error: Error | undefined; +}> { + const accessToken = getAccessToken(astroRequest); + const hasToken = !!accessToken; + try { + if (accessToken) { + const decoded = await verifyToken(accessToken); + return { accessTokenPayload: decoded, hasToken, error: undefined }; + } + return { accessTokenPayload: undefined, hasToken, error: undefined }; + } catch (error) { + if (error instanceof JsonWebToken.TokenExpiredError) { + return { accessTokenPayload: undefined, hasToken, error: undefined }; + } + return { accessTokenPayload: undefined, hasToken, error: error as Error }; + } +} + +export async function withSession( + astroRequest: Request, + handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, + options?: VerifySessionOptions, + userContext?: Record +): Promise { + try { + let baseRequest = createPreParsedRequest(astroRequest); + const { session, AstroResponse, baseResponse } = await getSessionDetails(baseRequest, options, userContext); + + if (AstroResponse !== undefined) { + return AstroResponse; + } + + let userResponse: Response; + + try { + userResponse = await handler(undefined, session); + } catch (err) { + await errorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { + if (errorHandlerError) { + throw errorHandlerError; + } + }); + + // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. + userResponse = new Response(baseResponse.body, { + status: baseResponse.statusCode, + headers: baseResponse.headers, + }); + } + + let didAddCookies = false; + let didAddHeaders = false; + + for (const respCookie of baseResponse.cookies) { + didAddCookies = true; + userResponse.headers.append( + "Set-Cookie", + serialize(respCookie.key, respCookie.value, { + domain: respCookie.domain, + expires: new Date(respCookie.expires), + httpOnly: respCookie.httpOnly, + path: respCookie.path, + sameSite: respCookie.sameSite, + secure: respCookie.secure, + }) + ); + } + + baseResponse.headers.forEach((value: string, key: string) => { + didAddHeaders = true; + userResponse.headers.set(key, value); + }); + if (didAddCookies || didAddHeaders) { + if (!userResponse.headers.has("Cache-Control")) { + // This is needed for production deployments with Vercel + userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); + } + } + + return userResponse; + } catch (error) { + return await handler(error as Error, undefined); + } +} diff --git a/examples/astro/with-thirdpartyemailpassword/src/env.d.ts b/examples/astro/with-thirdpartyemailpassword/src/env.d.ts new file mode 100644 index 00000000..e16c13c6 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/astro/with-thirdpartyemailpassword/src/layouts/Root.astro b/examples/astro/with-thirdpartyemailpassword/src/layouts/Root.astro new file mode 100644 index 00000000..58f54067 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/layouts/Root.astro @@ -0,0 +1,142 @@ +--- + +--- + + + + + + + + Astro + + +
+ +
+ + + + + + diff --git a/examples/astro/with-thirdpartyemailpassword/src/pages/auth/[...path]/[...route].ts b/examples/astro/with-thirdpartyemailpassword/src/pages/auth/[...path]/[...route].ts new file mode 100644 index 00000000..4573028b --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/pages/auth/[...path]/[...route].ts @@ -0,0 +1,23 @@ +import SuperTokens from "supertokens-node"; +import { initBE } from "../../../auth/STBEConfig"; +import type { APIRoute } from "astro"; +import { handleAuthAPIRequest } from "../../../auth/superTokensHelper"; + +const handleCall = handleAuthAPIRequest(Response); + +export const ALL: APIRoute = async ({ params, request }) => { + SuperTokens.init(initBE()); + + if (params.path === "callback") { + console.log("callback"); + } + + try { + return await handleCall(request); + } catch (error) { + console.error(error); + return new Response(JSON.stringify({ error: "Internal server error" }), { + status: 500, + }); + } +}; diff --git a/examples/astro/with-thirdpartyemailpassword/src/pages/auth/[...route].ts b/examples/astro/with-thirdpartyemailpassword/src/pages/auth/[...route].ts new file mode 100644 index 00000000..11b1eaf2 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/pages/auth/[...route].ts @@ -0,0 +1,19 @@ +import SuperTokens from "supertokens-node"; +import { initBE } from "../../auth/STBEConfig"; +import type { APIRoute } from "astro"; +import { handleAuthAPIRequest } from "../../auth/superTokensHelper"; + +const handleCall = handleAuthAPIRequest(Response); + +export const ALL: APIRoute = async ({ params, request }) => { + SuperTokens.init(initBE()); + + try { + return await handleCall(request); + } catch (error) { + console.error(error); + return new Response(JSON.stringify({ error: "Internal server error" }), { + status: 500, + }); + } +}; diff --git a/examples/astro/with-thirdpartyemailpassword/src/pages/auth/callback/[...path].astro b/examples/astro/with-thirdpartyemailpassword/src/pages/auth/callback/[...path].astro new file mode 100644 index 00000000..c3a1c504 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/pages/auth/callback/[...path].astro @@ -0,0 +1,15 @@ +--- +import Root from "../../../layouts/Root.astro"; +--- + + Redirecting... + + diff --git a/examples/astro/with-thirdpartyemailpassword/src/pages/auth/sessioninfo.ts b/examples/astro/with-thirdpartyemailpassword/src/pages/auth/sessioninfo.ts new file mode 100644 index 00000000..1b6b1482 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/pages/auth/sessioninfo.ts @@ -0,0 +1,19 @@ +import { type APIRoute } from "astro"; +import { withSession } from "../../auth/superTokensHelper"; + +export const GET: APIRoute = async ({ params, request }) => { + return withSession(request, async (err, session) => { + if (err) { + return new Response(JSON.stringify(err), { status: 500 }); + } + + return new Response( + JSON.stringify({ + note: "Fetch any data from your application for authenticated user after using verifySession middleware", + userId: session!.getUserId(), + sessionHandle: session!.getHandle(), + accessTokenPayload: session!.getAccessTokenPayload(), + }) + ); + }); +}; diff --git a/examples/astro/with-thirdpartyemailpassword/src/pages/dashboard.astro b/examples/astro/with-thirdpartyemailpassword/src/pages/dashboard.astro new file mode 100644 index 00000000..3477739f --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/pages/dashboard.astro @@ -0,0 +1,39 @@ +--- +import Root from "../layouts/Root.astro"; + +import { getSessionForSSR } from "../auth/superTokensHelper"; + +const { hasToken } = await getSessionForSSR(Astro.request); + +if (!hasToken) { + return Astro.redirect("/"); +} +--- + + +

Astro + SuperTokens | Dashboard

+
+
+ + +
+
+
+ + diff --git a/examples/astro/with-thirdpartyemailpassword/src/pages/index.astro b/examples/astro/with-thirdpartyemailpassword/src/pages/index.astro new file mode 100644 index 00000000..c66acf10 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/pages/index.astro @@ -0,0 +1,81 @@ +--- +import Root from "../layouts/Root.astro"; + +import { getSessionForSSR } from "../auth/superTokensHelper"; + +const { hasToken } = await getSessionForSSR(Astro.request); + +if (hasToken) { + return Astro.redirect("/dashboard"); +} +--- + + +

Astro + SuperTokens

+
+
+ + +
+ + +
+
+ +
+
+
+ + diff --git a/examples/astro/with-thirdpartyemailpassword/src/pages/nonauth.astro b/examples/astro/with-thirdpartyemailpassword/src/pages/nonauth.astro new file mode 100644 index 00000000..df8a7b29 --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/src/pages/nonauth.astro @@ -0,0 +1,8 @@ +--- +import Root from "../layouts/Root.astro"; +--- + + +

Astro + SuperTokens | Non-Auth

+

We don't care about auth on this page

+
diff --git a/examples/astro/with-thirdpartyemailpassword/tsconfig.json b/examples/astro/with-thirdpartyemailpassword/tsconfig.json new file mode 100644 index 00000000..418a1a1d --- /dev/null +++ b/examples/astro/with-thirdpartyemailpassword/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/strict" +} diff --git a/examples/solidjs/with-thirdpartyemailpassword/README.md b/examples/solidjs/with-thirdpartyemailpassword/README.md new file mode 100644 index 00000000..f2370e3d --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/README.md @@ -0,0 +1,35 @@ +![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) + +# SuperTokens ThirdPartyEmailPassword Demo app for SolidJS + +This demo app demonstrates the following use cases: + +- Social Login / Sign up +- Email & Password login +- Logout +- Session management & Calling APIs + +## Project setup + +Use `npm` to install the project dependencies: + +```bash +npm install +``` + +## Run the demo app + +```bash +npm run dev +``` + +The app will start on `http://localhost:3000` + +## Author + +Created with :heart: by the folks at supertokens.com. +Built live on [Hacking with SuperTokens 005 and 006](https://www.youtube.com/watch?v=ovjTQ-20fk0). + +## License + +This project is licensed under the Apache 2.0 license. diff --git a/examples/solidjs/with-thirdpartyemailpassword/index.html b/examples/solidjs/with-thirdpartyemailpassword/index.html new file mode 100644 index 00000000..d29a34b4 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Solid + TS + + +
+ + + diff --git a/examples/solidjs/with-thirdpartyemailpassword/npm b/examples/solidjs/with-thirdpartyemailpassword/npm new file mode 100644 index 00000000..e69de29b diff --git a/examples/solidjs/with-thirdpartyemailpassword/package.json b/examples/solidjs/with-thirdpartyemailpassword/package.json new file mode 100644 index 00000000..afe2b55f --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/package.json @@ -0,0 +1,28 @@ +{ + "name": "e005-solid-supertokens", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "concurrently --kill-others \"npm run dev-server\" \"npm run dev-client\"", + "dev-client": "vite", + "dev-server": "tsx server.ts", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@solidjs/router": "^0.13.6", + "cors": "^2.8.5", + "express": "^4.19.2", + "solid-js": "^1.8.17", + "supertokens-node": "^18.0.1", + "supertokens-web-js": "^0.12.0" + }, + "devDependencies": { + "concurrently": "^8.2.2", + "tsx": "^4.17.0", + "typescript": "^5.2.2", + "vite": "^5.3.1", + "vite-plugin-solid": "^2.10.2" + } +} diff --git a/examples/solidjs/with-thirdpartyemailpassword/public/vite.svg b/examples/solidjs/with-thirdpartyemailpassword/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/solidjs/with-thirdpartyemailpassword/server.ts b/examples/solidjs/with-thirdpartyemailpassword/server.ts new file mode 100644 index 00000000..854f4c8e --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/server.ts @@ -0,0 +1,101 @@ +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 Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +supertokens.init({ + framework: "express", + supertokens: { + connectionURI: "https://try.supertokens.com", + }, + appInfo: { + appName: "Hacking With SuperTokens", + apiDomain: "http://localhost:3001", + websiteDomain: "http://localhost:3000", + apiBasePath: "", + websiteBasePath: "/", + }, + recipeList: [ + EmailPassword.init(), + ThirdParty.init({ + // 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. + signInAndUpFeature: { + providers: [ + { + 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", + }, + }, + ], + }, + }, + ], + }, + }), + Session.init(), // initializes session features + ], +}); + +const app = express(); + +app.use( + cors({ + origin: "http://localhost:3000", + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + methods: ["GET", "PUT", "POST", "DELETE"], + credentials: true, + }) +); + +app.use(middleware()); + +app.get("/sessioninfo", verifySession(), async (req: SessionRequest, res) => { + let session = req.session; + res.send({ + sessionHandle: session!.getHandle(), + userId: session!.getUserId(), + accessTokenPayload: session!.getAccessTokenPayload(), + }); +}); + +// In case of session related errors, this error handler +// returns 401 to the client. +app.use(errorHandler()); + +app.listen(3001, () => console.log(`API Server listening on port 3001`)); diff --git a/examples/solidjs/with-thirdpartyemailpassword/src/App.css b/examples/solidjs/with-thirdpartyemailpassword/src/App.css new file mode 100644 index 00000000..0f650a48 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/src/App.css @@ -0,0 +1,132 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; + color: rgba(255, 255, 255, 0.87); +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +.form-wrap { + display: flex; + flex-direction: column; + gap: 1em; + max-width: 320px; +} + +.form-wrap input { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + border: 1px solid #646cff; + border-radius: 8px; + padding: 0.6em 1em; + font-size: 1em; + font-family: inherit; +} + +div.wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1em; + height: 100vh; + width: 100vw; + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +div.wrapper header { + height: 80px; + background: #ff9933; + display: flex; + align-items: center; + justify-content: center; +} + +div.wrapper main { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + min-width: 60%; +} + +div.wrapper footer { + height: 60px; + background: #1f1f1f; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/examples/solidjs/with-thirdpartyemailpassword/src/Auth.tsx b/examples/solidjs/with-thirdpartyemailpassword/src/Auth.tsx new file mode 100644 index 00000000..a6257632 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/src/Auth.tsx @@ -0,0 +1,393 @@ +import { doesEmailExist, signIn, signUp } from "supertokens-web-js/recipe/emailpassword"; +import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty"; +import { createSignal, onMount, Show } from "solid-js"; +import "./App.css"; +import { superTokensInit } from "./config/supertokens"; +import { useNavigate } from "@solidjs/router"; +import { signInAndUp } from "supertokens-web-js/recipe/thirdparty"; + +async function handleGoogleCallback(navigate: (path: string) => void) { + try { + const response = await signInAndUp(); + + if (response.status === "OK") { + console.log(response.user); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + console.log("sign up successful, google"); + } else { + console.log("sign in successful, google"); + } + // window.location.assign("/home"); + navigate("/dashboard/"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // SuperTokens requires that the third party provider + // gives an email for the user. If that's not the case, sign up / in + // will fail. + + // As a hack to solve this, you can override the backend functions to create a fake email for the user. + + window.alert("No email provided by social login. Please use another form of login"); + navigate("/auth"); // redirect back to login page + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +async function googleSignInClicked() { + try { + const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ + thirdPartyId: "google", + + // This is where Google should redirect the user back after login or error. + // This URL goes on the Google's dashboard as well. + frontendRedirectURI: "http://localhost:3000/auth/callback/google", + }); + + // we redirect the user to google for auth. + window.location.assign(authUrl); + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +async function handleGitHubCallback(navigate: (path: string) => void) { + try { + const response = await signInAndUp(); + + if (response.status === "OK") { + console.log(response.user); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + console.log("sign up successful, github"); + } else { + console.log("sign in successful, github"); + } + // window.location.assign("/home"); + navigate("/dashboard/"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // SuperTokens requires that the third party provider + // gives an email for the user. If that's not the case, sign up / in + // will fail. + + // As a hack to solve this, you can override the backend functions to create a fake email for the user. + + window.alert("No email provided by social login. Please use another form of login"); + navigate("/auth"); // redirect back to login page + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +async function githubSignInClicked() { + try { + const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ + thirdPartyId: "github", + + // This is where Google should redirect the user back after login or error. + // This URL goes on the Google's dashboard as well. + frontendRedirectURI: "http://localhost:3000/auth/callback/github", + }); + + // we redirect the user to google for auth. + window.location.assign(authUrl); + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +async function handleAppleCallback(navigate: (path: string) => void) { + try { + const response = await signInAndUp(); + + if (response.status === "OK") { + console.log(response.user); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + console.log("sign up successful, apple"); + } else { + console.log("sign in successful, apple"); + } + // window.location.assign("/home"); + navigate("/dashboard/"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // SuperTokens requires that the third party provider + // gives an email for the user. If that's not the case, sign up / in + // will fail. + + // As a hack to solve this, you can override the backend functions to create a fake email for the user. + + window.alert("No email provided by social login. Please use another form of login"); + navigate("/auth"); // redirect back to login page + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +async function appleSignInClicked() { + try { + const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ + thirdPartyId: "apple", + + frontendRedirectURI: "http://localhost:3000/auth/callback/apple", // This is an example callback URL on your frontend. You can use another path as well. + redirectURIOnProviderDashboard: "http://localhost:3000/auth/callback/apple", // This URL goes on the Apple's dashboard + }); + + // we redirect the user to apple for auth. + window.location.assign(authUrl); + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +async function signUpClicked(email: string, password: string, navigate: (path: string) => void) { + try { + let response = await signUp({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + }); + + if (response.status === "FIELD_ERROR") { + // one of the input formFields failed validaiton + response.formFields.forEach((formField) => { + if (formField.id === "email") { + // Email validation failed (for example incorrect email syntax), + // or the email is not unique. + window.alert(formField.error); + } else if (formField.id === "password") { + // Password validation failed. + // Maybe it didn't match the password strength + window.alert(formField.error); + } + }); + } else if (response.status === "SIGN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign up was not allowed. + window.alert(response.reason); + } else { + // sign up successful. The session tokens are automatically handled by + // the frontend SDK. + navigate("/dashboard/"); + } + } catch (err: any) { + console.log(err); + + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +async function checkEmail(email: string) { + try { + let response = await doesEmailExist({ + email, + }); + + return response; + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +async function signInClicked(email: string, password: string, navigate: (path: string) => void) { + try { + let response = await signIn({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + }); + + if (response.status === "FIELD_ERROR") { + response.formFields.forEach((formField) => { + if (formField.id === "email") { + // Email validation failed (for example incorrect email syntax). + window.alert(formField.error); + } + }); + } else if (response.status === "WRONG_CREDENTIALS_ERROR") { + window.alert("Email password combination is incorrect."); + } else if (response.status === "SIGN_IN_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason); + } else { + // sign in successful. The session tokens are automatically handled by + // the frontend SDK. + navigate("/dashboard/"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} + +function Auth() { + const navigate = useNavigate(); + superTokensInit(); + const [showOAuthLoading] = createSignal( + (() => { + if (window.location.pathname === "/auth/callback/google") { + return "Google"; + } + + if (window.location.pathname === "/auth/callback/github") { + return "GitHub"; + } + + if (window.location.pathname === "/auth/callback/apple") { + return "Apple"; + } + + return false; + })() + ); + + onMount(() => { + if (window.location.pathname === "/auth/callback/google") { + handleGoogleCallback(navigate); + } + + if (window.location.pathname === "/auth/callback/github") { + handleGitHubCallback(navigate); + } + + if (window.location.pathname === "/auth/callback/apple") { + handleAppleCallback(navigate); + } + }); + + const [email, setEmail] = createSignal(""); + const [password, setPassword] = createSignal(""); + + const handleSignUpClicked = async () => { + const res = await checkEmail(email()); + if (!res?.doesExist) { + signUpClicked(email(), password(), navigate); + } else { + window.alert("Email already exists. Please sign in instead"); + } + }; + + const handleSignInClicked = async () => { + const res = await checkEmail(email()); + if (res?.doesExist) { + signInClicked(email(), password(), navigate); + } else { + window.alert("Email does not exist. Please sign up instead"); + } + }; + + const handleGoogleSignInClicked = async () => { + googleSignInClicked(); + }; + + const handleGithubSignInClicked = async () => { + githubSignInClicked(); + }; + + const handleAppleSignInClicked = async () => { + appleSignInClicked(); + }; + + return ( +
+
+ Logging-in via {showOAuthLoading()}, please wait... + + setEmail((e.target as HTMLInputElement).value)} + /> + setPassword((e.target as HTMLInputElement).value)} + /> + + + + + + +
+
+ ); +} + +export default Auth; diff --git a/examples/solidjs/with-thirdpartyemailpassword/src/Dashboard.tsx b/examples/solidjs/with-thirdpartyemailpassword/src/Dashboard.tsx new file mode 100644 index 00000000..f6bc3c62 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/src/Dashboard.tsx @@ -0,0 +1,53 @@ +import { Show, createEffect, createSignal } from "solid-js"; +import "./App.css"; +import { superTokensInit } from "./config/supertokens"; +import Session from "supertokens-web-js/recipe/session"; +import { useNavigate } from "@solidjs/router"; + +function Dashboard() { + const navigate = useNavigate(); + superTokensInit(); + + const [loading, setLoading] = createSignal(true); + + const getSessionInfo = async () => { + const response = await fetch("http://localhost:3001/sessioninfo", { + headers: { + "Content-Type": "application/json", + }, + method: "GET", + credentials: "include", + }); + + const data = await response.json(); + + alert(JSON.stringify(data)); + }; + + async function signOut() { + await Session.signOut(); + navigate("/"); + } + + createEffect(async () => { + if (await Session.doesSessionExist()) { + setLoading(false); + } else { + navigate("/"); + } + }); + + return ( +
+
+ Loading... + + + + +
+
+ ); +} + +export default Dashboard; diff --git a/examples/solidjs/with-thirdpartyemailpassword/src/assets/solid.svg b/examples/solidjs/with-thirdpartyemailpassword/src/assets/solid.svg new file mode 100644 index 00000000..025aa303 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/src/assets/solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/solidjs/with-thirdpartyemailpassword/src/config/supertokens.ts b/examples/solidjs/with-thirdpartyemailpassword/src/config/supertokens.ts new file mode 100644 index 00000000..739ae642 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/src/config/supertokens.ts @@ -0,0 +1,23 @@ +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; +import EmailPassword from "supertokens-web-js/recipe/emailpassword"; +import ThirdParty from "supertokens-web-js/recipe/thirdparty"; + +let initialized = false; + +export const superTokensInit = () => { + if (initialized) { + return; + } + + SuperTokens.init({ + appInfo: { + apiDomain: "http://localhost:3001", + apiBasePath: "", + appName: "Hacking With SuperTokens", + }, + recipeList: [Session.init(), EmailPassword.init(), ThirdParty.init()], + }); + + initialized = true; +}; diff --git a/examples/solidjs/with-thirdpartyemailpassword/src/index.css b/examples/solidjs/with-thirdpartyemailpassword/src/index.css new file mode 100644 index 00000000..279fff56 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/solidjs/with-thirdpartyemailpassword/src/index.tsx b/examples/solidjs/with-thirdpartyemailpassword/src/index.tsx new file mode 100644 index 00000000..ebfb37f2 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/src/index.tsx @@ -0,0 +1,22 @@ +/* @refresh reload */ +import { render } from "solid-js/web"; +import { Router, Route } from "@solidjs/router"; + +import "./index.css"; +import Auth from "./Auth"; +import Dashboard from "./Dashboard"; + +const root = document.getElementById("root"); + +render( + () => ( + + + + + + + + ), + root! +); diff --git a/examples/solidjs/with-thirdpartyemailpassword/src/vite-env.d.ts b/examples/solidjs/with-thirdpartyemailpassword/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/solidjs/with-thirdpartyemailpassword/tsconfig.app.json b/examples/solidjs/with-thirdpartyemailpassword/tsconfig.app.json new file mode 100644 index 00000000..28bb9e70 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/solidjs/with-thirdpartyemailpassword/tsconfig.json b/examples/solidjs/with-thirdpartyemailpassword/tsconfig.json new file mode 100644 index 00000000..41ccf0f7 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/examples/solidjs/with-thirdpartyemailpassword/tsconfig.node.json b/examples/solidjs/with-thirdpartyemailpassword/tsconfig.node.json new file mode 100644 index 00000000..9277af59 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/solidjs/with-thirdpartyemailpassword/vite.config.ts b/examples/solidjs/with-thirdpartyemailpassword/vite.config.ts new file mode 100644 index 00000000..8cb1f085 --- /dev/null +++ b/examples/solidjs/with-thirdpartyemailpassword/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; +import solid from "vite-plugin-solid"; + +export default defineConfig({ + plugins: [solid()], + server: { + port: 3000, + }, +}); diff --git a/examples/vuejs/with-thirdpartyemailpassword/README.md b/examples/vuejs/with-thirdpartyemailpassword/README.md index dd9c4e9c..58e24f5e 100644 --- a/examples/vuejs/with-thirdpartyemailpassword/README.md +++ b/examples/vuejs/with-thirdpartyemailpassword/README.md @@ -20,7 +20,7 @@ npm install ## Run the demo app -This compiles and serves the React app and starts the backend API server on port 3001. +This compiles and serves the Vue app and starts the backend API server on port 3001. ```bash npm run dev