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.
Files changed (116) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +2409 -0
  3. package/package.json +37 -0
  4. package/templates/base-nextjs/.prettierignore +26 -0
  5. package/templates/base-nextjs/.prettierrc +7 -0
  6. package/templates/base-nextjs/README.md +256 -0
  7. package/templates/base-nextjs/_gitignore +2 -0
  8. package/templates/base-nextjs/components.json +24 -0
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +125 -0
  10. package/templates/base-nextjs/convex/_generated/api.js +23 -0
  11. package/templates/base-nextjs/convex/_generated/dataModel.d.ts +60 -0
  12. package/templates/base-nextjs/convex/_generated/server.d.ts +143 -0
  13. package/templates/base-nextjs/convex/_generated/server.js +93 -0
  14. package/templates/base-nextjs/convex/auth/adapter/index.ts +230 -0
  15. package/templates/base-nextjs/convex/auth/adapter/utils.ts +547 -0
  16. package/templates/base-nextjs/convex/auth/api.ts +8 -0
  17. package/templates/base-nextjs/convex/auth/config.ts +10 -0
  18. package/templates/base-nextjs/convex/auth/db.ts +303 -0
  19. package/templates/base-nextjs/convex/auth/index.ts +14 -0
  20. package/templates/base-nextjs/convex/auth/options.ts +63 -0
  21. package/templates/base-nextjs/convex/auth/plugins/index.ts +20 -0
  22. package/templates/base-nextjs/convex/auth/sessions.ts +60 -0
  23. package/templates/base-nextjs/convex/auth.config.ts +7 -0
  24. package/templates/base-nextjs/convex/convex.config.ts +5 -0
  25. package/templates/base-nextjs/convex/http.ts +28 -0
  26. package/templates/base-nextjs/convex/schema.ts +3 -0
  27. package/templates/base-nextjs/convex/vex/auth.ts +38 -0
  28. package/templates/base-nextjs/convex/vex/collections.ts +321 -0
  29. package/templates/base-nextjs/convex/vex/firstUser.ts +134 -0
  30. package/templates/base-nextjs/convex/vex/helpers.ts +33 -0
  31. package/templates/base-nextjs/convex/vex/impersonation.ts +51 -0
  32. package/templates/base-nextjs/convex/vex/media.ts +246 -0
  33. package/templates/base-nextjs/convex/vex/migrate.ts +74 -0
  34. package/templates/base-nextjs/convex/vex/model/collections.ts +196 -0
  35. package/templates/base-nextjs/convex/vex/model/media.ts +87 -0
  36. package/templates/base-nextjs/convex/vex/model/versions.ts +254 -0
  37. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +53 -0
  38. package/templates/base-nextjs/convex/vex/versions.ts +588 -0
  39. package/templates/base-nextjs/eslint.config.mjs +164 -0
  40. package/templates/base-nextjs/next.config.ts +10 -0
  41. package/templates/base-nextjs/package.json +67 -0
  42. package/templates/base-nextjs/postcss.config.mjs +7 -0
  43. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  44. package/templates/base-nextjs/public/file.svg +1 -0
  45. package/templates/base-nextjs/public/globe.svg +1 -0
  46. package/templates/base-nextjs/public/next.svg +1 -0
  47. package/templates/base-nextjs/public/vercel.svg +1 -0
  48. package/templates/base-nextjs/public/window.svg +1 -0
  49. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/page.tsx +9 -0
  50. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +23 -0
  51. package/templates/base-nextjs/src/app/(frontend)/@auth/default.tsx +3 -0
  52. package/templates/base-nextjs/src/app/(frontend)/@auth/page.tsx +3 -0
  53. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/page.tsx +9 -0
  54. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +11 -0
  55. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +14 -0
  56. package/templates/base-nextjs/src/app/(frontend)/page.tsx +116 -0
  57. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +33 -0
  58. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +38 -0
  59. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +101 -0
  60. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +15 -0
  61. package/templates/base-nextjs/src/app/admin/layout.tsx +33 -0
  62. package/templates/base-nextjs/src/app/api/auth/[...all]/route.ts +6 -0
  63. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  64. package/templates/base-nextjs/src/app/globals.css +130 -0
  65. package/templates/base-nextjs/src/app/layout.tsx +30 -0
  66. package/templates/base-nextjs/src/auth/client.tsx +41 -0
  67. package/templates/base-nextjs/src/auth/permissions.ts +102 -0
  68. package/templates/base-nextjs/src/auth/server.ts +16 -0
  69. package/templates/base-nextjs/src/auth/serverUtils.ts +64 -0
  70. package/templates/base-nextjs/src/auth/types.ts +3 -0
  71. package/templates/base-nextjs/src/components/component-example.tsx +474 -0
  72. package/templates/base-nextjs/src/components/example.tsx +52 -0
  73. package/templates/base-nextjs/src/components/providers/client.tsx +9 -0
  74. package/templates/base-nextjs/src/components/providers/convex.tsx +32 -0
  75. package/templates/base-nextjs/src/components/providers/server.tsx +15 -0
  76. package/templates/base-nextjs/src/components/providers/theme.tsx +11 -0
  77. package/templates/base-nextjs/src/components/ui/alert-dialog.tsx +162 -0
  78. package/templates/base-nextjs/src/components/ui/badge.tsx +52 -0
  79. package/templates/base-nextjs/src/components/ui/button.tsx +60 -0
  80. package/templates/base-nextjs/src/components/ui/card.tsx +92 -0
  81. package/templates/base-nextjs/src/components/ui/combobox.tsx +271 -0
  82. package/templates/base-nextjs/src/components/ui/dialog.tsx +135 -0
  83. package/templates/base-nextjs/src/components/ui/dropdown-menu.tsx +246 -0
  84. package/templates/base-nextjs/src/components/ui/field.tsx +224 -0
  85. package/templates/base-nextjs/src/components/ui/input-group.tsx +146 -0
  86. package/templates/base-nextjs/src/components/ui/input.tsx +20 -0
  87. package/templates/base-nextjs/src/components/ui/label.tsx +20 -0
  88. package/templates/base-nextjs/src/components/ui/select.tsx +189 -0
  89. package/templates/base-nextjs/src/components/ui/separator.tsx +21 -0
  90. package/templates/base-nextjs/src/components/ui/textarea.tsx +18 -0
  91. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +67 -0
  92. package/templates/base-nextjs/src/db/constants/auth.ts +8 -0
  93. package/templates/base-nextjs/src/db/constants/index.ts +44 -0
  94. package/templates/base-nextjs/src/db/types.ts +6 -0
  95. package/templates/base-nextjs/src/env.mjs +48 -0
  96. package/templates/base-nextjs/src/lib/utils.ts +6 -0
  97. package/templates/base-nextjs/src/proxy.ts +23 -0
  98. package/templates/base-nextjs/src/vexcms/access.ts +28 -0
  99. package/templates/base-nextjs/src/vexcms/auth.ts +4 -0
  100. package/templates/base-nextjs/src/vexcms/collections/index.ts +1 -0
  101. package/templates/base-nextjs/src/vexcms/collections/media.ts +15 -0
  102. package/templates/base-nextjs/src/vexcms/collections/users.ts +46 -0
  103. package/templates/base-nextjs/tsconfig.json +60 -0
  104. package/templates/base-nextjs/vex.config.ts +23 -0
  105. package/templates/marketing-site/convex/pages.ts +46 -0
  106. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +58 -0
  107. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +69 -0
  108. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +71 -0
  109. package/templates/marketing-site/src/db/constants/index.ts +52 -0
  110. package/templates/marketing-site/src/vexcms/collections/footers.ts +28 -0
  111. package/templates/marketing-site/src/vexcms/collections/headers.ts +35 -0
  112. package/templates/marketing-site/src/vexcms/collections/index.ts +7 -0
  113. package/templates/marketing-site/src/vexcms/collections/pages.ts +40 -0
  114. package/templates/marketing-site/src/vexcms/collections/site_settings.ts +31 -0
  115. package/templates/marketing-site/src/vexcms/collections/themes.ts +37 -0
  116. package/templates/marketing-site/vex.config.ts +32 -0
