diff --git a/.env.sample b/.env.sample index d15f01a..3c06e47 100644 --- a/.env.sample +++ b/.env.sample @@ -1,4 +1,5 @@ SUPABASE_SERVICE_KEY= PUBLIC_SUPABASE_ANON_KEY= SUPABASE_URL=http://localhost:54321 -SERVER_URL=http://localhost:8788 \ No newline at end of file +SERVER_URL=http://localhost:8788 +COOKIE_SECRET_1= \ No newline at end of file diff --git a/.gitignore b/.gitignore index f93e15d..3309f77 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules /.cache /functions/\[\[path\]\].js +/functions/\[\[path\]\].js.map /public/build .env @@ -9,3 +10,4 @@ node_modules **/supabase/.branches **/supabase/.temp supabase/config.toml +.mf/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4028ffe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + // Required: The website to display + "liveFrame.url": "http://localhost:8788", + + // Optional: Title for the pane tab heading + "liveFrame.title": "Simple B2B app", + + // Optional: Which pane to open the frame in + "liveFrame.pane": "Beside", + + // "files.autoSave": "afterDelay", + // "files.autoSaveDelay": 50, + "files.associations": { + "*.css": "tailwindcss" + }, + + "editor.quickSuggestions": { + "strings": true + } + +} \ No newline at end of file diff --git a/app/auth.server.ts b/app/auth.server.ts new file mode 100644 index 0000000..84476c0 --- /dev/null +++ b/app/auth.server.ts @@ -0,0 +1,63 @@ +import type { AppLoadContext } from "@remix-run/cloudflare"; +import { + createCloudflareKVSessionStorage, + createCookie, +} from "@remix-run/cloudflare"; +import type { Session } from "@supabase/supabase-js"; +import { Authenticator, AuthorizationError } from "remix-auth"; +import { SupabaseStrategy } from "remix-auth-supabase"; +import { supabaseAdmin } from "./supabase.server"; + +export const getAuth = (context: AppLoadContext) => { + const sessionCookie = createCookie("__session", { + secrets: [context.COOKIE_SECRET_1], + sameSite: true, + }); + + const sessionStorage = createCloudflareKVSessionStorage({ + kv: context.SESSION_KV, + cookie: sessionCookie, + }); + + const authStrategy = new SupabaseStrategy( + { + supabaseClient: supabaseAdmin(context), + sessionStorage, + sessionKey: "sb:session", + sessionErrorKey: "sb:error", + }, + async ({ req }) => { + const form = await req.formData(); + const session = form?.get("session"); + if (typeof session !== "string") + throw new AuthorizationError("session not found"); + + return JSON.parse(session); + } + ); + + const authenticator = new Authenticator(sessionStorage, { + sessionKey: authStrategy.sessionKey, + sessionErrorKey: authStrategy.sessionErrorKey, + }); + + authenticator.use(authStrategy, "sb-auth"); + + return { + authenticator, + authStrategy, + }; +}; + +export const getSession = async ( + context: AppLoadContext, + request: Request, + redirectBack: boolean = true +) => { + const redirect = redirectBack + ? `?redirectTo=${new URL(request.url).pathname}` + : ""; + return getAuth(context).authStrategy.checkSession(request, { + failureRedirect: `/signin${redirect}`, + }); +}; diff --git a/app/components/darkModeToggle.tsx b/app/components/darkModeToggle.tsx new file mode 100644 index 0000000..bd6f642 --- /dev/null +++ b/app/components/darkModeToggle.tsx @@ -0,0 +1,50 @@ +import React, { useEffect, useState } from "react"; + +const updateDark = (dark: Boolean) => { + if (typeof document === "undefined") return; + const d = document.documentElement; + const themes = ["light", "dark"]; + + d.classList.remove(...themes); + d.classList.add(dark ? "dark" : "light"); + localStorage.setItem("dark", JSON.stringify(dark)); +}; + +const loadDark = () => { + if (typeof document === "undefined") return true; + const prefersDarkMode = matchMedia("(prefers-color-scheme: dark)").matches; + const lsDark = + localStorage.getItem("dark") || JSON.stringify(prefersDarkMode); + return Boolean(JSON.parse(lsDark)); +}; + +const DarkModeToggle = ({ className }: { className?: string }) => { + const [dark, setDark] = useState(loadDark()); + + useEffect(() => { + updateDark(dark); + }, [dark]); + + const toggleDark = () => { + setDark(!dark); + }; + + const moon = + "M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"; + const sun = + "M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"; + + return ( + toggleDark()} + > + + + ); +}; + +export default DarkModeToggle; diff --git a/app/custom-types.d.ts b/app/custom-types.d.ts index 861f763..2cb43f7 100644 --- a/app/custom-types.d.ts +++ b/app/custom-types.d.ts @@ -7,5 +7,7 @@ declare module '@remix-run/cloudflare' { SUPABASE_URL: string; SUPABASE_SERVICE_KEY: string; PUBLIC_SUPABASE_ANON_KEY: string; + COOKIE_SECRET_1: string; + SESSION_KV: KVNamespace; } } \ No newline at end of file diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 14f26e2..deda5c8 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,7 +1,6 @@ import type { EntryContext } from "@remix-run/cloudflare"; import { RemixServer } from "@remix-run/react"; import { renderToString } from "react-dom/server"; -import { injectStylesIntoStaticMarkup } from '@mantine/ssr'; export default function handleRequest( request: Request, @@ -15,7 +14,7 @@ export default function handleRequest( responseHeaders.set("Content-Type", "text/html"); - return new Response(`${injectStylesIntoStaticMarkup(markup)}`, { + return new Response(`${markup}`, { status: responseStatusCode, headers: responseHeaders, }); diff --git a/app/root.tsx b/app/root.tsx index 7bbbb2f..f4d4a33 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,4 +1,8 @@ -import type { LoaderFunction, MetaFunction } from "@remix-run/cloudflare"; +import type { + LinksFunction, + LoaderFunction, + MetaFunction, +} from "@remix-run/cloudflare"; import { Links, LiveReload, @@ -9,6 +13,10 @@ import { useCatch, useLoaderData, } from "@remix-run/react"; +import DarkModeToggle from "./components/darkModeToggle"; +import styles from "./tailwind.css"; + +export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }]; export const meta: MetaFunction = () => ({ charset: "utf-8", @@ -17,19 +25,18 @@ export const meta: MetaFunction = () => ({ }); export const loader: LoaderFunction = ({ context }) => { - if (!context.SUPABASE_URL) - throw new Error('SUPABASE_URL is required') + if (!context.SUPABASE_URL) throw new Error("SUPABASE_URL is required"); if (!context.PUBLIC_SUPABASE_ANON_KEY) - throw new Error('PUBLIC_SUPABASE_ANON_KEY is required') + throw new Error("PUBLIC_SUPABASE_ANON_KEY is required"); return { env: { SUPABASE_URL: context.SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY: context.PUBLIC_SUPABASE_ANON_KEY, }, - } -} + }; +}; export default function App() { const { env } = useLoaderData(); @@ -42,12 +49,11 @@ export default function App() { +