@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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createAdapterFactory } from "better-auth/adapters";
|
|
2
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
3
|
+
function parseWhere(where) {
|
|
4
|
+
if (!where)
|
|
5
|
+
return [];
|
|
6
|
+
const whereArray = Array.isArray(where) ? where : [where];
|
|
7
|
+
return whereArray.map((w) => {
|
|
8
|
+
if (w.value instanceof Date)
|
|
9
|
+
return { ...w, value: w.value.getTime() };
|
|
10
|
+
return w;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async function handlePagination(next, { limit } = {}) {
|
|
14
|
+
const state = { isDone: false, cursor: null, docs: [], count: 0 };
|
|
15
|
+
const onResult = (result) => {
|
|
16
|
+
state.cursor = result.pageStatus === "SplitRecommended" || result.pageStatus === "SplitRequired" ? result.splitCursor ?? result.continueCursor : result.continueCursor;
|
|
17
|
+
if (result.page) {
|
|
18
|
+
state.docs.push(...result.page);
|
|
19
|
+
state.isDone = limit && state.docs.length >= limit || result.isDone;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (result.count) {
|
|
23
|
+
state.count += result.count;
|
|
24
|
+
state.isDone = limit && state.count >= limit || result.isDone;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
state.isDone = result.isDone;
|
|
28
|
+
};
|
|
29
|
+
do {
|
|
30
|
+
const result = await next({
|
|
31
|
+
paginationOpts: {
|
|
32
|
+
numItems: Math.min(200, (limit ?? 200) - state.docs.length, 200),
|
|
33
|
+
cursor: state.cursor
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
onResult(result);
|
|
37
|
+
} while (!state.isDone);
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
export function createConvexHttpAdapter(options) {
|
|
41
|
+
if (!options.url.startsWith("https://") || !options.url.includes(".convex.")) {
|
|
42
|
+
throw new Error(`Invalid Convex URL: ${options.url}. Expected format: https://your-app.convex.cloud`);
|
|
43
|
+
}
|
|
44
|
+
const client = new ConvexHttpClient(options.url);
|
|
45
|
+
return createAdapterFactory({
|
|
46
|
+
config: {
|
|
47
|
+
adapterId: "convex-http",
|
|
48
|
+
adapterName: "Convex HTTP Adapter",
|
|
49
|
+
debugLogs: options.debugLogs ?? false,
|
|
50
|
+
disableIdGeneration: true,
|
|
51
|
+
transaction: false,
|
|
52
|
+
supportsNumericIds: false,
|
|
53
|
+
supportsJSON: false,
|
|
54
|
+
supportsDates: false,
|
|
55
|
+
supportsArrays: true,
|
|
56
|
+
usePlural: false,
|
|
57
|
+
mapKeysTransformInput: { id: "_id" },
|
|
58
|
+
mapKeysTransformOutput: { _id: "id" },
|
|
59
|
+
customTransformInput: ({ data, fieldAttributes }) => {
|
|
60
|
+
if (data && fieldAttributes.type === "date")
|
|
61
|
+
return new Date(data).getTime();
|
|
62
|
+
return data;
|
|
63
|
+
},
|
|
64
|
+
customTransformOutput: ({ data, fieldAttributes }) => {
|
|
65
|
+
if (data && fieldAttributes.type === "date")
|
|
66
|
+
return new Date(data).getTime();
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
adapter: ({ options: authOptions }) => {
|
|
71
|
+
authOptions.telemetry = { enabled: false };
|
|
72
|
+
return {
|
|
73
|
+
id: "convex-http",
|
|
74
|
+
create: async ({ model, data, select }) => {
|
|
75
|
+
return client.mutation(options.api.create, {
|
|
76
|
+
input: { model, data },
|
|
77
|
+
select
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
findOne: async (data) => {
|
|
81
|
+
if (data.where?.every((w) => w.connector === "OR")) {
|
|
82
|
+
for (const w of data.where) {
|
|
83
|
+
const result = await client.query(options.api.findOne, {
|
|
84
|
+
...data,
|
|
85
|
+
model: data.model,
|
|
86
|
+
where: parseWhere(w)
|
|
87
|
+
});
|
|
88
|
+
if (result)
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return client.query(options.api.findOne, {
|
|
94
|
+
...data,
|
|
95
|
+
model: data.model,
|
|
96
|
+
where: parseWhere(data.where)
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
findMany: async (data) => {
|
|
100
|
+
if (data.offset)
|
|
101
|
+
throw new Error("offset not supported");
|
|
102
|
+
if (data.where?.some((w) => w.connector === "OR")) {
|
|
103
|
+
const results = await Promise.all(
|
|
104
|
+
data.where.map(
|
|
105
|
+
async (w) => handlePagination(async ({ paginationOpts }) => {
|
|
106
|
+
return client.query(options.api.findMany, {
|
|
107
|
+
...data,
|
|
108
|
+
model: data.model,
|
|
109
|
+
where: parseWhere(w),
|
|
110
|
+
paginationOpts
|
|
111
|
+
});
|
|
112
|
+
}, { limit: data.limit })
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
const allDocs = results.flatMap((r) => r.docs);
|
|
116
|
+
const uniqueDocs = [...new Map(allDocs.map((d) => [d._id, d])).values()];
|
|
117
|
+
if (data.sortBy) {
|
|
118
|
+
return uniqueDocs.sort((a, b) => {
|
|
119
|
+
const aVal = a[data.sortBy.field];
|
|
120
|
+
const bVal = b[data.sortBy.field];
|
|
121
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
122
|
+
return data.sortBy.direction === "asc" ? cmp : -cmp;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return uniqueDocs;
|
|
126
|
+
}
|
|
127
|
+
const result = await handlePagination(
|
|
128
|
+
async ({ paginationOpts }) => client.query(options.api.findMany, {
|
|
129
|
+
...data,
|
|
130
|
+
model: data.model,
|
|
131
|
+
where: parseWhere(data.where),
|
|
132
|
+
paginationOpts
|
|
133
|
+
}),
|
|
134
|
+
{ limit: data.limit }
|
|
135
|
+
);
|
|
136
|
+
return result.docs;
|
|
137
|
+
},
|
|
138
|
+
// Note: Convex doesn't have a native count query, so we fetch all docs and count client-side.
|
|
139
|
+
// This is inefficient for large datasets but acceptable for auth tables (typically small).
|
|
140
|
+
count: async (data) => {
|
|
141
|
+
if (data.where?.some((w) => w.connector === "OR")) {
|
|
142
|
+
const results = await Promise.all(
|
|
143
|
+
data.where.map(
|
|
144
|
+
async (w) => handlePagination(async ({ paginationOpts }) => {
|
|
145
|
+
return client.query(options.api.findMany, {
|
|
146
|
+
...data,
|
|
147
|
+
model: data.model,
|
|
148
|
+
where: parseWhere(w),
|
|
149
|
+
paginationOpts
|
|
150
|
+
});
|
|
151
|
+
})
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
const allDocs = results.flatMap((r) => r.docs);
|
|
155
|
+
const uniqueDocs = [...new Map(allDocs.map((d) => [d._id, d])).values()];
|
|
156
|
+
return uniqueDocs.length;
|
|
157
|
+
}
|
|
158
|
+
const result = await handlePagination(async ({ paginationOpts }) => client.query(options.api.findMany, {
|
|
159
|
+
...data,
|
|
160
|
+
model: data.model,
|
|
161
|
+
where: parseWhere(data.where),
|
|
162
|
+
paginationOpts
|
|
163
|
+
}));
|
|
164
|
+
return result.docs.length;
|
|
165
|
+
},
|
|
166
|
+
// Supports single eq or multiple AND-connected conditions (Better Auth's common patterns)
|
|
167
|
+
update: async (data) => {
|
|
168
|
+
const hasOrConnector = data.where?.some((w) => w.connector === "OR");
|
|
169
|
+
if (hasOrConnector) {
|
|
170
|
+
throw new Error("update() does not support OR conditions - use updateMany() or split into multiple calls");
|
|
171
|
+
}
|
|
172
|
+
return client.mutation(options.api.updateOne, {
|
|
173
|
+
input: {
|
|
174
|
+
model: data.model,
|
|
175
|
+
where: parseWhere(data.where),
|
|
176
|
+
update: data.update
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
updateMany: async (data) => {
|
|
181
|
+
const result = await handlePagination(async ({ paginationOpts }) => client.mutation(options.api.updateMany, {
|
|
182
|
+
input: {
|
|
183
|
+
...data,
|
|
184
|
+
model: data.model,
|
|
185
|
+
where: parseWhere(data.where)
|
|
186
|
+
},
|
|
187
|
+
paginationOpts
|
|
188
|
+
}));
|
|
189
|
+
return result.count;
|
|
190
|
+
},
|
|
191
|
+
delete: async (data) => {
|
|
192
|
+
await client.mutation(options.api.deleteOne, {
|
|
193
|
+
input: {
|
|
194
|
+
model: data.model,
|
|
195
|
+
where: parseWhere(data.where)
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
deleteMany: async (data) => {
|
|
200
|
+
const result = await handlePagination(async ({ paginationOpts }) => client.mutation(options.api.deleteMany, {
|
|
201
|
+
input: {
|
|
202
|
+
...data,
|
|
203
|
+
model: data.model,
|
|
204
|
+
where: parseWhere(data.where)
|
|
205
|
+
},
|
|
206
|
+
paginationOpts
|
|
207
|
+
}));
|
|
208
|
+
return result.count;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import createAppAuthClient from "#auth/client";
|
|
2
2
|
import { computed, nextTick, useRequestHeaders, useRequestURL, useRuntimeConfig, useState, watch } from "#imports";
|
|
3
3
|
let _client = null;
|
|
4
4
|
function getClient(baseURL) {
|
package/dist/runtime/config.d.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { BetterAuthOptions } from 'better-auth';
|
|
|
2
2
|
import type { ClientOptions } from 'better-auth/client';
|
|
3
3
|
import type { CasingOption } from '../schema-generator.js';
|
|
4
4
|
import type { ServerAuthContext } from './types/augment.js';
|
|
5
|
+
import { createAuthClient } from 'better-auth/vue';
|
|
5
6
|
export type { ServerAuthContext };
|
|
6
7
|
export interface ClientAuthContext {
|
|
7
8
|
siteUrl: string;
|
|
8
9
|
}
|
|
9
|
-
type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'>;
|
|
10
|
-
type ClientAuthConfig = Omit<ClientOptions, 'baseURL'> & {
|
|
10
|
+
export type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'>;
|
|
11
|
+
export type ClientAuthConfig = Omit<ClientOptions, 'baseURL'> & {
|
|
11
12
|
baseURL?: string;
|
|
12
13
|
};
|
|
13
14
|
export type ServerAuthConfigFn = (ctx: ServerAuthContext) => ServerAuthConfig;
|
|
@@ -44,5 +45,5 @@ export interface AuthRuntimeConfig {
|
|
|
44
45
|
export interface AuthPrivateRuntimeConfig {
|
|
45
46
|
secondaryStorage: boolean;
|
|
46
47
|
}
|
|
47
|
-
export declare function defineServerAuth<T extends ServerAuthConfig>(config: (ctx: ServerAuthContext) => T): (ctx: ServerAuthContext) => T;
|
|
48
|
-
export declare function defineClientAuth(config:
|
|
48
|
+
export declare function defineServerAuth<T extends ServerAuthConfig>(config: T | ((ctx: ServerAuthContext) => T)): (ctx: ServerAuthContext) => T;
|
|
49
|
+
export declare function defineClientAuth<T extends ClientAuthConfig>(config: T | ((ctx: ClientAuthContext) => T)): (baseURL: string) => ReturnType<typeof createAuthClient<T>>;
|
package/dist/runtime/config.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { createAuthClient } from "better-auth/vue";
|
|
1
2
|
export function defineServerAuth(config) {
|
|
2
|
-
return config;
|
|
3
|
+
return typeof config === "function" ? config : () => config;
|
|
3
4
|
}
|
|
4
5
|
export function defineClientAuth(config) {
|
|
5
|
-
return
|
|
6
|
+
return (baseURL) => {
|
|
7
|
+
const ctx = { siteUrl: baseURL };
|
|
8
|
+
const resolved = typeof config === "function" ? config(ctx) : config;
|
|
9
|
+
return createAuthClient({ ...resolved, baseURL });
|
|
10
|
+
};
|
|
6
11
|
}
|
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.16",
|
|
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",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"./config": {
|
|
27
27
|
"types": "./dist/runtime/config.d.ts",
|
|
28
28
|
"import": "./dist/runtime/config.js"
|
|
29
|
+
},
|
|
30
|
+
"./adapters/convex": {
|
|
31
|
+
"types": "./dist/runtime/adapters/convex.d.ts",
|
|
32
|
+
"import": "./dist/runtime/adapters/convex.js"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"main": "./dist/module.mjs",
|
|
@@ -36,6 +40,9 @@
|
|
|
36
40
|
],
|
|
37
41
|
"config": [
|
|
38
42
|
"./dist/runtime/config.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"adapters/convex": [
|
|
45
|
+
"./dist/runtime/adapters/convex.d.ts"
|
|
39
46
|
]
|
|
40
47
|
}
|
|
41
48
|
},
|
|
@@ -50,7 +57,7 @@
|
|
|
50
57
|
"dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
|
|
51
58
|
"dev:docs": "nuxi dev docs",
|
|
52
59
|
"build:docs": "nuxi build docs",
|
|
53
|
-
"release": "bumpp --push",
|
|
60
|
+
"release": "bumpp --push --no-push-all",
|
|
54
61
|
"lint": "eslint .",
|
|
55
62
|
"lint:fix": "eslint . --fix",
|
|
56
63
|
"typecheck": "vue-tsc --noEmit",
|
|
@@ -60,20 +67,26 @@
|
|
|
60
67
|
},
|
|
61
68
|
"peerDependencies": {
|
|
62
69
|
"@nuxthub/core": ">=0.10.0",
|
|
63
|
-
"better-auth": ">=1.0.0"
|
|
70
|
+
"better-auth": ">=1.0.0",
|
|
71
|
+
"convex": ">=1.25.0"
|
|
64
72
|
},
|
|
65
73
|
"peerDependenciesMeta": {
|
|
66
74
|
"@nuxthub/core": {
|
|
67
75
|
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"convex": {
|
|
78
|
+
"optional": true
|
|
68
79
|
}
|
|
69
80
|
},
|
|
70
81
|
"dependencies": {
|
|
71
82
|
"@better-auth/cli": "^1.5.0-beta.3",
|
|
72
83
|
"@nuxt/kit": "^4.2.2",
|
|
84
|
+
"@nuxt/ui": "^4.2.1",
|
|
73
85
|
"defu": "^6.1.4",
|
|
74
86
|
"jiti": "^2.4.2",
|
|
75
87
|
"pathe": "^2.0.3",
|
|
76
|
-
"radix3": "^1.1.2"
|
|
88
|
+
"radix3": "^1.1.2",
|
|
89
|
+
"std-env": "^3.10.0"
|
|
77
90
|
},
|
|
78
91
|
"devDependencies": {
|
|
79
92
|
"@antfu/eslint-config": "^4.12.0",
|
|
@@ -1,92 +0,0 @@
|
|
|
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_
|
|
@@ -1,153 +0,0 @@
|
|
|
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.
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Client-Only Mode (External Auth Backend)
|
|
2
|
-
|
|
3
|
-
When Better Auth runs on a separate backend (microservices, standalone server), use `clientOnly` mode.
|
|
4
|
-
|
|
5
|
-
## Configuration
|
|
6
|
-
|
|
7
|
-
### 1. Enable in nuxt.config.ts
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
export default defineNuxtConfig({
|
|
11
|
-
modules: ['@onmax/nuxt-better-auth'],
|
|
12
|
-
auth: {
|
|
13
|
-
clientOnly: true,
|
|
14
|
-
},
|
|
15
|
-
})
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### 2. Point client to external server
|
|
19
|
-
|
|
20
|
-
```ts [app/auth.config.ts]
|
|
21
|
-
import { createAuthClient } from 'better-auth/vue'
|
|
22
|
-
|
|
23
|
-
export function createAppAuthClient(_baseURL: string) {
|
|
24
|
-
return createAuthClient({
|
|
25
|
-
baseURL: 'https://auth.example.com', // External auth server
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### 3. Set frontend URL
|
|
31
|
-
|
|
32
|
-
```ini [.env]
|
|
33
|
-
NUXT_PUBLIC_SITE_URL="https://your-frontend.com"
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## What Changes
|
|
37
|
-
|
|
38
|
-
| Feature | Full Mode | Client-Only |
|
|
39
|
-
| ----------------------------------------------------------------------------- | --------------- | ----------------- |
|
|
40
|
-
| `server/auth.config.ts` | Required | Not needed |
|
|
41
|
-
| `/api/auth/**` handlers | Auto-registered | Skipped |
|
|
42
|
-
| `NUXT_BETTER_AUTH_SECRET` | Required | Not needed |
|
|
43
|
-
| Server utilities (`serverAuth()`, `getUserSession()`, `requireUserSession()`) | Available | **Not available** |
|
|
44
|
-
| SSR session hydration | Server-side | Client-side only |
|
|
45
|
-
| `useUserSession()`, route protection, `<BetterAuthState>` | Works | Works |
|
|
46
|
-
|
|
47
|
-
## CORS Requirements
|
|
48
|
-
|
|
49
|
-
Ensure external auth server:
|
|
50
|
-
|
|
51
|
-
- Allows requests from frontend (CORS with `credentials: true`)
|
|
52
|
-
- Uses `SameSite=None; Secure` cookies (HTTPS required)
|
|
53
|
-
- Includes frontend URL in `trustedOrigins`
|
|
54
|
-
|
|
55
|
-
## SSR Considerations
|
|
56
|
-
|
|
57
|
-
Session fetched client-side only:
|
|
58
|
-
|
|
59
|
-
- Server-rendered pages render as "unauthenticated" initially
|
|
60
|
-
- Hydrates with session data on client
|
|
61
|
-
- Use `<BetterAuthState>` for loading states
|
|
62
|
-
|
|
63
|
-
```vue
|
|
64
|
-
<BetterAuthState v-slot="{ isLoading, user }">
|
|
65
|
-
<div v-if="isLoading">Loading...</div>
|
|
66
|
-
<div v-else-if="user">Welcome, {{ user.name }}</div>
|
|
67
|
-
<div v-else>Please log in</div>
|
|
68
|
-
</BetterAuthState>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Use Cases
|
|
72
|
-
|
|
73
|
-
- **Microservices**: Auth service is separate deployment
|
|
74
|
-
- **Shared auth**: Multiple frontends share one auth backend
|
|
75
|
-
- **Existing backend**: Already have Better Auth server running elsewhere
|
|
76
|
-
|
|
77
|
-
## Architecture Example
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
┌─────────────────┐ ┌─────────────────┐
|
|
81
|
-
│ Nuxt App │────▶│ Auth Server │
|
|
82
|
-
│ (clientOnly) │ │ (Better Auth) │
|
|
83
|
-
│ │◀────│ │
|
|
84
|
-
└─────────────────┘ └────────┬────────┘
|
|
85
|
-
│
|
|
86
|
-
┌────────▼────────┐
|
|
87
|
-
│ Database │
|
|
88
|
-
└─────────────────┘
|
|
89
|
-
```
|