diff --git a/monorepo-example/.gitignore b/monorepo-example/.gitignore new file mode 100644 index 0000000..3a2b74e --- /dev/null +++ b/monorepo-example/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +.turbo +*.log +.next +dist +dist-ssr +*.local +.env +.cache +server/dist +public/dist +*.db \ No newline at end of file diff --git a/monorepo-example/.npmrc b/monorepo-example/.npmrc new file mode 100644 index 0000000..ee96585 --- /dev/null +++ b/monorepo-example/.npmrc @@ -0,0 +1 @@ +link-workspace-packages=true \ No newline at end of file diff --git a/monorepo-example/README.md b/monorepo-example/README.md new file mode 100644 index 0000000..f7f96c9 --- /dev/null +++ b/monorepo-example/README.md @@ -0,0 +1,21 @@ +# Better Auth Monorepo Example + +This is an example of how to use Better Auth inside a monorepo. + +**Implements the following features:** + +- Email & Password +- [Fastify auth server](apps/api) +- [NextJS app](apps/web) +- [SolidStart app](apps/dashboard) + +## How to run + +1. Clone the code sandbox (or the repo) and open it in your code editor +2. Rename all .env.example files to .env and provide necessary variables +3. Run the following commands + ```bash + pnpm install + pnpm apps:dev + ``` +4. Open the browser and navigate to `http://localhost:3000/sign-up` diff --git a/monorepo-example/apps/api/.env.example b/monorepo-example/apps/api/.env.example new file mode 100644 index 0000000..1bd4b51 --- /dev/null +++ b/monorepo-example/apps/api/.env.example @@ -0,0 +1 @@ +CLIENT_ORIGIN="http://localhost:3000,http://localhost:3001" \ No newline at end of file diff --git a/monorepo-example/apps/api/package.json b/monorepo-example/apps/api/package.json new file mode 100644 index 0000000..ce6d62a --- /dev/null +++ b/monorepo-example/apps/api/package.json @@ -0,0 +1,23 @@ +{ + "name": "@monorepo-example/api", + "version": "0.0.0", + "type": "module", + "private": true, + "scripts": { + "build": "tsdown", + "start": "node dist/index.js", + "dev": "tsdown --watch --on-success \"node dist/index.js\"" + }, + "dependencies": { + "@fastify/cors": "^11.1.0", + "@monorepo-example/auth": "workspace:*", + "dotenv": "^17.2.3", + "fastify": "^5.6.1" + }, + "devDependencies": { + "@monorepo-example/typescript-config": "workspace:*", + "@types/node": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:" + } +} diff --git a/monorepo-example/apps/api/src/index.ts b/monorepo-example/apps/api/src/index.ts new file mode 100644 index 0000000..76f1931 --- /dev/null +++ b/monorepo-example/apps/api/src/index.ts @@ -0,0 +1,69 @@ +import "dotenv/config"; +import Fastify from "fastify"; +import fastifyCors from "@fastify/cors"; +import { auth } from "@monorepo-example/auth"; + +const fastify = Fastify({ logger: true }); + +// Configure CORS policies +fastify.register(fastifyCors, { + origin: process.env.CLIENT_ORIGIN ? process.env.CLIENT_ORIGIN.split(",") : ["http://localhost:3000", "http://localhost:3001"], + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "X-Requested-With" + ], + credentials: true, + maxAge: 86400 +}); + +// Mount authentication handler after CORS registration +// Register authentication endpoint +fastify.route({ + method: ["GET", "POST"], + url: `${ + auth.options.basePath + ? auth.options.basePath.endsWith("/") + ? auth.options.basePath + : `${auth.options.basePath}` + : "/api/auth/" + }*`, + async handler(request, reply) { + try { + // Construct request URL + const url = new URL(request.url, `http://${request.headers.host}`); + // Convert Fastify headers to standard Headers object + const headers = new Headers(); + Object.entries(request.headers).forEach(([key, value]) => { + if (value) headers.append(key, value.toString()); + }); + // Create Fetch API-compatible request + const req = new Request(url.toString(), { + method: request.method, + headers, + body: request.body ? JSON.stringify(request.body) : undefined, + }); + // Process authentication request + const response = await auth.handler(req); + // Forward response to client + reply.status(response.status); + response.headers.forEach((value, key) => reply.header(key, value)); + reply.send(response.body ? await response.text() : null); + } catch (error) { + fastify.log.error(error, "Authentication Error"); + reply.status(500).send({ + error: "Internal authentication error", + code: "AUTH_FAILURE", + }); + } + }, +}); + +fastify.listen({ port: 4000 }, (err) => { + if (err) { + fastify.log.error(err); + process.exit(1); + } + console.log("Server running on port 4000"); +}); \ No newline at end of file diff --git a/monorepo-example/apps/api/tsconfig.json b/monorepo-example/apps/api/tsconfig.json new file mode 100644 index 0000000..51c7ab3 --- /dev/null +++ b/monorepo-example/apps/api/tsconfig.json @@ -0,0 +1,16 @@ +{ + "exclude": ["node_modules"], + "extends": "@monorepo-example/typescript-config/base.json", + "compilerOptions": { + "declarationMap": false, + "declaration": false, + "lib": ["es2021"], + "module": "node16", + "target": "es2021", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16" + }, +} \ No newline at end of file diff --git a/monorepo-example/apps/api/tsdown.config.ts b/monorepo-example/apps/api/tsdown.config.ts new file mode 100644 index 0000000..0c9fe1a --- /dev/null +++ b/monorepo-example/apps/api/tsdown.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig([ + { + entry: ["./src/index.ts"], + platform: "node", + dts: false, + } +]); \ No newline at end of file diff --git a/monorepo-example/apps/api/turbo.json b/monorepo-example/apps/api/turbo.json new file mode 100644 index 0000000..5709528 --- /dev/null +++ b/monorepo-example/apps/api/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "dist/**" + ] + } + } +} \ No newline at end of file diff --git a/monorepo-example/apps/dashboard/.env.example b/monorepo-example/apps/dashboard/.env.example new file mode 100644 index 0000000..c0e63eb --- /dev/null +++ b/monorepo-example/apps/dashboard/.env.example @@ -0,0 +1,2 @@ +VITE_API_URL="http://localhost:4000" +VITE_WEB_URL="http://localhost:3000" \ No newline at end of file diff --git a/monorepo-example/apps/dashboard/.gitignore b/monorepo-example/apps/dashboard/.gitignore new file mode 100644 index 0000000..751513c --- /dev/null +++ b/monorepo-example/apps/dashboard/.gitignore @@ -0,0 +1,28 @@ +dist +.wrangler +.output +.vercel +.netlify +.vinxi +app.config.timestamp_*.js + +# Environment +.env +.env*.local + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# System Files +.DS_Store +Thumbs.db diff --git a/monorepo-example/apps/dashboard/app.config.ts b/monorepo-example/apps/dashboard/app.config.ts new file mode 100644 index 0000000..a6a7ea3 --- /dev/null +++ b/monorepo-example/apps/dashboard/app.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "@solidjs/start/config"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + vite: { + plugins: [tailwindcss() as any], + }, + middleware: "src/middleware/index.ts", +}); diff --git a/monorepo-example/apps/dashboard/package.json b/monorepo-example/apps/dashboard/package.json new file mode 100644 index 0000000..d004a9a --- /dev/null +++ b/monorepo-example/apps/dashboard/package.json @@ -0,0 +1,28 @@ +{ + "name": "@monorepo-example/dashboard", + "type": "module", + "scripts": { + "dev": "vinxi dev --port 3001", + "build": "vinxi build", + "start": "vinxi start" + }, + "dependencies": { + "@kobalte/core": "^0.13.11", + "@monorepo-example/auth": "workspace:*", + "@monorepo-example/shared": "workspace:*", + "@solidjs/router": "^0.15.0", + "@solidjs/start": "^1.1.0", + "class-variance-authority": "catalog:", + "solid-js": "^1.9.5", + "tw-animate-css": "catalog:", + "vinxi": "^0.5.7" + }, + "devDependencies": { + "@tailwindcss/vite": "catalog:tailwind", + "@types/node": "catalog:", + "tailwindcss": "catalog:tailwind" + }, + "engines": { + "node": ">=22" + } +} diff --git a/monorepo-example/apps/dashboard/public/favicon.ico b/monorepo-example/apps/dashboard/public/favicon.ico new file mode 100644 index 0000000..fb282da Binary files /dev/null and b/monorepo-example/apps/dashboard/public/favicon.ico differ diff --git a/monorepo-example/apps/dashboard/src/app.css b/monorepo-example/apps/dashboard/src/app.css new file mode 100644 index 0000000..a356309 --- /dev/null +++ b/monorepo-example/apps/dashboard/src/app.css @@ -0,0 +1,123 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/monorepo-example/apps/dashboard/src/app.tsx b/monorepo-example/apps/dashboard/src/app.tsx new file mode 100644 index 0000000..d0d93a8 --- /dev/null +++ b/monorepo-example/apps/dashboard/src/app.tsx @@ -0,0 +1,19 @@ +import { Router } from "@solidjs/router"; +import { FileRoutes } from "@solidjs/start/router"; +import { Suspense } from "solid-js"; +import "./app.css"; + + +export default function App() { + return ( + ( + <> + {props.children} + + )} + > + + + ); +} diff --git a/monorepo-example/apps/dashboard/src/components/ui/avatar.tsx b/monorepo-example/apps/dashboard/src/components/ui/avatar.tsx new file mode 100644 index 0000000..0120c46 --- /dev/null +++ b/monorepo-example/apps/dashboard/src/components/ui/avatar.tsx @@ -0,0 +1,54 @@ +import type { ValidComponent } from "solid-js" +import { splitProps } from "solid-js" +import * as ImagePrimitive from "@kobalte/core/image" +import type { PolymorphicProps } from "@kobalte/core/polymorphic" +import { cn } from "@monorepo-example/shared/utils" + +type AvatarRootProps = ImagePrimitive.ImageRootProps & { + class?: string | undefined +} + +const Avatar = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as AvatarRootProps, ["class"]) + return ( + + ) +} + +type AvatarImageProps = ImagePrimitive.ImageImgProps & { + class?: string | undefined +} + +const AvatarImage = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as AvatarImageProps, ["class"]) + return +} + +type AvatarFallbackProps = + ImagePrimitive.ImageFallbackProps & { class?: string | undefined } + +const AvatarFallback = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as AvatarFallbackProps, ["class"]) + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } +export type { + AvatarRootProps, + AvatarImageProps, + AvatarFallbackProps, +} \ No newline at end of file diff --git a/monorepo-example/apps/dashboard/src/components/ui/button.tsx b/monorepo-example/apps/dashboard/src/components/ui/button.tsx new file mode 100644 index 0000000..b526dcd --- /dev/null +++ b/monorepo-example/apps/dashboard/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import { JSX, ValidComponent } from "solid-js"; +import { splitProps } from "solid-js"; +import * as ButtonPrimitive from "@kobalte/core/button"; +import type { PolymorphicProps } from "@kobalte/core"; +import type { VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import { cn } from "@monorepo-example/shared/utils"; + +const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +type ButtonProps = ButtonPrimitive.ButtonRootProps & VariantProps & { + class?: string | undefined; + children?: JSX.Element; +}; + +function Button(props: PolymorphicProps>) { + const [local, rest] = splitProps(props as ButtonProps, ["variant", "size", "class"]); + return ( + + ) +} + +export { Button, buttonVariants }; +export type { ButtonProps }; diff --git a/monorepo-example/apps/dashboard/src/components/ui/card.tsx b/monorepo-example/apps/dashboard/src/components/ui/card.tsx new file mode 100644 index 0000000..1f5587c --- /dev/null +++ b/monorepo-example/apps/dashboard/src/components/ui/card.tsx @@ -0,0 +1,42 @@ +import type { Component, ComponentProps } from "solid-js" +import { splitProps } from "solid-js" +import { cn } from "@monorepo-example/shared/utils" + +const Card: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return ( +
+ ) +} + +const CardHeader: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return
+} + +const CardTitle: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return ( +

+ ) +} + +const CardDescription: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return

