kofi-stack-template-generator 2.0.19 → 2.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -6
- package/dist/index.js +50 -68
- package/package.json +10 -10
- package/src/templates.generated.ts +18 -15
- package/templates/convex/_env.local.hbs +12 -7
- package/templates/convex/convex/auth.ts.hbs +24 -31
- package/templates/convex/convex/convex.config.ts.hbs +7 -0
- package/templates/convex/convex/http.ts.hbs +2 -1
- package/templates/convex/convex/schema.ts.hbs +4 -5
- package/templates/convex/convex/users.ts.hbs +7 -10
- package/templates/convex/package.json.hbs +2 -2
- package/templates/web/package.json.hbs +2 -2
- package/templates/web/src/app/api/auth/[...all]/route.ts.hbs +3 -0
- package/templates/web/src/components/auth/sign-in-form.tsx.hbs +15 -10
- package/templates/web/src/components/auth/sign-up-form.tsx.hbs +16 -11
- package/templates/web/src/components/dashboard/app-sidebar.tsx.hbs +3 -7
- package/templates/web/src/components/providers/convex-provider.tsx.hbs +3 -3
- package/templates/web/src/lib/auth-server.ts.hbs +6 -0
- package/templates/web/src/lib/auth.ts.hbs +13 -29
- package/templates/web/src/proxy.ts.hbs +22 -17
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
|
|
2
|
-
> kofi-stack-template-generator@2.0.
|
|
2
|
+
> kofi-stack-template-generator@2.0.21 build /Users/theodenanyoh/Documents/Krumalabs/create-kofi-stack-v2/packages/template-generator
|
|
3
3
|
> pnpm run prebuild && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> kofi-stack-template-generator@2.0.
|
|
6
|
+
> kofi-stack-template-generator@2.0.21 prebuild /Users/theodenanyoh/Documents/Krumalabs/create-kofi-stack-v2/packages/template-generator
|
|
7
7
|
> node scripts/generate-templates.js
|
|
8
8
|
|
|
9
9
|
Generating templates.generated.ts...
|
|
10
|
-
Generated templates.generated.ts with
|
|
10
|
+
Generated templates.generated.ts with 89 templates
|
|
11
11
|
CLI Building entry: src/index.ts
|
|
12
12
|
CLI Using tsconfig: tsconfig.json
|
|
13
13
|
CLI tsup v8.5.1
|
|
14
14
|
CLI Target: es2022
|
|
15
15
|
ESM Build start
|
|
16
|
-
ESM dist/index.js 99.
|
|
17
|
-
ESM ⚡️ Build success in
|
|
16
|
+
ESM dist/index.js 99.07 KB
|
|
17
|
+
ESM ⚡️ Build success in 12ms
|
|
18
18
|
DTS Build start
|
|
19
|
-
DTS ⚡️ Build success in
|
|
19
|
+
DTS ⚡️ Build success in 424ms
|
|
20
20
|
DTS dist/index.d.ts 2.96 KB
|
package/dist/index.js
CHANGED
|
@@ -243,11 +243,12 @@ function shouldIncludeFile(templatePath, config) {
|
|
|
243
243
|
var EMBEDDED_TEMPLATES = {
|
|
244
244
|
"base/_gitignore.hbs": "# Dependencies\nnode_modules\n.pnpm-store\n\n# Build outputs\n.next\ndist\n.turbo\nout\n\n# Testing\ncoverage\nplaywright-report\ntest-results\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n.DS_Store\n\n# Convex\n.convex\n\n# Vercel\n.vercel\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# TypeScript\n*.tsbuildinfo\n\n# Misc\n*.pem\n.cache\n",
|
|
245
245
|
"base/biome.json.hbs": '{\n "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",\n "organizeImports": {\n "enabled": true\n },\n "linter": {\n "enabled": true,\n "rules": {\n "recommended": true\n }\n },\n "formatter": {\n "enabled": true,\n "indentStyle": "space",\n "indentWidth": 2\n },\n "javascript": {\n "formatter": {\n "quoteStyle": "single",\n "semicolons": "asNeeded"\n }\n },\n "files": {\n "ignore": [\n "node_modules",\n ".next",\n "dist",\n ".turbo",\n "coverage",\n ".vercel",\n "_generated"\n ]\n }\n}\n',
|
|
246
|
-
"convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\n\n#
|
|
247
|
-
"convex/convex/auth.ts.hbs": "import
|
|
248
|
-
"convex/convex/
|
|
249
|
-
"convex/convex/
|
|
250
|
-
"convex/convex/
|
|
246
|
+
"convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\nNEXT_PUBLIC_CONVEX_SITE_URL=\n\n# Site URL (used for auth redirects)\nSITE_URL=http://localhost:3000\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n\n# Auth - GitHub OAuth\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n\n# Auth - Google OAuth\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\n\n# Email (Resend) - https://resend.com\nRESEND_API_KEY=\nRESEND_FROM_EMAIL=\n{{#if (eq integrations.analytics 'posthog')}}\n\n# PostHog\nNEXT_PUBLIC_POSTHOG_KEY=\nNEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\n{{/if}}\n{{#if (eq integrations.uploads 'convex-fs')}}\n\n# Convex FS - Built-in file storage (no additional config needed)\n{{/if}}\n{{#if (eq integrations.uploads 'r2')}}\n\n# Cloudflare R2\nR2_ACCESS_KEY_ID=\nR2_SECRET_ACCESS_KEY=\nR2_BUCKET=\nR2_ENDPOINT=\n{{/if}}\n{{#if (eq integrations.uploads 'uploadthing')}}\n\n# UploadThing\nUPLOADTHING_TOKEN=\n{{/if}}\n{{#if (eq integrations.uploads 's3')}}\n\n# AWS S3\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nAWS_REGION=\nAWS_S3_BUCKET=\n{{/if}}\n{{#if (eq integrations.uploads 'vercel-blob')}}\n\n# Vercel Blob\nBLOB_READ_WRITE_TOKEN=\n{{/if}}\n{{#if (eq integrations.payments 'stripe')}}\n\n# Stripe\nSTRIPE_SECRET_KEY=\nSTRIPE_WEBHOOK_SECRET=\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n{{/if}}\n{{#if (eq integrations.payments 'polar')}}\n\n# Polar\nPOLAR_ACCESS_TOKEN=\nPOLAR_WEBHOOK_SECRET=\nPOLAR_ORGANIZATION_ID=\n{{/if}}\n{{#if (includes addons 'rate-limiting')}}\n\n# Convex Rate Limiter - No additional config needed (uses Convex backend)\n{{/if}}\n{{#if (includes addons 'monitoring')}}\n\n# Sentry\nSENTRY_DSN=\nSENTRY_AUTH_TOKEN=\n{{/if}}\n",
|
|
247
|
+
"convex/convex/auth.ts.hbs": "import { BetterAuth } from '@convex-dev/better-auth'\nimport { betterAuth } from 'better-auth'\nimport { components } from './_generated/api'\n\nconst auth = new BetterAuth(components.betterAuth, {\n trustedOrigins: [process.env.SITE_URL!],\n})\n\nexport const { signIn, signUp, signOut, isAuthenticated, getSession } = auth.create(\n betterAuth({\n emailAndPassword: {\n enabled: true,\n },\n socialProviders: {\n github: {\n clientId: process.env.GITHUB_CLIENT_ID!,\n clientSecret: process.env.GITHUB_CLIENT_SECRET!,\n },\n google: {\n clientId: process.env.GOOGLE_CLIENT_ID!,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n },\n },\n })\n)\n\nexport { auth }\n",
|
|
248
|
+
"convex/convex/convex.config.ts.hbs": "import { defineApp } from 'convex/server'\nimport betterAuth from '@convex-dev/better-auth/convex.config'\n\nconst app = defineApp()\napp.use(betterAuth)\n\nexport default app\n",
|
|
249
|
+
"convex/convex/http.ts.hbs": "import { httpRouter } from 'convex/server'\nimport { auth } from './auth'\n\nconst http = httpRouter()\n\n// Register Better Auth routes\nauth.registerRoutes(http)\n\nexport default http\n",
|
|
250
|
+
"convex/convex/schema.ts.hbs": "import { defineSchema, defineTable } from 'convex/server'\nimport { v } from 'convex/values'\n\n// Better Auth manages its own tables via the betterAuth component\n// Add your custom application tables here\nexport default defineSchema({\n // Example:\n // posts: defineTable({\n // title: v.string(),\n // content: v.string(),\n // userId: v.string(), // Better Auth user ID\n // createdAt: v.number(),\n // }).index('by_user', ['userId']),\n})\n",
|
|
251
|
+
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { auth } from './auth'\n\n// Get current user from Better Auth session\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n const session = await auth.getSession(ctx)\n if (!session) return null\n return session.user\n },\n})\n\n// Alias for current user - used by dashboard components\nexport const viewer = query({\n args: {},\n handler: async (ctx) => {\n const session = await auth.getSession(ctx)\n if (!session) return null\n return session.user\n },\n})\n",
|
|
251
252
|
"convex/package.json.hbs": `{{#if (eq structure 'monorepo')}}{
|
|
252
253
|
"name": "@repo/backend",
|
|
253
254
|
"version": "0.1.0",
|
|
@@ -268,8 +269,8 @@ var EMBEDDED_TEMPLATES = {
|
|
|
268
269
|
},
|
|
269
270
|
"dependencies": {
|
|
270
271
|
"convex": "^1.25.0",
|
|
271
|
-
"@convex-dev/auth": "^0.0
|
|
272
|
-
"
|
|
272
|
+
"@convex-dev/better-auth": "^0.3.0",
|
|
273
|
+
"better-auth": "1.4.9",
|
|
273
274
|
"@convex-dev/resend": "^0.2.0"{{#if (eq integrations.uploads 'convex-fs')}},
|
|
274
275
|
"convex-fs": "^0.2.0"{{/if}}{{#if (eq integrations.uploads 'r2')}},
|
|
275
276
|
"@convex-dev/r2": "^0.8.0"{{/if}}{{#if (eq integrations.payments 'stripe')}},
|
|
@@ -610,8 +611,8 @@ export default function RootLayout({
|
|
|
610
611
|
"react": "^19.0.0",
|
|
611
612
|
"react-dom": "^19.0.0",
|
|
612
613
|
"convex": "^1.25.0",
|
|
613
|
-
"@convex-dev/auth": "^0.0
|
|
614
|
-
"
|
|
614
|
+
"@convex-dev/better-auth": "^0.3.0",
|
|
615
|
+
"better-auth": "1.4.9",
|
|
615
616
|
"@hugeicons/react": "^0.3.0",
|
|
616
617
|
"class-variance-authority": "^0.7.0",
|
|
617
618
|
"clsx": "^2.1.0",
|
|
@@ -654,6 +655,7 @@ export default function RootLayout({
|
|
|
654
655
|
"web/src/app/(auth)/layout.tsx.hbs": 'export default function AuthLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <div className="min-h-screen flex items-center justify-center bg-muted/50 p-4">\n <div className="w-full max-w-md">\n {children}\n </div>\n </div>\n )\n}\n',
|
|
655
656
|
"web/src/app/(auth)/sign-in/page.tsx.hbs": "import { SignInForm } from '@/components/auth/sign-in-form'\n\nexport default function SignInPage() {\n return <SignInForm />\n}\n",
|
|
656
657
|
"web/src/app/(auth)/sign-up/page.tsx.hbs": "import { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUpPage() {\n return <SignUpForm />\n}\n",
|
|
658
|
+
"web/src/app/api/auth/[...all]/route.ts.hbs": "import { auth } from '@/lib/auth-server'\n\nexport const { GET, POST } = auth.handlers\n",
|
|
657
659
|
"web/src/app/globals.css.hbs": '@import "tailwindcss";\n@import "tw-animate-css";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 0.625rem;\n --background: oklch(1 0 0);\n --foreground: oklch(0.145 0 0);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.145 0 0);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.145 0 0);\n --primary: oklch(0.205 0 0);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.97 0 0);\n --secondary-foreground: oklch(0.205 0 0);\n --muted: oklch(0.97 0 0);\n --muted-foreground: oklch(0.556 0 0);\n --accent: oklch(0.97 0 0);\n --accent-foreground: oklch(0.205 0 0);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.922 0 0);\n --input: oklch(0.922 0 0);\n --ring: oklch(0.708 0 0);\n --chart-1: oklch(0.646 0.222 41.116);\n --chart-2: oklch(0.6 0.118 184.704);\n --chart-3: oklch(0.398 0.07 227.392);\n --chart-4: oklch(0.828 0.189 84.429);\n --chart-5: oklch(0.769 0.188 70.08);\n --sidebar: oklch(0.985 0 0);\n --sidebar-foreground: oklch(0.145 0 0);\n --sidebar-primary: oklch(0.205 0 0);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.97 0 0);\n --sidebar-accent-foreground: oklch(0.205 0 0);\n --sidebar-border: oklch(0.922 0 0);\n --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.205 0 0);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.205 0 0);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.922 0 0);\n --primary-foreground: oklch(0.205 0 0);\n --secondary: oklch(0.269 0 0);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.269 0 0);\n --muted-foreground: oklch(0.708 0 0);\n --accent: oklch(0.269 0 0);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.556 0 0);\n --chart-1: oklch(0.488 0.243 264.376);\n --chart-2: oklch(0.696 0.17 162.48);\n --chart-3: oklch(0.769 0.188 70.08);\n --chart-4: oklch(0.627 0.265 303.9);\n --chart-5: oklch(0.645 0.246 16.439);\n --sidebar: oklch(0.205 0 0);\n --sidebar-foreground: oklch(0.985 0 0);\n --sidebar-primary: oklch(0.488 0.243 264.376);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.269 0 0);\n --sidebar-accent-foreground: oklch(0.985 0 0);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n }\n body {\n @apply bg-background text-foreground;\n }\n}\n',
|
|
658
660
|
"web/src/app/layout.tsx.hbs": "import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server'\nimport { ConvexClientProvider } from '@/components/providers/convex-provider'\n{{#if (eq integrations.analytics 'posthog')}}\nimport { PostHogProvider } from '@/components/providers/posthog-provider'\n{{/if}}\n{{#if (eq integrations.analytics 'vercel')}}\nimport { Analytics } from '@vercel/analytics/react'\nimport { SpeedInsights } from '@vercel/speed-insights/next'\n{{/if}}\nimport './globals.css'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '{{projectName}}',\n description: 'Built with create-kofi-stack',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsServerProvider>\n <html lang=\"en\" suppressHydrationWarning>\n <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>\n <ConvexClientProvider>\n {{#if (eq integrations.analytics 'posthog')}}\n <PostHogProvider>\n {children}\n </PostHogProvider>\n {{else}}\n {children}\n {{/if}}\n </ConvexClientProvider>\n {{#if (eq integrations.analytics 'vercel')}}\n <Analytics />\n <SpeedInsights />\n {{/if}}\n </body>\n </html>\n </ConvexAuthNextjsServerProvider>\n )\n}\n",
|
|
659
661
|
"web/src/app/page.tsx.hbs": `'use client'
|
|
@@ -790,7 +792,7 @@ export default function HomePage() {
|
|
|
790
792
|
import { useState } from 'react'
|
|
791
793
|
import { useRouter } from 'next/navigation'
|
|
792
794
|
import Link from 'next/link'
|
|
793
|
-
import {
|
|
795
|
+
import { signIn } from '@/lib/auth'
|
|
794
796
|
import { Button } from '@/components/ui/button'
|
|
795
797
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
796
798
|
import { Input } from '@/components/ui/input'
|
|
@@ -799,7 +801,6 @@ import { Separator } from '@/components/ui/separator'
|
|
|
799
801
|
|
|
800
802
|
export function SignInForm() {
|
|
801
803
|
const router = useRouter()
|
|
802
|
-
const { signIn } = useAuthActions()
|
|
803
804
|
const [email, setEmail] = useState('')
|
|
804
805
|
const [password, setPassword] = useState('')
|
|
805
806
|
const [isLoading, setIsLoading] = useState(false)
|
|
@@ -811,13 +812,16 @@ export function SignInForm() {
|
|
|
811
812
|
setError(null)
|
|
812
813
|
|
|
813
814
|
try {
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
815
|
+
const result = await signIn.email({
|
|
816
|
+
email,
|
|
817
|
+
password,
|
|
818
|
+
})
|
|
818
819
|
|
|
819
|
-
|
|
820
|
-
|
|
820
|
+
if (result.error) {
|
|
821
|
+
setError(result.error.message || 'Invalid email or password')
|
|
822
|
+
} else {
|
|
823
|
+
router.push('/')
|
|
824
|
+
}
|
|
821
825
|
} catch (err) {
|
|
822
826
|
setError('Invalid email or password')
|
|
823
827
|
} finally {
|
|
@@ -825,8 +829,11 @@ export function SignInForm() {
|
|
|
825
829
|
}
|
|
826
830
|
}
|
|
827
831
|
|
|
828
|
-
const handleSocialSignIn = (provider: 'github' | 'google') => {
|
|
829
|
-
|
|
832
|
+
const handleSocialSignIn = async (provider: 'github' | 'google') => {
|
|
833
|
+
await signIn.social({
|
|
834
|
+
provider,
|
|
835
|
+
callbackURL: '/',
|
|
836
|
+
})
|
|
830
837
|
}
|
|
831
838
|
|
|
832
839
|
return (
|
|
@@ -912,7 +919,7 @@ export function SignInForm() {
|
|
|
912
919
|
import { useState } from 'react'
|
|
913
920
|
import { useRouter } from 'next/navigation'
|
|
914
921
|
import Link from 'next/link'
|
|
915
|
-
import {
|
|
922
|
+
import { signUp, signIn } from '@/lib/auth'
|
|
916
923
|
import { Button } from '@/components/ui/button'
|
|
917
924
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
918
925
|
import { Input } from '@/components/ui/input'
|
|
@@ -921,7 +928,6 @@ import { Separator } from '@/components/ui/separator'
|
|
|
921
928
|
|
|
922
929
|
export function SignUpForm() {
|
|
923
930
|
const router = useRouter()
|
|
924
|
-
const { signIn } = useAuthActions()
|
|
925
931
|
const [name, setName] = useState('')
|
|
926
932
|
const [email, setEmail] = useState('')
|
|
927
933
|
const [password, setPassword] = useState('')
|
|
@@ -940,14 +946,17 @@ export function SignUpForm() {
|
|
|
940
946
|
}
|
|
941
947
|
|
|
942
948
|
try {
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
949
|
+
const result = await signUp.email({
|
|
950
|
+
email,
|
|
951
|
+
password,
|
|
952
|
+
name,
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
if (result.error) {
|
|
956
|
+
setError(result.error.message || 'Failed to create account')
|
|
957
|
+
} else {
|
|
958
|
+
router.push('/')
|
|
959
|
+
}
|
|
951
960
|
} catch (err) {
|
|
952
961
|
setError('Failed to create account. Email may already be in use.')
|
|
953
962
|
} finally {
|
|
@@ -955,8 +964,11 @@ export function SignUpForm() {
|
|
|
955
964
|
}
|
|
956
965
|
}
|
|
957
966
|
|
|
958
|
-
const handleSocialSignIn = (provider: 'github' | 'google') => {
|
|
959
|
-
|
|
967
|
+
const handleSocialSignIn = async (provider: 'github' | 'google') => {
|
|
968
|
+
await signIn.social({
|
|
969
|
+
provider,
|
|
970
|
+
callbackURL: '/',
|
|
971
|
+
})
|
|
960
972
|
}
|
|
961
973
|
|
|
962
974
|
return (
|
|
@@ -1052,8 +1064,6 @@ export function SignUpForm() {
|
|
|
1052
1064
|
"web/src/components/dashboard/app-sidebar.tsx.hbs": `'use client'
|
|
1053
1065
|
|
|
1054
1066
|
import {
|
|
1055
|
-
AudioWaveform,
|
|
1056
|
-
Command,
|
|
1057
1067
|
GalleryVerticalEnd,
|
|
1058
1068
|
Home,
|
|
1059
1069
|
Settings,
|
|
@@ -1062,9 +1072,7 @@ import {
|
|
|
1062
1072
|
User,
|
|
1063
1073
|
} from 'lucide-react'
|
|
1064
1074
|
import { useRouter } from 'next/navigation'
|
|
1065
|
-
import {
|
|
1066
|
-
import { useQuery } from 'convex/react'
|
|
1067
|
-
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'
|
|
1075
|
+
import { signOut, useSession } from '@/lib/auth'
|
|
1068
1076
|
import {
|
|
1069
1077
|
Sidebar,
|
|
1070
1078
|
SidebarContent,
|
|
@@ -1101,8 +1109,8 @@ const navigation = [
|
|
|
1101
1109
|
|
|
1102
1110
|
export function AppSidebar() {
|
|
1103
1111
|
const router = useRouter()
|
|
1104
|
-
const {
|
|
1105
|
-
const user =
|
|
1112
|
+
const { data: session } = useSession()
|
|
1113
|
+
const user = session?.user
|
|
1106
1114
|
|
|
1107
1115
|
const handleSignOut = async () => {
|
|
1108
1116
|
await signOut()
|
|
@@ -1252,37 +1260,11 @@ export function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayo
|
|
|
1252
1260
|
)
|
|
1253
1261
|
}
|
|
1254
1262
|
`,
|
|
1255
|
-
"web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport {
|
|
1256
|
-
"web/src/lib/auth.ts.hbs": "
|
|
1263
|
+
"web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport { ConvexBetterAuthProvider } from '@convex-dev/better-auth/react'\nimport { ConvexReactClient } from 'convex/react'\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)\n\nexport function ConvexClientProvider({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexBetterAuthProvider client={convex}>\n {children}\n </ConvexBetterAuthProvider>\n )\n}\n",
|
|
1264
|
+
"web/src/lib/auth-server.ts.hbs": "import { convexBetterAuthNextJs } from '@convex-dev/better-auth/nextjs'\n\nexport const { auth, getSession, signIn, signOut } = convexBetterAuthNextJs({\n convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,\n siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,\n})\n",
|
|
1265
|
+
"web/src/lib/auth.ts.hbs": "'use client'\n\nimport { createAuthClient } from 'better-auth/react'\nimport { convexClient } from '@convex-dev/better-auth/client'\n\nexport const authClient = createAuthClient({\n baseURL: process.env.NEXT_PUBLIC_SITE_URL,\n plugins: [convexClient()],\n})\n\nexport const {\n signIn,\n signUp,\n signOut,\n useSession,\n getSession,\n} = authClient\n",
|
|
1257
1266
|
"web/src/lib/utils.ts.hbs": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
1258
|
-
"web/src/proxy.ts.hbs":
|
|
1259
|
-
convexAuthNextjsMiddleware,
|
|
1260
|
-
createRouteMatcher,
|
|
1261
|
-
nextjsMiddlewareRedirect,
|
|
1262
|
-
} from '@convex-dev/auth/nextjs/server'
|
|
1263
|
-
|
|
1264
|
-
const isPublicRoute = createRouteMatcher(['/sign-in', '/sign-up'])
|
|
1265
|
-
|
|
1266
|
-
// Next.js 16 uses proxy.ts (renamed from middleware.ts)
|
|
1267
|
-
// @convex-dev/auth still uses "middleware" naming in exports
|
|
1268
|
-
export default convexAuthNextjsMiddleware(async (request, { convexAuth }) => {
|
|
1269
|
-
const isAuthenticated = await convexAuth.isAuthenticated()
|
|
1270
|
-
|
|
1271
|
-
// Redirect unauthenticated users to /sign-up
|
|
1272
|
-
if (!isPublicRoute(request) && !isAuthenticated) {
|
|
1273
|
-
return nextjsMiddlewareRedirect(request, '/sign-up')
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// Redirect authenticated users from auth pages to / (dashboard)
|
|
1277
|
-
if (isPublicRoute(request) && isAuthenticated) {
|
|
1278
|
-
return nextjsMiddlewareRedirect(request, '/')
|
|
1279
|
-
}
|
|
1280
|
-
})
|
|
1281
|
-
|
|
1282
|
-
export const config = {
|
|
1283
|
-
matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
|
|
1284
|
-
}
|
|
1285
|
-
`,
|
|
1267
|
+
"web/src/proxy.ts.hbs": "import { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\nimport { auth } from '@/lib/auth-server'\n\nconst publicRoutes = ['/sign-in', '/sign-up', '/api/auth']\n\nfunction isPublicRoute(pathname: string) {\n return publicRoutes.some(route => pathname.startsWith(route))\n}\n\nexport async function proxy(request: NextRequest) {\n const { pathname } = request.nextUrl\n\n // Allow public routes\n if (isPublicRoute(pathname)) {\n return NextResponse.next()\n }\n\n // Check authentication\n const session = await auth.getSession()\n\n // Redirect unauthenticated users to /sign-up\n if (!session) {\n return NextResponse.redirect(new URL('/sign-up', request.url))\n }\n\n return NextResponse.next()\n}\n\nexport const config = {\n matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'],\n}\n",
|
|
1286
1268
|
"web/tsconfig.json.hbs": '{\n "compilerOptions": {\n "target": "ES2017",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./src/*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n'
|
|
1287
1269
|
};
|
|
1288
1270
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kofi-stack-template-generator",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -10,20 +10,20 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "pnpm run prebuild && tsup src/index.ts --format esm --dts",
|
|
15
|
-
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
16
|
-
"prebuild": "node scripts/generate-templates.js",
|
|
17
|
-
"typecheck": "tsc --noEmit"
|
|
18
|
-
},
|
|
19
13
|
"dependencies": {
|
|
20
|
-
"kofi-stack-types": "^2.0.3",
|
|
21
14
|
"handlebars": "^4.7.8",
|
|
22
|
-
"memfs": "^4.9.0"
|
|
15
|
+
"memfs": "^4.9.0",
|
|
16
|
+
"kofi-stack-types": "^2.0.21"
|
|
23
17
|
},
|
|
24
18
|
"devDependencies": {
|
|
25
19
|
"@types/node": "^20.0.0",
|
|
26
20
|
"tsup": "^8.0.0",
|
|
27
21
|
"typescript": "^5.0.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "pnpm run prebuild && tsup src/index.ts --format esm --dts",
|
|
25
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
26
|
+
"prebuild": "node scripts/generate-templates.js",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
28
|
}
|
|
29
|
-
}
|
|
29
|
+
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// Auto-generated file. Do not edit manually.
|
|
2
2
|
// Run 'pnpm prebuild' to regenerate.
|
|
3
|
-
// Generated: 2026-01-14T02:
|
|
4
|
-
// Template count:
|
|
3
|
+
// Generated: 2026-01-14T02:26:03.517Z
|
|
4
|
+
// Template count: 89
|
|
5
5
|
|
|
6
6
|
export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
7
7
|
"base/_gitignore.hbs": "# Dependencies\nnode_modules\n.pnpm-store\n\n# Build outputs\n.next\ndist\n.turbo\nout\n\n# Testing\ncoverage\nplaywright-report\ntest-results\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n.DS_Store\n\n# Convex\n.convex\n\n# Vercel\n.vercel\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# TypeScript\n*.tsbuildinfo\n\n# Misc\n*.pem\n.cache\n",
|
|
8
8
|
"base/biome.json.hbs": "{\n \"$schema\": \"https://biomejs.dev/schemas/1.9.4/schema.json\",\n \"organizeImports\": {\n \"enabled\": true\n },\n \"linter\": {\n \"enabled\": true,\n \"rules\": {\n \"recommended\": true\n }\n },\n \"formatter\": {\n \"enabled\": true,\n \"indentStyle\": \"space\",\n \"indentWidth\": 2\n },\n \"javascript\": {\n \"formatter\": {\n \"quoteStyle\": \"single\",\n \"semicolons\": \"asNeeded\"\n }\n },\n \"files\": {\n \"ignore\": [\n \"node_modules\",\n \".next\",\n \"dist\",\n \".turbo\",\n \"coverage\",\n \".vercel\",\n \"_generated\"\n ]\n }\n}\n",
|
|
9
|
-
"convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\n\n#
|
|
10
|
-
"convex/convex/auth.ts.hbs": "import
|
|
11
|
-
"convex/convex/
|
|
12
|
-
"convex/convex/
|
|
13
|
-
"convex/convex/
|
|
14
|
-
"convex/
|
|
9
|
+
"convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\nNEXT_PUBLIC_CONVEX_SITE_URL=\n\n# Site URL (used for auth redirects)\nSITE_URL=http://localhost:3000\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n\n# Auth - GitHub OAuth\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n\n# Auth - Google OAuth\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\n\n# Email (Resend) - https://resend.com\nRESEND_API_KEY=\nRESEND_FROM_EMAIL=\n{{#if (eq integrations.analytics 'posthog')}}\n\n# PostHog\nNEXT_PUBLIC_POSTHOG_KEY=\nNEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\n{{/if}}\n{{#if (eq integrations.uploads 'convex-fs')}}\n\n# Convex FS - Built-in file storage (no additional config needed)\n{{/if}}\n{{#if (eq integrations.uploads 'r2')}}\n\n# Cloudflare R2\nR2_ACCESS_KEY_ID=\nR2_SECRET_ACCESS_KEY=\nR2_BUCKET=\nR2_ENDPOINT=\n{{/if}}\n{{#if (eq integrations.uploads 'uploadthing')}}\n\n# UploadThing\nUPLOADTHING_TOKEN=\n{{/if}}\n{{#if (eq integrations.uploads 's3')}}\n\n# AWS S3\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nAWS_REGION=\nAWS_S3_BUCKET=\n{{/if}}\n{{#if (eq integrations.uploads 'vercel-blob')}}\n\n# Vercel Blob\nBLOB_READ_WRITE_TOKEN=\n{{/if}}\n{{#if (eq integrations.payments 'stripe')}}\n\n# Stripe\nSTRIPE_SECRET_KEY=\nSTRIPE_WEBHOOK_SECRET=\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n{{/if}}\n{{#if (eq integrations.payments 'polar')}}\n\n# Polar\nPOLAR_ACCESS_TOKEN=\nPOLAR_WEBHOOK_SECRET=\nPOLAR_ORGANIZATION_ID=\n{{/if}}\n{{#if (includes addons 'rate-limiting')}}\n\n# Convex Rate Limiter - No additional config needed (uses Convex backend)\n{{/if}}\n{{#if (includes addons 'monitoring')}}\n\n# Sentry\nSENTRY_DSN=\nSENTRY_AUTH_TOKEN=\n{{/if}}\n",
|
|
10
|
+
"convex/convex/auth.ts.hbs": "import { BetterAuth } from '@convex-dev/better-auth'\nimport { betterAuth } from 'better-auth'\nimport { components } from './_generated/api'\n\nconst auth = new BetterAuth(components.betterAuth, {\n trustedOrigins: [process.env.SITE_URL!],\n})\n\nexport const { signIn, signUp, signOut, isAuthenticated, getSession } = auth.create(\n betterAuth({\n emailAndPassword: {\n enabled: true,\n },\n socialProviders: {\n github: {\n clientId: process.env.GITHUB_CLIENT_ID!,\n clientSecret: process.env.GITHUB_CLIENT_SECRET!,\n },\n google: {\n clientId: process.env.GOOGLE_CLIENT_ID!,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n },\n },\n })\n)\n\nexport { auth }\n",
|
|
11
|
+
"convex/convex/convex.config.ts.hbs": "import { defineApp } from 'convex/server'\nimport betterAuth from '@convex-dev/better-auth/convex.config'\n\nconst app = defineApp()\napp.use(betterAuth)\n\nexport default app\n",
|
|
12
|
+
"convex/convex/http.ts.hbs": "import { httpRouter } from 'convex/server'\nimport { auth } from './auth'\n\nconst http = httpRouter()\n\n// Register Better Auth routes\nauth.registerRoutes(http)\n\nexport default http\n",
|
|
13
|
+
"convex/convex/schema.ts.hbs": "import { defineSchema, defineTable } from 'convex/server'\nimport { v } from 'convex/values'\n\n// Better Auth manages its own tables via the betterAuth component\n// Add your custom application tables here\nexport default defineSchema({\n // Example:\n // posts: defineTable({\n // title: v.string(),\n // content: v.string(),\n // userId: v.string(), // Better Auth user ID\n // createdAt: v.number(),\n // }).index('by_user', ['userId']),\n})\n",
|
|
14
|
+
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { auth } from './auth'\n\n// Get current user from Better Auth session\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n const session = await auth.getSession(ctx)\n if (!session) return null\n return session.user\n },\n})\n\n// Alias for current user - used by dashboard components\nexport const viewer = query({\n args: {},\n handler: async (ctx) => {\n const session = await auth.getSession(ctx)\n if (!session) return null\n return session.user\n },\n})\n",
|
|
15
|
+
"convex/package.json.hbs": "{{#if (eq structure 'monorepo')}}{\n \"name\": \"@repo/backend\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"main\": \"./convex/_generated/api.js\",\n \"types\": \"./convex/_generated/api.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./convex/_generated/api.js\",\n \"types\": \"./convex/_generated/api.d.ts\"\n }\n },\n \"scripts\": {\n \"dev\": \"convex dev\",\n \"dev:setup\": \"convex dev --configure --until-success\",\n \"deploy\": \"convex deploy\"\n },\n \"dependencies\": {\n \"convex\": \"^1.25.0\",\n \"@convex-dev/better-auth\": \"^0.3.0\",\n \"better-auth\": \"1.4.9\",\n \"@convex-dev/resend\": \"^0.2.0\"{{#if (eq integrations.uploads 'convex-fs')}},\n \"convex-fs\": \"^0.2.0\"{{/if}}{{#if (eq integrations.uploads 'r2')}},\n \"@convex-dev/r2\": \"^0.8.0\"{{/if}}{{#if (eq integrations.payments 'stripe')}},\n \"@convex-dev/stripe\": \"^0.1.0\"{{/if}}{{#if (eq integrations.payments 'polar')}},\n \"@convex-dev/polar\": \"^0.7.0\"{{/if}}{{#if (includes addons 'rate-limiting')}},\n \"@convex-dev/rate-limiter\": \"^0.3.0\"{{/if}}\n },\n \"devDependencies\": {\n \"typescript\": \"^5.0.0\"\n }\n}{{/if}}\n",
|
|
15
16
|
"convex/tsconfig.json.hbs": "{{#if (eq structure 'monorepo')}}{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"noEmit\": true,\n \"outDir\": \"dist\"\n },\n \"include\": [\"convex/**/*.ts\"],\n \"exclude\": [\"node_modules\"]\n}{{/if}}\n",
|
|
16
17
|
"integrations/posthog/src/components/providers/posthog-provider.tsx.hbs": "'use client'\n\nimport posthog from 'posthog-js'\nimport { PostHogProvider as PHProvider } from 'posthog-js/react'\nimport { useEffect } from 'react'\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {\n api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',\n person_profiles: 'identified_only',\n capture_pageview: false, // We capture pageviews manually\n })\n }, [])\n\n return <PHProvider client={posthog}>{children}</PHProvider>\n}\n",
|
|
17
18
|
"marketing/nextjs/next.config.ts.hbs": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n transpilePackages: ['@repo/ui'],\n}\n\nexport default nextConfig\n",
|
|
@@ -73,21 +74,23 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
|
73
74
|
"packages/ui/tsconfig.json.hbs": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n \"strict\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"react-jsx\",\n \"incremental\": true,\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n }\n },\n \"include\": [\"src/**/*\"],\n \"exclude\": [\"node_modules\"]\n}\n",
|
|
74
75
|
"web/components.json.hbs": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {\n \"config\": \"\",\n \"css\": \"src/app/globals.css\",\n \"baseColor\": \"{{shadcn.baseColor}}\",\n \"cssVariables\": true\n },\n \"iconLibrary\": \"{{shadcn.iconLibrary}}\",\n \"aliases\": {\n \"components\": \"@/components\",\n \"utils\": \"@/lib/utils\",\n \"ui\": \"@/components/ui\",\n \"lib\": \"@/lib\",\n \"hooks\": \"@/hooks\"\n }\n}\n",
|
|
75
76
|
"web/next.config.ts.hbs": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n{{#if (eq structure 'monorepo')}}\n transpilePackages: ['@repo/ui', '@repo/backend'],\n{{/if}}\n}\n\nexport default nextConfig\n",
|
|
76
|
-
"web/package.json.hbs": "{\n \"name\": \"{{#if (eq structure 'monorepo')}}@repo/web{{else}}{{projectName}}{{/if}}\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"{{#if (eq structure 'monorepo')}}next dev --turbopack{{else}}node scripts/dev.mjs{{/if}}\",\n \"dev:next\": \"next dev --turbopack\",\n{{#unless (eq structure 'monorepo')}} \"dev:setup\": \"npx convex dev --configure --until-success\",\n{{/unless}} \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:e2e\": \"playwright test\"\n },\n \"dependencies\": {\n{{#if (eq structure 'monorepo')}} \"@repo/backend\": \"workspace:*\",\n{{/if}} \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"convex\": \"^1.25.0\",\n \"@convex-dev/auth\": \"^0.0
|
|
77
|
+
"web/package.json.hbs": "{\n \"name\": \"{{#if (eq structure 'monorepo')}}@repo/web{{else}}{{projectName}}{{/if}}\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"{{#if (eq structure 'monorepo')}}next dev --turbopack{{else}}node scripts/dev.mjs{{/if}}\",\n \"dev:next\": \"next dev --turbopack\",\n{{#unless (eq structure 'monorepo')}} \"dev:setup\": \"npx convex dev --configure --until-success\",\n{{/unless}} \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:e2e\": \"playwright test\"\n },\n \"dependencies\": {\n{{#if (eq structure 'monorepo')}} \"@repo/backend\": \"workspace:*\",\n{{/if}} \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"convex\": \"^1.25.0\",\n \"@convex-dev/better-auth\": \"^0.3.0\",\n \"better-auth\": \"1.4.9\",\n \"@hugeicons/react\": \"^0.3.0\",\n \"class-variance-authority\": \"^0.7.0\",\n \"clsx\": \"^2.1.0\",\n \"tailwind-merge\": \"^2.5.0\",\n \"tw-animate-css\": \"^1.3.0\",\n \"resend\": \"^4.0.0\",\n \"react-email\": \"^3.0.0\",\n \"@react-email/components\": \"^0.0.36\"{{#if (eq integrations.analytics 'posthog')}},\n \"posthog-js\": \"^1.200.0\",\n \"posthog-node\": \"^5.0.0\"{{/if}}{{#if (eq integrations.analytics 'vercel')}},\n \"@vercel/analytics\": \"^1.4.0\",\n \"@vercel/speed-insights\": \"^1.1.0\"{{/if}}{{#if (eq integrations.uploads 'uploadthing')}},\n \"uploadthing\": \"^7.0.0\",\n \"@uploadthing/react\": \"^7.0.0\"{{/if}}{{#if (eq integrations.uploads 's3')}},\n \"@aws-sdk/client-s3\": \"^3.700.0\",\n \"@aws-sdk/s3-request-presigner\": \"^3.700.0\"{{/if}}{{#if (eq integrations.uploads 'vercel-blob')}},\n \"@vercel/blob\": \"^2.0.0\"{{/if}}{{#if (includes addons 'rate-limiting')}},\n \"@arcjet/next\": \"^1.0.0-beta.16\"{{/if}}{{#if (includes addons 'monitoring')}},\n \"@sentry/nextjs\": \"^8.0.0\"{{/if}}\n },\n \"devDependencies\": {\n{{#if (eq structure 'monorepo')}} \"@repo/config-typescript\": \"workspace:*\",\n{{/if}} \"@types/node\": \"^20.0.0\",\n \"@types/react\": \"^19.0.0\",\n \"@types/react-dom\": \"^19.0.0\",\n \"tailwindcss\": \"^4.0.0\",\n \"@tailwindcss/postcss\": \"^4.0.0\",\n \"postcss\": \"^8.4.0\",\n \"typescript\": \"^5.0.0\",\n \"vitest\": \"^3.0.0\",\n \"@vitejs/plugin-react\": \"^4.3.0\",\n \"@testing-library/react\": \"^16.0.0\",\n \"jsdom\": \"^26.0.0\",\n \"playwright\": \"^1.50.0\",\n \"@playwright/test\": \"^1.50.0\"\n }\n}\n",
|
|
77
78
|
"web/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
|
|
78
79
|
"web/src/app/(auth)/layout.tsx.hbs": "export default function AuthLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-muted/50 p-4\">\n <div className=\"w-full max-w-md\">\n {children}\n </div>\n </div>\n )\n}\n",
|
|
79
80
|
"web/src/app/(auth)/sign-in/page.tsx.hbs": "import { SignInForm } from '@/components/auth/sign-in-form'\n\nexport default function SignInPage() {\n return <SignInForm />\n}\n",
|
|
80
81
|
"web/src/app/(auth)/sign-up/page.tsx.hbs": "import { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUpPage() {\n return <SignUpForm />\n}\n",
|
|
82
|
+
"web/src/app/api/auth/[...all]/route.ts.hbs": "import { auth } from '@/lib/auth-server'\n\nexport const { GET, POST } = auth.handlers\n",
|
|
81
83
|
"web/src/app/globals.css.hbs": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 0.625rem;\n --background: oklch(1 0 0);\n --foreground: oklch(0.145 0 0);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.145 0 0);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.145 0 0);\n --primary: oklch(0.205 0 0);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.97 0 0);\n --secondary-foreground: oklch(0.205 0 0);\n --muted: oklch(0.97 0 0);\n --muted-foreground: oklch(0.556 0 0);\n --accent: oklch(0.97 0 0);\n --accent-foreground: oklch(0.205 0 0);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.922 0 0);\n --input: oklch(0.922 0 0);\n --ring: oklch(0.708 0 0);\n --chart-1: oklch(0.646 0.222 41.116);\n --chart-2: oklch(0.6 0.118 184.704);\n --chart-3: oklch(0.398 0.07 227.392);\n --chart-4: oklch(0.828 0.189 84.429);\n --chart-5: oklch(0.769 0.188 70.08);\n --sidebar: oklch(0.985 0 0);\n --sidebar-foreground: oklch(0.145 0 0);\n --sidebar-primary: oklch(0.205 0 0);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.97 0 0);\n --sidebar-accent-foreground: oklch(0.205 0 0);\n --sidebar-border: oklch(0.922 0 0);\n --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.205 0 0);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.205 0 0);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.922 0 0);\n --primary-foreground: oklch(0.205 0 0);\n --secondary: oklch(0.269 0 0);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.269 0 0);\n --muted-foreground: oklch(0.708 0 0);\n --accent: oklch(0.269 0 0);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.556 0 0);\n --chart-1: oklch(0.488 0.243 264.376);\n --chart-2: oklch(0.696 0.17 162.48);\n --chart-3: oklch(0.769 0.188 70.08);\n --chart-4: oklch(0.627 0.265 303.9);\n --chart-5: oklch(0.645 0.246 16.439);\n --sidebar: oklch(0.205 0 0);\n --sidebar-foreground: oklch(0.985 0 0);\n --sidebar-primary: oklch(0.488 0.243 264.376);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.269 0 0);\n --sidebar-accent-foreground: oklch(0.985 0 0);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n }\n body {\n @apply bg-background text-foreground;\n }\n}\n",
|
|
82
84
|
"web/src/app/layout.tsx.hbs": "import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server'\nimport { ConvexClientProvider } from '@/components/providers/convex-provider'\n{{#if (eq integrations.analytics 'posthog')}}\nimport { PostHogProvider } from '@/components/providers/posthog-provider'\n{{/if}}\n{{#if (eq integrations.analytics 'vercel')}}\nimport { Analytics } from '@vercel/analytics/react'\nimport { SpeedInsights } from '@vercel/speed-insights/next'\n{{/if}}\nimport './globals.css'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '{{projectName}}',\n description: 'Built with create-kofi-stack',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsServerProvider>\n <html lang=\"en\" suppressHydrationWarning>\n <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>\n <ConvexClientProvider>\n {{#if (eq integrations.analytics 'posthog')}}\n <PostHogProvider>\n {children}\n </PostHogProvider>\n {{else}}\n {children}\n {{/if}}\n </ConvexClientProvider>\n {{#if (eq integrations.analytics 'vercel')}}\n <Analytics />\n <SpeedInsights />\n {{/if}}\n </body>\n </html>\n </ConvexAuthNextjsServerProvider>\n )\n}\n",
|
|
83
85
|
"web/src/app/page.tsx.hbs": "'use client'\n\nimport { useQuery } from 'convex/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../convex/_generated/api{{/if}}'\nimport { DashboardLayout } from '@/components/dashboard/dashboard-layout'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Skeleton } from '@/components/ui/skeleton'\n\nexport default function HomePage() {\n const user = useQuery(api.users.viewer)\n\n if (user === undefined) {\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <Skeleton className=\"h-8 w-64\" />\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n </DashboardLayout>\n )\n }\n\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <div>\n <h1 className=\"text-3xl font-bold tracking-tight\">\n Welcome back{user?.name ? `, ${user.name}` : ''}!\n </h1>\n <p className=\"text-muted-foreground\">\n Here's what's happening with your project today.\n </p>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Card>\n <CardHeader>\n <CardTitle>Getting Started</CardTitle>\n <CardDescription>Quick start guide for your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Customize your dashboard layout</li>\n <li>Add new pages to the sidebar</li>\n <li>Connect your data sources</li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Documentation</CardTitle>\n <CardDescription>Learn how to build with Kofi Stack</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>\n <a\n href=\"https://docs.convex.dev\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Convex Documentation\n </a>\n </li>\n <li>\n <a\n href=\"https://ui.shadcn.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n shadcn/ui Components\n </a>\n </li>\n <li>\n <a\n href=\"https://nextjs.org/docs\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Next.js Documentation\n </a>\n </li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Your Stack</CardTitle>\n <CardDescription>Technologies powering your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Next.js 15 with App Router</li>\n <li>Convex for backend & database</li>\n <li>Convex Auth for authentication</li>\n <li>shadcn/ui components</li>\n <li>Tailwind CSS for styling</li>\n </ul>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground\">\n Created with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-primary hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </DashboardLayout>\n )\n}\n",
|
|
84
|
-
"web/src/components/auth/sign-in-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport {
|
|
85
|
-
"web/src/components/auth/sign-up-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport {
|
|
86
|
-
"web/src/components/dashboard/app-sidebar.tsx.hbs": "'use client'\n\nimport {\n
|
|
86
|
+
"web/src/components/auth/sign-in-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport { signIn } from '@/lib/auth'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\n\nexport function SignInForm() {\n const router = useRouter()\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const result = await signIn.email({\n email,\n password,\n })\n\n if (result.error) {\n setError(result.error.message || 'Invalid email or password')\n } else {\n router.push('/')\n }\n } catch (err) {\n setError('Invalid email or password')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: 'github' | 'google') => {\n await signIn.social({\n provider,\n callbackURL: '/',\n })\n }\n\n return (\n <Card>\n <CardHeader className=\"text-center\">\n <CardTitle className=\"text-2xl\">Welcome back</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"Enter your password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n />\n </div>\n\n {error && (\n <p className=\"text-sm text-destructive\">{error}</p>\n )}\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Signing in...' : 'Sign In'}\n </Button>\n </form>\n\n <div className=\"relative my-6\">\n <div className=\"absolute inset-0 flex items-center\">\n <Separator className=\"w-full\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-card px-2 text-muted-foreground\">Or continue with</span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('github')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"/>\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('google')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"/>\n <path fill=\"currentColor\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/>\n <path fill=\"currentColor\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/>\n <path fill=\"currentColor\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/>\n </svg>\n Google\n </Button>\n </div>\n\n <p className=\"mt-6 text-center text-sm text-muted-foreground\">\n Don't have an account?{' '}\n <Link href=\"/sign-up\" className=\"text-primary hover:underline font-medium\">\n Sign up\n </Link>\n </p>\n </CardContent>\n </Card>\n )\n}\n",
|
|
87
|
+
"web/src/components/auth/sign-up-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport { signUp, signIn } from '@/lib/auth'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\n\nexport function SignUpForm() {\n const router = useRouter()\n const [name, setName] = useState('')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n if (password.length < 8) {\n setError('Password must be at least 8 characters')\n setIsLoading(false)\n return\n }\n\n try {\n const result = await signUp.email({\n email,\n password,\n name,\n })\n\n if (result.error) {\n setError(result.error.message || 'Failed to create account')\n } else {\n router.push('/')\n }\n } catch (err) {\n setError('Failed to create account. Email may already be in use.')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: 'github' | 'google') => {\n await signIn.social({\n provider,\n callbackURL: '/',\n })\n }\n\n return (\n <Card>\n <CardHeader className=\"text-center\">\n <CardTitle className=\"text-2xl\">Create an account</CardTitle>\n <CardDescription>Enter your details to get started</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"name\">Name</Label>\n <Input\n id=\"name\"\n type=\"text\"\n placeholder=\"Your name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"At least 8 characters\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n minLength={8}\n />\n </div>\n\n {error && (\n <p className=\"text-sm text-destructive\">{error}</p>\n )}\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Creating account...' : 'Create Account'}\n </Button>\n </form>\n\n <div className=\"relative my-6\">\n <div className=\"absolute inset-0 flex items-center\">\n <Separator className=\"w-full\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-card px-2 text-muted-foreground\">Or continue with</span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('github')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"/>\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('google')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"/>\n <path fill=\"currentColor\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/>\n <path fill=\"currentColor\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/>\n <path fill=\"currentColor\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/>\n </svg>\n Google\n </Button>\n </div>\n\n <p className=\"mt-6 text-center text-sm text-muted-foreground\">\n Already have an account?{' '}\n <Link href=\"/sign-in\" className=\"text-primary hover:underline font-medium\">\n Sign in\n </Link>\n </p>\n </CardContent>\n </Card>\n )\n}\n",
|
|
88
|
+
"web/src/components/dashboard/app-sidebar.tsx.hbs": "'use client'\n\nimport {\n GalleryVerticalEnd,\n Home,\n Settings,\n ChevronsUpDown,\n LogOut,\n User,\n} from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { signOut, useSession } from '@/lib/auth'\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n} from '@/components/ui/sidebar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'\n\nconst navigation = [\n {\n title: 'Home',\n url: '/',\n icon: Home,\n },\n {\n title: 'Settings',\n url: '/settings',\n icon: Settings,\n },\n]\n\nexport function AppSidebar() {\n const router = useRouter()\n const { data: session } = useSession()\n const user = session?.user\n\n const handleSignOut = async () => {\n await signOut()\n router.push('/sign-in')\n }\n\n const getInitials = (name?: string | null) => {\n if (!name) return 'U'\n return name\n .split(' ')\n .map((n) => n[0])\n .join('')\n .toUpperCase()\n .slice(0, 2)\n }\n\n return (\n <Sidebar>\n <SidebarHeader>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton size=\"lg\" asChild>\n <a href=\"/\">\n <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground\">\n <GalleryVerticalEnd className=\"size-4\" />\n </div>\n <div className=\"flex flex-col gap-0.5 leading-none\">\n <span className=\"font-semibold\">{{projectName}}</span>\n <span className=\"text-xs text-muted-foreground\">Dashboard</span>\n </div>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarHeader>\n <SidebarContent>\n <SidebarGroup>\n <SidebarGroupLabel>Navigation</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {navigation.map((item) => (\n <SidebarMenuItem key={item.title}>\n <SidebarMenuButton asChild>\n <a href={item.url}>\n <item.icon className=\"size-4\" />\n <span>{item.title}</span>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n </SidebarContent>\n <SidebarFooter>\n <SidebarMenu>\n <SidebarMenuItem>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <SidebarMenuButton\n size=\"lg\"\n className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n >\n <Avatar className=\"h-8 w-8 rounded-lg\">\n <AvatarImage src={user?.image ?? undefined} alt={user?.name ?? 'User'} />\n <AvatarFallback className=\"rounded-lg\">\n {getInitials(user?.name)}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-semibold\">{user?.name ?? 'User'}</span>\n <span className=\"truncate text-xs text-muted-foreground\">\n {user?.email ?? ''}\n </span>\n </div>\n <ChevronsUpDown className=\"ml-auto size-4\" />\n </SidebarMenuButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg\"\n side=\"bottom\"\n align=\"end\"\n sideOffset={4}\n >\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <User className=\"mr-2 size-4\" />\n Profile\n </a>\n </DropdownMenuItem>\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <Settings className=\"mr-2 size-4\" />\n Settings\n </a>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleSignOut}>\n <LogOut className=\"mr-2 size-4\" />\n Sign out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarFooter>\n </Sidebar>\n )\n}\n",
|
|
87
89
|
"web/src/components/dashboard/dashboard-layout.tsx.hbs": "'use client'\n\nimport { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'\nimport { Separator } from '@/components/ui/separator'\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbList,\n BreadcrumbPage,\n} from '@/components/ui/breadcrumb'\nimport { AppSidebar } from './app-sidebar'\n\ninterface DashboardLayoutProps {\n children: React.ReactNode\n title?: string\n}\n\nexport function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {\n return (\n <SidebarProvider>\n <AppSidebar />\n <SidebarInset>\n <header className=\"flex h-16 shrink-0 items-center gap-2 border-b px-4\">\n <SidebarTrigger className=\"-ml-1\" />\n <Separator orientation=\"vertical\" className=\"mr-2 h-4\" />\n <Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem>\n <BreadcrumbPage>{title}</BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </Breadcrumb>\n </header>\n <main className=\"flex-1 p-4 md:p-6\">{children}</main>\n </SidebarInset>\n </SidebarProvider>\n )\n}\n",
|
|
88
|
-
"web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport {
|
|
89
|
-
"web/src/lib/auth.ts.hbs": "
|
|
90
|
+
"web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport { ConvexBetterAuthProvider } from '@convex-dev/better-auth/react'\nimport { ConvexReactClient } from 'convex/react'\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)\n\nexport function ConvexClientProvider({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexBetterAuthProvider client={convex}>\n {children}\n </ConvexBetterAuthProvider>\n )\n}\n",
|
|
91
|
+
"web/src/lib/auth-server.ts.hbs": "import { convexBetterAuthNextJs } from '@convex-dev/better-auth/nextjs'\n\nexport const { auth, getSession, signIn, signOut } = convexBetterAuthNextJs({\n convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,\n siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,\n})\n",
|
|
92
|
+
"web/src/lib/auth.ts.hbs": "'use client'\n\nimport { createAuthClient } from 'better-auth/react'\nimport { convexClient } from '@convex-dev/better-auth/client'\n\nexport const authClient = createAuthClient({\n baseURL: process.env.NEXT_PUBLIC_SITE_URL,\n plugins: [convexClient()],\n})\n\nexport const {\n signIn,\n signUp,\n signOut,\n useSession,\n getSession,\n} = authClient\n",
|
|
90
93
|
"web/src/lib/utils.ts.hbs": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
91
|
-
"web/src/proxy.ts.hbs": "import {\
|
|
94
|
+
"web/src/proxy.ts.hbs": "import { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\nimport { auth } from '@/lib/auth-server'\n\nconst publicRoutes = ['/sign-in', '/sign-up', '/api/auth']\n\nfunction isPublicRoute(pathname: string) {\n return publicRoutes.some(route => pathname.startsWith(route))\n}\n\nexport async function proxy(request: NextRequest) {\n const { pathname } = request.nextUrl\n\n // Allow public routes\n if (isPublicRoute(pathname)) {\n return NextResponse.next()\n }\n\n // Check authentication\n const session = await auth.getSession()\n\n // Redirect unauthenticated users to /sign-up\n if (!session) {\n return NextResponse.redirect(new URL('/sign-up', request.url))\n }\n\n return NextResponse.next()\n}\n\nexport const config = {\n matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'],\n}\n",
|
|
92
95
|
"web/tsconfig.json.hbs": "{\n \"compilerOptions\": {\n \"target\": \"ES2017\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n \"strict\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"react-jsx\",\n \"incremental\": true,\n \"plugins\": [{ \"name\": \"next\" }],\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n }\n },\n \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\", \".next/dev/types/**/*.ts\"],\n \"exclude\": [\"node_modules\"]\n}\n"
|
|
93
96
|
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
# Convex
|
|
2
2
|
CONVEX_DEPLOYMENT=
|
|
3
3
|
NEXT_PUBLIC_CONVEX_URL=
|
|
4
|
+
NEXT_PUBLIC_CONVEX_SITE_URL=
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# Auth - Google OAuth
|
|
10
|
-
AUTH_GOOGLE_ID=
|
|
11
|
-
AUTH_GOOGLE_SECRET=
|
|
6
|
+
# Site URL (used for auth redirects)
|
|
7
|
+
SITE_URL=http://localhost:3000
|
|
8
|
+
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
|
12
9
|
|
|
13
10
|
# Better Auth Secret (generate with: openssl rand -base64 32)
|
|
14
11
|
BETTER_AUTH_SECRET=
|
|
15
12
|
|
|
13
|
+
# Auth - GitHub OAuth
|
|
14
|
+
GITHUB_CLIENT_ID=
|
|
15
|
+
GITHUB_CLIENT_SECRET=
|
|
16
|
+
|
|
17
|
+
# Auth - Google OAuth
|
|
18
|
+
GOOGLE_CLIENT_ID=
|
|
19
|
+
GOOGLE_CLIENT_SECRET=
|
|
20
|
+
|
|
16
21
|
# Email (Resend) - https://resend.com
|
|
17
22
|
RESEND_API_KEY=
|
|
18
23
|
RESEND_FROM_EMAIL=
|
|
@@ -1,34 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { convexAuth } from '@convex-dev/auth/server'
|
|
1
|
+
import { BetterAuth } from '@convex-dev/better-auth'
|
|
2
|
+
import { betterAuth } from 'better-auth'
|
|
3
|
+
import { components } from './_generated/api'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
GitHub,
|
|
9
|
-
Google,
|
|
10
|
-
Password,
|
|
11
|
-
],
|
|
5
|
+
const auth = new BetterAuth(components.betterAuth, {
|
|
6
|
+
trustedOrigins: [process.env.SITE_URL!],
|
|
12
7
|
})
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
// See: https://labs.convex.dev/auth/config/passwords
|
|
9
|
+
export const { signIn, signUp, signOut, isAuthenticated, getSession } = auth.create(
|
|
10
|
+
betterAuth({
|
|
11
|
+
emailAndPassword: {
|
|
12
|
+
enabled: true,
|
|
13
|
+
},
|
|
14
|
+
socialProviders: {
|
|
15
|
+
github: {
|
|
16
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
17
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
18
|
+
},
|
|
19
|
+
google: {
|
|
20
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
21
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export { auth }
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from 'convex/server'
|
|
2
|
-
import { authTables } from '@convex-dev/auth/server'
|
|
3
2
|
import { v } from 'convex/values'
|
|
4
3
|
|
|
4
|
+
// Better Auth manages its own tables via the betterAuth component
|
|
5
|
+
// Add your custom application tables here
|
|
5
6
|
export default defineSchema({
|
|
6
|
-
...authTables,
|
|
7
|
-
// Add your custom tables here
|
|
8
7
|
// Example:
|
|
9
8
|
// posts: defineTable({
|
|
10
9
|
// title: v.string(),
|
|
11
10
|
// content: v.string(),
|
|
12
|
-
//
|
|
11
|
+
// userId: v.string(), // Better Auth user ID
|
|
13
12
|
// createdAt: v.number(),
|
|
14
|
-
// }).index('
|
|
13
|
+
// }).index('by_user', ['userId']),
|
|
15
14
|
})
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { query } from './_generated/server'
|
|
2
2
|
import { auth } from './auth'
|
|
3
3
|
|
|
4
|
+
// Get current user from Better Auth session
|
|
4
5
|
export const current = query({
|
|
5
6
|
args: {},
|
|
6
7
|
handler: async (ctx) => {
|
|
7
|
-
const
|
|
8
|
-
if (!
|
|
9
|
-
|
|
10
|
-
const user = await ctx.db.get(userId)
|
|
11
|
-
return user
|
|
8
|
+
const session = await auth.getSession(ctx)
|
|
9
|
+
if (!session) return null
|
|
10
|
+
return session.user
|
|
12
11
|
},
|
|
13
12
|
})
|
|
14
13
|
|
|
@@ -16,10 +15,8 @@ export const current = query({
|
|
|
16
15
|
export const viewer = query({
|
|
17
16
|
args: {},
|
|
18
17
|
handler: async (ctx) => {
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
const user = await ctx.db.get(userId)
|
|
23
|
-
return user
|
|
18
|
+
const session = await auth.getSession(ctx)
|
|
19
|
+
if (!session) return null
|
|
20
|
+
return session.user
|
|
24
21
|
},
|
|
25
22
|
})
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"convex": "^1.25.0",
|
|
21
|
-
"@convex-dev/auth": "^0.0
|
|
22
|
-
"
|
|
21
|
+
"@convex-dev/better-auth": "^0.3.0",
|
|
22
|
+
"better-auth": "1.4.9",
|
|
23
23
|
"@convex-dev/resend": "^0.2.0"{{#if (eq integrations.uploads 'convex-fs')}},
|
|
24
24
|
"convex-fs": "^0.2.0"{{/if}}{{#if (eq integrations.uploads 'r2')}},
|
|
25
25
|
"@convex-dev/r2": "^0.8.0"{{/if}}{{#if (eq integrations.payments 'stripe')}},
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0",
|
|
24
24
|
"convex": "^1.25.0",
|
|
25
|
-
"@convex-dev/auth": "^0.0
|
|
26
|
-
"
|
|
25
|
+
"@convex-dev/better-auth": "^0.3.0",
|
|
26
|
+
"better-auth": "1.4.9",
|
|
27
27
|
"@hugeicons/react": "^0.3.0",
|
|
28
28
|
"class-variance-authority": "^0.7.0",
|
|
29
29
|
"clsx": "^2.1.0",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
5
|
import Link from 'next/link'
|
|
6
|
-
import {
|
|
6
|
+
import { signIn } from '@/lib/auth'
|
|
7
7
|
import { Button } from '@/components/ui/button'
|
|
8
8
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
9
9
|
import { Input } from '@/components/ui/input'
|
|
@@ -12,7 +12,6 @@ import { Separator } from '@/components/ui/separator'
|
|
|
12
12
|
|
|
13
13
|
export function SignInForm() {
|
|
14
14
|
const router = useRouter()
|
|
15
|
-
const { signIn } = useAuthActions()
|
|
16
15
|
const [email, setEmail] = useState('')
|
|
17
16
|
const [password, setPassword] = useState('')
|
|
18
17
|
const [isLoading, setIsLoading] = useState(false)
|
|
@@ -24,13 +23,16 @@ export function SignInForm() {
|
|
|
24
23
|
setError(null)
|
|
25
24
|
|
|
26
25
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const result = await signIn.email({
|
|
27
|
+
email,
|
|
28
|
+
password,
|
|
29
|
+
})
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
if (result.error) {
|
|
32
|
+
setError(result.error.message || 'Invalid email or password')
|
|
33
|
+
} else {
|
|
34
|
+
router.push('/')
|
|
35
|
+
}
|
|
34
36
|
} catch (err) {
|
|
35
37
|
setError('Invalid email or password')
|
|
36
38
|
} finally {
|
|
@@ -38,8 +40,11 @@ export function SignInForm() {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
const handleSocialSignIn = (provider: 'github' | 'google') => {
|
|
42
|
-
|
|
43
|
+
const handleSocialSignIn = async (provider: 'github' | 'google') => {
|
|
44
|
+
await signIn.social({
|
|
45
|
+
provider,
|
|
46
|
+
callbackURL: '/',
|
|
47
|
+
})
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
return (
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
5
|
import Link from 'next/link'
|
|
6
|
-
import {
|
|
6
|
+
import { signUp, signIn } from '@/lib/auth'
|
|
7
7
|
import { Button } from '@/components/ui/button'
|
|
8
8
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
9
9
|
import { Input } from '@/components/ui/input'
|
|
@@ -12,7 +12,6 @@ import { Separator } from '@/components/ui/separator'
|
|
|
12
12
|
|
|
13
13
|
export function SignUpForm() {
|
|
14
14
|
const router = useRouter()
|
|
15
|
-
const { signIn } = useAuthActions()
|
|
16
15
|
const [name, setName] = useState('')
|
|
17
16
|
const [email, setEmail] = useState('')
|
|
18
17
|
const [password, setPassword] = useState('')
|
|
@@ -31,14 +30,17 @@ export function SignUpForm() {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
try {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
const result = await signUp.email({
|
|
34
|
+
email,
|
|
35
|
+
password,
|
|
36
|
+
name,
|
|
37
|
+
})
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
if (result.error) {
|
|
40
|
+
setError(result.error.message || 'Failed to create account')
|
|
41
|
+
} else {
|
|
42
|
+
router.push('/')
|
|
43
|
+
}
|
|
42
44
|
} catch (err) {
|
|
43
45
|
setError('Failed to create account. Email may already be in use.')
|
|
44
46
|
} finally {
|
|
@@ -46,8 +48,11 @@ export function SignUpForm() {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
const handleSocialSignIn = (provider: 'github' | 'google') => {
|
|
50
|
-
|
|
51
|
+
const handleSocialSignIn = async (provider: 'github' | 'google') => {
|
|
52
|
+
await signIn.social({
|
|
53
|
+
provider,
|
|
54
|
+
callbackURL: '/',
|
|
55
|
+
})
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
return (
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
AudioWaveform,
|
|
5
|
-
Command,
|
|
6
4
|
GalleryVerticalEnd,
|
|
7
5
|
Home,
|
|
8
6
|
Settings,
|
|
@@ -11,9 +9,7 @@ import {
|
|
|
11
9
|
User,
|
|
12
10
|
} from 'lucide-react'
|
|
13
11
|
import { useRouter } from 'next/navigation'
|
|
14
|
-
import {
|
|
15
|
-
import { useQuery } from 'convex/react'
|
|
16
|
-
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'
|
|
12
|
+
import { signOut, useSession } from '@/lib/auth'
|
|
17
13
|
import {
|
|
18
14
|
Sidebar,
|
|
19
15
|
SidebarContent,
|
|
@@ -50,8 +46,8 @@ const navigation = [
|
|
|
50
46
|
|
|
51
47
|
export function AppSidebar() {
|
|
52
48
|
const router = useRouter()
|
|
53
|
-
const {
|
|
54
|
-
const user =
|
|
49
|
+
const { data: session } = useSession()
|
|
50
|
+
const user = session?.user
|
|
55
51
|
|
|
56
52
|
const handleSignOut = async () => {
|
|
57
53
|
await signOut()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ConvexBetterAuthProvider } from '@convex-dev/better-auth/react'
|
|
4
4
|
import { ConvexReactClient } from 'convex/react'
|
|
5
5
|
|
|
6
6
|
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)
|
|
@@ -11,8 +11,8 @@ export function ConvexClientProvider({
|
|
|
11
11
|
children: React.ReactNode
|
|
12
12
|
}) {
|
|
13
13
|
return (
|
|
14
|
-
<
|
|
14
|
+
<ConvexBetterAuthProvider client={convex}>
|
|
15
15
|
{children}
|
|
16
|
-
</
|
|
16
|
+
</ConvexBetterAuthProvider>
|
|
17
17
|
)
|
|
18
18
|
}
|
|
@@ -1,33 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'
|
|
3
|
+
import { createAuthClient } from 'better-auth/react'
|
|
4
|
+
import { convexClient } from '@convex-dev/better-auth/client'
|
|
6
5
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
export const authClient = createAuthClient({
|
|
7
|
+
baseURL: process.env.NEXT_PUBLIC_SITE_URL,
|
|
8
|
+
plugins: [convexClient()],
|
|
9
|
+
})
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
signInWithPassword: async (email: string, password: string, flow: 'signIn' | 'signUp' = 'signIn', name?: string) => {
|
|
20
|
-
const formData = new FormData()
|
|
21
|
-
formData.append('email', email)
|
|
22
|
-
formData.append('password', password)
|
|
23
|
-
formData.append('flow', flow)
|
|
24
|
-
if (name) {
|
|
25
|
-
formData.append('name', name)
|
|
26
|
-
}
|
|
27
|
-
await signIn('password', formData)
|
|
28
|
-
},
|
|
29
|
-
signOut: () => {
|
|
30
|
-
void signOut()
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
}
|
|
11
|
+
export const {
|
|
12
|
+
signIn,
|
|
13
|
+
signUp,
|
|
14
|
+
signOut,
|
|
15
|
+
useSession,
|
|
16
|
+
getSession,
|
|
17
|
+
} = authClient
|
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
nextjsMiddlewareRedirect,
|
|
5
|
-
} from '@convex-dev/auth/nextjs/server'
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { NextRequest } from 'next/server'
|
|
3
|
+
import { auth } from '@/lib/auth-server'
|
|
6
4
|
|
|
7
|
-
const
|
|
5
|
+
const publicRoutes = ['/sign-in', '/sign-up', '/api/auth']
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const isAuthenticated = await convexAuth.isAuthenticated()
|
|
7
|
+
function isPublicRoute(pathname: string) {
|
|
8
|
+
return publicRoutes.some(route => pathname.startsWith(route))
|
|
9
|
+
}
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
export async function proxy(request: NextRequest) {
|
|
12
|
+
const { pathname } = request.nextUrl
|
|
13
|
+
|
|
14
|
+
// Allow public routes
|
|
15
|
+
if (isPublicRoute(pathname)) {
|
|
16
|
+
return NextResponse.next()
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// Check authentication
|
|
20
|
+
const session = await auth.getSession()
|
|
21
|
+
|
|
22
|
+
// Redirect unauthenticated users to /sign-up
|
|
23
|
+
if (!session) {
|
|
24
|
+
return NextResponse.redirect(new URL('/sign-up', request.url))
|
|
22
25
|
}
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
return NextResponse.next()
|
|
28
|
+
}
|
|
24
29
|
|
|
25
30
|
export const config = {
|
|
26
31
|
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
|