create-vexcms 0.0.19 → 0.1.0-alpha.2

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.
Files changed (154) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +93 -83
  3. package/dist/index.js +246 -81
  4. package/dist/index.mjs +30976 -0
  5. package/package.json +35 -15
  6. package/templates/base-nextjs/README.md +53 -205
  7. package/templates/base-nextjs/_env.example +7 -9
  8. package/templates/base-nextjs/_gitignore +41 -1
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
  10. package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
  11. package/templates/base-nextjs/convex/_generated/server.js +8 -0
  12. package/templates/base-nextjs/convex/auth/api.ts +49 -3
  13. package/templates/base-nextjs/convex/auth/db.ts +42 -300
  14. package/templates/base-nextjs/convex/auth/index.ts +11 -7
  15. package/templates/base-nextjs/convex/auth/options.ts +5 -29
  16. package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
  17. package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
  18. package/templates/base-nextjs/convex/http.ts +18 -3
  19. package/templates/base-nextjs/convex/schema.ts +104 -2
  20. package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
  21. package/templates/base-nextjs/convex/vex/globals.ts +19 -82
  22. package/templates/base-nextjs/convex/vex/media.ts +19 -246
  23. package/templates/base-nextjs/convex/vex.schema.ts +138 -0
  24. package/templates/base-nextjs/convex/vex.ts +19 -0
  25. package/templates/base-nextjs/convex.json +6 -0
  26. package/templates/base-nextjs/eslint.config.mjs +1 -21
  27. package/templates/base-nextjs/next.config.ts +10 -0
  28. package/templates/base-nextjs/package.json +47 -48
  29. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
  30. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
  31. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
  32. package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
  33. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
  34. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
  35. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
  36. package/templates/base-nextjs/src/app/globals.css +240 -104
  37. package/templates/base-nextjs/src/app/layout.tsx +23 -13
  38. package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
  39. package/templates/base-nextjs/src/auth/access.ts +46 -0
  40. package/templates/base-nextjs/src/auth/client.tsx +17 -5
  41. package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
  42. package/templates/base-nextjs/src/auth/serverUtils.ts +15 -22
  43. package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
  44. package/templates/base-nextjs/src/components/component-example.tsx +56 -464
  45. package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
  46. package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
  47. package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
  48. package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
  49. package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
  50. package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
  51. package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
  52. package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
  53. package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
  54. package/templates/base-nextjs/src/db/constants/index.ts +22 -35
  55. package/templates/base-nextjs/src/lib/utils.ts +0 -12
  56. package/templates/base-nextjs/src/proxy.ts +98 -11
  57. package/templates/base-nextjs/src/vex.config.ts +35 -0
  58. package/templates/base-nextjs/src/vex.types.ts +293 -0
  59. package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
  60. package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
  61. package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
  62. package/templates/base-nextjs/tsconfig.json +3 -0
  63. package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
  64. package/templates/marketing-site/convex/_generated/api.js +23 -0
  65. package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
  66. package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
  67. package/templates/marketing-site/convex/_generated/server.js +101 -0
  68. package/templates/marketing-site/convex/footers.ts +11 -7
  69. package/templates/marketing-site/convex/headers.ts +15 -5
  70. package/templates/marketing-site/convex/pages.ts +20 -35
  71. package/templates/marketing-site/convex/schema.ts +119 -0
  72. package/templates/marketing-site/convex/seed.ts +582 -35
  73. package/templates/marketing-site/convex/siteSettings.ts +19 -5
  74. package/templates/marketing-site/convex/theme.ts +55 -61
  75. package/templates/marketing-site/convex/vex.schema.ts +355 -0
  76. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
  77. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
  78. package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
  79. package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
  80. package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
  81. package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
  82. package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
  83. package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
  84. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +2 -2
  85. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  86. package/templates/marketing-site/src/vex.config.ts +35 -0
  87. package/templates/marketing-site/src/vex.types.ts +634 -0
  88. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  90. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  92. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  93. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  94. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  96. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  97. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  98. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  100. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  102. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  104. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  105. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  106. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  107. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  108. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  110. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  111. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  112. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  113. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  114. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  115. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  116. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  117. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  118. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  119. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  120. package/templates/base-nextjs/convex/vex/collections.ts +0 -321
  121. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  122. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  123. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  124. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  125. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  126. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  127. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  128. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  129. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  130. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  131. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  132. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  133. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  134. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  135. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  136. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  137. package/templates/base-nextjs/src/db/types.ts +0 -6
  138. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  139. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  140. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  141. package/templates/base-nextjs/vex.config.ts +0 -25
  142. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  144. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  145. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  146. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  147. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  148. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  149. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  150. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  151. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  152. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  153. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  154. package/templates/marketing-site/vex.config.ts +0 -32