+} + +const CardContent: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return

+} + +const CardFooter: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return
+} + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } \ No newline at end of file diff --git a/monorepo-example/apps/dashboard/src/entry-client.tsx b/monorepo-example/apps/dashboard/src/entry-client.tsx new file mode 100644 index 0000000..0ca4e3c --- /dev/null +++ b/monorepo-example/apps/dashboard/src/entry-client.tsx @@ -0,0 +1,4 @@ +// @refresh reload +import { mount, StartClient } from "@solidjs/start/client"; + +mount(() => , document.getElementById("app")!); diff --git a/monorepo-example/apps/dashboard/src/entry-server.tsx b/monorepo-example/apps/dashboard/src/entry-server.tsx new file mode 100644 index 0000000..58d5e7a --- /dev/null +++ b/monorepo-example/apps/dashboard/src/entry-server.tsx @@ -0,0 +1,23 @@ +// @refresh reload +import { createHandler, StartServer } from "@solidjs/start/server"; + +export default createHandler(() => { + return ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> + ); +}); diff --git a/monorepo-example/apps/dashboard/src/env.d.ts b/monorepo-example/apps/dashboard/src/env.d.ts new file mode 100644 index 0000000..a7f9098 --- /dev/null +++ b/monorepo-example/apps/dashboard/src/env.d.ts @@ -0,0 +1,8 @@ +interface ImportMetaEnv { + readonly VITE_API_URL: string | undefined; + readonly VITE_WEB_URL: string | undefined; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/monorepo-example/apps/dashboard/src/global.d.ts b/monorepo-example/apps/dashboard/src/global.d.ts new file mode 100644 index 0000000..dc6f10c --- /dev/null +++ b/monorepo-example/apps/dashboard/src/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/monorepo-example/apps/dashboard/src/lib/authClient.ts b/monorepo-example/apps/dashboard/src/lib/authClient.ts new file mode 100644 index 0000000..25ebe84 --- /dev/null +++ b/monorepo-example/apps/dashboard/src/lib/authClient.ts @@ -0,0 +1,5 @@ +import { createAuthClient } from "@monorepo-example/auth/solid"; + +export const authClient = createAuthClient(); + +export const { signIn, signUp, signOut, useSession } = authClient; diff --git a/monorepo-example/apps/dashboard/src/middleware/index.ts b/monorepo-example/apps/dashboard/src/middleware/index.ts new file mode 100644 index 0000000..63e9aa4 --- /dev/null +++ b/monorepo-example/apps/dashboard/src/middleware/index.ts @@ -0,0 +1,19 @@ +import { redirect } from "@solidjs/router"; +import { createMiddleware } from "@solidjs/start/middleware"; +import { authClient } from "~/lib/authClient"; + +export default createMiddleware({ + onRequest: async (event) => { + const { data: session } = await authClient.getSession({ + fetchOptions: { + headers: event.request.headers, + } + }); + + if (!session?.user) { + return redirect(new URL("/sign-in", import.meta.env.VITE_WEB_URL || "http://localhost:3000").toString()); + } + + event.locals.session = session; + }, +}); \ No newline at end of file diff --git a/monorepo-example/apps/dashboard/src/routes/[...404].tsx b/monorepo-example/apps/dashboard/src/routes/[...404].tsx new file mode 100644 index 0000000..03e9484 --- /dev/null +++ b/monorepo-example/apps/dashboard/src/routes/[...404].tsx @@ -0,0 +1,25 @@ +import { A } from "@solidjs/router"; + +export default function NotFound() { + return ( +
+

