mirror of https://github.com/buster-so/buster.git
started adding supabase
This commit is contained in:
parent
9c052be5ad
commit
e2ee9e4c27
|
@ -36,7 +36,7 @@
|
|||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noConsole": "warn"
|
||||
"noConsole": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noExcessiveCognitiveComplexity": "off",
|
||||
|
|
|
@ -21,26 +21,29 @@
|
|||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@t3-oss/env-core": "^0.13.8",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/db": "^0.1.1",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/query-db-collection": "^0.2.0",
|
||||
"@tanstack/react-db": "^0.1.1",
|
||||
"@tanstack/react-devtools": "^0.2.2",
|
||||
"@tanstack/react-devtools": "^0.3.0",
|
||||
"@tanstack/react-form": "^1.19.1",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tanstack/react-query-devtools": "^5.84.2",
|
||||
"@tanstack/react-router": "^1.131.5",
|
||||
"@tanstack/react-router-devtools": "^1.131.5",
|
||||
"@tanstack/react-query": "^5.85.0",
|
||||
"@tanstack/react-query-devtools": "^5.85.0",
|
||||
"@tanstack/react-router": "^1.131.7",
|
||||
"@tanstack/react-router-devtools": "^1.131.7",
|
||||
"@tanstack/react-router-with-query": "^1.130.17",
|
||||
"@tanstack/react-start": "^1.131.6",
|
||||
"@tanstack/react-start": "^1.131.7",
|
||||
"@tanstack/react-store": "^0.7.3",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/router-plugin": "^1.131.5",
|
||||
"@tanstack/router-plugin": "^1.131.7",
|
||||
"@tanstack/store": "^0.7.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"ky": "^1.8.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.539.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
|
@ -64,7 +67,7 @@
|
|||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"jsdom": "^26.1.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.0",
|
||||
"vite": "^7.1.2",
|
||||
"vite-tsconfig-paths": "catalog:",
|
||||
"vitest": "^3.2.4",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export const BASE_API_URL = `${process.env.NEXT_PUBLIC_API_URL}`;
|
||||
export const BASE_API_URL_V2 = `${process.env.NEXT_PUBLIC_API2_URL}`;
|
||||
export const BASE_URL = `${BASE_API_URL}/api/v1`;
|
||||
export const BASE_URL_V2 = `${BASE_API_URL_V2}/api/v2`;
|
|
@ -0,0 +1,51 @@
|
|||
import isString from 'lodash/isString';
|
||||
|
||||
export const rustErrorHandler = (errors: unknown = {}): RustApiError => {
|
||||
// Type guards and safe property access
|
||||
const isErrorObject = (obj: unknown): obj is Record<string, unknown> =>
|
||||
typeof obj === 'object' && obj !== null;
|
||||
|
||||
const data =
|
||||
isErrorObject(errors) && isErrorObject(errors.response) ? errors.response.data : undefined;
|
||||
const status =
|
||||
isErrorObject(errors) && typeof errors.status === 'number' ? errors.status : undefined;
|
||||
|
||||
if (data && isString(data)) {
|
||||
return { message: String(data), status };
|
||||
}
|
||||
|
||||
if (isErrorObject(data) && data.message) {
|
||||
return { message: String(data.message), status };
|
||||
}
|
||||
|
||||
if (isErrorObject(data) && data.detail) {
|
||||
if (typeof data.detail === 'string') {
|
||||
return { message: String(data.detail), status };
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(data.detail) &&
|
||||
data.detail[0] &&
|
||||
isErrorObject(data.detail[0]) &&
|
||||
data.detail[0].msg
|
||||
) {
|
||||
return { message: String(data.detail[0].msg), status };
|
||||
}
|
||||
return { message: String(data.detail), status };
|
||||
}
|
||||
|
||||
if (isErrorObject(errors) && errors.message) {
|
||||
return { message: String(errors.message), status };
|
||||
}
|
||||
|
||||
if (typeof errors === 'string') {
|
||||
return { message: String(errors), status };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export interface RustApiError {
|
||||
message?: string;
|
||||
status?: number;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import ky from "ky";
|
||||
import { BASE_URL, BASE_URL_V2 } from "./config";
|
||||
|
||||
const mainApi = ky.create({
|
||||
prefixUrl: BASE_URL,
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async (request) => {
|
||||
// request.headers.set("Authorization", `Bearer ${token}`);
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const mainApiV2 = ky.create({ prefixUrl: BASE_URL_V2 });
|
||||
|
||||
export default mainApi;
|
||||
export { mainApi, mainApiV2 };
|
|
@ -41,3 +41,4 @@ export function makeData(...lens: number[]): Person[] {
|
|||
|
||||
return makeDataLevel();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import { createBrowserClient as createBrowserClientSSR } from "@supabase/ssr";
|
||||
|
||||
function createBrowserClient() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error("Missing Supabase environment variables for browser client");
|
||||
}
|
||||
|
||||
return createBrowserClientSSR(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
persistSession: true,
|
||||
autoRefreshToken: true,
|
||||
detectSessionInUrl: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let browserClient: ReturnType<typeof createBrowserClient> | null = null;
|
||||
|
||||
export const getBrowserClient = () => {
|
||||
if (!browserClient) {
|
||||
browserClient = createBrowserClient();
|
||||
}
|
||||
return browserClient;
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
import { getSupabaseServerClient } from "./server";
|
||||
import { signInWithAnonymousUser } from "./signIn";
|
||||
|
||||
type PromiseType<T extends Promise<unknown>> = T extends Promise<infer U> ? U : never;
|
||||
|
||||
export type UseSupabaseUserContextType = PromiseType<ReturnType<typeof getSupabaseUserContext>>;
|
||||
|
||||
export const getSupabaseUserContext = async (preemptiveRefreshMinutes = 5) => {
|
||||
const supabase = await getSupabaseServerClient();
|
||||
|
||||
// Get the session first
|
||||
const sessionResult = await supabase.auth.getSession();
|
||||
let sessionData = sessionResult.data;
|
||||
const sessionError = sessionResult.error;
|
||||
|
||||
if (sessionError) {
|
||||
console.error("Error getting session:", sessionError);
|
||||
}
|
||||
|
||||
// Check if we need to refresh the session
|
||||
if (sessionData.session) {
|
||||
const refreshedSessionData = (await refreshSessionIfNeeded(
|
||||
supabase,
|
||||
sessionData.session,
|
||||
preemptiveRefreshMinutes,
|
||||
)) as Awaited<ReturnType<typeof refreshSessionIfNeeded>>;
|
||||
|
||||
// If session was refreshed, get the updated session
|
||||
if (refreshedSessionData && "session" in refreshedSessionData) {
|
||||
// Replace the entire sessionData object to avoid type issues
|
||||
sessionData = refreshedSessionData;
|
||||
}
|
||||
}
|
||||
|
||||
// Get user data
|
||||
const { data: userData, error: userError } = await supabase.auth.getUser();
|
||||
|
||||
if (userError) {
|
||||
console.error("Error getting user:", userData, userError);
|
||||
}
|
||||
|
||||
if (!userData.user) {
|
||||
const { session: anonSession } = await signInWithAnonymousUser();
|
||||
console.info("created anon session", anonSession);
|
||||
return {
|
||||
user: anonSession?.user || null,
|
||||
accessToken: anonSession?.access_token,
|
||||
};
|
||||
}
|
||||
|
||||
const user = userData.user;
|
||||
const accessToken = sessionData.session?.access_token;
|
||||
const refreshToken = sessionData.session?.refresh_token;
|
||||
|
||||
if (!accessToken) {
|
||||
console.error("No access token found for user:", user);
|
||||
}
|
||||
|
||||
return { user, accessToken, refreshToken };
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to refresh the session if it's about to expire (less than 50 minutes)
|
||||
* Returns true if session was refreshed, false otherwise
|
||||
*/
|
||||
const refreshSessionIfNeeded = async (
|
||||
supabase: Awaited<ReturnType<typeof getSupabaseServerClient>>,
|
||||
session: NonNullable<Awaited<ReturnType<typeof supabase.auth.getSession>>["data"]["session"]>,
|
||||
preemptiveRefreshMinutes = 5,
|
||||
): Promise<false | Awaited<ReturnType<typeof supabase.auth.getSession>>["data"]> => {
|
||||
// Calculate if session is about to expire (less than 50 minutes)
|
||||
const expiresAt = session.expires_at;
|
||||
if (!expiresAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expiresAtTimestamp = expiresAt * 1000; // Convert to milliseconds
|
||||
const now = Date.now();
|
||||
const timeUntilExpiry = expiresAtTimestamp - now;
|
||||
const preemptiveRefreshInMs = preemptiveRefreshMinutes * 60 * 1000;
|
||||
|
||||
// If session expires in less than X minutes, refresh it
|
||||
if (timeUntilExpiry < preemptiveRefreshInMs) {
|
||||
const { data, error } = await supabase.auth.refreshSession();
|
||||
|
||||
if (error || !data.session) {
|
||||
console.error("Failed to refresh session:", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Session was successfully refreshed
|
||||
return data;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './getSupabaseUserContext';
|
|
@ -0,0 +1,36 @@
|
|||
'use server';
|
||||
|
||||
import { BusterRoutes, createBusterRoute } from '@/routes';
|
||||
import { createSupabaseServerClient } from './server';
|
||||
|
||||
export const resetPasswordEmailSend = async ({ email }: { email: string }) => {
|
||||
const supabase = await createSupabaseServerClient();
|
||||
|
||||
const authURLFull = `${process.env.NEXT_PUBLIC_URL}${createBusterRoute({
|
||||
route: BusterRoutes.AUTH_CALLBACK
|
||||
})}`;
|
||||
|
||||
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: authURLFull
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export const resetPassword = async ({ password }: { password: string }) => {
|
||||
'use server';
|
||||
|
||||
const supabase = await createSupabaseServerClient();
|
||||
|
||||
const { data, error } = await supabase.auth.updateUser({ password });
|
||||
|
||||
if (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
import { type CookieOptions, createServerClient } from "@supabase/ssr";
|
||||
import { parseCookies, setCookie } from "@tanstack/react-start/server";
|
||||
|
||||
export const COOKIE_OPTIONS: CookieOptions = {
|
||||
path: "/",
|
||||
secure: process.env.NODE_ENV === "production", // Only use secure in production
|
||||
sameSite: "lax", // Type assertion to fix the error
|
||||
httpOnly: true, // Make cookies HttpOnly
|
||||
maxAge: 60 * 60 * 24 * 7, // 1 week
|
||||
};
|
||||
|
||||
export function getSupabaseServerClient() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error("Missing Supabase environment variables for server client");
|
||||
}
|
||||
|
||||
return createServerClient(supabaseUrl, supabaseAnonKey, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return Object.entries(parseCookies()).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
}));
|
||||
},
|
||||
setAll(cookies) {
|
||||
cookies.forEach((cookie) => {
|
||||
setCookie(cookie.name, cookie.value, {
|
||||
...COOKIE_OPTIONS,
|
||||
...cookie.options,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
import { redirect } from "@tanstack/react-router";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { z } from "zod";
|
||||
import { ServerRoute as AuthCallbackRoute } from "../../routes/auth.callback";
|
||||
import { getSupabaseServerClient } from "./server";
|
||||
|
||||
const isValidRedirectUrl = (url: string): boolean => {
|
||||
try {
|
||||
const decoded = decodeURIComponent(url);
|
||||
return decoded.startsWith("/") && !decoded.startsWith("//");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const signInWithEmailAndPassword = createServerFn({ method: "POST" })
|
||||
.validator(
|
||||
z.object({ email: z.string(), password: z.string(), redirectUrl: z.string().optional() }),
|
||||
)
|
||||
.handler(async ({ data }) => {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
});
|
||||
if (error) {
|
||||
return {
|
||||
error: true,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Redirect to the prev page stored in the "redirect" search param
|
||||
throw redirect({
|
||||
href: data.redirectUrl || "/",
|
||||
});
|
||||
});
|
||||
|
||||
export const signInWithGoogle = createServerFn({ method: "POST" })
|
||||
.validator(z.object({ redirectUrl: z.string().optional() }))
|
||||
.handler(async ({ data: { redirectUrl } }) => {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
const redirectTo = redirectUrl || "/";
|
||||
|
||||
const callbackUrl = new URL(AuthCallbackRoute.to);
|
||||
|
||||
if (redirectTo && isValidRedirectUrl(redirectTo)) {
|
||||
callbackUrl.searchParams.set("next", redirectTo);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: "google",
|
||||
options: {
|
||||
redirectTo: callbackUrl.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
throw redirect({ to: data.url });
|
||||
});
|
||||
|
||||
// export const signInWithGithub = async ({
|
||||
// redirectTo,
|
||||
// }: {
|
||||
// redirectTo?: string | null;
|
||||
// } = {}): Promise<ServerActionResult<string>> => {
|
||||
// "use server";
|
||||
|
||||
// const supabase = await createSupabaseServerClient();
|
||||
|
||||
// const callbackUrl = new URL(authURLFull);
|
||||
// if (redirectTo && isValidRedirectUrl(redirectTo)) {
|
||||
// callbackUrl.searchParams.set("next", redirectTo);
|
||||
// }
|
||||
|
||||
// const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
// provider: "github",
|
||||
// options: {
|
||||
// redirectTo: callbackUrl.toString(),
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// return { success: false, error: error.message };
|
||||
// }
|
||||
|
||||
// revalidatePath("/", "layout");
|
||||
// return redirect(data.url);
|
||||
// };
|
||||
|
||||
// export const signInWithAzure = async ({
|
||||
// redirectTo,
|
||||
// }: {
|
||||
// redirectTo?: string | null;
|
||||
// } = {}): Promise<ServerActionResult<string>> => {
|
||||
// "use server";
|
||||
|
||||
// const supabase = await createSupabaseServerClient();
|
||||
|
||||
// const callbackUrl = new URL(authURLFull);
|
||||
// if (redirectTo && isValidRedirectUrl(redirectTo)) {
|
||||
// callbackUrl.searchParams.set("next", redirectTo);
|
||||
// }
|
||||
|
||||
// const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
// provider: "azure",
|
||||
// options: {
|
||||
// redirectTo: callbackUrl.toString(),
|
||||
// scopes: "email",
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// return { success: false, error: error.message };
|
||||
// }
|
||||
// revalidatePath("/", "layout");
|
||||
// return redirect(data.url);
|
||||
// };
|
||||
|
||||
// export const signUp = async ({
|
||||
// email,
|
||||
// password,
|
||||
// redirectTo,
|
||||
// }: {
|
||||
// email: string;
|
||||
// password: string;
|
||||
// redirectTo?: string | null;
|
||||
// }): Promise<ServerActionResult> => {
|
||||
// "use server";
|
||||
// const supabase = await createSupabaseServerClient();
|
||||
// const authURL = createBusterRoute({
|
||||
// route: BusterRoutes.AUTH_CONFIRM,
|
||||
// });
|
||||
// const authURLFull = `${process.env.NEXT_PUBLIC_URL}${authURL}`;
|
||||
|
||||
// const { error } = await supabase.auth.signUp({
|
||||
// email,
|
||||
// password,
|
||||
// options: {
|
||||
// emailRedirectTo: authURLFull,
|
||||
// },
|
||||
// });
|
||||
// if (error) {
|
||||
// console.error("supabase error in signUp", error);
|
||||
// // Return the actual Supabase error message
|
||||
// return { success: false, error: error.message };
|
||||
// }
|
||||
|
||||
// revalidatePath("/", "layout");
|
||||
// const finalRedirect =
|
||||
// redirectTo && isValidRedirectUrl(redirectTo)
|
||||
// ? decodeURIComponent(redirectTo)
|
||||
// : createBusterRoute({ route: BusterRoutes.APP_HOME });
|
||||
// return redirect(finalRedirect);
|
||||
// };
|
||||
|
||||
// export const signInWithAnonymousUser = async () => {
|
||||
// "use server";
|
||||
|
||||
// const supabase = await createSupabaseServerClient();
|
||||
|
||||
// const { data, error } = await supabase.auth.signInAnonymously();
|
||||
|
||||
// if (error) {
|
||||
// throw error;
|
||||
// }
|
||||
|
||||
// revalidatePath("/", "layout");
|
||||
|
||||
// return data;
|
||||
// };
|
|
@ -0,0 +1,28 @@
|
|||
"use server";
|
||||
|
||||
import { redirect } from "@tanstack/react-router";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { parseCookies, setCookie } from "@tanstack/react-start/server";
|
||||
import { getSupabaseServerClient } from "./server";
|
||||
|
||||
export const signOut = createServerFn({ method: "POST" }).handler(async () => {
|
||||
const supabase = await getSupabaseServerClient();
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
|
||||
// Clear all cookies by setting them with maxAge: 0
|
||||
const allCookies = parseCookies();
|
||||
for (const [cookieName] of Object.entries(allCookies)) {
|
||||
setCookie(cookieName, "", {
|
||||
path: "/",
|
||||
maxAge: 0, // This effectively deletes the cookie
|
||||
});
|
||||
}
|
||||
|
||||
throw redirect({
|
||||
href: "/",
|
||||
});
|
||||
});
|
|
@ -14,6 +14,7 @@ import { Route as rootRouteImport } from './routes/__root'
|
|||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DemoDbChatRouteImport } from './routes/demo.db-chat'
|
||||
import { ServerRoute as DemoDbChatApiServerRouteImport } from './routes/demo.db-chat-api'
|
||||
import { ServerRoute as AuthCallbackServerRouteImport } from './routes/auth.callback'
|
||||
import { ServerRoute as ApiDemoTqTodosServerRouteImport } from './routes/api.demo-tq-todos'
|
||||
import { ServerRoute as ApiDemoNamesServerRouteImport } from './routes/api.demo-names'
|
||||
|
||||
|
@ -34,6 +35,11 @@ const DemoDbChatApiServerRoute = DemoDbChatApiServerRouteImport.update({
|
|||
path: '/demo/db-chat-api',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const AuthCallbackServerRoute = AuthCallbackServerRouteImport.update({
|
||||
id: '/auth/callback',
|
||||
path: '/auth/callback',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiDemoTqTodosServerRoute = ApiDemoTqTodosServerRouteImport.update({
|
||||
id: '/api/demo-tq-todos',
|
||||
path: '/api/demo-tq-todos',
|
||||
|
@ -73,34 +79,47 @@ export interface RootRouteChildren {
|
|||
export interface FileServerRoutesByFullPath {
|
||||
'/api/demo-names': typeof ApiDemoNamesServerRoute
|
||||
'/api/demo-tq-todos': typeof ApiDemoTqTodosServerRoute
|
||||
'/auth/callback': typeof AuthCallbackServerRoute
|
||||
'/demo/db-chat-api': typeof DemoDbChatApiServerRoute
|
||||
}
|
||||
export interface FileServerRoutesByTo {
|
||||
'/api/demo-names': typeof ApiDemoNamesServerRoute
|
||||
'/api/demo-tq-todos': typeof ApiDemoTqTodosServerRoute
|
||||
'/auth/callback': typeof AuthCallbackServerRoute
|
||||
'/demo/db-chat-api': typeof DemoDbChatApiServerRoute
|
||||
}
|
||||
export interface FileServerRoutesById {
|
||||
__root__: typeof rootServerRouteImport
|
||||
'/api/demo-names': typeof ApiDemoNamesServerRoute
|
||||
'/api/demo-tq-todos': typeof ApiDemoTqTodosServerRoute
|
||||
'/auth/callback': typeof AuthCallbackServerRoute
|
||||
'/demo/db-chat-api': typeof DemoDbChatApiServerRoute
|
||||
}
|
||||
export interface FileServerRouteTypes {
|
||||
fileServerRoutesByFullPath: FileServerRoutesByFullPath
|
||||
fullPaths: '/api/demo-names' | '/api/demo-tq-todos' | '/demo/db-chat-api'
|
||||
fullPaths:
|
||||
| '/api/demo-names'
|
||||
| '/api/demo-tq-todos'
|
||||
| '/auth/callback'
|
||||
| '/demo/db-chat-api'
|
||||
fileServerRoutesByTo: FileServerRoutesByTo
|
||||
to: '/api/demo-names' | '/api/demo-tq-todos' | '/demo/db-chat-api'
|
||||
to:
|
||||
| '/api/demo-names'
|
||||
| '/api/demo-tq-todos'
|
||||
| '/auth/callback'
|
||||
| '/demo/db-chat-api'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/api/demo-names'
|
||||
| '/api/demo-tq-todos'
|
||||
| '/auth/callback'
|
||||
| '/demo/db-chat-api'
|
||||
fileServerRoutesById: FileServerRoutesById
|
||||
}
|
||||
export interface RootServerRouteChildren {
|
||||
ApiDemoNamesServerRoute: typeof ApiDemoNamesServerRoute
|
||||
ApiDemoTqTodosServerRoute: typeof ApiDemoTqTodosServerRoute
|
||||
AuthCallbackServerRoute: typeof AuthCallbackServerRoute
|
||||
DemoDbChatApiServerRoute: typeof DemoDbChatApiServerRoute
|
||||
}
|
||||
|
||||
|
@ -131,6 +150,13 @@ declare module '@tanstack/react-start/server' {
|
|||
preLoaderRoute: typeof DemoDbChatApiServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/auth/callback': {
|
||||
id: '/auth/callback'
|
||||
path: '/auth/callback'
|
||||
fullPath: '/auth/callback'
|
||||
preLoaderRoute: typeof AuthCallbackServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/demo-tq-todos': {
|
||||
id: '/api/demo-tq-todos'
|
||||
path: '/api/demo-tq-todos'
|
||||
|
@ -158,6 +184,7 @@ export const routeTree = rootRouteImport
|
|||
const rootServerRouteChildren: RootServerRouteChildren = {
|
||||
ApiDemoNamesServerRoute: ApiDemoNamesServerRoute,
|
||||
ApiDemoTqTodosServerRoute: ApiDemoTqTodosServerRoute,
|
||||
AuthCallbackServerRoute: AuthCallbackServerRoute,
|
||||
DemoDbChatApiServerRoute: DemoDbChatApiServerRoute,
|
||||
}
|
||||
export const serverRouteTree = rootServerRouteImport
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { createRouter as createTanstackRouter } from "@tanstack/react-router";
|
||||
import { routerWithQueryClient } from "@tanstack/react-router-with-query";
|
||||
import * as TanstackQuery from "./integrations/tanstack-query/root-provider";
|
||||
|
||||
// Import the generated route tree
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
export interface AppRouterContext {
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
|
||||
// Create a new router instance
|
||||
export const createRouter = () => {
|
||||
const rqContext = TanstackQuery.getContext();
|
||||
|
@ -12,7 +15,8 @@ export const createRouter = () => {
|
|||
return routerWithQueryClient(
|
||||
createTanstackRouter({
|
||||
routeTree,
|
||||
context: { ...rqContext },
|
||||
context: { ...rqContext }, //context is defined in the root route
|
||||
scrollRestoration: true,
|
||||
defaultPreload: "intent",
|
||||
Wrap: (props) => {
|
||||
return <TanstackQuery.Provider {...rqContext}>{props.children}</TanstackQuery.Provider>;
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import { TanstackDevtools } from "@tanstack/react-devtools";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { createRootRouteWithContext, HeadContent, Scripts } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import Header from "../components/Header";
|
||||
|
||||
import TanStackQueryDevtools from "../integrations/tanstack-query/devtools";
|
||||
import StoreDevtools from "../lib/demo-store-devtools";
|
||||
import type { AppRouterContext } from "../router";
|
||||
import appCss from "../styles.css?url";
|
||||
|
||||
interface MyRouterContext {
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
export const Route = createRootRouteWithContext<AppRouterContext>()({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import { createServerFileRoute } from "@tanstack/react-start/server";
|
||||
import { z } from "zod";
|
||||
import { getSupabaseServerClient } from "@/integrations/supabase/server";
|
||||
|
||||
// Define the search parameters schema for type safety
|
||||
const searchParamsSchema = z.object({
|
||||
code: z.string().optional(),
|
||||
code_challenge: z.string().optional(),
|
||||
next: z.string().optional(),
|
||||
});
|
||||
|
||||
// Type for the validated search parameters
|
||||
type SearchParams = z.infer<typeof searchParamsSchema>;
|
||||
|
||||
export const ServerRoute = createServerFileRoute("/auth/callback").methods({
|
||||
GET: async ({ request }) => {
|
||||
// Parse query parameters from the URL
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Extract and validate search parameters
|
||||
const searchParams: SearchParams = {
|
||||
code: url.searchParams.get("code") || undefined,
|
||||
code_challenge: url.searchParams.get("code_challenge") || undefined,
|
||||
next: url.searchParams.get("next") || undefined,
|
||||
};
|
||||
|
||||
// Validate the parameters (optional - provides runtime validation)
|
||||
const validatedParams = searchParamsSchema.parse(searchParams);
|
||||
|
||||
const code = validatedParams.code_challenge || validatedParams.code;
|
||||
const next = validatedParams.next;
|
||||
|
||||
if (!next) {
|
||||
return new Response(null, { status: 302, headers: { Location: "/" } });
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return new Response("Missing code exchange code", { status: 400 });
|
||||
}
|
||||
|
||||
const supabase = await getSupabaseServerClient();
|
||||
|
||||
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
||||
|
||||
if (error) {
|
||||
return new Response("Error exchanging code for session", { status: 500 });
|
||||
}
|
||||
|
||||
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||
const origin = request.headers.get("origin");
|
||||
const isLocalEnv = process.env.NODE_ENV === "development";
|
||||
|
||||
if (isLocalEnv) {
|
||||
const redirectPath = next?.startsWith("/") ? next : "/app";
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { Location: `${origin}${redirectPath}` },
|
||||
});
|
||||
}
|
||||
|
||||
if (forwardedHost) {
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { Location: `https://${forwardedHost}${next}` },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { Location: `${origin}${next}` },
|
||||
});
|
||||
},
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@buster/typescript-config/web-vite.json",
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
|
|
|
@ -5,6 +5,9 @@ import { defineConfig } from "vite";
|
|||
import viteTsConfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
const config = defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
plugins: [
|
||||
// this is the plugin that enables path aliases
|
||||
viteTsConfigPaths({
|
||||
|
|
501
pnpm-lock.yaml
501
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue