@onmax/nuxt-better-auth 0.0.2-alpha.13 → 0.0.2-alpha.14
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 +46 -159
- package/dist/runtime/server/utils/auth.js +38 -3
- package/package.json +14 -6
- package/skills/nuxt-better-auth/SKILL.md +92 -0
- package/skills/nuxt-better-auth/references/client-auth.md +153 -0
- package/skills/nuxt-better-auth/references/client-only.md +89 -0
- package/skills/nuxt-better-auth/references/database.md +115 -0
- package/skills/nuxt-better-auth/references/installation.md +126 -0
- package/skills/nuxt-better-auth/references/plugins.md +138 -0
- package/skills/nuxt-better-auth/references/route-protection.md +105 -0
- package/skills/nuxt-better-auth/references/server-auth.md +135 -0
- package/skills/nuxt-better-auth/references/types.md +142 -0
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { consola as consola$1 } from 'consola';
|
|
|
5
5
|
import { defu } from 'defu';
|
|
6
6
|
import { join } from 'pathe';
|
|
7
7
|
import { toRouteMatcher, createRouter } from 'radix3';
|
|
8
|
-
import
|
|
8
|
+
import { generateDrizzleSchema as generateDrizzleSchema$1 } from '@better-auth/cli/api';
|
|
9
9
|
export { defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
|
|
10
10
|
|
|
11
11
|
function setupDevTools(nuxt) {
|
|
@@ -23,160 +23,51 @@ function setupDevTools(nuxt) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function
|
|
27
|
-
return
|
|
26
|
+
function dialectToProvider(dialect) {
|
|
27
|
+
return dialect === "postgresql" ? "pg" : dialect;
|
|
28
28
|
}
|
|
29
|
-
function generateDrizzleSchema(
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (options?.casing === "snake_case" && !hasCustomModelName)
|
|
53
|
-
dbTableName = toSnakeCase(dbTableName);
|
|
54
|
-
if (options?.usePlural && !hasCustomModelName)
|
|
55
|
-
dbTableName = pluralize(dbTableName);
|
|
56
|
-
const fields = Object.entries(table.fields).map(([fieldName, field]) => generateField(fieldName, field, dialect, allTables, options)).join(",\n ");
|
|
57
|
-
const idField = generateIdField(dialect, options);
|
|
58
|
-
return `export const ${tableName} = ${tableFunc}('${dbTableName}', {
|
|
59
|
-
${idField},
|
|
60
|
-
${fields}
|
|
61
|
-
})`;
|
|
62
|
-
}
|
|
63
|
-
function generateIdField(dialect, options) {
|
|
64
|
-
switch (dialect) {
|
|
65
|
-
case "sqlite":
|
|
66
|
-
if (options?.useUuid)
|
|
67
|
-
consola$1.warn("[@onmax/nuxt-better-auth] useUuid ignored for SQLite (no native uuid type). Using text.");
|
|
68
|
-
return `id: text('id').primaryKey()`;
|
|
69
|
-
case "postgresql":
|
|
70
|
-
return options?.useUuid ? `id: uuid('id').defaultRandom().primaryKey()` : `id: text('id').primaryKey()`;
|
|
71
|
-
case "mysql":
|
|
72
|
-
return `id: varchar('id', { length: 36 }).primaryKey()`;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function generateField(fieldName, field, dialect, allTables, options) {
|
|
76
|
-
const dbFieldName = options?.casing === "snake_case" ? toSnakeCase(fieldName) : fieldName;
|
|
77
|
-
const isFkToId = options?.useUuid && field.references?.field === "id";
|
|
78
|
-
let fieldDef;
|
|
79
|
-
if (isFkToId && dialect === "postgresql")
|
|
80
|
-
fieldDef = `uuid('${dbFieldName}')`;
|
|
81
|
-
else if (isFkToId && dialect === "mysql")
|
|
82
|
-
fieldDef = `varchar('${dbFieldName}', { length: 36 })`;
|
|
83
|
-
else
|
|
84
|
-
fieldDef = getFieldType(field.type, dialect, dbFieldName);
|
|
85
|
-
if (field.required && field.defaultValue === void 0)
|
|
86
|
-
fieldDef += ".notNull()";
|
|
87
|
-
if (field.unique)
|
|
88
|
-
fieldDef += ".unique()";
|
|
89
|
-
if (field.defaultValue !== void 0) {
|
|
90
|
-
if (typeof field.defaultValue === "boolean")
|
|
91
|
-
fieldDef += `.default(${field.defaultValue})`;
|
|
92
|
-
else if (typeof field.defaultValue === "string")
|
|
93
|
-
fieldDef += `.default('${field.defaultValue}')`;
|
|
94
|
-
else if (typeof field.defaultValue === "function")
|
|
95
|
-
fieldDef += `.$defaultFn(${field.defaultValue})`;
|
|
96
|
-
else
|
|
97
|
-
fieldDef += `.default(${field.defaultValue})`;
|
|
98
|
-
if (field.required)
|
|
99
|
-
fieldDef += ".notNull()";
|
|
100
|
-
}
|
|
101
|
-
if (typeof field.onUpdate === "function" && field.type === "date")
|
|
102
|
-
fieldDef += `.$onUpdate(${field.onUpdate})`;
|
|
103
|
-
if (field.references) {
|
|
104
|
-
const refTable = field.references.model;
|
|
105
|
-
if (allTables[refTable])
|
|
106
|
-
fieldDef += `.references(() => ${refTable}.${field.references.field})`;
|
|
107
|
-
}
|
|
108
|
-
return `${fieldName}: ${fieldDef}`;
|
|
109
|
-
}
|
|
110
|
-
function getFieldType(type, dialect, fieldName) {
|
|
111
|
-
const normalizedType = Array.isArray(type) ? "string" : type;
|
|
112
|
-
switch (dialect) {
|
|
113
|
-
case "sqlite":
|
|
114
|
-
return getSqliteType(normalizedType, fieldName);
|
|
115
|
-
case "postgresql":
|
|
116
|
-
return getPostgresType(normalizedType, fieldName);
|
|
117
|
-
case "mysql":
|
|
118
|
-
return getMysqlType(normalizedType, fieldName);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function getSqliteType(type, fieldName) {
|
|
122
|
-
switch (type) {
|
|
123
|
-
case "string":
|
|
124
|
-
return `text('${fieldName}')`;
|
|
125
|
-
case "boolean":
|
|
126
|
-
return `integer('${fieldName}', { mode: 'boolean' })`;
|
|
127
|
-
case "date":
|
|
128
|
-
return `integer('${fieldName}', { mode: 'timestamp' })`;
|
|
129
|
-
case "number":
|
|
130
|
-
return `integer('${fieldName}')`;
|
|
131
|
-
default:
|
|
132
|
-
return `text('${fieldName}')`;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
function getPostgresType(type, fieldName) {
|
|
136
|
-
switch (type) {
|
|
137
|
-
case "string":
|
|
138
|
-
return `text('${fieldName}')`;
|
|
139
|
-
case "boolean":
|
|
140
|
-
return `boolean('${fieldName}')`;
|
|
141
|
-
case "date":
|
|
142
|
-
return `timestamp('${fieldName}')`;
|
|
143
|
-
case "number":
|
|
144
|
-
return `integer('${fieldName}')`;
|
|
145
|
-
default:
|
|
146
|
-
return `text('${fieldName}')`;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function getMysqlType(type, fieldName) {
|
|
150
|
-
switch (type) {
|
|
151
|
-
case "string":
|
|
152
|
-
return `text('${fieldName}')`;
|
|
153
|
-
case "boolean":
|
|
154
|
-
return `boolean('${fieldName}')`;
|
|
155
|
-
case "date":
|
|
156
|
-
return `timestamp('${fieldName}')`;
|
|
157
|
-
case "number":
|
|
158
|
-
return `int('${fieldName}')`;
|
|
159
|
-
default:
|
|
160
|
-
return `text('${fieldName}')`;
|
|
29
|
+
async function generateDrizzleSchema(authOptions, dialect, schemaOptions) {
|
|
30
|
+
const provider = dialectToProvider(dialect);
|
|
31
|
+
const options = {
|
|
32
|
+
...authOptions,
|
|
33
|
+
advanced: {
|
|
34
|
+
...authOptions.advanced,
|
|
35
|
+
database: {
|
|
36
|
+
...authOptions.advanced?.database,
|
|
37
|
+
...schemaOptions?.useUuid && { generateId: "uuid" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const adapter = {
|
|
42
|
+
id: "drizzle",
|
|
43
|
+
options: {
|
|
44
|
+
provider,
|
|
45
|
+
camelCase: schemaOptions?.casing !== "snake_case",
|
|
46
|
+
adapterConfig: { usePlural: schemaOptions?.usePlural ?? false }
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const result = await generateDrizzleSchema$1({ adapter, options });
|
|
50
|
+
if (!result.code) {
|
|
51
|
+
throw new Error(`Schema generation returned empty result for ${dialect}`);
|
|
161
52
|
}
|
|
53
|
+
return result.code;
|
|
162
54
|
}
|
|
163
55
|
async function loadUserAuthConfig(configPath, throwOnError = false) {
|
|
164
56
|
const { createJiti } = await import('jiti');
|
|
165
57
|
const { defineServerAuth } = await import('../dist/runtime/config.js');
|
|
166
58
|
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
167
|
-
|
|
168
|
-
const g = globalThis;
|
|
169
|
-
if (!g[key]) {
|
|
59
|
+
if (!globalThis.defineServerAuth) {
|
|
170
60
|
defineServerAuth._count = 0;
|
|
171
|
-
|
|
61
|
+
globalThis.defineServerAuth = defineServerAuth;
|
|
172
62
|
}
|
|
173
|
-
|
|
63
|
+
globalThis.defineServerAuth._count++;
|
|
174
64
|
try {
|
|
175
65
|
const mod = await jiti.import(configPath);
|
|
176
66
|
const configFn = typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
|
|
177
67
|
if (typeof configFn === "function") {
|
|
178
68
|
return configFn({ runtimeConfig: {}, db: null });
|
|
179
69
|
}
|
|
70
|
+
consola$1.warn("[@onmax/nuxt-better-auth] auth.config.ts does not export a function. Expected: export default defineServerAuth(...)");
|
|
180
71
|
if (throwOnError) {
|
|
181
72
|
throw new Error("auth.config.ts must export default defineServerAuth(...)");
|
|
182
73
|
}
|
|
@@ -188,9 +79,9 @@ async function loadUserAuthConfig(configPath, throwOnError = false) {
|
|
|
188
79
|
consola$1.error("[@onmax/nuxt-better-auth] Failed to load auth config for schema generation. Schema may be incomplete:", error);
|
|
189
80
|
return {};
|
|
190
81
|
} finally {
|
|
191
|
-
|
|
192
|
-
if (!
|
|
193
|
-
|
|
82
|
+
globalThis.defineServerAuth._count--;
|
|
83
|
+
if (!globalThis.defineServerAuth._count) {
|
|
84
|
+
globalThis.defineServerAuth = void 0;
|
|
194
85
|
}
|
|
195
86
|
}
|
|
196
87
|
}
|
|
@@ -257,14 +148,15 @@ const module$1 = defineNuxtModule({
|
|
|
257
148
|
consola.info("clientOnly mode enabled - server utilities (serverAuth, getUserSession, requireUserSession) are not available");
|
|
258
149
|
}
|
|
259
150
|
if (!clientOnly) {
|
|
260
|
-
const
|
|
151
|
+
const currentSecret = nuxt.options.runtimeConfig.betterAuthSecret;
|
|
152
|
+
nuxt.options.runtimeConfig.betterAuthSecret = currentSecret || process.env.BETTER_AUTH_SECRET || "";
|
|
153
|
+
const betterAuthSecret = nuxt.options.runtimeConfig.betterAuthSecret;
|
|
261
154
|
if (!nuxt.options.dev && !betterAuthSecret) {
|
|
262
155
|
throw new Error("[nuxt-better-auth] BETTER_AUTH_SECRET is required in production. Set BETTER_AUTH_SECRET or NUXT_BETTER_AUTH_SECRET environment variable.");
|
|
263
156
|
}
|
|
264
157
|
if (betterAuthSecret && betterAuthSecret.length < 32) {
|
|
265
158
|
throw new Error("[nuxt-better-auth] BETTER_AUTH_SECRET must be at least 32 characters for security");
|
|
266
159
|
}
|
|
267
|
-
nuxt.options.runtimeConfig.betterAuthSecret = betterAuthSecret;
|
|
268
160
|
nuxt.options.runtimeConfig.auth = defu(nuxt.options.runtimeConfig.auth, {
|
|
269
161
|
secondaryStorage: secondaryStorageEnabled
|
|
270
162
|
});
|
|
@@ -465,27 +357,22 @@ async function setupBetterAuthSchema(nuxt, serverConfigPath, options) {
|
|
|
465
357
|
const extendedConfig = {};
|
|
466
358
|
await nuxt.callHook("better-auth:config:extend", extendedConfig);
|
|
467
359
|
const plugins = [...userConfig.plugins || [], ...extendedConfig.plugins || []];
|
|
468
|
-
const
|
|
469
|
-
|
|
360
|
+
const authOptions = {
|
|
361
|
+
...userConfig,
|
|
470
362
|
plugins,
|
|
471
|
-
secondaryStorage: options.secondaryStorage ? {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
delete: async (_key) => {
|
|
476
|
-
}
|
|
477
|
-
} : void 0
|
|
478
|
-
});
|
|
479
|
-
const useUuid = userConfig.advanced?.database?.generateId === "uuid";
|
|
363
|
+
secondaryStorage: options.secondaryStorage ? { get: async (_key) => null, set: async (_key, _value, _ttl) => {
|
|
364
|
+
}, delete: async (_key) => {
|
|
365
|
+
} } : void 0
|
|
366
|
+
};
|
|
480
367
|
const hubCasing = getHubCasing(hub);
|
|
481
|
-
const schemaOptions = { ...options.schema, useUuid, casing: options.schema?.casing ?? hubCasing };
|
|
482
|
-
const schemaCode = generateDrizzleSchema(
|
|
368
|
+
const schemaOptions = { ...options.schema, useUuid: userConfig.advanced?.database?.generateId === "uuid", casing: options.schema?.casing ?? hubCasing };
|
|
369
|
+
const schemaCode = await generateDrizzleSchema(authOptions, dialect, schemaOptions);
|
|
483
370
|
const schemaDir = join(nuxt.options.buildDir, "better-auth");
|
|
484
371
|
const schemaPath = join(schemaDir, `schema.${dialect}.ts`);
|
|
485
372
|
await mkdir(schemaDir, { recursive: true });
|
|
486
373
|
await writeFile(schemaPath, schemaCode);
|
|
487
374
|
addTemplate({ filename: `better-auth/schema.${dialect}.ts`, getContents: () => schemaCode, write: true });
|
|
488
|
-
consola.info(`Generated ${dialect} schema
|
|
375
|
+
consola.info(`Generated ${dialect} schema`);
|
|
489
376
|
} catch (error) {
|
|
490
377
|
if (isProduction) {
|
|
491
378
|
throw error;
|
|
@@ -2,8 +2,9 @@ import { createDatabase, db } from "#auth/database";
|
|
|
2
2
|
import { createSecondaryStorage } from "#auth/secondary-storage";
|
|
3
3
|
import createServerAuth from "#auth/server";
|
|
4
4
|
import { betterAuth } from "better-auth";
|
|
5
|
-
import {
|
|
5
|
+
import { getRequestHost, getRequestProtocol } from "h3";
|
|
6
6
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
7
|
+
import { withoutProtocol } from "ufo";
|
|
7
8
|
let _auth = null;
|
|
8
9
|
function validateURL(url) {
|
|
9
10
|
try {
|
|
@@ -12,12 +13,46 @@ function validateURL(url) {
|
|
|
12
13
|
throw new Error(`Invalid siteUrl: "${url}". Must be a valid URL.`);
|
|
13
14
|
}
|
|
14
15
|
}
|
|
16
|
+
function getNitroOrigin(e) {
|
|
17
|
+
const cert = process.env.NITRO_SSL_CERT;
|
|
18
|
+
const key = process.env.NITRO_SSL_KEY;
|
|
19
|
+
let host = process.env.NITRO_HOST || process.env.HOST;
|
|
20
|
+
let port;
|
|
21
|
+
if (import.meta.dev)
|
|
22
|
+
port = process.env.NITRO_PORT || process.env.PORT || "3000";
|
|
23
|
+
let protocol = cert && key || !import.meta.dev ? "https" : "http";
|
|
24
|
+
try {
|
|
25
|
+
if ((import.meta.dev || import.meta.prerender) && process.env.__NUXT_DEV__) {
|
|
26
|
+
const origin = JSON.parse(process.env.__NUXT_DEV__).proxy.url;
|
|
27
|
+
host = withoutProtocol(origin);
|
|
28
|
+
protocol = origin.includes("https") ? "https" : "http";
|
|
29
|
+
} else if ((import.meta.dev || import.meta.prerender) && process.env.NUXT_VITE_NODE_OPTIONS) {
|
|
30
|
+
const origin = JSON.parse(process.env.NUXT_VITE_NODE_OPTIONS).baseURL.replace("/__nuxt_vite_node__", "");
|
|
31
|
+
host = withoutProtocol(origin);
|
|
32
|
+
protocol = origin.includes("https") ? "https" : "http";
|
|
33
|
+
} else if (e) {
|
|
34
|
+
host = getRequestHost(e, { xForwardedHost: true }) || host;
|
|
35
|
+
protocol = getRequestProtocol(e, { xForwardedProto: true }) || protocol;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
if (!host)
|
|
40
|
+
return void 0;
|
|
41
|
+
if (host.includes(":") && !host.startsWith("[")) {
|
|
42
|
+
const hostParts = host.split(":");
|
|
43
|
+
port = hostParts.pop();
|
|
44
|
+
host = hostParts.join(":");
|
|
45
|
+
}
|
|
46
|
+
const portSuffix = port ? `:${port}` : "";
|
|
47
|
+
return `${protocol}://${host}${portSuffix}`;
|
|
48
|
+
}
|
|
15
49
|
function getBaseURL(event) {
|
|
16
50
|
const config = useRuntimeConfig();
|
|
17
51
|
if (config.public.siteUrl && typeof config.public.siteUrl === "string")
|
|
18
52
|
return validateURL(config.public.siteUrl);
|
|
19
|
-
|
|
20
|
-
|
|
53
|
+
const nitroOrigin = getNitroOrigin(event);
|
|
54
|
+
if (nitroOrigin)
|
|
55
|
+
return validateURL(nitroOrigin);
|
|
21
56
|
if (process.env.VERCEL_URL)
|
|
22
57
|
return validateURL(`https://${process.env.VERCEL_URL}`);
|
|
23
58
|
if (process.env.CF_PAGES_URL)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onmax/nuxt-better-auth",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.2-alpha.
|
|
4
|
+
"version": "0.0.2-alpha.14",
|
|
5
5
|
"packageManager": "pnpm@10.15.1",
|
|
6
6
|
"description": "Nuxt module for Better Auth integration with NuxtHub, route protection, session management, and role-based access",
|
|
7
7
|
"author": "onmax",
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "https://github.com/onmax/nuxt-better-auth"
|
|
12
12
|
},
|
|
13
|
+
"agents": {
|
|
14
|
+
"skills": [
|
|
15
|
+
{
|
|
16
|
+
"name": "nuxt-better-auth",
|
|
17
|
+
"path": "./skills/nuxt-better-auth"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
13
21
|
"exports": {
|
|
14
22
|
".": {
|
|
15
23
|
"types": "./dist/types.d.mts",
|
|
@@ -32,7 +40,8 @@
|
|
|
32
40
|
}
|
|
33
41
|
},
|
|
34
42
|
"files": [
|
|
35
|
-
"dist"
|
|
43
|
+
"dist",
|
|
44
|
+
"skills"
|
|
36
45
|
],
|
|
37
46
|
"scripts": {
|
|
38
47
|
"prepack": "nuxt-module-build build",
|
|
@@ -59,16 +68,15 @@
|
|
|
59
68
|
}
|
|
60
69
|
},
|
|
61
70
|
"dependencies": {
|
|
71
|
+
"@better-auth/cli": "^1.5.0-beta.3",
|
|
62
72
|
"@nuxt/kit": "^4.2.2",
|
|
63
73
|
"defu": "^6.1.4",
|
|
64
74
|
"jiti": "^2.4.2",
|
|
65
75
|
"pathe": "^2.0.3",
|
|
66
|
-
"pluralize": "^8.0.0",
|
|
67
76
|
"radix3": "^1.1.2"
|
|
68
77
|
},
|
|
69
78
|
"devDependencies": {
|
|
70
79
|
"@antfu/eslint-config": "^4.12.0",
|
|
71
|
-
"@better-auth/cli": "^1.4.6",
|
|
72
80
|
"@libsql/client": "^0.15.15",
|
|
73
81
|
"@nuxt/devtools": "^3.1.1",
|
|
74
82
|
"@nuxt/devtools-kit": "^3.1.1",
|
|
@@ -78,8 +86,7 @@
|
|
|
78
86
|
"@nuxthub/core": "^0.10.3",
|
|
79
87
|
"@types/better-sqlite3": "^7.6.13",
|
|
80
88
|
"@types/node": "latest",
|
|
81
|
-
"
|
|
82
|
-
"better-auth": "^1.4.7",
|
|
89
|
+
"better-auth": "^1.5.0-beta.3",
|
|
83
90
|
"better-sqlite3": "^11.9.1",
|
|
84
91
|
"bumpp": "^10.3.2",
|
|
85
92
|
"changelogen": "^0.6.2",
|
|
@@ -87,6 +94,7 @@
|
|
|
87
94
|
"drizzle-kit": "^0.31.8",
|
|
88
95
|
"drizzle-orm": "^0.38.4",
|
|
89
96
|
"eslint": "^9.39.1",
|
|
97
|
+
"npm-agentskills": "https://pkg.pr.new/onmax/npm-agentskills@394499e",
|
|
90
98
|
"nuxt": "^4.2.2",
|
|
91
99
|
"tinyexec": "^1.0.2",
|
|
92
100
|
"typescript": "~5.9.3",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nuxt-better-auth
|
|
3
|
+
description: Use when implementing auth in Nuxt apps with @onmax/nuxt-better-auth - provides useUserSession composable, server auth helpers, route protection, and Better Auth plugins integration.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Nuxt Better Auth
|
|
8
|
+
|
|
9
|
+
Authentication module for Nuxt 4+ built on [Better Auth](https://www.better-auth.com/). Provides composables, server utilities, and route protection.
|
|
10
|
+
|
|
11
|
+
> **Alpha Status**: This module is currently in alpha (v0.0.2-alpha.12) and not recommended for production use. APIs may change.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- Installing/configuring `@onmax/nuxt-better-auth`
|
|
16
|
+
- Implementing login/signup/signout flows
|
|
17
|
+
- Protecting routes (client and server)
|
|
18
|
+
- Accessing user session in API routes
|
|
19
|
+
- Integrating Better Auth plugins (admin, passkey, 2FA)
|
|
20
|
+
- Setting up database with NuxtHub
|
|
21
|
+
- Using clientOnly mode for external auth backends
|
|
22
|
+
|
|
23
|
+
**For Nuxt patterns:** use `nuxt` skill
|
|
24
|
+
**For NuxtHub database:** use `nuxthub` skill
|
|
25
|
+
|
|
26
|
+
## Available Guidance
|
|
27
|
+
|
|
28
|
+
| File | Topics |
|
|
29
|
+
| -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
30
|
+
| **[references/installation.md](references/installation.md)** | Module setup, env vars, config files |
|
|
31
|
+
| **[references/client-auth.md](references/client-auth.md)** | useUserSession, signIn/signUp/signOut, BetterAuthState, safe redirects |
|
|
32
|
+
| **[references/server-auth.md](references/server-auth.md)** | serverAuth, getUserSession, requireUserSession |
|
|
33
|
+
| **[references/route-protection.md](references/route-protection.md)** | routeRules, definePageMeta, middleware |
|
|
34
|
+
| **[references/plugins.md](references/plugins.md)** | Better Auth plugins (admin, passkey, 2FA) |
|
|
35
|
+
| **[references/database.md](references/database.md)** | NuxtHub integration, Drizzle schema |
|
|
36
|
+
| **[references/client-only.md](references/client-only.md)** | External auth backend, clientOnly mode, CORS |
|
|
37
|
+
| **[references/types.md](references/types.md)** | AuthUser, AuthSession, type augmentation |
|
|
38
|
+
|
|
39
|
+
## Usage Pattern
|
|
40
|
+
|
|
41
|
+
**Load based on context:**
|
|
42
|
+
|
|
43
|
+
- Installing module? → [references/installation.md](references/installation.md)
|
|
44
|
+
- Login/signup forms? → [references/client-auth.md](references/client-auth.md)
|
|
45
|
+
- API route protection? → [references/server-auth.md](references/server-auth.md)
|
|
46
|
+
- Route rules/page meta? → [references/route-protection.md](references/route-protection.md)
|
|
47
|
+
- Using plugins? → [references/plugins.md](references/plugins.md)
|
|
48
|
+
- Database setup? → [references/database.md](references/database.md)
|
|
49
|
+
- External auth backend? → [references/client-only.md](references/client-only.md)
|
|
50
|
+
- TypeScript types? → [references/types.md](references/types.md)
|
|
51
|
+
|
|
52
|
+
**DO NOT read all files at once.** Load based on context.
|
|
53
|
+
|
|
54
|
+
## Key Concepts
|
|
55
|
+
|
|
56
|
+
| Concept | Description |
|
|
57
|
+
| ---------------------- | --------------------------------------------------------------- |
|
|
58
|
+
| `useUserSession()` | Client composable - user, session, loggedIn, signIn/Out methods |
|
|
59
|
+
| `requireUserSession()` | Server helper - throws 401/403 if not authenticated |
|
|
60
|
+
| `auth` route mode | `'user'`, `'guest'`, `{ user: {...} }`, or `false` |
|
|
61
|
+
| `serverAuth()` | Get Better Auth instance in server routes |
|
|
62
|
+
|
|
63
|
+
## Quick Reference
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// Client: useUserSession()
|
|
67
|
+
const { user, loggedIn, signIn, signOut } = useUserSession()
|
|
68
|
+
await signIn.email({ email, password }, { onSuccess: () => navigateTo('/') })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// Server: requireUserSession()
|
|
73
|
+
const { user } = await requireUserSession(event, { user: { role: 'admin' } })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// nuxt.config.ts: Route protection
|
|
78
|
+
routeRules: {
|
|
79
|
+
'/admin/**': { auth: { user: { role: 'admin' } } },
|
|
80
|
+
'/login': { auth: 'guest' },
|
|
81
|
+
'/app/**': { auth: 'user' }
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Resources
|
|
86
|
+
|
|
87
|
+
- [Module Docs](https://github.com/onmax/nuxt-better-auth)
|
|
88
|
+
- [Better Auth Docs](https://www.better-auth.com/)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
_Token efficiency: Main skill ~300 tokens, each sub-file ~800-1200 tokens_
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Client-Side Authentication
|
|
2
|
+
|
|
3
|
+
## useUserSession()
|
|
4
|
+
|
|
5
|
+
Main composable for auth state and methods.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const {
|
|
9
|
+
user, // Ref<AuthUser | null>
|
|
10
|
+
session, // Ref<AuthSession | null>
|
|
11
|
+
loggedIn, // ComputedRef<boolean>
|
|
12
|
+
ready, // ComputedRef<boolean> - session fetch complete
|
|
13
|
+
client, // Better Auth client (client-side only)
|
|
14
|
+
signIn, // Proxy to client.signIn
|
|
15
|
+
signUp, // Proxy to client.signUp
|
|
16
|
+
signOut, // Sign out and clear session
|
|
17
|
+
fetchSession, // Manually refresh session
|
|
18
|
+
updateUser // Optimistic local user update
|
|
19
|
+
} = useUserSession()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Sign In
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// Email/password
|
|
26
|
+
await signIn.email({
|
|
27
|
+
email: 'user@example.com',
|
|
28
|
+
password: 'password123'
|
|
29
|
+
}, {
|
|
30
|
+
onSuccess: () => navigateTo('/dashboard')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// OAuth
|
|
34
|
+
await signIn.social({ provider: 'github' })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Sign Up
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
await signUp.email({
|
|
41
|
+
email: 'user@example.com',
|
|
42
|
+
password: 'password123',
|
|
43
|
+
name: 'John Doe'
|
|
44
|
+
}, {
|
|
45
|
+
onSuccess: () => navigateTo('/welcome')
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Sign Out
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
await signOut()
|
|
53
|
+
// or with redirect
|
|
54
|
+
await signOut({ redirect: '/login' })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Check Auth State
|
|
58
|
+
|
|
59
|
+
```vue
|
|
60
|
+
<script setup>
|
|
61
|
+
const { user, loggedIn, ready } = useUserSession()
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<div v-if="!ready">Loading...</div>
|
|
66
|
+
<div v-else-if="loggedIn">Welcome, {{ user?.name }}</div>
|
|
67
|
+
<div v-else>Please log in</div>
|
|
68
|
+
</template>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Safe Redirects
|
|
72
|
+
|
|
73
|
+
Always validate redirect URLs from query params to prevent open redirects:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
function getSafeRedirect() {
|
|
77
|
+
const redirect = route.query.redirect as string
|
|
78
|
+
// Must start with / and not // (prevents protocol-relative URLs)
|
|
79
|
+
if (!redirect?.startsWith('/') || redirect.startsWith('//')) {
|
|
80
|
+
return '/'
|
|
81
|
+
}
|
|
82
|
+
return redirect
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await signIn.email({
|
|
86
|
+
email, password
|
|
87
|
+
}, {
|
|
88
|
+
onSuccess: () => navigateTo(getSafeRedirect())
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Wait for Session
|
|
93
|
+
|
|
94
|
+
Useful when needing session before rendering:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
await waitForSession() // 5s timeout
|
|
98
|
+
if (loggedIn.value) {
|
|
99
|
+
// Session is ready
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Manual Session Refresh
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// Refetch from server
|
|
107
|
+
await fetchSession({ force: true })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Session Management
|
|
111
|
+
|
|
112
|
+
Additional session management via Better Auth client:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const { client } = useUserSession()
|
|
116
|
+
|
|
117
|
+
// List all active sessions for current user
|
|
118
|
+
const sessions = await client.listSessions()
|
|
119
|
+
|
|
120
|
+
// Revoke a specific session
|
|
121
|
+
await client.revokeSession({ sessionId: 'xxx' })
|
|
122
|
+
|
|
123
|
+
// Revoke all sessions except current
|
|
124
|
+
await client.revokeOtherSessions()
|
|
125
|
+
|
|
126
|
+
// Revoke all sessions (logs out everywhere)
|
|
127
|
+
await client.revokeSessions()
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
These methods require the user to be authenticated.
|
|
131
|
+
|
|
132
|
+
## BetterAuthState Component
|
|
133
|
+
|
|
134
|
+
Renders once session hydration completes (`ready === true`), with loading placeholder support.
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<BetterAuthState>
|
|
138
|
+
<template #default="{ loggedIn, user, session, signOut }">
|
|
139
|
+
<p v-if="loggedIn">Hi {{ user?.name }}</p>
|
|
140
|
+
<button v-else @click="navigateTo('/login')">Sign in</button>
|
|
141
|
+
</template>
|
|
142
|
+
<template #placeholder>
|
|
143
|
+
<p>Loading…</p>
|
|
144
|
+
</template>
|
|
145
|
+
</BetterAuthState>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Slots:**
|
|
149
|
+
|
|
150
|
+
- `default` - Renders when `ready === true`, provides `{ loggedIn, user, session, signOut }`
|
|
151
|
+
- `placeholder` - Renders while session hydrates
|
|
152
|
+
|
|
153
|
+
Useful in clientOnly mode or for graceful SSR loading states.
|