@onmax/nuxt-better-auth 0.0.2-alpha.14 → 0.0.2-alpha.16
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/module.json +1 -1
- package/dist/module.mjs +281 -17
- package/dist/runtime/adapters/convex.d.ts +111 -0
- package/dist/runtime/adapters/convex.js +213 -0
- package/dist/runtime/app/composables/useUserSession.js +1 -1
- package/dist/runtime/config.d.ts +5 -4
- package/dist/runtime/config.js +7 -2
- package/package.json +17 -4
- package/skills/nuxt-better-auth/SKILL.md +0 -92
- package/skills/nuxt-better-auth/references/client-auth.md +0 -153
- package/skills/nuxt-better-auth/references/client-only.md +0 -89
- package/skills/nuxt-better-auth/references/database.md +0 -115
- package/skills/nuxt-better-auth/references/installation.md +0 -126
- package/skills/nuxt-better-auth/references/plugins.md +0 -138
- package/skills/nuxt-better-auth/references/route-protection.md +0 -105
- package/skills/nuxt-better-auth/references/server-auth.md +0 -135
- package/skills/nuxt-better-auth/references/types.md +0 -142
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { existsSync, writeFileSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { defineNuxtModule, createResolver, hasNuxtModule, addTemplate, addTypeTemplate, updateTemplates, addServerImportsDir, addServerImports, addServerScanDir, addServerHandler, addImportsDir, addPlugin, addComponentsDir, extendPages } from '@nuxt/kit';
|
|
4
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addTemplate, addTypeTemplate, updateTemplates, addServerImportsDir, addServerImports, addServerScanDir, addServerHandler, addImportsDir, addPlugin, addComponentsDir, installModule, extendPages } from '@nuxt/kit';
|
|
4
5
|
import { consola as consola$1 } from 'consola';
|
|
5
6
|
import { defu } from 'defu';
|
|
6
|
-
import { join } from 'pathe';
|
|
7
|
+
import { join, dirname } from 'pathe';
|
|
7
8
|
import { toRouteMatcher, createRouter } from 'radix3';
|
|
9
|
+
import { isCI, isTest } from 'std-env';
|
|
8
10
|
import { generateDrizzleSchema as generateDrizzleSchema$1 } from '@better-auth/cli/api';
|
|
11
|
+
import { getAuthTables } from 'better-auth/db';
|
|
9
12
|
export { defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
|
|
10
13
|
|
|
14
|
+
const version = "0.0.2-alpha.16";
|
|
15
|
+
|
|
11
16
|
function setupDevTools(nuxt) {
|
|
12
17
|
nuxt.hook("devtools:customTabs", (tabs) => {
|
|
13
18
|
tabs.push({
|
|
@@ -52,6 +57,101 @@ async function generateDrizzleSchema(authOptions, dialect, schemaOptions) {
|
|
|
52
57
|
}
|
|
53
58
|
return result.code;
|
|
54
59
|
}
|
|
60
|
+
const convexIndexFields = {
|
|
61
|
+
account: ["accountId", ["accountId", "providerId"], ["providerId", "userId"]],
|
|
62
|
+
rateLimit: ["key"],
|
|
63
|
+
session: ["expiresAt", ["expiresAt", "userId"]],
|
|
64
|
+
verification: ["expiresAt", "identifier"],
|
|
65
|
+
user: [["email", "name"], "name", "userId"],
|
|
66
|
+
passkey: ["credentialID"],
|
|
67
|
+
oauthConsent: [["clientId", "userId"]]
|
|
68
|
+
};
|
|
69
|
+
function getConvexSpecialFields(tables) {
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
Object.entries(tables).map(([key, table]) => {
|
|
72
|
+
const fields = Object.fromEntries(
|
|
73
|
+
Object.entries(table.fields).map(([fieldKey, field]) => [
|
|
74
|
+
field.fieldName ?? fieldKey,
|
|
75
|
+
{
|
|
76
|
+
...field.sortable ? { sortable: true } : {},
|
|
77
|
+
...field.unique ? { unique: true } : {},
|
|
78
|
+
...field.references ? { references: field.references } : {}
|
|
79
|
+
}
|
|
80
|
+
]).filter(([_key, value]) => typeof value === "object" ? Object.keys(value).length > 0 : true)
|
|
81
|
+
);
|
|
82
|
+
return [key, fields];
|
|
83
|
+
}).filter(([_key, value]) => typeof value === "object" ? Object.keys(value).length > 0 : true)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
function getMergedConvexIndexFields(tables) {
|
|
87
|
+
return Object.fromEntries(
|
|
88
|
+
Object.entries(tables).map(([key, table]) => {
|
|
89
|
+
const manualIndexes = convexIndexFields[key]?.map((index) => {
|
|
90
|
+
return typeof index === "string" ? table.fields[index]?.fieldName ?? index : index.map((i) => table.fields[i]?.fieldName ?? i);
|
|
91
|
+
}) || [];
|
|
92
|
+
const specialFieldsObj = getConvexSpecialFields(tables);
|
|
93
|
+
const specialFieldIndexes = Object.keys(specialFieldsObj[key] || {}).filter(
|
|
94
|
+
(index) => !manualIndexes.some((m) => Array.isArray(m) ? m[0] === index : m === index)
|
|
95
|
+
);
|
|
96
|
+
return [key, manualIndexes.concat(specialFieldIndexes)];
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
async function generateConvexSchema(authOptions) {
|
|
101
|
+
const tables = getAuthTables(authOptions);
|
|
102
|
+
let code = `/**
|
|
103
|
+
* Auto-generated Better Auth tables for Convex.
|
|
104
|
+
* Import these tables in your convex/schema.ts:
|
|
105
|
+
*
|
|
106
|
+
* import { defineSchema } from 'convex/server'
|
|
107
|
+
* import { authTables } from './_generated/auth-tables'
|
|
108
|
+
*
|
|
109
|
+
* export default defineSchema({ ...authTables, ...yourTables })
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
import { defineTable } from 'convex/server'
|
|
113
|
+
import { v } from 'convex/values'
|
|
114
|
+
|
|
115
|
+
export const authTables = {
|
|
116
|
+
`;
|
|
117
|
+
const getType = (_name, field) => {
|
|
118
|
+
const type = field.type;
|
|
119
|
+
const typeMap = {
|
|
120
|
+
"string": "v.string()",
|
|
121
|
+
"boolean": "v.boolean()",
|
|
122
|
+
"number": "v.number()",
|
|
123
|
+
"date": "v.number()",
|
|
124
|
+
"json": "v.string()",
|
|
125
|
+
"number[]": "v.array(v.number())",
|
|
126
|
+
"string[]": "v.array(v.string())"
|
|
127
|
+
};
|
|
128
|
+
return typeMap[type];
|
|
129
|
+
};
|
|
130
|
+
for (const tableKey in tables) {
|
|
131
|
+
const table = tables[tableKey];
|
|
132
|
+
const modelName = table.modelName;
|
|
133
|
+
const fields = Object.fromEntries(Object.entries(table.fields).filter(([key]) => key !== "id"));
|
|
134
|
+
const indexes = getMergedConvexIndexFields(tables)[tableKey]?.map((index) => {
|
|
135
|
+
const indexArray = Array.isArray(index) ? index.sort() : [index];
|
|
136
|
+
const indexName = indexArray.join("_");
|
|
137
|
+
return `.index('${indexName}', ${JSON.stringify(indexArray)})`;
|
|
138
|
+
}) || [];
|
|
139
|
+
const schema = `${modelName}: defineTable({
|
|
140
|
+
${Object.keys(fields).map((field) => {
|
|
141
|
+
const attr = fields[field];
|
|
142
|
+
const type = getType(field, attr);
|
|
143
|
+
const optional = (fieldSchema) => attr.required ? fieldSchema : `v.optional(v.union(v.null(), ${fieldSchema}))`;
|
|
144
|
+
return ` ${attr.fieldName ?? field}: ${optional(type)},`;
|
|
145
|
+
}).join("\n")}
|
|
146
|
+
})${indexes.length > 0 ? `
|
|
147
|
+
${indexes.join("\n ")}` : ""},
|
|
148
|
+
`;
|
|
149
|
+
code += ` ${schema}`;
|
|
150
|
+
}
|
|
151
|
+
code += `}
|
|
152
|
+
`;
|
|
153
|
+
return code;
|
|
154
|
+
}
|
|
55
155
|
async function loadUserAuthConfig(configPath, throwOnError = false) {
|
|
56
156
|
const { createJiti } = await import('jiti');
|
|
57
157
|
const { defineServerAuth } = await import('../dist/runtime/config.js');
|
|
@@ -63,11 +163,11 @@ async function loadUserAuthConfig(configPath, throwOnError = false) {
|
|
|
63
163
|
globalThis.defineServerAuth._count++;
|
|
64
164
|
try {
|
|
65
165
|
const mod = await jiti.import(configPath);
|
|
66
|
-
const configFn =
|
|
166
|
+
const configFn = mod.default;
|
|
67
167
|
if (typeof configFn === "function") {
|
|
68
168
|
return configFn({ runtimeConfig: {}, db: null });
|
|
69
169
|
}
|
|
70
|
-
consola$1.warn("[@onmax/nuxt-better-auth] auth.config.ts does not export
|
|
170
|
+
consola$1.warn("[@onmax/nuxt-better-auth] auth.config.ts does not export default. Expected: export default defineServerAuth(...)");
|
|
71
171
|
if (throwOnError) {
|
|
72
172
|
throw new Error("auth.config.ts must export default defineServerAuth(...)");
|
|
73
173
|
}
|
|
@@ -101,8 +201,85 @@ function getHubCasing(hub) {
|
|
|
101
201
|
return hub.db.casing;
|
|
102
202
|
}
|
|
103
203
|
const consola = consola$1.withTag("nuxt-better-auth");
|
|
204
|
+
function resolveConvexUrl(nuxt) {
|
|
205
|
+
const convexConfig = nuxt.options.convex;
|
|
206
|
+
if (convexConfig?.url)
|
|
207
|
+
return convexConfig.url;
|
|
208
|
+
const runtimeUrl = nuxt.options.runtimeConfig.public?.convex?.url;
|
|
209
|
+
if (runtimeUrl)
|
|
210
|
+
return runtimeUrl;
|
|
211
|
+
if (process.env.CONVEX_URL)
|
|
212
|
+
return process.env.CONVEX_URL;
|
|
213
|
+
if (process.env.NUXT_PUBLIC_CONVEX_URL)
|
|
214
|
+
return process.env.NUXT_PUBLIC_CONVEX_URL;
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
const generateSecret = () => randomBytes(32).toString("hex");
|
|
218
|
+
function readEnvFile(rootDir) {
|
|
219
|
+
const envPath = join(rootDir, ".env");
|
|
220
|
+
return existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
|
|
221
|
+
}
|
|
222
|
+
function hasEnvSecret(rootDir) {
|
|
223
|
+
const match = readEnvFile(rootDir).match(/^BETTER_AUTH_SECRET=(.+)$/m);
|
|
224
|
+
return !!match && !!match[1] && match[1].trim().length > 0;
|
|
225
|
+
}
|
|
226
|
+
function appendSecretToEnv(rootDir, secret) {
|
|
227
|
+
const envPath = join(rootDir, ".env");
|
|
228
|
+
let content = readEnvFile(rootDir);
|
|
229
|
+
if (content.length > 0 && !content.endsWith("\n"))
|
|
230
|
+
content += "\n";
|
|
231
|
+
content += `BETTER_AUTH_SECRET=${secret}
|
|
232
|
+
`;
|
|
233
|
+
writeFileSync(envPath, content, "utf-8");
|
|
234
|
+
}
|
|
235
|
+
async function promptForSecret(rootDir) {
|
|
236
|
+
if (process.env.BETTER_AUTH_SECRET || hasEnvSecret(rootDir))
|
|
237
|
+
return void 0;
|
|
238
|
+
if (isCI || isTest) {
|
|
239
|
+
const secret2 = generateSecret();
|
|
240
|
+
appendSecretToEnv(rootDir, secret2);
|
|
241
|
+
consola.info("Generated BETTER_AUTH_SECRET and added to .env (CI mode)");
|
|
242
|
+
return secret2;
|
|
243
|
+
}
|
|
244
|
+
consola.box("BETTER_AUTH_SECRET is required for authentication.\nThis will be appended to your .env file.");
|
|
245
|
+
const choice = await consola.prompt("How do you want to set it?", {
|
|
246
|
+
type: "select",
|
|
247
|
+
options: [
|
|
248
|
+
{ label: "Generate for me", value: "generate", hint: "uses crypto.randomBytes(32)" },
|
|
249
|
+
{ label: "Enter manually", value: "paste" },
|
|
250
|
+
{ label: "Skip", value: "skip", hint: "will fail in production" }
|
|
251
|
+
],
|
|
252
|
+
cancel: "null"
|
|
253
|
+
});
|
|
254
|
+
if (typeof choice === "symbol" || choice === "skip") {
|
|
255
|
+
consola.warn("Skipping BETTER_AUTH_SECRET. Auth will fail without it in production.");
|
|
256
|
+
return void 0;
|
|
257
|
+
}
|
|
258
|
+
let secret;
|
|
259
|
+
if (choice === "generate") {
|
|
260
|
+
secret = generateSecret();
|
|
261
|
+
} else {
|
|
262
|
+
const input = await consola.prompt("Paste your secret (min 32 chars):", { type: "text", cancel: "null" });
|
|
263
|
+
if (typeof input === "symbol" || !input || input.length < 32) {
|
|
264
|
+
consola.warn("Invalid secret. Skipping.");
|
|
265
|
+
return void 0;
|
|
266
|
+
}
|
|
267
|
+
secret = input;
|
|
268
|
+
}
|
|
269
|
+
const preview = `${secret.slice(0, 8)}...${secret.slice(-4)}`;
|
|
270
|
+
const confirm = await consola.prompt(`Add to .env:
|
|
271
|
+
BETTER_AUTH_SECRET=${preview}
|
|
272
|
+
Proceed?`, { type: "confirm", initial: true, cancel: "null" });
|
|
273
|
+
if (typeof confirm === "symbol" || !confirm) {
|
|
274
|
+
consola.info("Cancelled. Secret not written.");
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
appendSecretToEnv(rootDir, secret);
|
|
278
|
+
consola.success("Added BETTER_AUTH_SECRET to .env");
|
|
279
|
+
return secret;
|
|
280
|
+
}
|
|
104
281
|
const module$1 = defineNuxtModule({
|
|
105
|
-
meta: { name: "@onmax/nuxt-better-auth", configKey: "auth", compatibility: { nuxt: ">=3.0.0" } },
|
|
282
|
+
meta: { name: "@onmax/nuxt-better-auth", version, configKey: "auth", compatibility: { nuxt: ">=3.0.0" } },
|
|
106
283
|
defaults: {
|
|
107
284
|
clientOnly: false,
|
|
108
285
|
serverConfig: "server/auth.config",
|
|
@@ -110,8 +287,40 @@ const module$1 = defineNuxtModule({
|
|
|
110
287
|
redirects: { login: "/login", guest: "/" },
|
|
111
288
|
secondaryStorage: false
|
|
112
289
|
},
|
|
290
|
+
async onInstall(nuxt) {
|
|
291
|
+
const generatedSecret = await promptForSecret(nuxt.options.rootDir);
|
|
292
|
+
if (generatedSecret)
|
|
293
|
+
process.env.BETTER_AUTH_SECRET = generatedSecret;
|
|
294
|
+
const serverPath = join(nuxt.options.rootDir, "server/auth.config.ts");
|
|
295
|
+
const clientPath = join(nuxt.options.srcDir, "auth.config.ts");
|
|
296
|
+
const serverTemplate = `import { defineServerAuth } from '@onmax/nuxt-better-auth/config'
|
|
297
|
+
|
|
298
|
+
export default defineServerAuth({
|
|
299
|
+
emailAndPassword: { enabled: true },
|
|
300
|
+
})
|
|
301
|
+
`;
|
|
302
|
+
const clientTemplate = `import { defineClientAuth } from '@onmax/nuxt-better-auth/config'
|
|
303
|
+
|
|
304
|
+
export default defineClientAuth({})
|
|
305
|
+
`;
|
|
306
|
+
if (!existsSync(serverPath)) {
|
|
307
|
+
await mkdir(dirname(serverPath), { recursive: true });
|
|
308
|
+
await writeFile(serverPath, serverTemplate);
|
|
309
|
+
consola.success("Created server/auth.config.ts");
|
|
310
|
+
}
|
|
311
|
+
if (!existsSync(clientPath)) {
|
|
312
|
+
await mkdir(dirname(clientPath), { recursive: true });
|
|
313
|
+
await writeFile(clientPath, clientTemplate);
|
|
314
|
+
const relativePath = clientPath.replace(`${nuxt.options.rootDir}/`, "");
|
|
315
|
+
consola.success(`Created ${relativePath}`);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
113
318
|
async setup(options, nuxt) {
|
|
114
319
|
const resolver = createResolver(import.meta.url);
|
|
320
|
+
if (options.clientConfig === "app/auth.config") {
|
|
321
|
+
const srcDirRelative = nuxt.options.srcDir.replace(`${nuxt.options.rootDir}/`, "");
|
|
322
|
+
options.clientConfig = srcDirRelative === nuxt.options.srcDir ? "auth.config" : `${srcDirRelative}/auth.config`;
|
|
323
|
+
}
|
|
115
324
|
const clientOnly = options.clientOnly;
|
|
116
325
|
const serverConfigFile = options.serverConfig;
|
|
117
326
|
const clientConfigFile = options.clientConfig;
|
|
@@ -120,12 +329,18 @@ const module$1 = defineNuxtModule({
|
|
|
120
329
|
const serverConfigExists = existsSync(`${serverConfigPath}.ts`) || existsSync(`${serverConfigPath}.js`);
|
|
121
330
|
const clientConfigExists = existsSync(`${clientConfigPath}.ts`) || existsSync(`${clientConfigPath}.js`);
|
|
122
331
|
if (!clientOnly && !serverConfigExists)
|
|
123
|
-
throw new Error(`[nuxt-better-auth] Missing ${serverConfigFile}.ts -
|
|
332
|
+
throw new Error(`[nuxt-better-auth] Missing ${serverConfigFile}.ts - export default defineServerAuth(...)`);
|
|
124
333
|
if (!clientConfigExists)
|
|
125
|
-
throw new Error(`[nuxt-better-auth] Missing ${clientConfigFile}.ts - export
|
|
334
|
+
throw new Error(`[nuxt-better-auth] Missing ${clientConfigFile}.ts - export default defineClientAuth(...)`);
|
|
126
335
|
const hasNuxtHub = hasNuxtModule("@nuxthub/core", nuxt);
|
|
127
336
|
const hub = hasNuxtHub ? nuxt.options.hub : void 0;
|
|
128
337
|
const hasHubDb = !clientOnly && hasNuxtHub && !!hub?.db;
|
|
338
|
+
const hasConvex = hasNuxtModule("nuxt-convex", nuxt);
|
|
339
|
+
const convexUrl = resolveConvexUrl(nuxt);
|
|
340
|
+
const hasConvexDb = !clientOnly && hasConvex && !!convexUrl && !hasHubDb;
|
|
341
|
+
if (hasConvexDb) {
|
|
342
|
+
consola.info("Detected Convex - using Convex HTTP adapter for Better Auth database");
|
|
343
|
+
}
|
|
129
344
|
let secondaryStorageEnabled = options.secondaryStorage ?? false;
|
|
130
345
|
if (secondaryStorageEnabled && clientOnly) {
|
|
131
346
|
consola.warn("secondaryStorage is not available in clientOnly mode. Disabling.");
|
|
@@ -137,7 +352,7 @@ const module$1 = defineNuxtModule({
|
|
|
137
352
|
nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
|
|
138
353
|
nuxt.options.runtimeConfig.public.auth = defu(nuxt.options.runtimeConfig.public.auth, {
|
|
139
354
|
redirects: { login: options.redirects?.login ?? "/login", guest: options.redirects?.guest ?? "/" },
|
|
140
|
-
useDatabase: hasHubDb,
|
|
355
|
+
useDatabase: hasHubDb || hasConvexDb,
|
|
141
356
|
clientOnly
|
|
142
357
|
});
|
|
143
358
|
if (clientOnly) {
|
|
@@ -151,7 +366,7 @@ const module$1 = defineNuxtModule({
|
|
|
151
366
|
const currentSecret = nuxt.options.runtimeConfig.betterAuthSecret;
|
|
152
367
|
nuxt.options.runtimeConfig.betterAuthSecret = currentSecret || process.env.BETTER_AUTH_SECRET || "";
|
|
153
368
|
const betterAuthSecret = nuxt.options.runtimeConfig.betterAuthSecret;
|
|
154
|
-
if (!nuxt.options.dev && !betterAuthSecret) {
|
|
369
|
+
if (!nuxt.options.dev && !nuxt.options._prepare && !betterAuthSecret) {
|
|
155
370
|
throw new Error("[nuxt-better-auth] BETTER_AUTH_SECRET is required in production. Set BETTER_AUTH_SECRET or NUXT_BETTER_AUTH_SECRET environment variable.");
|
|
156
371
|
}
|
|
157
372
|
if (betterAuthSecret && betterAuthSecret.length < 32) {
|
|
@@ -183,13 +398,36 @@ export function createSecondaryStorage() {
|
|
|
183
398
|
throw new Error("[nuxt-better-auth] hub:db not found. Ensure @nuxthub/core is loaded before this module and hub.db is configured.");
|
|
184
399
|
}
|
|
185
400
|
const hubDialect = getHubDialect(hub) ?? "sqlite";
|
|
186
|
-
const
|
|
401
|
+
const usePlural = options.schema?.usePlural ?? false;
|
|
402
|
+
const camelCase = (options.schema?.casing ?? getHubCasing(hub)) !== "snake_case";
|
|
403
|
+
let databaseCode;
|
|
404
|
+
if (hasHubDb) {
|
|
405
|
+
databaseCode = `import { db, schema } from '../hub/db.mjs'
|
|
187
406
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
188
407
|
const rawDialect = '${hubDialect}'
|
|
189
408
|
const dialect = rawDialect === 'postgresql' ? 'pg' : rawDialect
|
|
190
|
-
export function createDatabase() { return drizzleAdapter(db, { provider: dialect, schema }) }
|
|
191
|
-
export { db }
|
|
409
|
+
export function createDatabase() { return drizzleAdapter(db, { provider: dialect, schema, usePlural: ${usePlural}, camelCase: ${camelCase} }) }
|
|
410
|
+
export { db }`;
|
|
411
|
+
} else if (hasConvexDb) {
|
|
412
|
+
nuxt.options.runtimeConfig.betterAuth = defu(
|
|
413
|
+
nuxt.options.runtimeConfig.betterAuth || {},
|
|
414
|
+
{ convexUrl }
|
|
415
|
+
);
|
|
416
|
+
databaseCode = `import { useRuntimeConfig } from '#imports'
|
|
417
|
+
import { createConvexHttpAdapter } from '@onmax/nuxt-better-auth/adapters/convex'
|
|
418
|
+
import { api } from '#convex/api'
|
|
419
|
+
|
|
420
|
+
export function createDatabase() {
|
|
421
|
+
const config = useRuntimeConfig()
|
|
422
|
+
const convexUrl = config.betterAuth?.convexUrl || config.public?.convex?.url
|
|
423
|
+
if (!convexUrl) throw new Error('[nuxt-better-auth] CONVEX_URL not configured')
|
|
424
|
+
return createConvexHttpAdapter({ url: convexUrl, api: api.auth })
|
|
425
|
+
}
|
|
426
|
+
export const db = undefined`;
|
|
427
|
+
} else {
|
|
428
|
+
databaseCode = `export function createDatabase() { return undefined }
|
|
192
429
|
export const db = undefined`;
|
|
430
|
+
}
|
|
193
431
|
const databaseTemplate = addTemplate({ filename: "better-auth/database.mjs", getContents: () => databaseCode, write: true });
|
|
194
432
|
nuxt.options.alias["#auth/database"] = databaseTemplate.dst;
|
|
195
433
|
addTypeTemplate({
|
|
@@ -220,9 +458,9 @@ declare module '#auth/database' {
|
|
|
220
458
|
getContents: () => `
|
|
221
459
|
import type { InferUser, InferSession, InferPluginTypes } from 'better-auth'
|
|
222
460
|
import type { RuntimeConfig } from 'nuxt/schema'
|
|
223
|
-
import type
|
|
461
|
+
import type createServerAuth from '${serverConfigPath}'
|
|
224
462
|
|
|
225
|
-
type _Config = ReturnType<typeof
|
|
463
|
+
type _Config = ReturnType<typeof createServerAuth>
|
|
226
464
|
|
|
227
465
|
declare module '#nuxt-better-auth' {
|
|
228
466
|
interface AuthUser extends InferUser<_Config> {}
|
|
@@ -243,7 +481,7 @@ interface _AugmentedServerAuthContext {
|
|
|
243
481
|
declare module '@onmax/nuxt-better-auth/config' {
|
|
244
482
|
import type { BetterAuthOptions } from 'better-auth'
|
|
245
483
|
type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'>
|
|
246
|
-
export function defineServerAuth<T extends ServerAuthConfig>(config: (ctx: _AugmentedServerAuthContext) => T): (ctx: _AugmentedServerAuthContext) => T
|
|
484
|
+
export function defineServerAuth<T extends ServerAuthConfig>(config: T | ((ctx: _AugmentedServerAuthContext) => T)): (ctx: _AugmentedServerAuthContext) => T
|
|
247
485
|
}
|
|
248
486
|
`
|
|
249
487
|
}, { nuxt: true, nitro: true, node: true });
|
|
@@ -280,7 +518,7 @@ export type { AuthMeta, AuthMode, AuthRouteRules, UserMatch, RequireSessionOptio
|
|
|
280
518
|
addTypeTemplate({
|
|
281
519
|
filename: "types/nuxt-better-auth-client.d.ts",
|
|
282
520
|
getContents: () => `
|
|
283
|
-
import type
|
|
521
|
+
import type createAppAuthClient from '${clientConfigPath}'
|
|
284
522
|
declare module '#nuxt-better-auth' {
|
|
285
523
|
export type AppAuthClient = ReturnType<typeof createAppAuthClient>
|
|
286
524
|
}
|
|
@@ -309,8 +547,13 @@ declare module '#nuxt-better-auth' {
|
|
|
309
547
|
if (hasHubDb) {
|
|
310
548
|
await setupBetterAuthSchema(nuxt, serverConfigPath, options);
|
|
311
549
|
}
|
|
550
|
+
if (hasConvexDb) {
|
|
551
|
+
await setupConvexAuthSchema(nuxt, serverConfigPath);
|
|
552
|
+
}
|
|
312
553
|
const isProduction = process.env.NODE_ENV === "production" || !nuxt.options.dev;
|
|
313
554
|
if (!isProduction && !clientOnly) {
|
|
555
|
+
if (!hasNuxtModule("@nuxt/ui"))
|
|
556
|
+
await installModule("@nuxt/ui");
|
|
314
557
|
setupDevTools(nuxt);
|
|
315
558
|
addServerHandler({ route: "/api/_better-auth/config", method: "get", handler: resolver.resolve("./runtime/server/api/_better-auth/config.get") });
|
|
316
559
|
if (hasHubDb) {
|
|
@@ -387,5 +630,26 @@ async function setupBetterAuthSchema(nuxt, serverConfigPath, options) {
|
|
|
387
630
|
}
|
|
388
631
|
});
|
|
389
632
|
}
|
|
633
|
+
async function setupConvexAuthSchema(nuxt, serverConfigPath) {
|
|
634
|
+
const isProduction = !nuxt.options.dev;
|
|
635
|
+
try {
|
|
636
|
+
const configFile = `${serverConfigPath}.ts`;
|
|
637
|
+
const userConfig = await loadUserAuthConfig(configFile, isProduction);
|
|
638
|
+
const authOptions = { ...userConfig };
|
|
639
|
+
const schemaCode = await generateConvexSchema(authOptions);
|
|
640
|
+
const schemaDir = join(nuxt.options.buildDir, "better-auth");
|
|
641
|
+
const schemaPath = join(schemaDir, "auth-tables.convex.ts");
|
|
642
|
+
await mkdir(schemaDir, { recursive: true });
|
|
643
|
+
await writeFile(schemaPath, schemaCode);
|
|
644
|
+
addTemplate({ filename: "better-auth/auth-tables.convex.ts", getContents: () => schemaCode, write: true });
|
|
645
|
+
nuxt.options.alias["#auth/convex-schema"] = schemaPath;
|
|
646
|
+
consola.info("Generated Convex auth schema at .nuxt/better-auth/auth-tables.convex.ts");
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (isProduction) {
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
consola.error("Failed to generate Convex schema:", error);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
390
654
|
|
|
391
655
|
export { module$1 as default };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { FunctionReference } from 'convex/server';
|
|
2
|
+
interface ConvexCleanedWhere {
|
|
3
|
+
field: string;
|
|
4
|
+
value: string | number | boolean | string[] | number[] | null;
|
|
5
|
+
operator?: 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'in' | 'not_in' | 'ne' | 'contains' | 'starts_with' | 'ends_with';
|
|
6
|
+
connector?: 'AND' | 'OR';
|
|
7
|
+
}
|
|
8
|
+
interface PaginationResult<T> {
|
|
9
|
+
page: T[];
|
|
10
|
+
isDone: boolean;
|
|
11
|
+
continueCursor: string | null;
|
|
12
|
+
splitCursor?: string;
|
|
13
|
+
pageStatus?: 'SplitRecommended' | 'SplitRequired' | string;
|
|
14
|
+
count?: number;
|
|
15
|
+
}
|
|
16
|
+
interface ConvexAuthApi {
|
|
17
|
+
create: FunctionReference<'mutation', 'public', {
|
|
18
|
+
input: {
|
|
19
|
+
model: string;
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
select?: string[];
|
|
23
|
+
}, unknown>;
|
|
24
|
+
findOne: FunctionReference<'query', 'public', {
|
|
25
|
+
model: string;
|
|
26
|
+
where?: ConvexCleanedWhere[];
|
|
27
|
+
select?: string[];
|
|
28
|
+
}, unknown>;
|
|
29
|
+
findMany: FunctionReference<'query', 'public', {
|
|
30
|
+
model: string;
|
|
31
|
+
where?: ConvexCleanedWhere[];
|
|
32
|
+
limit?: number;
|
|
33
|
+
sortBy?: {
|
|
34
|
+
direction: 'asc' | 'desc';
|
|
35
|
+
field: string;
|
|
36
|
+
};
|
|
37
|
+
paginationOpts: {
|
|
38
|
+
numItems: number;
|
|
39
|
+
cursor: string | null;
|
|
40
|
+
};
|
|
41
|
+
}, PaginationResult<unknown>>;
|
|
42
|
+
updateOne: FunctionReference<'mutation', 'public', {
|
|
43
|
+
input: {
|
|
44
|
+
model: string;
|
|
45
|
+
where?: ConvexCleanedWhere[];
|
|
46
|
+
update: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
}, unknown>;
|
|
49
|
+
updateMany: FunctionReference<'mutation', 'public', {
|
|
50
|
+
input: {
|
|
51
|
+
model: string;
|
|
52
|
+
where?: ConvexCleanedWhere[];
|
|
53
|
+
update: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
paginationOpts: {
|
|
56
|
+
numItems: number;
|
|
57
|
+
cursor: string | null;
|
|
58
|
+
};
|
|
59
|
+
}, PaginationResult<unknown> & {
|
|
60
|
+
count: number;
|
|
61
|
+
}>;
|
|
62
|
+
deleteOne: FunctionReference<'mutation', 'public', {
|
|
63
|
+
input: {
|
|
64
|
+
model: string;
|
|
65
|
+
where?: ConvexCleanedWhere[];
|
|
66
|
+
};
|
|
67
|
+
}, unknown>;
|
|
68
|
+
deleteMany: FunctionReference<'mutation', 'public', {
|
|
69
|
+
input: {
|
|
70
|
+
model: string;
|
|
71
|
+
where?: ConvexCleanedWhere[];
|
|
72
|
+
};
|
|
73
|
+
paginationOpts: {
|
|
74
|
+
numItems: number;
|
|
75
|
+
cursor: string | null;
|
|
76
|
+
};
|
|
77
|
+
}, PaginationResult<unknown> & {
|
|
78
|
+
count: number;
|
|
79
|
+
}>;
|
|
80
|
+
}
|
|
81
|
+
export interface ConvexHttpAdapterOptions {
|
|
82
|
+
/** Convex deployment URL (e.g., https://your-app.convex.cloud) */
|
|
83
|
+
url: string;
|
|
84
|
+
/** Convex API functions for auth operations - import from your convex/_generated/api */
|
|
85
|
+
api: ConvexAuthApi;
|
|
86
|
+
/** Enable debug logging for adapter operations */
|
|
87
|
+
debugLogs?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Creates a Better Auth adapter that communicates with Convex via HTTP.
|
|
91
|
+
* Uses ConvexHttpClient for server-side auth operations.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { api } from '~/convex/_generated/api'
|
|
96
|
+
*
|
|
97
|
+
* export default defineServerAuth(() => ({
|
|
98
|
+
* database: createConvexHttpAdapter({
|
|
99
|
+
* url: process.env.CONVEX_URL!,
|
|
100
|
+
* api: api.auth,
|
|
101
|
+
* }),
|
|
102
|
+
* }))
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @limitations
|
|
106
|
+
* - `update()` only supports AND-connected where clauses (no OR support)
|
|
107
|
+
* - `count()` fetches all documents client-side (Convex limitation)
|
|
108
|
+
* - `offset` pagination not supported in `findMany()`
|
|
109
|
+
*/
|
|
110
|
+
export declare function createConvexHttpAdapter(options: ConvexHttpAdapterOptions): import("better-auth/adapters").AdapterFactory;
|
|
111
|
+
export {};
|