@xenterprises/nuxt-x-auth-better 0.2.6 → 0.3.1

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 CHANGED
@@ -4,54 +4,94 @@ Copyright (c) 2024-2026 X Enterprises LLC. All Rights Reserved.
4
4
 
5
5
  This software and associated documentation files (the "Software") are the
6
6
  exclusive property of X Enterprises LLC, a Washington limited liability
7
- company.
7
+ company ("X Enterprises"). The Software is distributed through public
8
+ package registries (including npm) for operational convenience only; such
9
+ distribution does not grant any rights beyond those expressly stated below.
8
10
 
9
11
  TERMS AND CONDITIONS
10
12
 
11
13
  1. OWNERSHIP
12
14
  All rights, title, and interest in and to the Software, including all
13
15
  intellectual property rights, are and shall remain the exclusive property
14
- of X Enterprises LLC.
16
+ of X Enterprises. No rights are granted except as expressly set forth in
17
+ this License.
15
18
 
16
- 2. RESTRICTIONS
17
- Without the prior written consent of X Enterprises LLC, you may not:
18
- - Copy, modify, or distribute the Software
19
- - Reverse engineer, decompile, or disassemble the Software
20
- - Sublicense, sell, lease, or otherwise transfer the Software
21
- - Remove or alter any proprietary notices or labels
19
+ 2. PERMITTED USE
20
+ Subject to the restrictions in Section 3, you are permitted to download,
21
+ install, and execute the Software solely as a dependency of:
22
22
 
23
- 3. AUTHORIZED USE
24
- Use of this Software is limited to authorized employees, contractors, and
25
- agents of X Enterprises LLC, solely for purposes approved by X Enterprises
26
- LLC.
23
+ (a) software developed, owned, or operated by X Enterprises;
24
+
25
+ (b) software that X Enterprises has developed, delivered, or licensed to
26
+ a third party ("Client") under a written engagement agreement with
27
+ X Enterprises, when such use is performed by or on behalf of that
28
+ Client; or
29
+
30
+ (c) end-user access to, or consumption of, a product or service described
31
+ in (a) or (b), provided that such access does not involve
32
+ redistribution, modification, or separate use of the Software.
33
+
34
+ Permitted Use includes automated installation and execution by continuous
35
+ integration systems, container builds, hosting platforms, and similar
36
+ infrastructure, to the extent necessary to support (a), (b), or (c).
37
+
38
+ 3. RESTRICTIONS
39
+ Except as expressly permitted in Section 2, and without the prior written
40
+ consent of X Enterprises, you may not:
41
+
42
+ (a) copy, modify, adapt, translate, or create derivative works of the
43
+ Software for any purpose outside the scope of Section 2;
44
+
45
+ (b) redistribute, republish, sublicense, sell, lease, rent, or otherwise
46
+ transfer the Software, in whole or in part, whether standalone or
47
+ bundled with other software;
48
+
49
+ (c) reverse engineer, decompile, disassemble, or attempt to derive the
50
+ source code or underlying ideas, algorithms, structure, or
51
+ organization of the Software, except to the extent such activity is
52
+ expressly permitted by applicable law notwithstanding this
53
+ restriction;
54
+
55
+ (d) use the Software, in whole or in part, to develop, operate, or
56
+ provide any product or service that competes with or substitutes for
57
+ any X Enterprises product or service;
58
+
59
+ (e) remove, obscure, or alter any copyright, trademark, license, or other
60
+ proprietary notice contained in or on the Software; or
61
+
62
+ (f) use the Software in violation of any applicable law or regulation.
27
63
 
28
64
  4. NO WARRANTY
29
65
  THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
66
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
67
  FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
32
- X ENTERPRISES LLC BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
33
- WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF,
34
- OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
- SOFTWARE.
68
+ X ENTERPRISES BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
69
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT
70
+ OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
71
+ THE SOFTWARE.
36
72
 
37
73
  5. LIMITATION OF LIABILITY
38
- IN NO EVENT SHALL X ENTERPRISES LLC BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
39
- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
40
- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
41
- OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
42
- WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
43
- OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
44
- ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
74
+ IN NO EVENT SHALL X ENTERPRISES BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
75
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
76
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
77
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
78
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
79
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
80
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45
81
 
46
82
  6. GOVERNING LAW
47
- This license shall be governed by and construed in accordance with the laws
48
- of the State of Washington, United States, without regard to its conflict
49
- of law provisions.
83
+ This License shall be governed by and construed in accordance with the
84
+ laws of the State of Washington, United States, without regard to its
85
+ conflict of law provisions. Exclusive jurisdiction for any dispute
86
+ arising out of this License shall lie in the state or federal courts
87
+ located in King County, Washington.
50
88
 
51
89
  7. TERMINATION
52
- This license is effective until terminated. X Enterprises LLC may terminate
53
- this license at any time without notice. Upon termination, you must destroy
54
- all copies of the Software in your possession.
90
+ This License is effective until terminated. Your rights under this
91
+ License will terminate automatically and without notice if you fail to
92
+ comply with any term herein. Upon termination, you must cease all use of
93
+ the Software and destroy all copies in your possession or control.
94
+ Sections 1, 3, 4, 5, 6, and 7 survive termination.
55
95
 
56
96
  For licensing inquiries, contact: legal@x.enterprises
57
97
 
package/README.md CHANGED
@@ -1,19 +1,8 @@
1
1
  # @xenterprises/nuxt-x-auth-better
2
2
 
3
- BetterAuth authentication layer for Nuxt 4.
3
+ BetterAuth authentication layer for Nuxt 4. Pre-built UI components, route protection, OAuth, magic link, and password reset flows — all configurable via `app.config.ts`.
4
4
 
5
- ## Features
6
-
7
- - **Better Auth integration** - Industry-standard authentication with Better Auth
8
- - **OAuth providers** - Google, GitHub, and more
9
- - **Magic link authentication** - Passwordless login via email
10
- - **Secure token storage** - HttpOnly cookies with SameSite protection
11
- - **Field normalization** - Unified user object across providers
12
- - **Pre-built UI components** - Login, Signup, Forgot Password, Magic Link, OAuth
13
- - **Route protection** - Global middleware for authenticated routes
14
- - **Responsive design** - Built with Nuxt UI v4
15
-
16
- ## Installation
5
+ ## Install
17
6
 
18
7
  ```bash
19
8
  npm install @xenterprises/nuxt-x-auth-better better-auth
@@ -26,36 +15,30 @@ npm install @xenterprises/nuxt-x-auth-better better-auth
26
15
  ```ts
27
16
  // nuxt.config.ts
28
17
  export default defineNuxtConfig({
29
- extends: ['@xenterprises/nuxt-x-auth-better']
18
+ extends: ['@xenterprises/nuxt-x-auth-better'],
30
19
  })
31
20
  ```
32
21
 
33
- ### 2. Configure your provider
22
+ ### 2. Configure
34
23
 
35
24
  ```ts
36
25
  // app.config.ts
