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,303 +1,45 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { internalMutation, internalQuery } from "@convex/_generated/server"
|
|
2
|
+
import { authDbApi } from "@vexcms/better-auth"
|
|
3
3
|
|
|
4
|
-
import { v } from "convex/values"
|
|
5
|
-
|
|
6
|
-
import type { WhereClause } from "./adapter/utils"
|
|
7
|
-
|
|
8
|
-
import { internalMutation, internalQuery } from "../_generated/server"
|
|
9
4
|
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
5
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Convex internal mutations and queries that implement Better Auth's DB interface.
|
|
8
|
+
*
|
|
9
|
+
* Wire these into your Convex schema so `@vexcms/better-auth` can call them
|
|
10
|
+
* via `ctx.runMutation(anyApi.auth.db.dbCreate, ...)`:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // convex/schema.ts
|
|
14
|
+
* import { dbCreate, dbFindOne, ... } from "./auth/db";
|
|
15
|
+
*
|
|
16
|
+
* // register as internal (visibility: internal) in your schema tables:
|
|
17
|
+
* export default defineSchema({
|
|
18
|
+
* users: defineTable(...).index("by_email", ["email"]),
|
|
19
|
+
* sessions: defineTable(...).index("by_token", ["token"]),
|
|
20
|
+
* }, {
|
|
21
|
+
* internal: {
|
|
22
|
+
* dbCreate,
|
|
23
|
+
* dbFindOne,
|
|
24
|
+
* dbFindMany,
|
|
25
|
+
* dbCount,
|
|
26
|
+
* dbUpdate,
|
|
27
|
+
* dbUpdateMany,
|
|
28
|
+
* dbDelete,
|
|
29
|
+
* dbDeleteMany,
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @see {@link authDbApi} for the factory that produces these functions
|
|
35
|
+
*/
|
|
36
|
+
export const {
|
|
37
|
+
dbCreate,
|
|
38
|
+
dbFindOne,
|
|
39
|
+
dbFindMany,
|
|
40
|
+
dbCount,
|
|
41
|
+
dbUpdate,
|
|
42
|
+
dbUpdateMany,
|
|
43
|
+
dbDelete,
|
|
44
|
+
dbDeleteMany,
|
|
45
|
+
} = authDbApi({ schema, internalQuery, internalMutation })
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { GenericActionCtx } from "convex/server"
|
|
2
2
|
|
|
3
|
+
import { createBetterAuthAdapter } from "@vexcms/better-auth"
|
|
3
4
|
import { betterAuth } from "better-auth"
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
+
import { authOptions } from "./options"
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import type { DataModel } from "../_generated/dataModel"
|
|
8
9
|
|
|
9
|
-
export const createAuth = (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
export const createAuth = (ctx: GenericActionCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
|
|
11
|
+
return betterAuth({
|
|
12
|
+
database: createBetterAuthAdapter(ctx),
|
|
13
|
+
logger: {
|
|
14
|
+
disabled: optionsOnly,
|
|
15
|
+
},
|
|
16
|
+
...authOptions,
|
|
17
|
+
})
|
|
14
18
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import schema from "@convex/schema"
|
|
3
|
-
import { type BetterAuthOptions } from "better-auth"
|
|
4
|
-
import { type GenericActionCtx } from "convex/server"
|
|
1
|
+
import type { BetterAuthOptions } from "better-auth"
|
|
5
2
|
|
|
6
3
|
import {
|
|
7
4
|
TABLE_SLUG_ACCOUNTS,
|
|
@@ -11,20 +8,16 @@ import {
|
|
|
11
8
|
USER_ROLES,
|
|
12
9
|
} from "~/db/constants"
|
|
13
10
|
|
|
14
|
-
import {
|
|
15
|
-
import betterAuthPlugins from "./plugins"
|
|
11
|
+
import { createPlugins } from "./plugins"
|
|
16
12
|
|
|
17
|
-
export const
|
|
13
|
+
export const authOptions: BetterAuthOptions = {
|
|
18
14
|
account: {
|
|
19
15
|
modelName: TABLE_SLUG_ACCOUNTS,
|
|
20
16
|
},
|
|
21
17
|
baseURL: process.env.SITE_URL,
|
|
22
18
|
// {{EMAIL_PASSWORD_AUTH}}
|
|
23
19
|
// {{OAUTH_PROVIDERS}}
|
|
24
|
-
|
|
25
|
-
disabled: false,
|
|
26
|
-
},
|
|
27
|
-
plugins: betterAuthPlugins,
|
|
20
|
+
plugins: createPlugins(),
|
|
28
21
|
secret: process.env.BETTER_AUTH_SECRET,
|
|
29
22
|
session: {
|
|
30
23
|
modelName: TABLE_SLUG_SESSIONS,
|
|
@@ -32,7 +25,7 @@ export const betterAuthOptions: BetterAuthOptions = {
|
|
|
32
25
|
trustedOrigins: [process.env.SITE_URL!],
|
|
33
26
|
user: {
|
|
34
27
|
additionalFields: {
|
|
35
|
-
|
|
28
|
+
roles: {
|
|
36
29
|
type: "string[]",
|
|
37
30
|
defaultValue: [USER_ROLES.user],
|
|
38
31
|
required: true,
|
|
@@ -44,20 +37,3 @@ export const betterAuthOptions: BetterAuthOptions = {
|
|
|
44
37
|
modelName: TABLE_SLUG_VERIFICATIONS,
|
|
45
38
|
},
|
|
46
39
|
}
|
|
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
|
-
}
|
|
@@ -1,20 +1,30 @@
|
|
|
1
|
+
import { apiKey } from "@better-auth/api-key"
|
|
1
2
|
import { convex } from "@convex-dev/better-auth/plugins"
|
|
2
3
|
import authConfig from "@convex/auth.config"
|
|
3
4
|
import { nextCookies } from "better-auth/next-js"
|
|
4
|
-
import { admin,
|
|
5
|
+
import { admin, anonymous } from "better-auth/plugins"
|
|
5
6
|
// {{ORGANIZATIONS_IMPORT}}
|
|
6
7
|
|
|
7
8
|
import { USER_ROLES } from "~/db/constants"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Returns a fresh array of Better Auth plugins for each VexCMS auth session.
|
|
12
|
+
*
|
|
13
|
+
* Returns a new array on every call so that plugin initialization — including
|
|
14
|
+
* the `convex()` factory, which internally calls the deprecated oidc-provider
|
|
15
|
+
* plugin — runs inside `createAuth()` rather than at module-eval time. This
|
|
16
|
+
* lets `http.ts`'s console.warn filter suppress the deprecation noise before
|
|
17
|
+
* it fires.
|
|
18
|
+
*/
|
|
19
|
+
export const createPlugins = () => [
|
|
10
20
|
admin({
|
|
11
21
|
adminRoles: [USER_ROLES.admin],
|
|
12
22
|
defaultRole: USER_ROLES.user,
|
|
13
23
|
}),
|
|
24
|
+
anonymous(),
|
|
14
25
|
apiKey(),
|
|
15
26
|
convex({ authConfig }),
|
|
16
|
-
nextCookies(),
|
|
17
27
|
// {{ORGANIZATIONS_PLUGIN}}
|
|
28
|
+
// this plugin must be last, per the convex dev CLI's own warnings
|
|
29
|
+
nextCookies(),
|
|
18
30
|
]
|
|
19
|
-
|
|
20
|
-
export default plugins
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { type Id } from "@convex/_generated/dataModel"
|
|
1
2
|
import { v } from "convex/values"
|
|
2
3
|
|
|
3
4
|
import { TABLE_SLUG_SESSIONS, type TABLE_SLUG_USERS } from "~/db/constants"
|
|
4
5
|
|
|
5
|
-
import type { Id } from "../_generated/dataModel"
|
|
6
|
-
|
|
7
6
|
import { query } from "../_generated/server"
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -15,28 +14,23 @@ export const getSessionWithUser = query({
|
|
|
15
14
|
sessionToken: v.string(),
|
|
16
15
|
},
|
|
17
16
|
handler: async (ctx, args) => {
|
|
18
|
-
// Find the session by token
|
|
19
17
|
const session = await ctx.db
|
|
20
18
|
.query(TABLE_SLUG_SESSIONS)
|
|
21
19
|
.withIndex("by_token", (q) => q.eq("token", args.sessionToken))
|
|
22
20
|
.first()
|
|
23
21
|
|
|
24
|
-
if (!session)
|
|
25
|
-
return null
|
|
26
|
-
}
|
|
22
|
+
if (!session) return null
|
|
27
23
|
|
|
28
|
-
// Check if session is expired
|
|
29
24
|
if (session.expiresAt < Date.now()) {
|
|
30
|
-
console.error("Session Expired")
|
|
25
|
+
console.error("[getSessionWithUser] Session Expired")
|
|
31
26
|
return null
|
|
32
27
|
}
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
const userId = session.userId
|
|
30
|
+
if (!userId) return null
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
32
|
+
const user = await ctx.db.get(userId as Id<typeof TABLE_SLUG_USERS>)
|
|
33
|
+
if (!user) return null
|
|
40
34
|
|
|
41
35
|
return {
|
|
42
36
|
session: {
|
|
@@ -48,12 +42,12 @@ export const getSessionWithUser = query({
|
|
|
48
42
|
userId: session.userId,
|
|
49
43
|
},
|
|
50
44
|
user: {
|
|
51
|
-
id: user._id,
|
|
45
|
+
id: user.userId ?? user._id,
|
|
52
46
|
name: user.name,
|
|
53
47
|
email: user.email,
|
|
54
48
|
emailVerified: user.emailVerified,
|
|
55
49
|
image: user.image,
|
|
56
|
-
|
|
50
|
+
roles: user.roles,
|
|
57
51
|
},
|
|
58
52
|
}
|
|
59
53
|
},
|
|
@@ -7,9 +7,24 @@ const http = httpRouter()
|
|
|
7
7
|
|
|
8
8
|
// Auth request handler - basePath hardcoded to /api/auth for now
|
|
9
9
|
const authRequestHandler = httpAction(async (ctx, request) => {
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// Suppress the oidc-provider deprecation warning spammed by @convex-dev/better-auth
|
|
11
|
+
// on every request. Convex wraps console.warn per-invocation, so this must run
|
|
12
|
+
// inside the handler (not at module level) to intercept after Convex's wrapper is set.
|
|
13
|
+
// Remove once @convex-dev/better-auth migrates to @better-auth/oauth-provider.
|
|
14
|
+
const _warn = console.warn
|
|
15
|
+
console.warn = (...args: unknown[]) => {
|
|
16
|
+
if (typeof args[0] === "string" && args[0].includes("oidc-provider")) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
_warn(...args)
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
23
|
+
const auth = createAuth(ctx)
|
|
24
|
+
return await auth.handler(request)
|
|
25
|
+
} finally {
|
|
26
|
+
console.warn = _warn
|
|
27
|
+
}
|
|
13
28
|
})
|
|
14
29
|
|
|
15
30
|
// Register auth routes for GET and POST
|
|
@@ -1,3 +1,105 @@
|
|
|
1
|
-
import { defineSchema } from "convex/server"
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server"
|
|
2
|
+
import { v } from "convex/values"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_USERS } from "~/db/constants"
|
|
5
|
+
|
|
6
|
+
import { apikey, images, jwks, session, verification } from "./vex.schema"
|
|
7
|
+
|
|
8
|
+
export default defineSchema({
|
|
9
|
+
// `organization`/`team`/`teamMember`/`member`/`invitation`/`vex_globals` are
|
|
10
|
+
// hand-written, not imported from `./vex.schema` — the generator only emits
|
|
11
|
+
// them when the `organization()` plugin (`--orgs`) or a registered global is
|
|
12
|
+
// actually present in `vex.config.ts`. `access.ts` and every `getAuth`
|
|
13
|
+
// caller (`convex/vex.ts`, `convex/vex/media.ts`, `convex/vex/globals.ts`)
|
|
14
|
+
// reference `orgCollectionSlug`/`resolveOrgs: true` unconditionally, so
|
|
15
|
+
// these tables must exist in the schema from day one — enabling `--orgs`
|
|
16
|
+
// or registering a global later needs no schema migration.
|
|
17
|
+
vex_globals: defineTable({
|
|
18
|
+
slug: v.string(),
|
|
19
|
+
data: v.any(),
|
|
20
|
+
}).index("by_slug", ["slug"]),
|
|
21
|
+
images,
|
|
22
|
+
team: defineTable({
|
|
23
|
+
name: v.string(),
|
|
24
|
+
organizationId: v.string(),
|
|
25
|
+
createdAt: v.number(),
|
|
26
|
+
updatedAt: v.optional(v.number()),
|
|
27
|
+
}).index("by_organizationId", ["organizationId"]),
|
|
28
|
+
teamMember: defineTable({
|
|
29
|
+
teamId: v.string(),
|
|
30
|
+
userId: v.string(),
|
|
31
|
+
createdAt: v.optional(v.number()),
|
|
32
|
+
})
|
|
33
|
+
.index("by_teamId", ["teamId"])
|
|
34
|
+
.index("by_userId", ["userId"]),
|
|
35
|
+
organization: defineTable({
|
|
36
|
+
name: v.string(),
|
|
37
|
+
slug: v.string(),
|
|
38
|
+
logo: v.optional(v.string()),
|
|
39
|
+
createdAt: v.number(),
|
|
40
|
+
metadata: v.optional(v.string()),
|
|
41
|
+
}).index("by_slug", ["slug"]),
|
|
42
|
+
member: defineTable({
|
|
43
|
+
organizationId: v.string(),
|
|
44
|
+
userId: v.string(),
|
|
45
|
+
role: v.string(),
|
|
46
|
+
createdAt: v.number(),
|
|
47
|
+
})
|
|
48
|
+
.index("by_organizationId", ["organizationId"])
|
|
49
|
+
.index("by_userId", ["userId"]),
|
|
50
|
+
invitation: defineTable({
|
|
51
|
+
organizationId: v.string(),
|
|
52
|
+
email: v.string(),
|
|
53
|
+
role: v.optional(v.string()),
|
|
54
|
+
teamId: v.optional(v.string()),
|
|
55
|
+
status: v.string(),
|
|
56
|
+
expiresAt: v.number(),
|
|
57
|
+
createdAt: v.number(),
|
|
58
|
+
inviterId: v.string(),
|
|
59
|
+
roles: v.array(v.string()),
|
|
60
|
+
})
|
|
61
|
+
.index("by_organizationId", ["organizationId"])
|
|
62
|
+
.index("by_email", ["email"]),
|
|
63
|
+
session,
|
|
64
|
+
verification,
|
|
65
|
+
apikey,
|
|
66
|
+
jwks,
|
|
67
|
+
|
|
68
|
+
[TABLE_SLUG_USERS]: defineTable({
|
|
69
|
+
name: v.string(),
|
|
70
|
+
banExpires: v.optional(v.number()), // admin plugin
|
|
71
|
+
banned: v.optional(v.boolean()), // admin plugin
|
|
72
|
+
banReason: v.optional(v.string()), // admin plugin
|
|
73
|
+
createdAt: v.number(),
|
|
74
|
+
displayUsername: v.optional(v.union(v.null(), v.string())),
|
|
75
|
+
email: v.string(),
|
|
76
|
+
emailVerified: v.boolean(),
|
|
77
|
+
image: v.optional(v.string()),
|
|
78
|
+
isAnonymous: v.optional(v.union(v.null(), v.boolean())),
|
|
79
|
+
phoneNumber: v.optional(v.union(v.null(), v.string())),
|
|
80
|
+
phoneNumberVerified: v.optional(v.union(v.null(), v.boolean())),
|
|
81
|
+
role: v.optional(v.string()), // admin plugin — single string in BA 1.6
|
|
82
|
+
roles: v.array(v.string()), // our multi-role field via additionalFields
|
|
83
|
+
twoFactorEnabled: v.optional(v.union(v.null(), v.boolean())),
|
|
84
|
+
updatedAt: v.number(),
|
|
85
|
+
userId: v.optional(v.union(v.null(), v.string())),
|
|
86
|
+
username: v.optional(v.union(v.null(), v.string())),
|
|
87
|
+
}).index("by_email", ["email"]),
|
|
88
|
+
|
|
89
|
+
[TABLE_SLUG_ACCOUNTS]: defineTable({
|
|
90
|
+
accessToken: v.optional(v.string()),
|
|
91
|
+
accessTokenExpiresAt: v.optional(v.number()),
|
|
92
|
+
accountId: v.string(),
|
|
93
|
+
createdAt: v.number(),
|
|
94
|
+
idToken: v.optional(v.string()),
|
|
95
|
+
password: v.optional(v.string()),
|
|
96
|
+
providerId: v.string(),
|
|
97
|
+
refreshToken: v.optional(v.string()),
|
|
98
|
+
refreshTokenExpiresAt: v.optional(v.number()),
|
|
99
|
+
scope: v.optional(v.string()),
|
|
100
|
+
updatedAt: v.number(),
|
|
101
|
+
userId: v.string(),
|
|
102
|
+
})
|
|
103
|
+
.index("by_userId", ["userId"])
|
|
104
|
+
.index("by_accountId", ["accountId"]),
|
|
105
|
+
})
|