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.
- package/LICENSE +202 -0
- package/README.md +93 -83
- package/dist/index.js +246 -81
- package/dist/index.mjs +30976 -0
- package/package.json +35 -15
- package/templates/base-nextjs/README.md +53 -205
- package/templates/base-nextjs/_env.example +7 -9
- package/templates/base-nextjs/_gitignore +41 -1
- package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
- package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
- package/templates/base-nextjs/convex/_generated/server.js +8 -0
- package/templates/base-nextjs/convex/auth/api.ts +49 -3
- package/templates/base-nextjs/convex/auth/db.ts +42 -300
- package/templates/base-nextjs/convex/auth/index.ts +11 -7
- package/templates/base-nextjs/convex/auth/options.ts +5 -29
- package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
- package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
- package/templates/base-nextjs/convex/http.ts +18 -3
- package/templates/base-nextjs/convex/schema.ts +104 -2
- package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
- package/templates/base-nextjs/convex/vex/globals.ts +19 -82
- package/templates/base-nextjs/convex/vex/media.ts +19 -246
- package/templates/base-nextjs/convex/vex.schema.ts +138 -0
- package/templates/base-nextjs/convex/vex.ts +19 -0
- package/templates/base-nextjs/convex.json +6 -0
- package/templates/base-nextjs/eslint.config.mjs +1 -21
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +47 -48
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
- package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
- package/templates/base-nextjs/src/app/globals.css +240 -104
- package/templates/base-nextjs/src/app/layout.tsx +23 -13
- package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
- package/templates/base-nextjs/src/auth/access.ts +46 -0
- package/templates/base-nextjs/src/auth/client.tsx +17 -5
- package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
- package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
- package/templates/base-nextjs/src/components/component-example.tsx +56 -464
- package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
- package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
- package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
- package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
- package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
- package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
- package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
- package/templates/base-nextjs/src/db/constants/index.ts +22 -35
- package/templates/base-nextjs/src/lib/utils.ts +0 -12
- package/templates/base-nextjs/src/proxy.ts +97 -12
- package/templates/base-nextjs/src/vex.config.ts +35 -0
- package/templates/base-nextjs/src/vex.types.ts +293 -0
- package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
- package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
- package/templates/base-nextjs/tsconfig.json +3 -0
- package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
- package/templates/marketing-site/convex/_generated/api.js +23 -0
- package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
- package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
- package/templates/marketing-site/convex/_generated/server.js +101 -0
- package/templates/marketing-site/convex/footers.ts +11 -7
- package/templates/marketing-site/convex/headers.ts +15 -5
- package/templates/marketing-site/convex/pages.ts +20 -35
- package/templates/marketing-site/convex/schema.ts +119 -0
- package/templates/marketing-site/convex/seed.ts +582 -35
- package/templates/marketing-site/convex/siteSettings.ts +19 -5
- package/templates/marketing-site/convex/theme.ts +55 -61
- package/templates/marketing-site/convex/vex.schema.ts +355 -0
- package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
- package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
- package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
- package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
- package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
- package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
- package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
- package/templates/marketing-site/src/lib/metadata.ts +51 -72
- package/templates/marketing-site/src/vex.config.ts +35 -0
- package/templates/marketing-site/src/vex.types.ts +634 -0
- package/templates/marketing-site/src/vexcms/api.ts +23 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
- package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
- package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
- package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
- package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
- package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
- package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
- package/templates/base-nextjs/convex/auth/config.ts +0 -10
- package/templates/base-nextjs/convex/vex/auth.ts +0 -38
- package/templates/base-nextjs/convex/vex/collections.ts +0 -333
- package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
- package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
- package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
- package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
- package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
- package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
- package/templates/base-nextjs/convex/vex/versions.ts +0 -648
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
- package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
- package/templates/base-nextjs/src/db/types.ts +0 -6
- package/templates/base-nextjs/src/vexcms/access.ts +0 -44
- package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
- package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
- package/templates/base-nextjs/vex.config.ts +0 -25
- package/templates/marketing-site/public/favicons/favicon.ico +0 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
- package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
- package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
- package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
- package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
- package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
- package/templates/marketing-site/src/db/constants/index.ts +0 -52
- package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
- package/templates/marketing-site/src/vexcms/access.ts +0 -36
- 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
|
|
45
|
-
<ChevronUpIcon
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
17
|
-
|
|
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 })
|
|
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
|
|
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 "
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
"
|
|
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,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
|
-
|
|
11
|
-
|
|
12
|
-
export const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|