Not Found

+

+ Visit{" "} + + solidjs.com + {" "} + to learn how to build Solid apps. +

+

+ + Home + + {" - "} + + About Page + +

+
+ ); +} diff --git a/monorepo-example/apps/dashboard/src/routes/index.tsx b/monorepo-example/apps/dashboard/src/routes/index.tsx new file mode 100644 index 0000000..f6ab0f9 --- /dev/null +++ b/monorepo-example/apps/dashboard/src/routes/index.tsx @@ -0,0 +1,66 @@ +import { action, createAsync, query, useAction, useSubmission } from "@solidjs/router"; +import { getRequestEvent } from "solid-js/web"; +import type { Session } from "@monorepo-example/auth/types"; +import { authClient } from "~/lib/authClient"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; + +const getSession = query(async () => { + "use server"; + const event = getRequestEvent(); + return event?.locals.session as Session; +}, "session"); + +const signOut = action(async () => { + await authClient.signOut({ + fetchOptions: { + onSuccess: () => { + window.location.href = new URL( + "/sign-in", + import.meta.env.VITE_WEB_URL || "http://localhost:3000" + ).toString(); + }, + }, + }); +}, "signOut"); + +export default function Home() { + const session = createAsync(() => getSession()); + const signOutAction = useAction(signOut); + const signOutSubmission = useSubmission(signOut); + + return ( +
+ + + Welcome, {session()?.user.name} + + +
+ +
+

+ {session()?.user.name} +

+

{session()?.user.email}

+
+ +
+
+            {JSON.stringify(session(), null, 2)}
+          
+
+
+
+ ); +} diff --git a/monorepo-example/apps/dashboard/tsconfig.json b/monorepo-example/apps/dashboard/tsconfig.json new file mode 100644 index 0000000..3ad477f --- /dev/null +++ b/monorepo-example/apps/dashboard/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "noEmit": true, + "strict": true, + "types": ["vinxi/types/client"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/monorepo-example/apps/dashboard/turbo.json b/monorepo-example/apps/dashboard/turbo.json new file mode 100644 index 0000000..5709528 --- /dev/null +++ b/monorepo-example/apps/dashboard/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "dist/**" + ] + } + } +} \ No newline at end of file diff --git a/monorepo-example/apps/web/.env.example b/monorepo-example/apps/web/.env.example new file mode 100644 index 0000000..03e094b --- /dev/null +++ b/monorepo-example/apps/web/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_API_URL="http://localhost:4000" +NEXT_PUBLIC_DASHBOARD_URL="http://localhost:3001" \ No newline at end of file diff --git a/monorepo-example/apps/web/.gitignore b/monorepo-example/apps/web/.gitignore new file mode 100644 index 0000000..7b8da95 --- /dev/null +++ b/monorepo-example/apps/web/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/monorepo-example/apps/web/app/favicon.ico b/monorepo-example/apps/web/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/monorepo-example/apps/web/app/favicon.ico differ diff --git a/monorepo-example/apps/web/app/globals.css b/monorepo-example/apps/web/app/globals.css new file mode 100644 index 0000000..133a68b --- /dev/null +++ b/monorepo-example/apps/web/app/globals.css @@ -0,0 +1,123 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/monorepo-example/apps/web/app/layout.tsx b/monorepo-example/apps/web/app/layout.tsx new file mode 100644 index 0000000..7371307 --- /dev/null +++ b/monorepo-example/apps/web/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/monorepo-example/apps/web/app/page.tsx b/monorepo-example/apps/web/app/page.tsx new file mode 100644 index 0000000..721a4be --- /dev/null +++ b/monorepo-example/apps/web/app/page.tsx @@ -0,0 +1,103 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + app/page.tsx + + . +
  2. +
  3. + Save and see your changes instantly. +
  4. +
