@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 +69 -29
- package/README.md +193 -105
- package/app/components/XAuth/Handler.vue +11 -18
- package/app/components/XAuth/Login.vue +0 -32
- package/app/composables/useXAuth.ts +85 -2
- package/app/middleware/auth.global.ts +2 -0
- package/app/pages/auth/reset-password.vue +121 -0
- package/package.json +2 -2
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
|
|
16
|
+
of X Enterprises. No rights are granted except as expressly set forth in
|
|
17
|
+
this License.
|
|
15
18
|
|
|
16
|
-
2.
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
33
|
-
WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT
|
|
34
|
-
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
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
|
|
39
|
-
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
40
|
-
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
41
|
-
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
42
|
-
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
43
|
-
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
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
|
|
48
|
-
of the State of Washington, United States, without regard to its
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
logout,
|
|
68
|
+
loginWithProvider,
|
|
86
69
|
sendMagicLink,
|
|
87
70
|
forgotPassword,
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
84
|
+
await changePassword('oldpass', 'newpass')
|
|
85
|
+
await updateUser({ name: 'New Name' })
|
|
108
86
|
await logout()
|
|
109
87
|
</script>
|
|
110
88
|
```
|
|
111
89
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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:
|
|
119
|
-
email:
|
|
120
|
-
name:
|
|
121
|
-
avatar
|
|
122
|
-
emailVerified:
|
|
123
|
-
metadata
|
|
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
|
|
144
|
-
| `XAuthOAuthButton` | Single OAuth provider button |
|
|
145
|
-
| `XAuthOAuthButtonGroup` | All configured OAuth buttons |
|
|
146
|
-
| `XAuthHandler` | OAuth
|
|
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 (
|
|
205
|
+
## Pages (auto-registered)
|
|
149
206
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
217
|
+
All other routes are **protected** (require authentication).
|
|
158
218
|
|
|
159
|
-
|
|
219
|
+
## Environment Variables
|
|
160
220
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
225
|
+
The auth path is configurable via `runtimeConfig.public.x.auth.authPath` (default: `/auth`).
|
|
171
226
|
|
|
172
|
-
##
|
|
227
|
+
## Error Reference
|
|
173
228
|
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
132
|
-
//
|
|
133
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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: `${
|
|
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 = `${
|
|
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
|
};
|
|
@@ -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.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "BetterAuth authentication layer for Nuxt",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"author": "X Enterprises",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|