create-vexcms 0.0.3
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/dist/index.d.ts +1 -0
- package/dist/index.js +2409 -0
- package/package.json +37 -0
- package/templates/base-nextjs/.prettierignore +26 -0
- package/templates/base-nextjs/.prettierrc +7 -0
- package/templates/base-nextjs/README.md +256 -0
- package/templates/base-nextjs/_gitignore +2 -0
- package/templates/base-nextjs/components.json +24 -0
- package/templates/base-nextjs/convex/_generated/api.d.ts +125 -0
- package/templates/base-nextjs/convex/_generated/api.js +23 -0
- package/templates/base-nextjs/convex/_generated/dataModel.d.ts +60 -0
- package/templates/base-nextjs/convex/_generated/server.d.ts +143 -0
- package/templates/base-nextjs/convex/_generated/server.js +93 -0
- package/templates/base-nextjs/convex/auth/adapter/index.ts +230 -0
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +547 -0
- package/templates/base-nextjs/convex/auth/api.ts +8 -0
- package/templates/base-nextjs/convex/auth/config.ts +10 -0
- package/templates/base-nextjs/convex/auth/db.ts +303 -0
- package/templates/base-nextjs/convex/auth/index.ts +14 -0
- package/templates/base-nextjs/convex/auth/options.ts +63 -0
- package/templates/base-nextjs/convex/auth/plugins/index.ts +20 -0
- package/templates/base-nextjs/convex/auth/sessions.ts +60 -0
- package/templates/base-nextjs/convex/auth.config.ts +7 -0
- package/templates/base-nextjs/convex/convex.config.ts +5 -0
- package/templates/base-nextjs/convex/http.ts +28 -0
- package/templates/base-nextjs/convex/schema.ts +3 -0
- package/templates/base-nextjs/convex/vex/auth.ts +38 -0
- package/templates/base-nextjs/convex/vex/collections.ts +321 -0
- package/templates/base-nextjs/convex/vex/firstUser.ts +134 -0
- package/templates/base-nextjs/convex/vex/helpers.ts +33 -0
- package/templates/base-nextjs/convex/vex/impersonation.ts +51 -0
- package/templates/base-nextjs/convex/vex/media.ts +246 -0
- package/templates/base-nextjs/convex/vex/migrate.ts +74 -0
- package/templates/base-nextjs/convex/vex/model/collections.ts +196 -0
- package/templates/base-nextjs/convex/vex/model/media.ts +87 -0
- package/templates/base-nextjs/convex/vex/model/versions.ts +254 -0
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +53 -0
- package/templates/base-nextjs/convex/vex/versions.ts +588 -0
- package/templates/base-nextjs/eslint.config.mjs +164 -0
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +67 -0
- package/templates/base-nextjs/postcss.config.mjs +7 -0
- package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
- package/templates/base-nextjs/public/file.svg +1 -0
- package/templates/base-nextjs/public/globe.svg +1 -0
- package/templates/base-nextjs/public/next.svg +1 -0
- package/templates/base-nextjs/public/vercel.svg +1 -0
- package/templates/base-nextjs/public/window.svg +1 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/page.tsx +9 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +23 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/default.tsx +3 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/page.tsx +3 -0
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/page.tsx +9 -0
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +11 -0
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +14 -0
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +116 -0
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +33 -0
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +38 -0
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +101 -0
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +15 -0
- package/templates/base-nextjs/src/app/admin/layout.tsx +33 -0
- package/templates/base-nextjs/src/app/api/auth/[...all]/route.ts +6 -0
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/app/globals.css +130 -0
- package/templates/base-nextjs/src/app/layout.tsx +30 -0
- package/templates/base-nextjs/src/auth/client.tsx +41 -0
- package/templates/base-nextjs/src/auth/permissions.ts +102 -0
- package/templates/base-nextjs/src/auth/server.ts +16 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +64 -0
- package/templates/base-nextjs/src/auth/types.ts +3 -0
- package/templates/base-nextjs/src/components/component-example.tsx +474 -0
- package/templates/base-nextjs/src/components/example.tsx +52 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +9 -0
- package/templates/base-nextjs/src/components/providers/convex.tsx +32 -0
- package/templates/base-nextjs/src/components/providers/server.tsx +15 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +11 -0
- package/templates/base-nextjs/src/components/ui/alert-dialog.tsx +162 -0
- package/templates/base-nextjs/src/components/ui/badge.tsx +52 -0
- package/templates/base-nextjs/src/components/ui/button.tsx +60 -0
- package/templates/base-nextjs/src/components/ui/card.tsx +92 -0
- package/templates/base-nextjs/src/components/ui/combobox.tsx +271 -0
- package/templates/base-nextjs/src/components/ui/dialog.tsx +135 -0
- package/templates/base-nextjs/src/components/ui/dropdown-menu.tsx +246 -0
- package/templates/base-nextjs/src/components/ui/field.tsx +224 -0
- package/templates/base-nextjs/src/components/ui/input-group.tsx +146 -0
- package/templates/base-nextjs/src/components/ui/input.tsx +20 -0
- package/templates/base-nextjs/src/components/ui/label.tsx +20 -0
- package/templates/base-nextjs/src/components/ui/select.tsx +189 -0
- package/templates/base-nextjs/src/components/ui/separator.tsx +21 -0
- package/templates/base-nextjs/src/components/ui/textarea.tsx +18 -0
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +67 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +8 -0
- package/templates/base-nextjs/src/db/constants/index.ts +44 -0
- package/templates/base-nextjs/src/db/types.ts +6 -0
- package/templates/base-nextjs/src/env.mjs +48 -0
- package/templates/base-nextjs/src/lib/utils.ts +6 -0
- package/templates/base-nextjs/src/proxy.ts +23 -0
- package/templates/base-nextjs/src/vexcms/access.ts +28 -0
- package/templates/base-nextjs/src/vexcms/auth.ts +4 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +1 -0
- package/templates/base-nextjs/src/vexcms/collections/media.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/users.ts +46 -0
- package/templates/base-nextjs/tsconfig.json +60 -0
- package/templates/base-nextjs/vex.config.ts +23 -0
- package/templates/marketing-site/convex/pages.ts +46 -0
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +58 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +69 -0
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +71 -0
- package/templates/marketing-site/src/db/constants/index.ts +52 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +28 -0
- package/templates/marketing-site/src/vexcms/collections/headers.ts +35 -0
- package/templates/marketing-site/src/vexcms/collections/index.ts +7 -0
- package/templates/marketing-site/src/vexcms/collections/pages.ts +40 -0
- package/templates/marketing-site/src/vexcms/collections/site_settings.ts +31 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +37 -0
- package/templates/marketing-site/vex.config.ts +32 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SidebarTrigger } from "@vexcms/ui"
|
|
2
|
+
import { sanitizeConfigForClient } from "@vexcms/core"
|
|
3
|
+
|
|
4
|
+
import config from "~/../vex.config"
|
|
5
|
+
import { getCurrentUser } from "~/auth/serverUtils"
|
|
6
|
+
import { AdminLayoutWrapper } from "./AdminLayoutWrapper"
|
|
7
|
+
|
|
8
|
+
const clientConfig = sanitizeConfigForClient(config)
|
|
9
|
+
|
|
10
|
+
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
11
|
+
const user = await getCurrentUser()
|
|
12
|
+
|
|
13
|
+
const permissionUser = user
|
|
14
|
+
? {
|
|
15
|
+
id: user.id,
|
|
16
|
+
roles: Array.isArray(user.role) ? user.role : [user.role].filter(Boolean),
|
|
17
|
+
name: user.name,
|
|
18
|
+
email: user.email,
|
|
19
|
+
avatar: user.image,
|
|
20
|
+
}
|
|
21
|
+
: undefined
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<AdminLayoutWrapper
|
|
25
|
+
config={clientConfig}
|
|
26
|
+
user={user ? { name: user.name, avatar: user.image, email: user.email } : undefined}
|
|
27
|
+
permissionUser={permissionUser}
|
|
28
|
+
>
|
|
29
|
+
<SidebarTrigger />
|
|
30
|
+
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">{children}</div>
|
|
31
|
+
</AdminLayoutWrapper>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
|
|
5
|
+
@source "../../node_modules/@vexcms/ui/dist/**/*.js";
|
|
6
|
+
@source "../../node_modules/@vexcms/admin-next/dist/**/*.js";
|
|
7
|
+
@source "../../node_modules/@vexcms/admin-next/src/**/*.tsx";
|
|
8
|
+
|
|
9
|
+
@custom-variant dark (&:is(.dark *));
|
|
10
|
+
|
|
11
|
+
@theme inline {
|
|
12
|
+
--color-background: var(--background);
|
|
13
|
+
--color-foreground: var(--foreground);
|
|
14
|
+
--font-sans: var(--font-sans);
|
|
15
|
+
--font-mono: var(--font-geist-mono);
|
|
16
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
17
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
18
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
19
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
20
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
21
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
22
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
23
|
+
--color-sidebar: var(--sidebar);
|
|
24
|
+
--color-chart-5: var(--chart-5);
|
|
25
|
+
--color-chart-4: var(--chart-4);
|
|
26
|
+
--color-chart-3: var(--chart-3);
|
|
27
|
+
--color-chart-2: var(--chart-2);
|
|
28
|
+
--color-chart-1: var(--chart-1);
|
|
29
|
+
--color-ring: var(--ring);
|
|
30
|
+
--color-input: var(--input);
|
|
31
|
+
--color-border: var(--border);
|
|
32
|
+
--color-destructive: var(--destructive);
|
|
33
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
34
|
+
--color-accent: var(--accent);
|
|
35
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
36
|
+
--color-muted: var(--muted);
|
|
37
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
38
|
+
--color-secondary: var(--secondary);
|
|
39
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
40
|
+
--color-primary: var(--primary);
|
|
41
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
42
|
+
--color-popover: var(--popover);
|
|
43
|
+
--color-card-foreground: var(--card-foreground);
|
|
44
|
+
--color-card: var(--card);
|
|
45
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
46
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
47
|
+
--radius-lg: var(--radius);
|
|
48
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
49
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
50
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
51
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:root {
|
|
55
|
+
--radius: 0.5rem;
|
|
56
|
+
--background: oklch(1 0 0);
|
|
57
|
+
--foreground: oklch(0.145 0.004 240);
|
|
58
|
+
--card: oklch(1 0 0);
|
|
59
|
+
--card-foreground: oklch(0.145 0.004 240);
|
|
60
|
+
--popover: oklch(1 0 0);
|
|
61
|
+
--popover-foreground: oklch(0.145 0.004 240);
|
|
62
|
+
--primary: oklch(0.205 0.003 240);
|
|
63
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
64
|
+
--secondary: oklch(0.965 0.002 240);
|
|
65
|
+
--secondary-foreground: oklch(0.205 0.003 240);
|
|
66
|
+
--muted: oklch(0.965 0.002 240);
|
|
67
|
+
--muted-foreground: oklch(0.556 0.002 240);
|
|
68
|
+
--accent: oklch(0.965 0.002 240);
|
|
69
|
+
--accent-foreground: oklch(0.205 0.003 240);
|
|
70
|
+
--destructive: oklch(0.577 0.184 27.3);
|
|
71
|
+
--border: oklch(0.922 0.003 240);
|
|
72
|
+
--input: oklch(0.922 0.003 240);
|
|
73
|
+
--ring: oklch(0.708 0.003 240);
|
|
74
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
75
|
+
--chart-2: oklch(0.6 0.118 184.714);
|
|
76
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
77
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
78
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
79
|
+
--sidebar: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-foreground: oklch(0.145 0.004 240);
|
|
81
|
+
--sidebar-primary: oklch(0.205 0.003 240);
|
|
82
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
83
|
+
--sidebar-accent: oklch(0.965 0.002 240);
|
|
84
|
+
--sidebar-accent-foreground: oklch(0.205 0.003 240);
|
|
85
|
+
--sidebar-border: oklch(0.922 0.003 240);
|
|
86
|
+
--sidebar-ring: oklch(0.708 0.003 240);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.dark {
|
|
90
|
+
--background: oklch(0.145 0.004 240);
|
|
91
|
+
--foreground: oklch(0.985 0 0);
|
|
92
|
+
--card: oklch(0.145 0.004 240);
|
|
93
|
+
--card-foreground: oklch(0.985 0 0);
|
|
94
|
+
--popover: oklch(0.145 0.004 240);
|
|
95
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
96
|
+
--primary: oklch(0.985 0 0);
|
|
97
|
+
--primary-foreground: oklch(0.205 0.003 240);
|
|
98
|
+
--secondary: oklch(0.269 0.006 240);
|
|
99
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
100
|
+
--muted: oklch(0.269 0.006 240);
|
|
101
|
+
--muted-foreground: oklch(0.708 0.003 240);
|
|
102
|
+
--accent: oklch(0.269 0.006 240);
|
|
103
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
104
|
+
--destructive: oklch(0.396 0.141 25.768);
|
|
105
|
+
--border: oklch(0.269 0.006 240);
|
|
106
|
+
--input: oklch(0.269 0.006 240);
|
|
107
|
+
--ring: oklch(0.439 0.003 240);
|
|
108
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
109
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
110
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
111
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
112
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
113
|
+
--sidebar: oklch(0.205 0.003 240);
|
|
114
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
115
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
116
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
117
|
+
--sidebar-accent: oklch(0.269 0.006 240);
|
|
118
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
119
|
+
--sidebar-border: oklch(0.269 0.006 240);
|
|
120
|
+
--sidebar-ring: oklch(0.439 0.003 240);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@layer base {
|
|
124
|
+
* {
|
|
125
|
+
@apply border-border outline-ring/50;
|
|
126
|
+
}
|
|
127
|
+
body {
|
|
128
|
+
@apply bg-background text-foreground;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
import { Geist, Geist_Mono, Inter } from "next/font/google"
|
|
3
|
+
|
|
4
|
+
import ClientProviders from "~/components/providers/client"
|
|
5
|
+
import ServerProviders from "~/components/providers/server"
|
|
6
|
+
|
|
7
|
+
import "./globals.css"
|
|
8
|
+
|
|
9
|
+
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" })
|
|
10
|
+
const geistSans = Geist({ subsets: ["latin"], variable: "--font-geist-sans" })
|
|
11
|
+
const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" })
|
|
12
|
+
|
|
13
|
+
export const metadata: Metadata = {
|
|
14
|
+
description: "Test application for Vex CMS development",
|
|
15
|
+
title: "Vex CMS Test App",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
19
|
+
return (
|
|
20
|
+
<html lang="en" suppressHydrationWarning className={`${inter.variable} ${geistSans.variable} ${geistMono.variable}`}>
|
|
21
|
+
<body className="antialiased">
|
|
22
|
+
<ServerProviders>
|
|
23
|
+
<ClientProviders>
|
|
24
|
+
{children}
|
|
25
|
+
</ClientProviders>
|
|
26
|
+
</ServerProviders>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
import { convexClient } from "@convex-dev/better-auth/client/plugins"
|
|
4
|
+
import { AuthUIProvider } from "@daveyplate/better-auth-ui"
|
|
5
|
+
import { adminClient, apiKeyClient } from "better-auth/client/plugins"
|
|
6
|
+
import { createAuthClient } from "better-auth/react"
|
|
7
|
+
import Link from "next/link"
|
|
8
|
+
import { useRouter } from "next/navigation"
|
|
9
|
+
|
|
10
|
+
import { env } from "~/env.mjs"
|
|
11
|
+
|
|
12
|
+
export const authClient = createAuthClient({
|
|
13
|
+
basePath: "/api/auth",
|
|
14
|
+
baseURL: env.NEXT_PUBLIC_SITE_URL,
|
|
15
|
+
plugins: [adminClient(), apiKeyClient(), convexClient()],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const { signIn, signOut, useSession } = authClient
|
|
19
|
+
|
|
20
|
+
// copied from code example, unsure if this is actually useful or not
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/no-empty-function
|
|
22
|
+
authClient.$store.listen("$sessionSignal", async () => { })
|
|
23
|
+
|
|
24
|
+
export default function BetterAuthClientProvider({ children }: { children: ReactNode }) {
|
|
25
|
+
const router = useRouter()
|
|
26
|
+
return (
|
|
27
|
+
<AuthUIProvider
|
|
28
|
+
authClient={authClient}
|
|
29
|
+
/* {{EMAIL_PASSWORD_CREDENTIALS}} */
|
|
30
|
+
// {{OAUTH_UI_PROVIDERS}}
|
|
31
|
+
Link={Link}
|
|
32
|
+
navigate={router.push}
|
|
33
|
+
onSessionChange={() => {
|
|
34
|
+
router.refresh()
|
|
35
|
+
}}
|
|
36
|
+
replace={router.replace}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</AuthUIProvider>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { User } from "~/db/types"
|
|
2
|
+
|
|
3
|
+
import { TABLE_SLUG_USERS, USER_ROLES, type UserRole } from "~/db/constants"
|
|
4
|
+
|
|
5
|
+
export const AUTH_ACTIONS = {
|
|
6
|
+
create: "create",
|
|
7
|
+
delete: "delete",
|
|
8
|
+
read: "read",
|
|
9
|
+
update: "update",
|
|
10
|
+
} as const
|
|
11
|
+
export type AuthAction = (typeof AUTH_ACTIONS)[keyof typeof AUTH_ACTIONS]
|
|
12
|
+
|
|
13
|
+
export const RESOURCES = {
|
|
14
|
+
users: TABLE_SLUG_USERS,
|
|
15
|
+
} as const
|
|
16
|
+
export type Permissions = {
|
|
17
|
+
[TABLE_SLUG_USERS]: {
|
|
18
|
+
action: AuthAction
|
|
19
|
+
dataType: User
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Resource = (typeof RESOURCES)[keyof typeof RESOURCES]
|
|
24
|
+
|
|
25
|
+
export type RolesWithPermissions = Record<
|
|
26
|
+
UserRole,
|
|
27
|
+
Partial<{
|
|
28
|
+
[Key in keyof Permissions]: Partial<Record<Permissions[Key]["action"], PermissionCheck<Key>>>
|
|
29
|
+
}>
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
type PermissionCheck<Key extends keyof Permissions> =
|
|
33
|
+
| (({ data, user }: { data: Permissions[Key]["dataType"]; user: User }) => boolean)
|
|
34
|
+
| boolean
|
|
35
|
+
|
|
36
|
+
// type PermissionDataTypeExcluding<K extends keyof Permissions> = Permissions[Exclude<keyof Permissions, K>]["dataType"];
|
|
37
|
+
// usage: T extends PermissionDataTypeExcluding<"orgs" | "users">
|
|
38
|
+
|
|
39
|
+
const ROLES = {
|
|
40
|
+
[USER_ROLES.admin]: {
|
|
41
|
+
[TABLE_SLUG_USERS]: {
|
|
42
|
+
create: true,
|
|
43
|
+
delete: true,
|
|
44
|
+
read: true,
|
|
45
|
+
update: true,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
[USER_ROLES.editor]: {
|
|
49
|
+
[TABLE_SLUG_USERS]: {
|
|
50
|
+
read: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
[USER_ROLES.author]: {
|
|
54
|
+
[TABLE_SLUG_USERS]: {
|
|
55
|
+
read: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
56
|
+
update: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
[USER_ROLES.member]: {
|
|
60
|
+
[TABLE_SLUG_USERS]: {
|
|
61
|
+
read: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
[USER_ROLES.user]: {
|
|
65
|
+
[TABLE_SLUG_USERS]: {
|
|
66
|
+
create: false,
|
|
67
|
+
delete: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
68
|
+
read: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
69
|
+
update: ({ data: targetUser, user }) => user._id === targetUser._id,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
} as const satisfies RolesWithPermissions
|
|
73
|
+
|
|
74
|
+
export function hasPermission<Resource extends keyof Permissions>({
|
|
75
|
+
action,
|
|
76
|
+
data,
|
|
77
|
+
resource,
|
|
78
|
+
user,
|
|
79
|
+
}: {
|
|
80
|
+
action: Permissions[Resource]["action"]
|
|
81
|
+
data?: Permissions[Resource]["dataType"]
|
|
82
|
+
resource: Resource
|
|
83
|
+
user?: null | User
|
|
84
|
+
}): boolean {
|
|
85
|
+
if (!user?.role) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return user.role.some((role) => {
|
|
90
|
+
const permission = (ROLES as RolesWithPermissions)[role as UserRole][resource]?.[action]
|
|
91
|
+
|
|
92
|
+
if (!permission) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof permission === "boolean") {
|
|
97
|
+
return permission
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return data != null && permission({ data, user })
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs"
|
|
2
|
+
|
|
3
|
+
import { env } from "~/env.mjs"
|
|
4
|
+
|
|
5
|
+
export const {
|
|
6
|
+
fetchAuthAction,
|
|
7
|
+
fetchAuthMutation,
|
|
8
|
+
fetchAuthQuery,
|
|
9
|
+
getToken,
|
|
10
|
+
handler,
|
|
11
|
+
isAuthenticated,
|
|
12
|
+
preloadAuthQuery,
|
|
13
|
+
} = convexBetterAuthNextJs({
|
|
14
|
+
convexSiteUrl: env.NEXT_PUBLIC_CONVEX_SITE_URL,
|
|
15
|
+
convexUrl: env.NEXT_PUBLIC_CONVEX_URL,
|
|
16
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { api } from "@convex/_generated/api"
|
|
2
|
+
import { fetchQuery } from "convex/nextjs"
|
|
3
|
+
import { cookies } from "next/headers"
|
|
4
|
+
|
|
5
|
+
export async function getSessionToken() {
|
|
6
|
+
const cookieStore = await cookies()
|
|
7
|
+
const sessionToken = cookieStore.get("better-auth.session_token")?.value.split(".")[0]
|
|
8
|
+
if (!sessionToken) {
|
|
9
|
+
console.error("Error getting session token")
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
return sessionToken
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get current user session in Next.js server components/actions
|
|
17
|
+
* Validates the Better Auth session token from cookies and fetches user data
|
|
18
|
+
*/
|
|
19
|
+
export const getCurrentUser = async () => {
|
|
20
|
+
try {
|
|
21
|
+
const sessionToken = await getSessionToken()
|
|
22
|
+
|
|
23
|
+
if (!sessionToken) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
|
|
28
|
+
sessionToken,
|
|
29
|
+
})
|
|
30
|
+
if (!session?.user) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
return session.user
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Error getting current user:", error)
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get full session (user + session data) in Next.js server components/actions
|
|
42
|
+
*/
|
|
43
|
+
export async function getSession() {
|
|
44
|
+
try {
|
|
45
|
+
const sessionToken = await getSessionToken()
|
|
46
|
+
|
|
47
|
+
if (!sessionToken) {
|
|
48
|
+
console.log("no session token")
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
|
|
53
|
+
sessionToken,
|
|
54
|
+
})
|
|
55
|
+
if (!session?.user) {
|
|
56
|
+
console.log("no session")
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
return session
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error("Error getting session:", error)
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|