@@ -98,7 +98,7 @@ export default tseslint.config(
98
98
  "perfectionist/sort-named-imports": "warn",
99
99
  "perfectionist/sort-object-types": "warn",
100
100
  "perfectionist/sort-objects": [
101
- "warn",
101
+ "off",
102
102
  {
103
103
  customGroups: [
104
104
  {
@@ -124,26 +124,6 @@ export default tseslint.config(
124
124
  "import/no-anonymous-default-export": "off",
125
125
  },
126
126
  },
127
- {
128
- // Disable object sorting in VEX collection/config files — field order determines
129
- // the order of fields in admin panel forms
130
- files: [
131
- "**/vexcms/collections/**/*.ts",
132
- "**/vexcms/access.*",
133
- "**/vex.config.*",
134
- "**/db/constants/**/*.ts",
135
- ],
136
- rules: {
137
- "perfectionist/sort-objects": "off",
138
- },
139
- },
140
- {
141
- files: ["**/src/convex/**/*.ts"],
142
- plugins: {
143
- "@convex-dev": convexPlugin,
144
- },
145
- rules: convexRules,
146
- },
147
127
  {
148
128
  languageOptions: {
149
129
  parserOptions: {
@@ -3,8 +3,18 @@ import type { NextConfig } from "next"
3
3
  import "./src/env.mjs"
4
4
 
5
5
  const nextConfig: NextConfig = {
6
+ devIndicators: {
7
+ position: "bottom-right",
8
+ },
6
9
  allowedDevOrigins: ["127.0.01", "localhost"],
7
10
  reactCompiler: true,
11
+ images: {
12
+ remotePatterns: [
13
+ // Add your Convex deployment's hostname here once `npx convex dev` has
14
+ // run — Convex file storage URLs are served from `<deployment>.convex.cloud`,
15
+ // e.g. { hostname: "your-deployment-575.convex.cloud" }.
16
+ ],
17
+ },
8
18
  }
9
19
 
10
20
  export default nextConfig
@@ -6,7 +6,8 @@
6
6
  "dev": "next dev --port=3010",
7
7
  "vex:dev": "vex dev",
8
8
  "vex:generate": "vex dev --once",
9
- "vex:update": "pnpm add @vexcms/core@latest @vexcms/cli@latest @vexcms/admin-next@latest @vexcms/ui@latest @vexcms/richtext@latest @vexcms/better-auth@latest @vexcms/file-storage-convex@latest",
9
+ "vex:update": "pnpm add @vexcms/core@latest @vexcms/react@latest @vexcms/next@latest @vexcms/better-auth@latest @vexcms/file-storage-convex@latest @vexcms/cli@latest",
10
+ "seed": "convex run seed:init",
10
11
  "build": "next build",
11
12
  "start": "next start",
12
13
  "lint": "eslint .",
@@ -15,55 +16,53 @@
15
16
  "secret:create": "openssl rand -base64 32 | tr -d '\\n' | tee /dev/stderr | pbcopy && echo '\\n✓ Secret copied to clipboard'"
16
17
  },
17
18
  "dependencies": {
18
- "@base-ui/react": "^1.1.0",
19
- "@convex-dev/better-auth": "0.10.11",
20
- "@convex-dev/react-query": "^0.1.0",
21
- "@daveyplate/better-auth-ui": "^3.3.15",
22
- "@t3-oss/env-nextjs": "^0.13.10",
23
- "@tanstack/react-form": "^1.27.7",
24
- "@tanstack/react-query": "^5.90.17",
25
- "@vexcms/admin-next": "~0.0.19",
26
- "@vexcms/better-auth": "~0.0.19",
27
- "@vexcms/core": "~0.0.19",
28
- "@vexcms/richtext": "~0.0.19",
29
- "@vexcms/ui": "~0.0.19",
30
- "@vexcms/file-storage-convex": "~0.0.19",
31
- "better-auth": ">=1.4.9 <1.5.0",
32
- "class-variance-authority": "^0.7.1",
33
- "clsx": "^2.1.1",
34
- "convex": "^1.31.5",
35
- "culori": "^4.0.2",
36
- "convex-helpers": "^0.1.111",
37
- "lucide-react": "^0.562.0",
38
- "motion": "^12.38.0",
39
- "next": "^16.2.1",
40
- "next-themes": "^0.4.6",
41
- "nuqs": "^2.8.8",
42
- "react": "^19.2.4",
43
- "react-dom": "^19.2.4",
44
- "shadcn": "^3.6.3",
45
- "tailwind-merge": "^3.4.0",
46
- "tw-animate-css": "^1.4.0",
47
- "zod": "^4.3.5"
19
+ "@base-ui/react": "1.2.0",
20
+ "@better-auth/api-key": "1.6.23",
21
+ "@convex-dev/better-auth": "0.11.5",
22
+ "@convex-dev/react-query": "0.1.0",
23
+ "@daveyplate/better-auth-ui": "3.4.0",
24
+ "@t3-oss/env-nextjs": "0.13.11",
25
+ "@tanstack/react-form": "1.33.1",
26
+ "@tanstack/react-query": "5.101.2",
27
+ "@vexcms/better-auth": "~0.1.0-alpha.2",
28
+ "@vexcms/core": "~0.1.0-alpha.2",
29
+ "@vexcms/file-storage-convex": "~0.1.0-alpha.2",
30
+ "@vexcms/next": "~0.1.0-alpha.2",
31
+ "@vexcms/react": "~0.1.0-alpha.2",
32
+ "better-auth": "1.6.23",
33
+ "class-variance-authority": "0.7.1",
34
+ "clsx": "2.1.1",
35
+ "convex": "1.44.0",
36
+ "convex-helpers": "0.1.120",
37
+ "lucide-react": "0.577.0",
38
+ "motion": "13.1.1",
39
+ "next": "16.3.3",
40
+ "nuqs": "2.9.0",
41
+ "react": "19.2.7",
42
+ "react-dom": "19.2.7",
43
+ "shadcn": "3.6.3",
44
+ "tailwind-merge": "3.5.0",
45
+ "tw-animate-css": "1.4.0",
46
+ "zod": "4.4.3"
48
47
  },
49
48
  "devDependencies": {
50
- "@convex-dev/eslint-plugin": "^1.1.1",
51
- "@eslint/eslintrc": "^3.3.3",
52
- "@next/eslint-plugin-next": "^15.4.3",
53
- "@tailwindcss/postcss": "^4",
54
- "@types/node": "^20",
55
- "@types/react": "^19",
56
- "@types/react-dom": "^19",
57
- "@vexcms/cli": "~0.0.19",
58
- "babel-plugin-react-compiler": "^1.0.0",
59
- "eslint": "^9",
49
+ "@convex-dev/eslint-plugin": "1.2.2",
50
+ "@eslint/eslintrc": "3.3.6",
51
+ "@next/eslint-plugin-next": "15.5.20",
52
+ "@tailwindcss/postcss": "4.3.2",
53
+ "@types/node": "20.19.43",
54
+ "@types/react": "19.2.17",
55
+ "@types/react-dom": "19.2.3",
56
+ "@vexcms/cli": "~0.1.0-alpha.2",
57
+ "babel-plugin-react-compiler": "1.0.0",
58
+ "eslint": "9.39.5",
60
59
  "eslint-config-next": "15.5.9",
61
- "eslint-plugin-import-x": "^4.16.1",
62
- "eslint-plugin-perfectionist": "^5.3.1",
63
- "eslint-plugin-react-hooks": "^7.0.1",
64
- "prettier": "^3.8.1",
65
- "tailwindcss": "^4",
66
- "typescript": "^5.9.3",
67
- "typescript-eslint": "^8.53.0"
60
+ "eslint-plugin-import-x": "4.17.1",
61
+ "eslint-plugin-perfectionist": "5.10.0",
62
+ "eslint-plugin-react-hooks": "7.1.1",
63
+ "prettier": "3.9.5",
64
+ "tailwindcss": "4.3.2",
65
+ "typescript": "6.0.3",
66
+ "typescript-eslint": "8.63.0"
68
67
  }
69
68
  }
@@ -1,9 +1,21 @@
1
- import { AuthView } from "@daveyplate/better-auth-ui"
2
- import { Activity } from "react"
1
+ "use client";
3
2
 
4
- import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog"
3
+ import { AuthView } from "@daveyplate/better-auth-ui";
4
+ import { useSearchParams } from "next/navigation";
5
+ import { Activity } from "react";
6
+
7
+ import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog";
5
8
 
6
9
  export default function AuthCard({ pathname }: { pathname: string }) {
10
+ const searchParams = useSearchParams();
11
+ const isGuardRedirect = searchParams.has("redirectTo");
12
+ if (isGuardRedirect) {
13
+ return (
14
+ <main className="absolute inset-0 grid place-items-center">
15
+ <AuthView path={pathname} />
16
+ </main>
17
+ );
18
+ }
7
19
  return (
8
20
  <main>
9
21
  <Dialog open>
@@ -12,12 +24,12 @@ export default function AuthCard({ pathname }: { pathname: string }) {
12
24
  </Activity>
13
25
  <DialogContent
14
26
  aria-describedby={undefined}
15
- className="grid place-items-center border-none bg-transparent shadow-none ring-0"
27
+ className="grid place-items-center bg-transparent shadow-none ring-transparent"
16
28
  showCloseButton={false}
17
29
  >
18
30
  <AuthView path={pathname} />
19
31
  </DialogContent>
20
32
  </Dialog>
21
33
  </main>
22
- )
34
+ );
23
35
  }
@@ -1,11 +1,9 @@
1
- "use client"
2
-
3
- import { AuthView } from "@daveyplate/better-auth-ui"
1
+ import { AuthView } from "@daveyplate/better-auth-ui";
4
2
 
5
3
  export default function AuthCard({ pathname }: { pathname: string }) {
6
4
  return (
7
5
  <main className="absolute inset-0 grid place-items-center">
8
6
  <AuthView path={pathname} />
9
7
  </main>
10
- )
8
+ );
11
9
  }
@@ -1,9 +1,19 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ /**
4
+ * Public-site layout.
5
+ *
6
+ * Exists so the `@auth` parallel slot is received by the route group that
7
+ * declares it, rather than leaking up to the root layout — which is also shared
8
+ * with `/admin`, where an auth modal slot means nothing. Restores the structure
9
+ * the marketing template and the pre-rebuild app both used.
10
+ */
1
11
  export default function FrontendLayout({
2
12
  auth,
3
13
  children,
4
14
  }: Readonly<{
5
- auth: React.ReactNode
6
- children: React.ReactNode
15
+ auth: ReactNode
16
+ children: ReactNode
7
17
  }>) {
8
18
  return (
9
19
  <>
@@ -1,116 +1,5 @@
1
- "use client"
2
-
3
- import { useMutation, useQuery } from "convex/react"
4
- import { usePathname, useRouter } from "next/navigation"
5
- import { useEffect, useState } from "react"
6
-
7
- import { api } from "@convex/_generated/api"
8
- import { useSession } from "~/auth/client"
9
-
10
- function LoadingSpinner() {
11
- return (
12
- <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
13
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
14
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
15
- </svg>
16
- )
17
- }
1
+ import { WelcomePage } from "~/components/WelcomePage"
18
2
 
19
3
  export default function Page() {
20
- const isBootstrapped = useQuery(api.vex.firstUser.isBootstrapped)
21
- const promoteFirstAdmin = useMutation(api.vex.firstUser.promoteFirstAdmin)
22
- const { data: session } = useSession()
23
- const router = useRouter()
24
- const pathname = usePathname()
25
- const [promoting, setPromoting] = useState(false)
26
- const [navigating, setNavigating] = useState(false)
27
-
28
- // Reset navigating state when the user comes back to the home page
29
- // (e.g. closing the intercepting auth dialog)
30
- useEffect(() => {
31
- if (pathname === "/") {
32
- setNavigating(false)
33
- }
34
- }, [pathname])
35
-
36
- // If user is signed in and this is a fresh project, try to promote them
37
- useEffect(() => {
38
- if (session?.user && isBootstrapped === false && !promoting) {
39
- setPromoting(true)
40
- setNavigating(true)
41
- promoteFirstAdmin()
42
- .then((result) => {
43
- if (result.promoted) {
44
- router.push("/admin")
45
- } else {
46
- setNavigating(false)
47
- }
48
- })
49
- .catch(() => {
50
- setPromoting(false)
51
- setNavigating(false)
52
- })
53
- }
54
- }, [session, isBootstrapped, promoting, promoteFirstAdmin, router])
55
-
56
- // Treat undefined (query still loading) as not bootstrapped — show the welcome page immediately
57
- const bootstrapped = isBootstrapped === true
58
-
59
- const buttonClass =
60
- "inline-flex items-center justify-center gap-2 rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
61
-
62
- const handleNavigate = (path: string) => {
63
- setNavigating(true)
64
- router.push(path)
65
- }
66
-
67
- return (
68
- <div className="flex min-h-screen flex-col items-center justify-center gap-8 p-8">
69
- <div className="text-center">
70
- <h1 className="text-4xl font-bold tracking-tight">VEX CMS</h1>
71
- <p className="mt-3 text-lg text-muted-foreground">
72
- {bootstrapped
73
- ? "Your content management system is ready."
74
- : "Welcome! Create your admin account to get started."}
75
- </p>
76
- </div>
77
-
78
- <div className="flex gap-4">
79
- {session?.user ? (
80
- <button
81
- onClick={() => handleNavigate("/admin")}
82
- disabled={navigating}
83
- className={buttonClass}
84
- >
85
- {navigating && <LoadingSpinner />}
86
- {navigating ? "Loading..." : "Go to Admin Panel"}
87
- </button>
88
- ) : bootstrapped ? (
89
- <button
90
- onClick={() => handleNavigate("/auth/sign-in")}
91
- disabled={navigating}
92
- className={buttonClass}
93
- >
94
- {navigating && <LoadingSpinner />}
95
- {navigating ? "Loading..." : "Sign In"}
96
- </button>
97
- ) : (
98
- <button
99
- onClick={() => handleNavigate("/auth/sign-up")}
100
- disabled={navigating}
101
- className={buttonClass}
102
- >
103
- {navigating && <LoadingSpinner />}
104
- {navigating ? "Loading..." : "Create Admin Account"}
105
- </button>
106
- )}
107
- </div>
108
-
109
- {promoting && (
110
- <p className="text-sm text-muted-foreground animate-pulse">
111
- Setting up your admin account...
112
- </p>
113
- )}
114
- </div>
115
- )
4
+ return <WelcomePage />
116
5
  }
@@ -0,0 +1,40 @@
1
+ import { api } from "@convex/_generated/api";
2
+ import { NextAdminPage } from "@vexcms/next/server";
3
+ import { canAccessAdminPanel } from "@vexcms/core";
4
+ import { redirect } from "next/navigation";
5
+
6
+ import { fetchAuthQuery, getToken } from "~/auth/server";
7
+ import config from "~/vex.config";
8
+
9
+ export const dynamic = "force-dynamic";
10
+
11
+ /**
12
+ * Admin panel route. Gates on two separate things, in order:
13
+ *
14
+ * 1. **Authentication** — no session token means sign in first.
15
+ * 2. **Authorization** — a valid session is not enough; the caller must also
16
+ * satisfy `adminPanel.access` in the access matrix. Without this second check
17
+ * any authenticated user reaches the full panel regardless of their roles.
18
+ *
19
+ * Both run server-side, before the panel renders, so an unauthorized caller
20
+ * never receives the admin shell (which would leak collection and global names).
21
+ */
22
+ export default async function AdminPage({ params }: { params: Promise<{ path?: string[] }> }) {
23
+ const token = await getToken();
24
+ if (!token) {
25
+ redirect("/auth/sign-in?redirectTo=/admin");
26
+ }
27
+
28
+ const auth = await fetchAuthQuery(api.auth.api.getUserOrg, {});
29
+ if (
30
+ !canAccessAdminPanel({
31
+ access: config.access,
32
+ user: auth.user,
33
+ organization: auth.organization,
34
+ })
35
+ ) {
36
+ redirect("/unauthorized");
37
+ }
38
+
39
+ return <NextAdminPage config={config} params={params} token={token} />;
40
+ }
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ import { uploadFile } from "@vexcms/file-storage-convex";
4
+ import { StorageAdapterContextProvider, VexAccessProvider } from "@vexcms/react";
5
+
6
+ import { access } from "~/auth/access";
7
+
8
+ /**
9
+ * Provides client-side upload functions for VexCMS storage adapters.
10
+ *
11
+ * This is a "use client" component so that uploadFile is imported and
12
+ * owned on the client — it never crosses the RSC boundary as a prop.
13
+ *
14
+ * Add a new entry here when registering an additional storage adapter.
15
+ */
16
+ export function ClientProviders({ children }: { children: React.ReactNode }) {
17
+ return (
18
+ <StorageAdapterContextProvider adapterClients={{ convex: uploadFile }}>
19
+ <VexAccessProvider access={access}>{children}</VexAccessProvider>
20
+ </StorageAdapterContextProvider>
21
+ );
22
+ }
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ import { NextAdminLayout } from "@vexcms/next/client"
4
+
5
+ import { getCurrentUser } from "~/auth/serverUtils"
6
+ import config from "~/vex.config"
7
+
8
+ import { ClientProviders } from "./clientProviders"
9
+
10
+ export default async function AdminLayout({ children }: { children: ReactNode }) {
11
+ const user = await getCurrentUser()
12
+ return (
13
+ <ClientProviders>
14
+ <NextAdminLayout config={config} user={user ?? undefined}>
15
+ {children}
16
+ </NextAdminLayout>
17
+ </ClientProviders>
18
+ )
19
+ }