+ + +
+ +
+ ); +} diff --git a/monorepo-example/apps/web/app/sign-in/page.tsx b/monorepo-example/apps/web/app/sign-in/page.tsx new file mode 100644 index 0000000..eefbd65 --- /dev/null +++ b/monorepo-example/apps/web/app/sign-in/page.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { Button, buttonVariants } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { authClient } from "@/lib/authClient"; +import { ArrowLeftIcon, Loader2Icon } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState, useTransition } from "react"; + +export default function SignIn() { + const router = useRouter(); + const [error, setError] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [rememberMe, setRememberMe] = useState(true); + const [loading, startTransition] = useTransition(); + + const signIn = () => { + startTransition(async () => { + await authClient.signIn.email({ + email, + password, + rememberMe, + callbackURL: process.env.NEXT_PUBLIC_DASHBOARD_URL || "http://localhost:3001", + fetchOptions: { + onError: (context) => { + setError(context.error.message); + }, + onSuccess: () => { + router.replace(process.env.NEXT_PUBLIC_DASHBOARD_URL || "http://localhost:3001"); + } + }, + }); + }); + } + + return ( +
+
+ Sign In + Enter your email below to login to your account + + + Email + setEmail(e.target.value)} autoFocus /> + + + Password + setPassword(e.target.value)} /> + + + setRememberMe(!!value)} /> + Remember me + + {error !== "" && ( + {error} + )} + + + + + Sign Up + + + +
+
+ ) +} \ No newline at end of file diff --git a/monorepo-example/apps/web/app/sign-up/page.tsx b/monorepo-example/apps/web/app/sign-up/page.tsx new file mode 100644 index 0000000..a781c88 --- /dev/null +++ b/monorepo-example/apps/web/app/sign-up/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { Button, buttonVariants } from "@/components/ui/button"; +import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { authClient } from "@/lib/authClient"; +import { ArrowRightIcon, Loader2Icon } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState, useTransition } from "react"; + +export default function SignUp() { + const router = useRouter(); + const [error, setError] = useState(""); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, startTransition] = useTransition(); + + const signUp = () => { + startTransition(async () => { + await authClient.signUp.email({ + name, + email, + password, + fetchOptions: { + onError: (context) => { + setError(context.error.message); + }, + onSuccess: () => { + router.replace(process.env.NEXT_PUBLIC_DASHBOARD_URL || "http://localhost:3001"); + } + }, + }); + }); + } + + return ( +
+
+ Sign Up + Enter your information to create an account + + + Name + setName(e.target.value)} autoFocus /> + + + Email + setEmail(e.target.value)} /> + + + Password + setPassword(e.target.value)} /> + + {error !== "" && ( + {error} + )} + + + + Sign In + + + + +
+
+ ) +} diff --git a/monorepo-example/apps/web/components.json b/monorepo-example/apps/web/components.json new file mode 100644 index 0000000..29bd0b9 --- /dev/null +++ b/monorepo-example/apps/web/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@monorepo-example/shared/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/monorepo-example/apps/web/components/ui/button.tsx b/monorepo-example/apps/web/components/ui/button.tsx new file mode 100644 index 0000000..db94a0f --- /dev/null +++ b/monorepo-example/apps/web/components/ui/button.tsx @@ -0,0 +1,63 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@monorepo-example/shared/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +type ButtonProps = React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }; + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: ButtonProps) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } +export type { ButtonProps } \ No newline at end of file diff --git a/monorepo-example/apps/web/components/ui/checkbox.tsx b/monorepo-example/apps/web/components/ui/checkbox.tsx new file mode 100644 index 0000000..a8a42de --- /dev/null +++ b/monorepo-example/apps/web/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@monorepo-example/shared/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/monorepo-example/apps/web/components/ui/field.tsx b/monorepo-example/apps/web/components/ui/field.tsx new file mode 100644 index 0000000..75138bb --- /dev/null +++ b/monorepo-example/apps/web/components/ui/field.tsx @@ -0,0 +1,244 @@ +"use client" + +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@monorepo-example/shared/utils" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-slot=field-group]]:gap-4", + className + )} + {...props} + /> + ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + responsive: [ + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +