create-vexcms 0.0.20 → 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 (153) 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 -25
  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 +97 -12
  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/lib/metadata.ts +51 -72
  85. package/templates/marketing-site/src/vex.config.ts +35 -0
  86. package/templates/marketing-site/src/vex.types.ts +634 -0
  87. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  88. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  90. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  92. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  93. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  94. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  96. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  97. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  98. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  100. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  102. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  110. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  111. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  112. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  113. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  114. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  115. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  116. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  117. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  118. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  119. package/templates/base-nextjs/convex/vex/collections.ts +0 -333
  120. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  121. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  122. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  123. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  124. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  125. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  126. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  127. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  128. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  129. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  130. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  131. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  132. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  133. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  134. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  135. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  136. package/templates/base-nextjs/src/db/types.ts +0 -6
  137. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  138. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  139. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  140. package/templates/base-nextjs/vex.config.ts +0 -25
  141. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  142. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  144. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  145. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  146. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  147. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  148. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  149. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  150. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  151. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  152. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  153. package/templates/marketing-site/vex.config.ts +0 -32
@@ -1,25 +1,46 @@
1
- "use client"
2
-
3
1
  import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
2
+ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
4
3
 
5
4
  import { cn } from "~/lib/utils"
6
- import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
7
5
 
8
6
  function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
9
7
  return (
10
8
  <AccordionPrimitive.Root
11
- data-slot="accordion"
12
9
  className={cn("flex w-full flex-col", className)}
10
+ data-slot="accordion"
13
11
  {...props}
14
12
  />
15
13
  )
16
14
  }
17
15
 
16
+ function AccordionContent({
17
+ className,
18
+ children,
19
+ ...props
20
+ }: AccordionPrimitive.Panel.Props) {
21
+ return (
22
+ <AccordionPrimitive.Panel
23
+ className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
24
+ data-slot="accordion-content"
25
+ {...props}
26
+ >
27
+ <div
28
+ className={cn(
29
+ "h-(--accordion-panel-height) pt-0 pb-4 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
30
+ className
31
+ )}
32
+ >
33
+ {children}
34
+ </div>
35
+ </AccordionPrimitive.Panel>
36
+ )
37
+ }
38
+
18
39
  function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
19
40
  return (
20
41
  <AccordionPrimitive.Item
21
- data-slot="accordion-item"
22
42
  className={cn("not-last:border-b", className)}
43
+ data-slot="accordion-item"
23
44
  {...props}
24
45
  />
25
46
  )
@@ -33,42 +54,19 @@ function AccordionTrigger({
33
54
  return (
34
55
  <AccordionPrimitive.Header className="flex">
35
56
  <AccordionPrimitive.Trigger
36
- data-slot="accordion-trigger"
37
57
  className={cn(
38
58
  "group/accordion-trigger relative flex flex-1 items-start justify-between rounded-md border border-transparent py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
39
59
  className
40
60
  )}
61
+ data-slot="accordion-trigger"
41
62
  {...props}
42
63
  >
43
64
  {children}
44
- <ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
45
- <ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
65
+ <ChevronDownIcon className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" data-slot="accordion-trigger-icon" />
66
+ <ChevronUpIcon className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" data-slot="accordion-trigger-icon" />
46
67
  </AccordionPrimitive.Trigger>
47
68
  </AccordionPrimitive.Header>
48
69
  )
49
70
  }
50
71
 
51
- function AccordionContent({
52
- className,
53
- children,
54
- ...props
55
- }: AccordionPrimitive.Panel.Props) {
56
- return (
57
- <AccordionPrimitive.Panel
58
- data-slot="accordion-content"
59
- className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
60
- {...props}
61
- >
62
- <div
63
- className={cn(
64
- "h-(--accordion-panel-height) pt-0 pb-4 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
65
- className
66
- )}
67
- >
68
- {children}
69
- </div>
70
- </AccordionPrimitive.Panel>
71
- )
72
- }
73
-
74
- export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
72
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
@@ -5,39 +5,36 @@ import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "~/lib/utils"
6
6
 
7
7
  const badgeVariants = cva(
8
- "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
8
+ "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge",
9
9
  {
10
+ defaultVariants: {
11
+ variant: "default",
12
+ },
10
13
  variants: {
11
14
  variant: {
12
15
  default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
- secondary:
14
- "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
16
  destructive:
16
- "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
- outline:
18
- "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
- ghost:
20
- "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
17
+ "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
18
+ ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
19
  link: "text-primary underline-offset-4 hover:underline",
20
+ outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
21
+ secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
22
22
  },
23
23
  },
24
- defaultVariants: {
25
- variant: "default",
26
- },
27
24
  }
28
25
  )
29
26
 
30
27
  function Badge({
31
28
  className,
32
- variant = "default",
33
29
  render,
30
+ variant = "default",
34
31
  ...props
35
32
  }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
36
33
  return useRender({
37
34
  defaultTagName: "span",
38
35
  props: mergeProps<"span">(
39
36
  {
40
- className: cn(badgeVariants({ variant }), className),
37
+ className: cn(badgeVariants({ className, variant })),
41
38
  },
42
39
  props
43
40
  ),
@@ -1,13 +1,31 @@
1
- import { Input as InputPrimitive } from "@base-ui/react/input"
1
+ import { Input as InputPrimitive } from "@base-ui/react"
2
+ import { Loader2 } from "lucide-react"
2
3
  import * as React from "react"
3
4
 
4
- import { cn } from "~/lib/utils"
5
+ import { cn } from "../../lib/utils"
5
6
 
6
- function Input({ type, className, ...props }: React.ComponentProps<"input">) {
7
- return (
7
+ /**
8
+ * Single-line text input with optional in-input loading spinner.
9
+ *
10
+ * When `loading` is true, a spinner renders on the right edge and the input
11
+ * gains right padding. The default path (no `loading` prop) is unchanged —
12
+ * a bare `<input>` element with no wrapper.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <Input value={query} onChange={onChange} loading={isPending} placeholder="Search…" />
17
+ */
18
+ function Input({
19
+ className,
20
+ type,
21
+ loading,
22
+ ...props
23
+ }: React.ComponentProps<"input"> & { loading?: boolean }) {
24
+ const input = (
8
25
  <InputPrimitive
9
26
  className={cn(
10
- "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
27
+ "h-8 w-full min-w-0 rounded-sm border border-input bg-card px-2.5 py-1 text-[13px] shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
28
+ loading && "pr-9",
11
29
  className
12
30
  )}
13
31
  data-slot="input"
@@ -15,6 +33,18 @@ function Input({ type, className, ...props }: React.ComponentProps<"input">) {
15
33
  {...props}
16
34
  />
17
35
  )
36
+ if (!loading) {
37
+ return input
38
+ }
39
+ return (
40
+ <span className="relative block w-full">
41
+ {input}
42
+ <Loader2
43
+ aria-hidden="true"
44
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 size-4 animate-spin text-muted-foreground pointer-events-none"
45
+ />
46
+ </span>
47
+ )
18
48
  }
19
49
 
20
50
  export { Input }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+
5
+ import { type OrganizationDoc, type UserDoc } from "~/db/constants";
6
+
7
+ export interface CurrentAuth {
8
+ organization?: OrganizationDoc;
9
+ user: null | UserDoc; // null → unauthenticated → no roles → deny
10
+ }
11
+ export const AuthContext = createContext<CurrentAuth>({ user: null });
12
+
13
+ export function AuthProvider(props: {
14
+ children: React.ReactNode;
15
+ value: {
16
+ organization?: OrganizationDoc;
17
+ user: null | UserDoc; // null → unauthenticated → no roles → deny
18
+ };
19
+ }) {
20
+ return <AuthContext.Provider value={props.value}>{props.children}</AuthContext.Provider>;
21
+ }
22
+ /** @returns the current caller `{ user, organization }` from the server layout. */
23
+ export function useAuth(): CurrentAuth {
24
+ const context = useContext(AuthContext);
25
+ if (context === null) {
26
+ throw new Error("useAuth must be used within a AuthProvider");
27
+ }
28
+ return context;
29
+ }
@@ -1,6 +1,5 @@
1
1
  export const USER_ROLES = {
2
2
  admin: "admin",
3
- editor: "editor",
4
3
  user: "user",
5
4
  } as const
6
5
  export type UserRole = (typeof USER_ROLES)[keyof typeof USER_ROLES]
@@ -1,44 +1,31 @@
1
+ import { type Doc, type Id } from "@convex/_generated/dataModel"
2
+
1
3
  export * from "./auth"
2
4
 
3
5
  // Better Auth
4
6
  export const TABLE_SLUG_USERS = "user" as const
7
+ export type UserDoc = Doc<typeof TABLE_SLUG_USERS>
8
+ export type UserID = Id<typeof TABLE_SLUG_USERS>
9
+
10
+ export const TABLE_SLUG_ORGANIZATIONS = "organization" as const
11
+ export type OrganizationDoc = Doc<typeof TABLE_SLUG_ORGANIZATIONS>
12
+ export type OrganizationID = Id<typeof TABLE_SLUG_ORGANIZATIONS>
13
+
5
14
  export const TABLE_SLUG_ACCOUNTS = "account" as const
15
+
6
16
  export const TABLE_SLUG_SESSIONS = "session" as const
17
+ export type Session = Doc<typeof TABLE_SLUG_SESSIONS>
18
+ export type SessionID = Id<typeof TABLE_SLUG_SESSIONS>
19
+
7
20
  export const TABLE_SLUG_VERIFICATIONS = "verification" as const
8
21
  export const TABLE_SLUG_JWKS = "jwks" as const
22
+ export const TABLE_SLUG_API_KEYS = "apikey" as const
23
+
24
+ export const TABLE_SLUG_IMAGES = "images" as const
9
25
 
10
- export const TABLE_SLUG_MEDIA = "media" as const
11
-
12
- export const AUTH_PROVIDERS = {
13
- apple: "apple",
14
- atlassian: "atlassian",
15
- cognito: "cognito",
16
- discord: "discord",
17
- dropbox: "dropbox",
18
- facebook: "facebook",
19
- figma: "figma",
20
- github: "github",
21
- gitlab: "gitlab",
22
- google: "google",
23
- huggingface: "huggingface",
24
- kakao: "kakao",
25
- kick: "kick",
26
- line: "line",
27
- linear: "linear",
28
- linkedin: "linkedin",
29
- microsoft: "microsoft",
30
- naver: "naver",
31
- notion: "notion",
32
- paypal: "paypal",
33
- reddit: "reddit",
34
- roblox: "roblox",
35
- salesforce: "salesforce",
36
- slack: "slack",
37
- spotify: "spotify",
38
- tiktok: "tiktok",
39
- twitch: "twitch",
40
- twitter: "twitter",
41
- vk: "vk",
42
- zoom: "zoom",
43
- } as const
44
- export type AuthProvider = (typeof AUTH_PROVIDERS)[keyof typeof AUTH_PROVIDERS]
26
+ // Site content (marketing-site overlay)
27
+ export const TABLE_SLUG_PAGES = "pages" as const
28
+ export const TABLE_SLUG_HEADERS = "headers" as const
29
+ export const TABLE_SLUG_FOOTERS = "footers" as const
30
+ export const TABLE_SLUG_THEMES = "themes" as const
31
+ export const GLOBAL_SLUG_SITE_SETTINGS = "siteSettings" as const
@@ -4,15 +4,3 @@ import { twMerge } from "tailwind-merge"
4
4
  export function cn(...inputs: ClassValue[]) {
5
5
  return twMerge(clsx(inputs))
6
6
  }
7
-
8
- /**
9
- * Normalize a page slug for consistent lookup.
10
- * - Strips leading/trailing slashes
11
- * - Treats "/" and "/home" and "home" all as "home"
12
- * - "/features" becomes "features"
13
- */
14
- export function normalizeSlug(slug?: string): string {
15
- if (!slug) return "home"
16
- const cleaned = slug.replace(/^\/+|\/+$/g, "")
17
- return cleaned === "" ? "home" : cleaned
18
- }
@@ -1,25 +1,110 @@
1
- import { cookies } from "next/headers"
2
- import { type NextRequest, NextResponse } from "next/server"
1
+ import { api } from "@convex/_generated/api";
2
+ import { fetchQuery } from "convex/nextjs";
3
+ import { cookies } from "next/headers";
4
+ import { type NextRequest, NextResponse } from "next/server";
3
5
 
6
+ const SESSION_COOKIES = {
7
+ https: "__Secure-better-auth.session_token",
8
+ http: "better-auth.session_token",
9
+ } as const;
10
+
11
+ /**
12
+ * Paths the proxy must never guard, because they are where it sends people.
13
+ * Guarding them turns a Convex outage into an infinite redirect loop.
14
+ */
15
+ const ALWAYS_ALLOWED: Record<string, true> = { "/": true, "/unauthorized": true };
16
+
17
+ /**
18
+ * Session gate for every route except the public ones in `config.matcher`.
19
+ *
20
+ * Fails **closed**: if the session cannot be verified — no cookie, no matching
21
+ * session, or the Convex lookup throwing — the request is redirected rather than
22
+ * served. A verification error is not evidence of a valid session, and serving
23
+ * the page anyway would drop authentication for every guarded route the moment
24
+ * Convex became unreachable.
25
+ *
26
+ * Two distinct outcomes, deliberately:
27
+ * - **No/invalid session** → `/auth/sign-in`, clearing the stale cookie so the
28
+ * presence check cannot lie on the next request.
29
+ * - **Verification failed** (Convex unreachable, timeout) → `/unauthorized`,
30
+ * leaving cookies intact: the session may well be fine, so signing the user
31
+ * out and discarding it would be wrong.
32
+ *
33
+ * @param request - The incoming request.
34
+ * @returns `NextResponse.next()` when the session is verified, otherwise a
35
+ * redirect to sign-in or to the unauthorized page.
36
+ */
4
37
  export async function proxy(request: NextRequest) {
5
- if (request.nextUrl.pathname === "/") {
6
- return NextResponse.next()
38
+ if (ALWAYS_ALLOWED[request.nextUrl.pathname]) {
39
+ return NextResponse.next();
7
40
  }
8
41
 
9
- const cookieStore = await cookies()
42
+ const cookieStore = await cookies();
10
43
  const sessionToken =
11
- cookieStore.get("__Secure-better-auth.session_token") ??
12
- cookieStore.get("better-auth.session_token")
13
-
44
+ cookieStore.get(SESSION_COOKIES.https)?.value ?? cookieStore.get(SESSION_COOKIES.http)?.value;
14
45
  if (!sessionToken) {
15
- return NextResponse.redirect(new URL("/auth/sign-in", request.url))
46
+ return redirectToSignIn(request);
47
+ }
48
+
49
+ try {
50
+ const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
51
+ sessionToken: extractToken(sessionToken),
52
+ });
53
+ if (!session?.user) {
54
+ return redirectToSignIn(request);
55
+ }
56
+ } catch {
57
+ // Fail closed. Previously this returned `NextResponse.next()`, which served
58
+ // every guarded route unauthenticated whenever this lookup threw.
59
+ return redirectToUnauthorized(request);
60
+ }
61
+
62
+ return NextResponse.next();
63
+ }
64
+
65
+ /** Extract the raw token — better-auth stores "<raw_token>.<hmac_signature>" */
66
+ function extractToken(cookieValue: string) {
67
+ return cookieValue.includes(".") ? cookieValue.split(".")[0] : cookieValue;
68
+ }
69
+
70
+ /**
71
+ * Redirect for a caller with no usable session. Clears the session cookies so
72
+ * the presence check cannot lie on the next request.
73
+ *
74
+ * @param request - The request being rejected.
75
+ * @returns A redirect to sign-in preserving the intended destination.
76
+ */
77
+ function redirectToSignIn(request: NextRequest) {
78
+ const signInUrl = new URL("/auth/sign-in", request.url);
79
+ // Preserve where the user was headed (path + query)
80
+ signInUrl.searchParams.set("redirectTo", request.nextUrl.pathname + request.nextUrl.search);
81
+
82
+ const response = NextResponse.redirect(signInUrl);
83
+ for (const cookie of Object.values(SESSION_COOKIES)) {
84
+ // Clear the stale cookie so the presence-check never lies again
85
+ response.cookies.delete(cookie);
16
86
  }
87
+ return response;
88
+ }
17
89
 
18
- return NextResponse.next()
90
+ /**
91
+ * Redirect for a caller whose session could not be *verified* — as opposed to
92
+ * one who has none.
93
+ *
94
+ * Cookies are deliberately left intact: the session is probably valid and the
95
+ * verification path (Convex) is simply unavailable, so discarding it would sign
96
+ * out every user during an outage. `/unauthorized` is exempt from this proxy, so
97
+ * this cannot loop.
98
+ *
99
+ * @param request - The request that could not be verified.
100
+ * @returns A redirect to the unauthorized page.
101
+ */
102
+ function redirectToUnauthorized(request: NextRequest) {
103
+ return NextResponse.redirect(new URL("/unauthorized", request.url));
19
104
  }
20
105
 
21
106
  export const config = {
22
107
  matcher: [
23
- "/((?!api|_next/static|_next/image|favicon.ico|models/*|staging/*|auth/sign-in|auth/sign-up|$).*)",
108
+ "/((?!api|_next/static|_next/image|favicon.ico|favicons|models/*|staging/*|auth/sign-in|auth/sign-up|$).*)",
24
109
  ],
25
- }
110
+ };
@@ -0,0 +1,35 @@
1
+ import { betterAuthAdapter } from "@vexcms/better-auth"
2
+ import { defineConfig } from "@vexcms/core"
3
+ import { convexFileStorage } from "@vexcms/file-storage-convex"
4
+
5
+ import { authOptions } from "@convex/auth/options"
6
+
7
+ import { access } from "~/auth/access"
8
+ import { images, users } from "~/vexcms/collections"
9
+
10
+ /**
11
+ * VexCMS configuration for this project.
12
+ *
13
+ * Registers the admin sidebar layout, the Better Auth adapter, Convex file
14
+ * storage, and every collection. Add collections/globals here as you define
15
+ * them — `vex dev` / `vex generate` derive the Convex schema and TypeScript
16
+ * types from this file.
17
+ *
18
+ * @see defineConfig in @vexcms/core
19
+ * @see betterAuthAdapter in @vexcms/better-auth
20
+ */
21
+ const vexConfig = defineConfig({
22
+ access,
23
+ admin: {
24
+ sidebar: {
25
+ side: "right",
26
+ },
27
+ },
28
+ authAdapter: betterAuthAdapter({ config: authOptions }),
29
+ storage: {
30
+ adapters: [convexFileStorage({ mediaCollections: [images] })],
31
+ },
32
+ collections: [users],
33
+ })
34
+
35
+ export default vexConfig