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,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,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,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
|
+
}
|