@@ -0,0 +1,303 @@
1
+ import type { BetterAuthDBSchema } from "better-auth/db"
2
+ import type { GenericId } from "convex/values"
3
+
4
+ import { v } from "convex/values"
5
+
6
+ import type { WhereClause } from "./adapter/utils"
7
+
8
+ import { internalMutation, internalQuery } from "../_generated/server"
9
+ import schema from "../schema"
10
+ import { checkUniqueFields, listOne, paginate, selectFields } from "./adapter/utils"
11
+
12
+ // Helper to get Better Auth schema - we'll pass it from the adapter
13
+ const getBetterAuthSchema = (schemaJson: string): BetterAuthDBSchema => {
14
+ return JSON.parse(schemaJson)
15
+ }
16
+
17
+ // Create (insert) operation
18
+ export const dbCreate = internalMutation({
19
+ args: {
20
+ betterAuthSchema: v.string(),
21
+ data: v.any(),
22
+ model: v.string(),
23
+ select: v.optional(v.array(v.string())),
24
+ },
25
+ handler: async (ctx, { betterAuthSchema, data, model, select }) => {
26
+ const authSchema = getBetterAuthSchema(betterAuthSchema)
27
+
28
+ // Check unique fields before insert
29
+ await checkUniqueFields(ctx, schema, authSchema, model, data)
30
+
31
+ const id = await ctx.db.insert(model as any, data)
32
+ const doc = await ctx.db.get(id)
33
+ if (!doc) {
34
+ throw new Error(`Failed to create ${model}`)
35
+ }
36
+
37
+ return selectFields(doc, select)
38
+ },
39
+ })
40
+
41
+ // Find one operation
42
+ export const dbFindOne = internalQuery({
43
+ args: {
44
+ betterAuthSchema: v.string(),
45
+ model: v.string(),
46
+ select: v.optional(v.array(v.string())),
47
+ where: v.array(v.any()),
48
+ },
49
+ handler: async (ctx, { betterAuthSchema, model, select, where }) => {
50
+ const authSchema = getBetterAuthSchema(betterAuthSchema)
51
+ // @ts-expect-error GenericCtx type is a superset
52
+ const result = await listOne(ctx, schema, authSchema, {
53
+ model,
54
+ select,
55
+ where: where as WhereClause[],
56
+ })
57
+
58
+ return result
59
+ },
60
+ })
61
+
62
+ // Find many operation
63
+ export const dbFindMany = internalQuery({
64
+ args: {
65
+ betterAuthSchema: v.string(),
66
+ limit: v.optional(v.number()),
67
+ model: v.string(),
68
+ sortBy: v.optional(
69
+ v.object({
70
+ direction: v.union(v.literal("asc"), v.literal("desc")),
71
+ field: v.string(),
72
+ })
73
+ ),
74
+ where: v.optional(v.array(v.any())),
75
+ },
76
+ handler: async (ctx, { betterAuthSchema, limit, model, sortBy, where }) => {
77
+ const authSchema = getBetterAuthSchema(betterAuthSchema)
78
+ const parsedWhere = (where ?? []) as WhereClause[]
79
+
80
+ // Handle OR connector by running parallel queries
81
+ if (parsedWhere.some((w) => w.connector === "OR")) {
82
+ const results = await Promise.all(
83
+ parsedWhere.map(async (w) => {
84
+ // @ts-expect-error GenericCtx type is a superset
85
+ const result = await paginate(ctx, schema, authSchema, {
86
+ model,
87
+ paginationOpts: { cursor: null, numItems: limit ?? 200 },
88
+ sortBy,
89
+ where: [w],
90
+ })
91
+ return result.page
92
+ })
93
+ )
94
+
95
+ // De-duplicate and flatten
96
+ const seen = new Set<string>()
97
+ const uniqueDocs: any[] = []
98
+ for (const docs of results) {
99
+ for (const doc of docs) {
100
+ const docId = doc._id as string
101
+ if (!seen.has(docId)) {
102
+ seen.add(docId)
103
+ uniqueDocs.push(doc)
104
+ }
105
+ }
106
+ }
107
+
108
+ // Apply sorting if needed
109
+ if (sortBy) {
110
+ uniqueDocs.sort((a, b) => {
111
+ const aVal = a[sortBy.field]
112
+ const bVal = b[sortBy.field]
113
+ if (aVal === bVal) {
114
+ return 0
115
+ }
116
+ const comparison = aVal > bVal ? 1 : -1
117
+ return sortBy.direction === "desc" ? -comparison : comparison
118
+ })
119
+ }
120
+
121
+ return uniqueDocs.slice(0, limit)
122
+ }
123
+
124
+ // Normal case without OR
125
+ // @ts-expect-error GenericCtx type is a superset
126
+ const result = await paginate(ctx, schema, authSchema, {
127
+ model,
128
+ paginationOpts: { cursor: null, numItems: limit ?? 200 },
129
+ sortBy,
130
+ where: parsedWhere,
131
+ })
132
+
133
+ return result.page
134
+ },
135
+ })
136
+
137
+ // Count operation
138
+ export const dbCount = internalQuery({
139
+ args: {
140
+ betterAuthSchema: v.string(),
141
+ model: v.string(),
142
+ where: v.optional(v.array(v.any())),
143
+ },
144
+ handler: async (ctx, { betterAuthSchema, model, where }) => {
145
+ const authSchema = getBetterAuthSchema(betterAuthSchema)
146
+ const parsedWhere = (where ?? []) as WhereClause[]
147
+
148
+ // Handle OR connector
149
+ if (parsedWhere.some((w) => w.connector === "OR")) {
150
+ const results = await Promise.all(
151
+ parsedWhere.map(async (w) => {
152
+ // @ts-expect-error GenericCtx type is a superset
153
+ const result = await paginate(ctx, schema, authSchema, {
154
+ model,
155
+ paginationOpts: { cursor: null, numItems: 200 },
156
+ where: [w],
157
+ })
158
+ return result.page
159
+ })
160
+ )
161
+
162
+ // De-duplicate and count
163
+ const seen = new Set<string>()
164
+ for (const docs of results) {
165
+ for (const doc of docs) {
166
+ const docId = doc._id as string
167
+ seen.add(docId)
168
+ }
169
+ }
170
+ return seen.size
171
+ }
172
+
173
+ // Normal case
174
+ // @ts-expect-error GenericCtx type is a superset
175
+ const result = await paginate(ctx, schema, authSchema, {
176
+ model,
177
+ paginationOpts: { cursor: null, numItems: 200 },
178
+ where: parsedWhere,
179
+ })
180
+
181
+ return result.page.length
182
+ },
183
+ })
184
+
185
+ // Update operation
186
+ export const dbUpdate = internalMutation({
187
+ args: {
188
+ betterAuthSchema: v.string(),
189
+ model: v.string(),
190
+ update: v.any(),
191
+ where: v.array(v.any()),
192
+ },
193
+ handler: async (ctx, { betterAuthSchema, model, update, where }) => {
194
+ const authSchema = getBetterAuthSchema(betterAuthSchema)
195
+ const parsedWhere = where as WhereClause[]
196
+
197
+ // Find the document to update
198
+ const doc = await listOne(ctx, schema, authSchema, {
199
+ model,
200
+ where: parsedWhere,
201
+ })
202
+
203
+ if (!doc) {
204
+ return null
205
+ }
206
+
207
+ // Check unique fields before update
208
+ await checkUniqueFields(ctx, schema, authSchema, model, update, doc)
209
+
210
+ await ctx.db.patch(doc._id as GenericId<any>, update)
211
+ return await ctx.db.get(doc._id as GenericId<any>)
212
+ },
213
+ })
214
+
215
+ // Update many operation
216
+ export const dbUpdateMany = internalMutation({
217
+ args: {
218
+ betterAuthSchema: v.string(),
219
+ model: v.string(),
220
+ update: v.any(),
221
+ where: v.array(v.any()),
222
+ },
223
+ handler: async (ctx, { betterAuthSchema, model, update, where }) => {
224
+ const authSchema = getBetterAuthSchema(betterAuthSchema)
225
+ const parsedWhere = where as WhereClause[]
226
+
227
+ const result = await paginate(ctx, schema, authSchema, {
228
+ model,
229
+ paginationOpts: { cursor: null, numItems: 200 },
230
+ where: parsedWhere,
231
+ })
232
+
233
+ // Check unique fields if updating multiple docs
234
+ if (result.page.length > 1) {
235
+ const uniqueFieldKeys = Object.keys(update).filter(
236
+ (key) => authSchema[model]?.fields?.[key]?.unique
237
+ )
238
+ if (uniqueFieldKeys.length > 0) {
239
+ throw new Error(
240
+ `Attempted to set unique fields in multiple documents in ${model} with the same value. Fields: ${uniqueFieldKeys.join(", ")}`
241
+ )
242
+ }
243
+ }
244
+
245
+ // Update each document
246
+ for (const doc of result.page) {
247
+ await checkUniqueFields(ctx, schema, authSchema, model, update, doc)
248
+ await ctx.db.patch(doc._id as GenericId<any>, update)
249
+ }
250
+
251
+ return result.page.length
252
+ },
253
+ })
254
+
255
+ // Delete operation
256
+ export const dbDelete = internalMutation({
257
+ args: {
258
+ betterAuthSchema: v.string(),
259
+ model: v.string(),
260
+ where: v.array(v.any()),
261
+ },
262
+ handler: async (ctx, { betterAuthSchema, model, where }) => {
263
+ const authSchema = getBetterAuthSchema(betterAuthSchema)
264
+ const parsedWhere = where as WhereClause[]
265
+
266
+ const doc = await listOne(ctx, schema, authSchema, {
267
+ model,
268
+ where: parsedWhere,
269
+ })
270
+
271
+ if (!doc) {
272
+ return
273
+ }
274
+
275
+ await ctx.db.delete(doc._id as GenericId<any>)
276
+ },
277
+ })
278
+
279
+ // Delete many operation
280
+ export const dbDeleteMany = internalMutation({
281
+ args: {
282
+ betterAuthSchema: v.string(),
283
+ model: v.string(),
284
+ where: v.array(v.any()),
285
+ },
286
+ handler: async (ctx, { betterAuthSchema, model, where }) => {
287
+ const authSchema = getBetterAuthSchema(betterAuthSchema)
288
+ const parsedWhere = where as WhereClause[]
289
+
290
+ const result = await paginate(ctx, schema, authSchema, {
291
+ model,
292
+ paginationOpts: { cursor: null, numItems: 200 },
293
+ where: parsedWhere,
294
+ })
295
+
296
+ // Delete each document
297
+ for (const doc of result.page) {
298
+ await ctx.db.delete(doc._id as GenericId<any>)
299
+ }
300
+
301
+ return result.page.length
302
+ },
303
+ })
@@ -0,0 +1,14 @@
1
+ import type { GenericActionCtx } from "convex/server"
2
+
3
+ import { betterAuth } from "better-auth"
4
+
5
+ import type { DataModel } from "../_generated/dataModel"
6
+
7
+ import { buildBetterAuthOptions } from "./options"
8
+
9
+ export const createAuth = (
10
+ ctx: GenericActionCtx<DataModel>,
11
+ { optionsOnly } = { optionsOnly: false }
12
+ ) => {
13
+ return betterAuth(buildBetterAuthOptions({ ctx, optionsOnly }))
14
+ }
@@ -0,0 +1,63 @@
1
+ import { type DataModel } from "@convex/_generated/dataModel"
2
+ import schema from "@convex/schema"
3
+ import { type BetterAuthOptions } from "better-auth"
4
+ import { type GenericActionCtx } from "convex/server"
5
+
6
+ import {
7
+ TABLE_SLUG_ACCOUNTS,
8
+ TABLE_SLUG_SESSIONS,
9
+ TABLE_SLUG_USERS,
10
+ TABLE_SLUG_VERIFICATIONS,
11
+ USER_ROLES,
12
+ } from "~/db/constants"
13
+
14
+ import { convexAdapter } from "./adapter"
15
+ import betterAuthPlugins from "./plugins"
16
+
17
+ export const betterAuthOptions: BetterAuthOptions = {
18
+ account: {
19
+ modelName: TABLE_SLUG_ACCOUNTS,
20
+ },
21
+ baseURL: process.env.SITE_URL,
22
+ // {{EMAIL_PASSWORD_AUTH}}
23
+ // {{OAUTH_PROVIDERS}}
24
+ logger: {
25
+ disabled: false,
26
+ },
27
+ plugins: betterAuthPlugins,
28
+ secret: process.env.BETTER_AUTH_SECRET,
29
+ session: {
30
+ modelName: TABLE_SLUG_SESSIONS,
31
+ },
32
+ trustedOrigins: [process.env.SITE_URL!],
33
+ user: {
34
+ additionalFields: {
35
+ role: {
36
+ type: "string[]",
37
+ defaultValue: [USER_ROLES.user],
38
+ required: true,
39
+ },
40
+ },
41
+ modelName: TABLE_SLUG_USERS,
42
+ },
43
+ verification: {
44
+ modelName: TABLE_SLUG_VERIFICATIONS,
45
+ },
46
+ }
47
+
48
+ export function buildBetterAuthOptions({
49
+ ctx,
50
+ optionsOnly = false,
51
+ }: {
52
+ ctx: GenericActionCtx<DataModel>
53
+ optionsOnly?: boolean
54
+ }) {
55
+ return {
56
+ ...betterAuthOptions,
57
+ database: convexAdapter(ctx, schema),
58
+ logger: {
59
+ ...betterAuthOptions.logger,
60
+ disabled: optionsOnly,
61
+ },
62
+ }
63
+ }
@@ -0,0 +1,20 @@
1
+ import { convex } from "@convex-dev/better-auth/plugins"
2
+ import authConfig from "@convex/auth.config"
3
+ import { nextCookies } from "better-auth/next-js"
4
+ import { admin, apiKey } from "better-auth/plugins"
5
+ // {{ORGANIZATIONS_IMPORT}}
6
+
7
+ import { USER_ROLES } from "~/db/constants"
8
+
9
+ const plugins = [
10
+ admin({
11
+ adminRoles: [USER_ROLES.admin],
12
+ defaultRole: USER_ROLES.user,
13
+ }),
14
+ apiKey(),
15
+ convex({ authConfig }),
16
+ nextCookies(),
17
+ // {{ORGANIZATIONS_PLUGIN}}
18
+ ]
19
+
20
+ export default plugins
@@ -0,0 +1,60 @@
1
+ import { v } from "convex/values"
2
+
3
+ import { TABLE_SLUG_SESSIONS, type TABLE_SLUG_USERS } from "~/db/constants"
4
+
5
+ import type { Id } from "../_generated/dataModel"
6
+
7
+ import { query } from "../_generated/server"
8
+
9
+ /**
10
+ * Get session with user data by session token
11
+ * Used for server-side authentication in Next.js
12
+ */
13
+ export const getSessionWithUser = query({
14
+ args: {
15
+ sessionToken: v.string(),
16
+ },
17
+ handler: async (ctx, args) => {
18
+ // Find the session by token
19
+ const session = await ctx.db
20
+ .query(TABLE_SLUG_SESSIONS)
21
+ .withIndex("by_token", (q) => q.eq("token", args.sessionToken))
22
+ .first()
23
+
24
+ if (!session) {
25
+ return null
26
+ }
27
+
28
+ // Check if session is expired
29
+ if (session.expiresAt < Date.now()) {
30
+ console.error("Session Expired")
31
+ return null
32
+ }
33
+
34
+ // Get the user data
35
+ const user = await ctx.db.get(session.userId as Id<typeof TABLE_SLUG_USERS>)
36
+
37
+ if (!user) {
38
+ return null
39
+ }
40
+
41
+ return {
42
+ session: {
43
+ id: session._id,
44
+ expiresAt: session.expiresAt,
45
+ ipAddress: session.ipAddress,
46
+ token: session.token,
47
+ userAgent: session.userAgent,
48
+ userId: session.userId,
49
+ },
50
+ user: {
51
+ id: user._id,
52
+ name: user.name,
53
+ email: user.email,
54
+ emailVerified: user.emailVerified,
55
+ image: user.image,
56
+ role: user.role,
57
+ },
58
+ }
59
+ },
60
+ })
@@ -0,0 +1,7 @@
1
+ import type { AuthConfig } from "convex/server"
2
+
3
+ import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config"
4
+
5
+ export default {
6
+ providers: [getAuthConfigProvider()],
7
+ } satisfies AuthConfig
@@ -0,0 +1,5 @@
1
+ import { defineApp } from "convex/server"
2
+
3
+ const app = defineApp()
4
+
5
+ export default app
@@ -0,0 +1,28 @@
1
+ import { httpRouter } from "convex/server"
2
+
3
+ import { httpAction } from "./_generated/server"
4
+ import { createAuth } from "./auth"
5
+
6
+ const http = httpRouter()
7
+
8
+ // Auth request handler - basePath hardcoded to /api/auth for now
9
+ const authRequestHandler = httpAction(async (ctx, request) => {
10
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
11
+ const auth = createAuth(ctx)
12
+ return await auth.handler(request)
13
+ })
14
+
15
+ // Register auth routes for GET and POST
16
+ http.route({
17
+ handler: authRequestHandler,
18
+ method: "GET",
19
+ pathPrefix: "/api/auth/",
20
+ })
21
+
22
+ http.route({
23
+ handler: authRequestHandler,
24
+ method: "POST",
25
+ pathPrefix: "/api/auth/",
26
+ })
27
+
28
+ export default http
@@ -0,0 +1,3 @@
1
+ import { defineSchema } from "convex/server"
2
+
3
+ export default defineSchema({})
@@ -0,0 +1,38 @@
1
+ import type { VexQueryCtx } from "@vexcms/core"
2
+
3
+ import { TABLE_SLUG_USERS } from "~/db/constants"
4
+
5
+ import type { MutationCtx, QueryCtx } from "../_generated/server"
6
+
7
+ export type UserAuth = {
8
+ roles: string[]
9
+ user: Record<string, unknown>
10
+ }
11
+
12
+ /**
13
+ * Returns the authenticated user and their roles, or null if unauthenticated.
14
+ * Called by all generated collection query and mutation files.
15
+ */
16
+ export async function getUser(ctx: QueryCtx): Promise<null | UserAuth>
17
+ export async function getUser(ctx: MutationCtx): Promise<null | UserAuth>
18
+ export async function getUser(ctx: VexQueryCtx): Promise<null | UserAuth>
19
+ export async function getUser(ctx: MutationCtx | QueryCtx | VexQueryCtx): Promise<null | UserAuth> {
20
+ const identity = await ctx.auth.getUserIdentity()
21
+ if (!identity?.email) {
22
+ return null
23
+ }
24
+
25
+ const user = await (ctx.db as QueryCtx["db"])
26
+ .query(TABLE_SLUG_USERS)
27
+ .withIndex("by_email", (q) => q.eq("email", identity.email!))
28
+ .first()
29
+
30
+ if (!user) {
31
+ return null
32
+ }
33
+
34
+ return {
35
+ roles: (user.role as string[]) ?? [],
36
+ user: user as Record<string, unknown>,
37
+ }
38
+ }