37
26
  export default defineAppConfig({
38
27
  xAuth: {
39
- tokens: {
40
- accessCookie: 'x_auth_access',
41
- refreshCookie: 'x_auth_refresh',
42
- hasRefresh: true,
43
- },
44
- redirects: {
45
- login: '/auth/login',
46
- signup: '/auth/signup',
47
- afterLogin: '/dashboard',
48
- afterSignup: '/dashboard',
49
- afterLogout: '/auth/login',
50
- forgotPassword: '/auth/forgot-password',
51
- },
52
28
  features: {
53
29
  oauth: true,
54
30
  magicLink: true,
55
31
  forgotPassword: true,
56
32
  signup: true,
57
33
  },
58
- oauthProviders: ['google', 'github'],
34
+ redirects: {
35
+ afterLogin: '/dashboard',
36
+ afterSignup: '/dashboard',
37
+ },
38
+ oauthProviders: [
39
+ { id: 'google', label: 'Google', icon: 'i-simple-icons-google' },
40
+ { id: 'github', label: 'GitHub', icon: 'i-simple-icons-github' },
41
+ ],
59
42
  ui: {
60
43
  showLogo: true,
61
44
  logoUrl: '/logo.svg',
@@ -73,127 +56,232 @@ NUXT_PUBLIC_X_AUTH_BASE_URL=https://api.example.com
73
56
 
74
57
  ## Usage
75
58
 
76
- ### Composable
77
-
78
59
  ```vue
79
60
  <script setup>
80
61
  const {
81
62
  user,
63
+ isAuthenticated,
64
+ isLoading,
82
65
  login,
83
- logout,
84
66
  signup,
85
- loginWithOAuth,
67
+ logout,
68
+ loginWithProvider,
86
69
  sendMagicLink,
87
70
  forgotPassword,
88
- isLoading,
89
- isAuthenticated
71
+ resetPassword,
72
+ changePassword,
73
+ updateUser,
74
+ deleteAccount,
75
+ getToken,
76
+ getAuthHeaders,
90
77
  } = useXAuth()
91
78
 
92
- // Email/password login
93
79
  await login('email@example.com', 'password')
94
-
95
- // OAuth login
96
- await loginWithOAuth('google')
97
-
98
- // Magic link
80
+ await loginWithProvider('google')
99
81
  await sendMagicLink('email@example.com')
100
-
101
- // Signup
102
- await signup('email@example.com', 'password', { name: 'John' })
103
-
104
- // Password reset
82
+ await signup('email@example.com', 'password', 'John Doe')
105
83
  await forgotPassword('email@example.com')
106
-
107
- // Logout
84
+ await changePassword('oldpass', 'newpass')
85
+ await updateUser({ name: 'New Name' })
108
86
  await logout()
109
87
  </script>
110
88
  ```
111
89
 
112
- ### Normalized User Object
113
-
114
- Better Auth returns a normalized user object:
90
+ ## Options (app.config.ts)
91
+
92
+ ### `xAuth.tokens`
93
+
94
+ | Name | Type | Default | Description |
95
+ |------|------|---------|-------------|
96
+ | `accessCookie` | `string` | `"x_auth_access"` | Cookie name for access token |
97
+ | `refreshCookie` | `string` | `"x_auth_refresh"` | Cookie name for refresh token |
98
+ | `hasRefresh` | `boolean` | `true` | Enable refresh token storage |
99
+
100
+ ### `xAuth.redirects`
101
+
102
+ | Name | Type | Default | Description |
103
+ |------|------|---------|-------------|
104
+ | `login` | `string` | `"/auth/login"` | Login page path |
105
+ | `signup` | `string` | `"/auth/signup"` | Signup page path |
106
+ | `afterLogin` | `string` | `"/"` | Redirect after successful login |
107
+ | `afterSignup` | `string` | `"/"` | Redirect after successful signup |
108
+ | `afterLogout` | `string` | `"/auth/login"` | Redirect after logout |
109
+ | `forgotPassword` | `string` | `"/auth/forgot-password"` | Forgot password page path |
110
+
111
+ ### `xAuth.features`
112
+
113
+ | Name | Type | Default | Description |
114
+ |------|------|---------|-------------|
115
+ | `oauth` | `boolean` | `false` | Enable OAuth provider buttons |
116
+ | `magicLink` | `boolean` | `false` | Enable magic link authentication |
117
+ | `otp` | `boolean` | `false` | Enable OTP authentication |
118
+ | `forgotPassword` | `boolean` | `true` | Enable forgot password flow |
119
+ | `signup` | `boolean` | `true` | Enable signup form and link |
120
+
121
+ ### `xAuth.oauthProviders`
122
+
123
+ Array of `{ id, label, icon }` objects. Supported provider IDs: `google`, `github`, `microsoft`, `facebook`, `twitter`, `apple`, `linkedin`, `discord`, `spotify`.
124
+
125
+ ### `xAuth.ui`
126
+
127
+ | Name | Type | Default | Description |
128
+ |------|------|---------|-------------|
129
+ | `showLogo` | `boolean` | `true` | Show logo in auth layout |
130
+ | `showBrandName` | `boolean` | `true` | Show brand name below logo |
131
+ | `logoUrl` | `string` | `""` | URL to logo image |
132
+ | `brandName` | `string` | `""` | Brand name text |
133
+ | `tagline` | `string` | `""` | Tagline below brand name |
134
+ | `layout` | `"centered" \| "split"` | `"centered"` | Auth page layout style |
135
+ | `background.enabled` | `boolean` | `true` | Enable background image |
136
+ | `background.imageUrl` | `string` | `""` | Custom background image URL (random default if empty) |
137
+ | `background.overlayOpacity` | `number` | `55` | Overlay opacity (0-100) |
138
+ | `background.blur` | `boolean` | `true` | Blur background image |
139
+ | `card.glass` | `boolean` | `false` | Glass morphism effect on card |
140
+ | `card.logoUrl` | `string` | `""` | Logo shown inside the auth card |
141
+ | `form.showSeparator` | `boolean` | `true` | Show "or" separator between OAuth and form |
142
+ | `legal.copyright` | `string` | `""` | Footer copyright text |
143
+ | `legal.links` | `Array<{label, to}>` | `[]` | Footer legal links |
144
+
145
+ ## Composable: `useXAuth()`
146
+
147
+ ### Reactive State
148
+
149
+ | Property | Type | Description |
150
+ |----------|------|-------------|
151
+ | `user` | `Ref<AuthUser \| null>` | Current authenticated user |
152
+ | `isAuthenticated` | `Ref<boolean>` | Whether user is authenticated |
153
+ | `isLoading` | `Ref<boolean>` | Whether session is loading |
154
+ | `emailSent` | `Ref<boolean>` | Whether magic link / reset email was sent |
155
+ | `codeSent` | `Ref<boolean>` | Whether OTP code was sent |
156
+ | `session` | `Ref<SessionData>` | Raw Better Auth session data |
157
+ | `sessionError` | `Ref<Error>` | Session error if any |
158
+ | `config` | `Ref<AuthConfig>` | Current auth configuration |
159
+
160
+ ### Methods
161
+
162
+ | Method | Returns | Description |
163
+ |--------|---------|-------------|
164
+ | `login(email, password)` | `AuthUser \| null` | Email/password sign in |
165
+ | `signup(email, password, name?)` | `AuthUser \| null` | Create account |
166
+ | `logout()` | `boolean` | Sign out and redirect |
167
+ | `loginWithProvider(provider)` | `boolean` | Start OAuth flow |
168
+ | `sendMagicLink(email, options?)` | `boolean` | Send magic link email |
169
+ | `forgotPassword(email)` | `boolean` | Send password reset email |
170
+ | `resetPassword(token, newPassword)` | `true \| { error }` | Reset password with token |
171
+ | `changePassword(current, new)` | `true \| { error }` | Change password (authenticated) |
172
+ | `updateUser({ name?, image? })` | `true \| { error }` | Update user profile |
173
+ | `deleteAccount()` | `boolean` | Permanently delete account |
174
+ | `getCurrentUser()` | `AuthUser \| null` | Get current user (fetches if needed) |
175
+ | `getToken()` | `string \| null` | Get access token |
176
+ | `getAuthHeaders()` | `Record<string, string>` | Get `{ Authorization: 'Bearer ...' }` |
177
+ | `handleMagicLinkCallback(token)` | `true \| { error }` | Verify magic link token |
178
+ | `resetState()` | `void` | Reset emailSent / codeSent flags |
179
+
180
+ ### AuthUser Type
115
181
 
116
182
  ```ts
117
- {
118
- id: 'user-123',
119
- email: 'user@example.com',
120
- name: 'John Doe',
121
- avatar: 'https://example.com/avatar.jpg',
122
- emailVerified: true,
123
- metadata: { /* provider-specific data */ }
183
+ interface AuthUser {
184
+ id: string
185
+ email: string
186
+ name: string
187
+ avatar?: string
188
+ emailVerified: boolean
189
+ metadata?: Record<string, any>
124
190
  }
125
191
  ```
126
192
 
127
- ### Protected Routes
128
-
129
- Routes are automatically protected by the global middleware.
130
-
131
- **Route types:**
132
- - **Guest-only routes** - `/auth/login`, `/auth/signup`, `/auth/forgot-password`, `/auth/magic-link` (redirects logged-in users)
133
- - **Public routes** - `/auth/handler/*`, `/auth/logout` (accessible by anyone)
134
- - **Protected routes** - Everything else (requires authentication)
135
-
136
193
  ## Components
137
194
 
138
195
  | Component | Description |
139
196
  |-----------|-------------|
140
- | `XAuthLogin` | Email/password login form |
141
- | `XAuthSignup` | Registration form |
142
- | `XAuthForgotPassword` | Password reset request |
143
- | `XAuthMagicLink` | Magic link authentication |
144
- | `XAuthOAuthButton` | Single OAuth provider button |
145
- | `XAuthOAuthButtonGroup` | All configured OAuth buttons |
146
- | `XAuthHandler` | OAuth callback handler |
197
+ | `XAuthLogin` | Email/password login form with OAuth, magic link, and forgot password links |
198
+ | `XAuthSignup` | Registration form with terms/privacy links |
199
+ | `XAuthForgotPassword` | Password reset request form with success state |
200
+ | `XAuthMagicLink` | Magic link email form with success state |
201
+ | `XAuthOAuthButton` | Single OAuth provider button with brand icon |
202
+ | `XAuthOAuthButtonGroup` | All configured OAuth provider buttons |
203
+ | `XAuthHandler` | Callback handler for OAuth, magic link, password reset, email verification |
147
204
 
148
- ## Pages (Auto-registered)
205
+ ## Pages (auto-registered)
149
206
 
150
- - `/auth/login` - Login page
151
- - `/auth/signup` - Registration page
152
- - `/auth/forgot-password` - Password reset
153
- - `/auth/magic-link` - Magic link authentication
154
- - `/auth/logout` - Logout handler
155
- - `/auth/handler/*` - OAuth callback handlers
207
+ | Path | Description | Route Type |
208
+ |------|-------------|------------|
209
+ | `/auth/login` | Login page | Guest-only |
210
+ | `/auth/signup` | Registration page | Guest-only |
211
+ | `/auth/forgot-password` | Password reset request | Guest-only |
212
+ | `/auth/magic-link` | Magic link authentication | Guest-only |
213
+ | `/auth/reset-password` | Set new password (with token) | Guest-only |
214
+ | `/auth/logout` | Logout handler | Public |
215
+ | `/auth/handler/*` | OAuth / magic link / password reset callbacks | Public |
156
216
 
157
- ## OAuth Providers
217
+ All other routes are **protected** (require authentication).
158
218
 
159
- Configure which OAuth providers to show:
219
+ ## Environment Variables
160
220
 
161
- ```ts
162
- // app.config.ts
163
- export default defineAppConfig({
164
- xAuth: {
165
- oauthProviders: ['google', 'github', 'microsoft', 'apple'],
166
- },
167
- })
168
- ```
221
+ | Name | Required | Description |
222
+ |------|----------|-------------|
223
+ | `NUXT_PUBLIC_X_AUTH_BASE_URL` | Yes | Base URL of your Better Auth backend (e.g. `https://api.example.com`) |
169
224
 
170
- Available providers depend on your Better Auth backend configuration.
225
+ The auth path is configurable via `runtimeConfig.public.x.auth.authPath` (default: `/auth`).
171
226
 
172
- ## Token Storage
227
+ ## Error Reference
173
228
 
174
- Tokens are stored in secure cookies:
175
- - `HttpOnly` - Not accessible via JavaScript
176
- - `Secure` - Only sent over HTTPS in production
177
- - `SameSite=Lax` - CSRF protection
229
+ All errors are displayed as toast notifications. Methods return `null`, `false`, or `{ error: string }` on failure.
178
230
 
179
- Cookie names are configurable via `tokens.accessCookie` and `tokens.refreshCookie`.
231
+ | Context | Error | When |
232
+ |---------|-------|------|
233
+ | Login | "Please enter both email and password." | Empty email or password |
234
+ | Login | "Invalid credentials." | Wrong email/password (from API) |
235
+ | Signup | "Please enter both email and password." | Empty email or password |
236
+ | Signup | "Could not create account." | API error (e.g. email exists) |
237
+ | Forgot Password | "Please enter your email address." | Empty email |
238
+ | Reset Password | "Token and new password are required" | Missing token or password |
239
+ | Change Password | "Please enter both current and new passwords." | Empty input |
240
+ | Magic Link | "Please enter your email address." | Empty email |
241
+ | OAuth | "`{provider}` Login Failed" | OAuth flow error |
242
+ | Logout | "Failed to sign out." | API error |
243
+ | Update Profile | "Could not update profile." | API error |
244
+ | Delete Account | "Could not delete account." | API error |
245
+
246
+ ## How It Works
247
+
248
+ This layer uses Better Auth's official Vue client (`better-auth/vue`) via `createAuthClient`. The client communicates with your Better Auth backend using cookie-based sessions (`credentials: 'include'`).
249
+
250
+ 1. **Session management**: `createAuthClient` is instantiated once (singleton) and provides a reactive `useSession()` ref that auto-updates. The composable wraps this with convenience methods.
251
+
252
+ 2. **Route protection**: A global middleware (`auth.global.ts`) runs on every navigation. It classifies routes as public, guest-only, or protected, then calls `getCurrentUser()` to check auth state.
253
+
254
+ 3. **Auth layout**: All `/auth/*` pages use the `auth` layout which provides a card-style UI with background image, branding, and animations.
255
+
256
+ 4. **OAuth flow**: `loginWithProvider()` calls Better Auth's `signIn.social()` which redirects to the provider. The callback lands on `/auth/handler/oauth-callback` where the Handler component refreshes the session.
257
+
258
+ 5. **Token access**: `getToken()` and `getAuthHeaders()` extract the access token from the Better Auth session for use with your own API calls.
259
+
260
+ ## Layer Architecture
261
+
262
+ - **`nuxt.config.ts`**: Registers `@nuxt/ui` module, loads the auth CSS, and defines `runtimeConfig.public.x.auth` for base URL and auth path.
263
+ - **`app/app.config.ts`**: Defines all configurable options (tokens, redirects, features, OAuth providers, UI settings) with defaults. Consumer apps override these in their own `app.config.ts`.
264
+ - **`app/composables/useXAuth.ts`**: Core composable wrapping Better Auth's Vue client with typed methods for all auth flows.
265
+ - **`app/middleware/auth.global.ts`**: Global route middleware for auth-based route protection.
266
+ - **`app/plugins/auth-token.ts`**: Provides `$getAuthToken` for use in other plugins/composables.
267
+ - **`app/layouts/auth.vue`**: Visual auth layout with card design, background image, and branding.
268
+ - **`app/components/XAuth/*`**: Pre-built form components using Nuxt UI's `UAuthForm`.
269
+ - **`app/pages/auth/*`**: Auto-registered auth pages.
270
+ - **`app/utils/`**: Cookie storage helper and field normalization utilities.
180
271
 
181
272
  ## Testing
182
273
 
183
274
  ```bash
184
- # Unit tests
185
- npm run test
186
-
187
- # E2E tests
188
- npx playwright test
275
+ npm run test # Watch mode
276
+ npm run test:run # Single run
189
277
  ```
190
278
 
191
279
  ## Requirements
192
280
 
193
281
  - Nuxt 4+
194
282
  - Better Auth 1.0+
195
- - @nuxt/ui v4+ (included)
283
+ - @nuxt/ui v4+ (included as dependency)
196
284
 
197
285
  ## License
198
286
 
199
- MIT
287
+ UNLICENSED
@@ -92,7 +92,7 @@ const props = defineProps({
92
92
  const config = useAppConfig();
93
93
  const router = useRouter();
94
94
  const route = useRoute();
95
- const { handleMagicLinkCallback, resetPassword } = useXAuth();
95
+ const { handleMagicLinkCallback, resetPassword, getCurrentUser } = useXAuth();
96
96
 
97
97
  const isLoading = ref(true);
98
98
  const error = ref(null);
@@ -128,12 +128,9 @@ onMounted(async () => {
128
128
  return; // Don't redirect for password reset
129
129
 
130
130
  case "oauth":
131
- // OAuth callbacks are typically handled automatically
132
- // Just check if we're authenticated after the callback
133
- const result = await handleMagicLinkCallback(url);
134
- if (result?.error) {
135
- throw new Error(result.error);
136
- }
131
+ // OAuth callbacks are handled by Better Auth automatically via cookies.
132
+ // Refresh the session to pick up the new auth state.
133
+ await getCurrentUser();
137
134
  break;
138
135
 
139
136
  case "email-verification":
@@ -153,17 +150,13 @@ onMounted(async () => {
153
150
  let redirectPath = props.redirectTo || config.xAuth?.redirects?.afterLogin || "/";
154
151
 
155
152
  // Check if there's a redirect_to in the query or state parameter
156
- if (route.query.redirect_to) {
157
- redirectPath = route.query.redirect_to;
158
- } else if (route.query.state) {
159
- try {
160
- const stateObj = JSON.parse(route.query.state);
161
- if (stateObj.redirect_to) {
162
- redirectPath = stateObj.redirect_to;
163
- }
164
- } catch {
165
- // Ignore invalid state parameter
166
- }
153
+ const candidateRedirect = route.query.redirect_to
154
+ || (() => { try { return JSON.parse(route.query.state)?.redirect_to; } catch { return null; } })();
155
+
156
+ // Validate redirect path to prevent open redirects — only allow relative paths
157
+ if (candidateRedirect && typeof candidateRedirect === 'string'
158
+ && candidateRedirect.startsWith('/') && !candidateRedirect.startsWith('//')) {
159
+ redirectPath = candidateRedirect;
167
160
  }
168
161
 
169
162
  // Redirect after delay
@@ -120,35 +120,3 @@ async function handleOAuth(providerId) {
120
120
  }
121
121
  </script>
122
122
 
123
- <style scoped>
124
- .password-strength {
125
- display: flex;
126
- gap: 0.25rem;
127
- margin-top: 0.5rem;
128
- }
129
-
130
- .password-strength__bar {
131
- flex: 1;
132
- height: 3px;
133
- border-radius: 9999px;
134
- background: rgb(var(--color-neutral-200));
135
- transition: background 0.2s ease;
136
- }
137
-
138
- .dark .password-strength__bar {
139
- background: rgb(var(--color-neutral-800));
140
- }
141
-
142
- .password-strength[data-strength="weak"] .password-strength__bar:nth-child(1) {
143
- background: rgb(var(--color-error-500));
144
- }
145
-
146
- .password-strength[data-strength="medium"] .password-strength__bar:nth-child(1),
147
- .password-strength[data-strength="medium"] .password-strength__bar:nth-child(2) {
148
- background: rgb(var(--color-warning-500));
149
- }
150
-
151
- .password-strength[data-strength="strong"] .password-strength__bar {
152
- background: rgb(var(--color-success-500));
153
- }
154
- </style>
@@ -4,6 +4,13 @@ import type { AuthUser, AuthConfig } from './types';
4
4
 
5
5
  let authClientInstance: ReturnType<typeof createAuthClient> | null = null;
6
6
 
7
+ function getOrigin(): string {
8
+ if (import.meta.client) {
9
+ return window.location.origin;
10
+ }
11
+ return '';
12
+ }
13
+
7
14
  export function useXAuth() {
8
15
  const config = useRuntimeConfig();
9
16
  const appConfig = useAppConfig();
@@ -152,7 +159,7 @@ export function useXAuth() {
152
159
  try {
153
160
  const result = await authClient.resetPassword({
154
161
  email,
155
- redirectTo: `${window.location.origin}/auth/reset-password`,
162
+ redirectTo: `${getOrigin()}/auth/handler/password-reset`,
156
163
  });
157
164
 
158
165
  emailSent.value = true;
@@ -166,8 +173,13 @@ export function useXAuth() {
166
173
  };
167
174
 
168
175
  const resetPassword = async (token: string, newPassword: string) => {
176
+ if (!token || !newPassword) {
177
+ return { error: 'Token and new password are required' };
178
+ }
179
+
169
180
  try {
170
181
  const result = await authClient.resetPassword({
182
+ token,
171
183
  newPassword,
172
184
  });
173
185
 
@@ -184,7 +196,7 @@ export function useXAuth() {
184
196
 
185
197
  const loginWithProvider = async (providerName: string) => {
186
198
  try {
187
- const callbackUrl = `${window.location.origin}/auth/handler/oauth-callback`;
199
+ const callbackUrl = `${getOrigin()}/auth/handler/oauth-callback`;
188
200
 
189
201
  await authClient.signIn.social({
190
202
  provider: providerName as any,
@@ -286,6 +298,74 @@ export function useXAuth() {
286
298
  }
287
299
  };
288
300
 
301
+ const changePassword = async (currentPassword: string, newPassword: string) => {
302
+ if (!currentPassword || !newPassword) {
303
+ showError('Change Password Failed', 'Please enter both current and new passwords.');
304
+ return { error: 'Both current and new passwords are required' };
305
+ }
306
+
307
+ try {
308
+ const result = await authClient.changePassword({
309
+ currentPassword,
310
+ newPassword,
311
+ });
312
+
313
+ if (result.error) {
314
+ showError('Change Password Failed', result.error.message || 'Could not change password.');
315
+ return { error: result.error.message || 'Could not change password' };
316
+ }
317
+
318
+ showSuccess('Password Changed', 'Your password has been updated successfully.');
319
+ return true;
320
+ } catch (err: any) {
321
+ showError('Change Password Failed', err.message || 'An error occurred.');
322
+ return { error: err.message || 'An error occurred' };
323
+ }
324
+ };
325
+
326
+ const updateUser = async (data: { name?: string; image?: string }) => {
327
+ try {
328
+ const result = await authClient.updateUser(data);
329
+
330
+ if (result.error) {
331
+ showError('Update Failed', result.error.message || 'Could not update profile.');
332
+ return { error: result.error.message || 'Could not update profile' };
333
+ }
334
+
335
+ await authClient.getSession();
336
+ showSuccess('Profile Updated', 'Your profile has been updated.');
337
+ return true;
338
+ } catch (err: any) {
339
+ showError('Update Failed', err.message || 'An error occurred.');
340
+ return { error: err.message || 'An error occurred' };
341
+ }
342
+ };
343
+
344
+ const deleteAccount = async () => {
345
+ try {
346
+ const result = await authClient.deleteUser();
347
+
348
+ if (result.error) {
349
+ showError('Delete Failed', result.error.message || 'Could not delete account.');
350
+ return false;
351
+ }
352
+
353
+ showSuccess('Account Deleted', 'Your account has been permanently deleted.');
354
+
355
+ const redirectTo = authConfig.value?.redirects?.afterLogout || '/auth/login';
356
+ if (import.meta.client) {
357
+ window.location.href = redirectTo;
358
+ } else {
359
+ await navigateTo(redirectTo, { replace: true });
360
+ }
361
+
362
+ return true;
363
+ } catch (err: any) {
364
+ showError('Delete Failed', err.message || 'An error occurred.');
365
+ return false;
366
+ }
367
+ };
368
+
289
369
  const resetState = () => {
290
370
  emailSent.value = false;
291
371
  codeSent.value = false;
@@ -311,6 +391,9 @@ export function useXAuth() {
311
391
  getToken,
312
392
  getAuthHeaders,
313
393
  handleMagicLinkCallback,
394
+ changePassword,
395
+ updateUser,
396
+ deleteAccount,
314
397
  resetState,
315
398
  authClient,
316
399
  };
@@ -10,6 +10,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
10
10
  "/auth/login",
11
11
  "/auth/signup",
12
12
  "/auth/forgot-password",
13
+ "/auth/magic-link",
14
+ "/auth/reset-password",
13
15
  ];
14
16
 
15
17
  const publicRoutes = [
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div>
3
+ <Transition
4
+ enter-active-class="transition-all duration-300 ease-out"
5
+ enter-from-class="opacity-0 translate-y-2"
6
+ enter-to-class="opacity-100 translate-y-0"
7
+ leave-active-class="transition-all duration-200 ease-in"
8
+ leave-from-class="opacity-100 translate-y-0"
9
+ leave-to-class="opacity-0 -translate-y-2"
10
+ mode="out-in"
11
+ >
12
+ <UAuthForm
13
+ v-if="!resetDone"
14
+ key="form"
15
+ title="Set a new password"
16
+ description="Enter your new password below"
17
+ :fields="fields"
18
+ :schema="schema"
19
+ :loading="isSubmitting"
20
+ :submit="{ label: 'Reset password' }"
21
+ @submit="handleReset"
22
+ >
23
+ <template v-if="formError" #footer>
24
+ <div class="p-3 rounded-lg bg-error/10 ring-1 ring-error/20">
25
+ <p class="text-sm text-error">{{ formError }}</p>
26
+ </div>
27
+ </template>
28
+ </UAuthForm>
29
+
30
+ <article v-else key="success" class="text-center space-y-6">
31
+ <figure class="mx-auto flex size-16 items-center justify-center rounded-full bg-success/10 ring-1 ring-success/20">
32
+ <UIcon name="i-lucide-check" class="size-8 text-success" />
33
+ </figure>
34
+
35
+ <header class="space-y-2">
36
+ <h3 class="text-xl font-semibold tracking-tight text-highlighted">
37
+ Password reset
38
+ </h3>
39
+ <p class="text-sm text-muted">
40
+ Your password has been changed successfully.
41
+ </p>
42
+ </header>
43
+
44
+ <UButton
45
+ label="Sign in"
46
+ block
47
+ size="lg"
48
+ @click="goToLogin"
49
+ />
50
+ </article>
51
+ </Transition>
52
+ </div>
53
+ </template>
54
+
55
+ <script setup>
56
+ import { z } from "zod"
57
+
58
+ definePageMeta({
59
+ layout: 'auth'
60
+ })
61
+
62
+ const config = useAppConfig()
63
+ const route = useRoute()
64
+ const router = useRouter()
65
+ const { resetPassword } = useXAuth()
66
+
67
+ const isSubmitting = ref(false)
68
+ const resetDone = ref(false)
69
+ const formError = ref('')
70
+
71
+ const token = computed(() => route.query.token || '')
72
+
73
+ const fields = [
74
+ {
75
+ name: "password",
76
+ type: "password",
77
+ label: "New Password",
78
+ placeholder: "At least 8 characters",
79
+ required: true,
80
+ },
81
+ {
82
+ name: "confirmPassword",
83
+ type: "password",
84
+ label: "Confirm Password",
85
+ placeholder: "Re-enter your password",
86
+ required: true,
87
+ },
88
+ ]
89
+
90
+ const schema = z.object({
91
+ password: z.string().min(8, "Password must be at least 8 characters"),
92
+ confirmPassword: z.string().min(1, "Please confirm your password"),
93
+ }).refine(data => data.password === data.confirmPassword, {
94
+ message: "Passwords do not match",
95
+ path: ["confirmPassword"],
96
+ })
97
+
98
+ async function handleReset(payload) {
99
+ if (!token.value) {
100
+ formError.value = 'Invalid or missing reset token. Please request a new password reset.'
101
+ return
102
+ }
103
+
104
+ isSubmitting.value = true
105
+ formError.value = ''
106
+
107
+ const result = await resetPassword(token.value, payload.data.password)
108
+
109
+ isSubmitting.value = false
110
+
111
+ if (result === true) {
112
+ resetDone.value = true
113
+ } else {
114
+ formError.value = result?.error || 'Failed to reset password. Please try again.'
115
+ }
116
+ }
117
+
118
+ function goToLogin() {
119
+ router.push(config.xAuth?.redirects?.login || '/auth/login')
120
+ }
121
+ </script>
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@xenterprises/nuxt-x-auth-better",
3
3
  "type": "module",
4
- "version": "0.2.6",
4
+ "version": "0.3.1",
5
5
  "description": "BetterAuth authentication layer for Nuxt",
6
- "license": "UNLICENSED",
6
+ "license": "SEE LICENSE IN LICENSE",
7
7
  "author": "X Enterprises",
8
8
  "repository": {
9
9
  "type": "git",