This commit is contained in:
yohlo
2025-08-20 22:35:40 -05:00
commit f51c278cd3
169 changed files with 8173 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import SuperTokens from 'supertokens-web-js';
import Session from 'supertokens-web-js/recipe/session';
import Passwordless from 'supertokens-web-js/recipe/passwordless';
import { appInfo } from './config';
import { logger } from './';
export const frontendConfig = () => {
return {
appInfo,
recipeList: [
Passwordless.init(),
Session.init({
tokenTransferMethod: "cookie",
sessionTokenBackendDomain: undefined,
preAPIHook: async (context) => {
context.requestInit.credentials = "include";
return context;
},
})
]
};
}
let initialized = false;
export function ensureSuperTokensFrontend() {
if (typeof window === 'undefined') return;
if (!initialized) {
SuperTokens.init(frontendConfig());
initialized = true;
logger.info("Initialized");
Session.doesSessionExist().then(exists => {
logger.info(`Session does${exists ? '' : 'NOT'} exist on load!`);
});
}
}

View File

@@ -0,0 +1,7 @@
export const appInfo = {
appName: 'Tanstack Start SuperTokens',
apiDomain: import.meta.env.VITE_API_DOMAIN || 'http://localhost:3000',
websiteDomain: import.meta.env.VITE_WEBSITE_DOMAIN || 'http://localhost:3000',
apiBasePath: '/api/auth',
websiteBasePath: '/auth',
}

View File

@@ -0,0 +1,4 @@
import { Logger } from "@/lib/logger";
const logger = new Logger('SuperTokens');
export { logger };

View File

@@ -0,0 +1,30 @@
import Passwordless from "supertokens-node/recipe/passwordless";
import { logger } from "../";
const init = () => (
Passwordless.init({
flowType: "USER_INPUT_CODE",
contactMethod: "PHONE",
smsDelivery: {
override: (originalImplementation) => {
return {
...originalImplementation,
sendSms: async ({ userInputCode }) => {
if (!userInputCode) {
throw new Error("No user input code provided to sendSms");
}
logger.info('Sending Code',
'######################',
'## SuperTokens Code ##',
`## ${userInputCode} ##`,
'######################'
);
}
}
}
}
})
)
export default { init };

View File

@@ -0,0 +1,65 @@
import { useSession } from "@tanstack/react-start/server";
import Passwordless from "supertokens-node/recipe/passwordless";
import { sendVerifyCode, updateVerify } from "@/lib/twilio";
const init = () => (
Passwordless.init({
flowType: "USER_INPUT_CODE",
contactMethod: "PHONE",
smsDelivery: {
override: (originalImplementation) => {
return {
...originalImplementation,
sendSms: async ({ userInputCode, phoneNumber, preAuthSessionId }) => {
if (!userInputCode) {
throw new Error("No user input code provided to sendSms");
}
const sid = await sendVerifyCode(phoneNumber, userInputCode);
const session = await useSession({
password: preAuthSessionId
});
await session.update({
twilioSid: sid
});
}
}
}
},
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
consumeCode: async (input) => {
const session = await useSession({ password: input.preAuthSessionId });
const twilioSid = session?.data.twilioSid;
if (!twilioSid) {
throw new Error("Twilio SID not found in session");
}
let response = await originalImplementation.consumeCode(input);
if (response.status === "OK") {
await updateVerify(twilioSid);
await session.update({
twilioSid: undefined,
userId: response?.user.id
})
} else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") {
if (response.failedCodeInputAttemptCount !== response.maximumCodeInputAttempts) {
await updateVerify(twilioSid);
}
}
return response;
}
}
}
}
})
)
export default { init };

View File

@@ -0,0 +1,48 @@
import { getSessionForSSR } from "supertokens-node/custom";
import { ensureSuperTokensBackend } from "../server";
import { logger } from "../";
export async function getSessionForStart(request: Request, options?: { sessionRequired?: boolean }) {
ensureSuperTokensBackend();
try {
const session = await getSessionForSSR(request);
if (session.hasToken) {
return {
hasToken: true,
accessTokenPayload: session.accessTokenPayload,
userId: session.accessTokenPayload?.sub,
sessionHandle: session.accessTokenPayload?.sessionHandle,
};
}
return null;
} catch (error: any) {
logger.error("Session error", error);
if (error.type === "TRY_REFRESH_TOKEN") {
return {
hasToken: false,
needsRefresh: true,
error: 'TRY_REFRESH_TOKEN'
};
}
if (options?.sessionRequired === false) {
return null;
}
throw error;
}
}
export async function verifySession(request: Request, options?: { sessionRequired?: boolean }) {
const session = await getSessionForStart(request, options);
if (!session && options?.sessionRequired !== false) {
throw new Response("Unauthorized", { status: 401 });
}
return session;
}

View File

@@ -0,0 +1,42 @@
import SuperTokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import { TypeInput } from "supertokens-node/types";
import Dashboard from "supertokens-node/recipe/dashboard";
import UserRoles from "supertokens-node/recipe/userroles";
import { appInfo } from "./config";
import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode";
import { logger } from "./";
export const backendConfig = (): TypeInput => {
return {
framework: 'custom',
supertokens: {
connectionURI: import.meta.env.VITE_SUPERTOKENS_URI || "https://try.supertokens.io",
},
appInfo,
recipeList: [
PasswordlessDevelopmentMode.init(),
Session.init({
cookieSameSite: "lax",
cookieSecure: process.env.NODE_ENV === 'production',
cookieDomain: process.env.NODE_ENV === 'production' ? ".example.com" : undefined,
antiCsrf: process.env.NODE_ENV === 'production' ? "VIA_TOKEN" : "NONE",
// Debug only
exposeAccessTokenToFrontendInCookieBasedAuth: true,
}),
Dashboard.init(),
UserRoles.init()
],
telemetry: process.env.NODE_ENV !== 'production',
};
}
let initialized = false;
export function ensureSuperTokensBackend() {
if (!initialized) {
SuperTokens.init(backendConfig());
initialized = true;
logger.simple("Backend initialized");
}
}