create-nuxt-base 1.0.2 → 1.1.0
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/AUTH.md +289 -0
- package/CHANGELOG.md +18 -0
- package/README.md +138 -3
- package/nuxt-base-template/.env.example +0 -2
- package/nuxt-base-template/README.md +76 -29
- package/nuxt-base-template/app/composables/use-better-auth.ts +175 -8
- package/nuxt-base-template/app/lib/auth-client.ts +166 -78
- package/nuxt-base-template/app/pages/auth/2fa.vue +23 -8
- package/nuxt-base-template/app/pages/auth/login.vue +66 -7
- package/package.json +1 -1
package/AUTH.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Better Auth Integration
|
|
2
|
+
|
|
3
|
+
This document describes the Better Auth integration in the nuxt-base-starter template.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The template uses [Better Auth](https://www.better-auth.com/) for authentication with the following features:
|
|
8
|
+
|
|
9
|
+
| Feature | Status | Description |
|
|
10
|
+
|-----------------------|--------|----------------------------------------|
|
|
11
|
+
| Email & Password | ✅ | Standard email/password authentication |
|
|
12
|
+
| Two-Factor Auth (2FA) | ✅ | TOTP-based 2FA with backup codes |
|
|
13
|
+
| Passkey (WebAuthn) | ✅ | Passwordless authentication |
|
|
14
|
+
| Session Management | ✅ | Cookie-based sessions with SSR support |
|
|
15
|
+
| Password Hashing | ✅ | Client-side SHA256 hashing |
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
21
|
+
│ FRONTEND (Nuxt) │
|
|
22
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
23
|
+
│ │
|
|
24
|
+
│ ┌─────────────────┐ ┌─────────────────┐ │
|
|
25
|
+
│ │ auth-client │───▶│ useBetterAuth │ │
|
|
26
|
+
│ │ (lib/) │ │ (composable) │ │
|
|
27
|
+
│ └────────┬────────┘ └────────┬────────┘ │
|
|
28
|
+
│ │ │ │
|
|
29
|
+
│ │ SHA256 Hashing │ Cookie-based State │
|
|
30
|
+
│ │ Plugin Config │ Session Validation │
|
|
31
|
+
│ │ │ │
|
|
32
|
+
└───────────┼──────────────────────┼──────────────────────────────┘
|
|
33
|
+
│ │
|
|
34
|
+
▼ ▼
|
|
35
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ BACKEND (nest-server) │
|
|
37
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
38
|
+
│ /iam/sign-in/email /iam/session │
|
|
39
|
+
│ /iam/sign-up/email /iam/sign-out │
|
|
40
|
+
│ /iam/passkey/* /iam/two-factor/* │
|
|
41
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Files
|
|
45
|
+
|
|
46
|
+
| File | Purpose |
|
|
47
|
+
|--------------------------------------|----------------------------------|
|
|
48
|
+
| `app/lib/auth-client.ts` | Better Auth client configuration |
|
|
49
|
+
| `app/composables/use-better-auth.ts` | Auth state management composable |
|
|
50
|
+
| `app/pages/auth/login.vue` | Login page |
|
|
51
|
+
| `app/pages/auth/register.vue` | Registration page |
|
|
52
|
+
| `app/pages/auth/2fa.vue` | Two-factor authentication page |
|
|
53
|
+
| `app/pages/auth/forgot-password.vue` | Password reset request |
|
|
54
|
+
| `app/pages/auth/reset-password.vue` | Password reset form |
|
|
55
|
+
| `app/utils/crypto.ts` | SHA256 hashing utility |
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### Basic Authentication
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// In a Vue component
|
|
63
|
+
const { signIn, signUp, signOut, user, isAuthenticated } = useBetterAuth();
|
|
64
|
+
|
|
65
|
+
// Sign in
|
|
66
|
+
const result = await signIn.email({
|
|
67
|
+
email: 'user@example.com',
|
|
68
|
+
password: 'password123',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Sign up
|
|
72
|
+
const result = await signUp.email({
|
|
73
|
+
email: 'user@example.com',
|
|
74
|
+
name: 'John Doe',
|
|
75
|
+
password: 'password123',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Sign out
|
|
79
|
+
await signOut();
|
|
80
|
+
|
|
81
|
+
// Check auth state
|
|
82
|
+
if (isAuthenticated.value) {
|
|
83
|
+
console.log('User:', user.value);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Passkey Authentication
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { authClient } from '~/lib/auth-client';
|
|
91
|
+
|
|
92
|
+
// Sign in with passkey
|
|
93
|
+
const result = await authClient.signIn.passkey();
|
|
94
|
+
|
|
95
|
+
if (result.error) {
|
|
96
|
+
console.error('Passkey login failed:', result.error.message);
|
|
97
|
+
} else {
|
|
98
|
+
// Validate session to get user data (passkey returns session only)
|
|
99
|
+
await validateSession();
|
|
100
|
+
navigateTo('/app');
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Two-Factor Authentication
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { authClient } from '~/lib/auth-client';
|
|
108
|
+
|
|
109
|
+
// Verify TOTP code
|
|
110
|
+
const result = await authClient.twoFactor.verifyTotp({
|
|
111
|
+
code: '123456',
|
|
112
|
+
trustDevice: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Verify backup code
|
|
116
|
+
const result = await authClient.twoFactor.verifyBackupCode({
|
|
117
|
+
code: 'backup-code-here',
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Session Validation
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const { validateSession, user } = useBetterAuth();
|
|
125
|
+
|
|
126
|
+
// On app init, validate the session
|
|
127
|
+
const isValid = await validateSession();
|
|
128
|
+
|
|
129
|
+
if (isValid) {
|
|
130
|
+
console.log('Session valid, user:', user.value);
|
|
131
|
+
} else {
|
|
132
|
+
console.log('No valid session');
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
### Environment Variables
|
|
139
|
+
|
|
140
|
+
```env
|
|
141
|
+
# API URL (required)
|
|
142
|
+
API_URL=http://localhost:3000
|
|
143
|
+
|
|
144
|
+
# Or via Vite
|
|
145
|
+
VITE_API_URL=http://localhost:3000
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Custom Configuration
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { createBetterAuthClient } from '~/lib/auth-client';
|
|
152
|
+
|
|
153
|
+
// Create a custom client
|
|
154
|
+
const customClient = createBetterAuthClient({
|
|
155
|
+
baseURL: 'https://api.example.com',
|
|
156
|
+
basePath: '/auth', // Default: '/iam'
|
|
157
|
+
twoFactorRedirectPath: '/login/2fa', // Default: '/auth/2fa'
|
|
158
|
+
enableAdmin: false,
|
|
159
|
+
enableTwoFactor: true,
|
|
160
|
+
enablePasskey: true,
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Security
|
|
165
|
+
|
|
166
|
+
### Password Hashing
|
|
167
|
+
|
|
168
|
+
Passwords are hashed with SHA256 on the client-side before transmission:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// This happens automatically in auth-client.ts
|
|
172
|
+
const hashedPassword = await sha256(plainPassword);
|
|
173
|
+
// Result: 64-character hex string
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Why client-side hashing?**
|
|
177
|
+
1. Prevents plain text passwords in network logs
|
|
178
|
+
2. Works with nest-server's `normalizePasswordForIam()` which detects SHA256 hashes
|
|
179
|
+
3. Server re-hashes with bcrypt for storage
|
|
180
|
+
|
|
181
|
+
### Cookie-Based Sessions
|
|
182
|
+
|
|
183
|
+
Sessions are stored in cookies for SSR compatibility:
|
|
184
|
+
|
|
185
|
+
| Cookie | Purpose |
|
|
186
|
+
|-----------------------------|----------------------------|
|
|
187
|
+
| `auth-state` | User data (SSR-compatible) |
|
|
188
|
+
| `token` | Session token |
|
|
189
|
+
| `better-auth.session_token` | Better Auth native cookie |
|
|
190
|
+
|
|
191
|
+
### Cross-Origin Requests
|
|
192
|
+
|
|
193
|
+
The client is configured with `credentials: 'include'` for cross-origin cookie handling:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// In auth-client.ts
|
|
197
|
+
fetchOptions: {
|
|
198
|
+
credentials: 'include',
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Backend CORS Configuration:**
|
|
203
|
+
```typescript
|
|
204
|
+
// In nest-server config
|
|
205
|
+
cors: {
|
|
206
|
+
origin: 'http://localhost:3001', // Not '*'
|
|
207
|
+
credentials: true,
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Better Auth Endpoints
|
|
212
|
+
|
|
213
|
+
The following endpoints are provided by the nest-server backend:
|
|
214
|
+
|
|
215
|
+
### Authentication
|
|
216
|
+
|
|
217
|
+
| Endpoint | Method | Description |
|
|
218
|
+
|----------------------|--------|-----------------------------|
|
|
219
|
+
| `/iam/sign-in/email` | POST | Email/password sign in |
|
|
220
|
+
| `/iam/sign-up/email` | POST | Email/password registration |
|
|
221
|
+
| `/iam/sign-out` | POST | Sign out |
|
|
222
|
+
| `/iam/session` | GET | Get current session |
|
|
223
|
+
|
|
224
|
+
### Passkey (WebAuthn)
|
|
225
|
+
|
|
226
|
+
| Endpoint | Method | Description |
|
|
227
|
+
|----------------------------------------------|--------|--------------------------|
|
|
228
|
+
| `/iam/passkey/generate-register-options` | GET | Get registration options |
|
|
229
|
+
| `/iam/passkey/verify-registration` | POST | Verify registration |
|
|
230
|
+
| `/iam/passkey/generate-authenticate-options` | GET | Get auth options |
|
|
231
|
+
| `/iam/passkey/verify-authentication` | POST | Verify authentication |
|
|
232
|
+
| `/iam/passkey/list-user-passkeys` | GET | List user's passkeys |
|
|
233
|
+
| `/iam/passkey/delete-passkey` | POST | Delete a passkey |
|
|
234
|
+
|
|
235
|
+
### Two-Factor Authentication
|
|
236
|
+
|
|
237
|
+
| Endpoint | Method | Description |
|
|
238
|
+
|--------------------------------------|--------|--------------------|
|
|
239
|
+
| `/iam/two-factor/enable` | POST | Enable 2FA |
|
|
240
|
+
| `/iam/two-factor/disable` | POST | Disable 2FA |
|
|
241
|
+
| `/iam/two-factor/verify-totp` | POST | Verify TOTP code |
|
|
242
|
+
| `/iam/two-factor/verify-backup-code` | POST | Verify backup code |
|
|
243
|
+
|
|
244
|
+
## Troubleshooting
|
|
245
|
+
|
|
246
|
+
### "Passkey not found" Error
|
|
247
|
+
|
|
248
|
+
1. Ensure the user has registered a passkey first
|
|
249
|
+
2. Check that cookies are being sent (`credentials: 'include'`)
|
|
250
|
+
3. Verify CORS is configured correctly on the backend
|
|
251
|
+
|
|
252
|
+
### 2FA Redirect Not Working
|
|
253
|
+
|
|
254
|
+
Ensure the 2FA redirect is handled in the login page:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Check for 2FA redirect in login response
|
|
258
|
+
if (result.data?.twoFactorRedirect) {
|
|
259
|
+
await navigateTo('/auth/2fa');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Session Not Persisting After Passkey Login
|
|
265
|
+
|
|
266
|
+
The passkey response only contains the session, not the user. Call `validateSession()`:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
if (result.data?.session) {
|
|
270
|
+
await validateSession(); // Fetches user data
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Form Not Submitting (Nuxt UI)
|
|
275
|
+
|
|
276
|
+
Ensure UForm has the `:state` binding:
|
|
277
|
+
|
|
278
|
+
```vue
|
|
279
|
+
<UForm :schema="schema" :state="formState" @submit="onSubmit">
|
|
280
|
+
<UInput v-model="formState.field" />
|
|
281
|
+
</UForm>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## References
|
|
285
|
+
|
|
286
|
+
- [Better Auth Documentation](https://www.better-auth.com/docs)
|
|
287
|
+
- [Better Auth Passkey Plugin](https://www.better-auth.com/docs/plugins/passkey)
|
|
288
|
+
- [Better Auth Two-Factor Plugin](https://www.better-auth.com/docs/plugins/two-factor)
|
|
289
|
+
- [nest-server Better Auth Integration](https://github.com/lenneTech/nest-server)
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [1.1.0](https://github.com/lenneTech/nuxt-base-starter/compare/v1.0.3...v1.1.0) (2026-01-20)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add complete Better-Auth integration with Passkey support and comprehensive documentation ([e0d470c](https://github.com/lenneTech/nuxt-base-starter/commit/e0d470c8229c37bed2948d929676620f344f4878))
|
|
11
|
+
|
|
12
|
+
## [1.2.0](https://github.com/lenneTech/nuxt-base-starter/compare/v1.0.3...v1.2.0) (2026-01-20)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* add complete Better-Auth integration with Passkey support and comprehensive documentation ([70fbec1](https://github.com/lenneTech/nuxt-base-starter/commit/70fbec14e38673c5185195fe05f0cd82bf72a800))
|
|
18
|
+
|
|
19
|
+
## [1.1.0](https://github.com/lenneTech/nuxt-base-starter/compare/v1.0.3...v1.1.0) (2026-01-20)
|
|
20
|
+
|
|
21
|
+
### [1.0.3](https://github.com/lenneTech/nuxt-base-starter/compare/v1.0.2...v1.0.3) (2026-01-12)
|
|
22
|
+
|
|
5
23
|
### [1.0.2](https://github.com/lenneTech/nuxt-base-starter/compare/v1.0.1...v1.0.2) (2026-01-12)
|
|
6
24
|
|
|
7
25
|
|
package/README.md
CHANGED
|
@@ -1,9 +1,144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-nuxt-base
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A CLI tool to scaffold a production-ready **Nuxt 4** application with TypeScript, Tailwind CSS v4, NuxtUI v4, and modern tooling.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx create-nuxt-base my-awesome-project
|
|
9
|
+
cd my-awesome-project
|
|
10
|
+
npm run dev
|
|
9
11
|
```
|
|
12
|
+
|
|
13
|
+
The development server starts at **http://localhost:3001**
|
|
14
|
+
|
|
15
|
+
## What's Included
|
|
16
|
+
|
|
17
|
+
### Core Framework
|
|
18
|
+
|
|
19
|
+
| Technology | Version | Description |
|
|
20
|
+
|--------------|---------|---------------------------------------|
|
|
21
|
+
| Nuxt | 4.x | Vue 3 meta-framework with SSR support |
|
|
22
|
+
| TypeScript | 5.9.x | Strict type checking enabled |
|
|
23
|
+
| Tailwind CSS | 4.x | Utility-first CSS with Vite plugin |
|
|
24
|
+
| NuxtUI | 4.x | Component library with dark mode |
|
|
25
|
+
|
|
26
|
+
### Authentication (Better Auth)
|
|
27
|
+
|
|
28
|
+
Complete authentication system using [Better Auth](https://www.better-auth.com/):
|
|
29
|
+
|
|
30
|
+
| Feature | Description |
|
|
31
|
+
|--------------------|-------------------------------------------------------|
|
|
32
|
+
| Email/Password | Standard auth with client-side SHA256 hashing |
|
|
33
|
+
| Two-Factor (2FA) | TOTP-based 2FA with backup codes |
|
|
34
|
+
| Passkey/WebAuthn | Passwordless authentication (Touch ID, Face ID, etc.) |
|
|
35
|
+
| Password Reset | Email-based password reset flow |
|
|
36
|
+
| Session Management | SSR-compatible cookie-based sessions |
|
|
37
|
+
|
|
38
|
+
Pre-built auth pages: login, register, forgot-password, reset-password, 2fa
|
|
39
|
+
|
|
40
|
+
📖 **See [AUTH.md](./AUTH.md) for detailed documentation**
|
|
41
|
+
|
|
42
|
+
### State & Data
|
|
43
|
+
|
|
44
|
+
| Package | Purpose |
|
|
45
|
+
|-----------------------|-----------------------------|
|
|
46
|
+
| Pinia | State management |
|
|
47
|
+
| VueUse | Vue composition utilities |
|
|
48
|
+
| @hey-api/client-fetch | Type-safe API client |
|
|
49
|
+
| Valibot | Schema validation for forms |
|
|
50
|
+
|
|
51
|
+
### SEO & Analytics
|
|
52
|
+
|
|
53
|
+
- **@nuxtjs/seo** - Sitemap, robots.txt, OG images
|
|
54
|
+
- **@nuxtjs/plausible** - Privacy-friendly analytics
|
|
55
|
+
- **@nuxt/image** - Image optimization with IPX
|
|
56
|
+
|
|
57
|
+
### Developer Experience
|
|
58
|
+
|
|
59
|
+
| Tool | Purpose |
|
|
60
|
+
|--------------------|------------------------------------|
|
|
61
|
+
| OxLint | Fast linting |
|
|
62
|
+
| OxFmt | Code formatting |
|
|
63
|
+
| Playwright | E2E testing |
|
|
64
|
+
| @lenne.tech/bug.lt | Bug reporting to Linear (dev only) |
|
|
65
|
+
| dayjs-nuxt | Date/time handling |
|
|
66
|
+
|
|
67
|
+
### File Upload
|
|
68
|
+
|
|
69
|
+
- TUS resumable upload support (`tus-js-client`)
|
|
70
|
+
- Pre-built `TusFileUpload.vue` component
|
|
71
|
+
|
|
72
|
+
### Docker Support
|
|
73
|
+
|
|
74
|
+
- `Dockerfile.dev` for containerized development
|
|
75
|
+
- Hot reload enabled
|
|
76
|
+
|
|
77
|
+
## Project Structure
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
my-project/
|
|
81
|
+
├── app/
|
|
82
|
+
│ ├── assets/css/ # Tailwind CSS
|
|
83
|
+
│ ├── components/ # Vue components (auto-imported)
|
|
84
|
+
│ │ ├── Modal/ # Modal components
|
|
85
|
+
│ │ ├── Transition/ # Transition components
|
|
86
|
+
│ │ └── Upload/ # File upload components
|
|
87
|
+
│ ├── composables/ # Composables (auto-imported)
|
|
88
|
+
│ ├── interfaces/ # TypeScript interfaces
|
|
89
|
+
│ ├── layouts/ # Nuxt layouts
|
|
90
|
+
│ ├── lib/ # Auth client configuration
|
|
91
|
+
│ ├── middleware/ # Route middleware (auth, admin, guest)
|
|
92
|
+
│ ├── pages/ # File-based routing
|
|
93
|
+
│ │ ├── auth/ # Authentication pages
|
|
94
|
+
│ │ └── app/ # Protected app pages
|
|
95
|
+
│ ├── utils/ # Utility functions
|
|
96
|
+
│ └── app.config.ts # NuxtUI configuration
|
|
97
|
+
├── docs/ # Dev-only documentation layer
|
|
98
|
+
├── tests/ # Playwright E2E tests
|
|
99
|
+
├── nuxt.config.ts # Nuxt configuration
|
|
100
|
+
├── openapi-ts.config.ts # API type generation config
|
|
101
|
+
└── playwright.config.ts # E2E test configuration
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Available Scripts
|
|
105
|
+
|
|
106
|
+
| Script | Description |
|
|
107
|
+
|--------------------------|----------------------------------------|
|
|
108
|
+
| `npm run dev` | Start development server |
|
|
109
|
+
| `npm run build` | Build for production |
|
|
110
|
+
| `npm run preview` | Preview production build |
|
|
111
|
+
| `npm run generate-types` | Generate TypeScript types from OpenAPI |
|
|
112
|
+
| `npm run test` | Run Playwright E2E tests |
|
|
113
|
+
| `npm run lint` | Run OxLint |
|
|
114
|
+
| `npm run format` | Run OxFmt |
|
|
115
|
+
| `npm run check` | Run lint + format check |
|
|
116
|
+
| `npm run fix` | Auto-fix lint + format issues |
|
|
117
|
+
|
|
118
|
+
## Environment Variables
|
|
119
|
+
|
|
120
|
+
Create a `.env` file based on `.env.example`:
|
|
121
|
+
|
|
122
|
+
```env
|
|
123
|
+
# Required
|
|
124
|
+
SITE_URL=http://localhost:3001
|
|
125
|
+
API_URL=http://localhost:3000
|
|
126
|
+
APP_ENV=development
|
|
127
|
+
NODE_ENV=development
|
|
128
|
+
|
|
129
|
+
# Optional
|
|
130
|
+
WEB_PUSH_KEY= # Web push notifications
|
|
131
|
+
LINEAR_API_KEY= # Bug reporting
|
|
132
|
+
LINEAR_TEAM_NAME= # Bug reporting
|
|
133
|
+
LINEAR_PROJECT_NAME= # Bug reporting
|
|
134
|
+
STORAGE_PREFIX=base-dev # Local storage prefix
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Requirements
|
|
138
|
+
|
|
139
|
+
- Node.js >= 22
|
|
140
|
+
- npm >= 10
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nuxt Base Template
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A production-ready Nuxt 4 SSR starter with TypeScript, Tailwind CSS v4, NuxtUI v4, and Better Auth.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
@@ -33,6 +33,13 @@ Start the development server on http://localhost:3001
|
|
|
33
33
|
npm run dev
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
### Docker Development
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
docker build -f Dockerfile.dev -t nuxt-app-dev .
|
|
40
|
+
docker run -p 3001:3001 -v $(pwd):/app nuxt-app-dev
|
|
41
|
+
```
|
|
42
|
+
|
|
36
43
|
## Production
|
|
37
44
|
|
|
38
45
|
Build the application for production:
|
|
@@ -62,8 +69,8 @@ Run linting and formatting checks before committing:
|
|
|
62
69
|
```bash
|
|
63
70
|
npm run check # Run lint + format check
|
|
64
71
|
npm run fix # Auto-fix lint + format issues
|
|
65
|
-
npm run lint #
|
|
66
|
-
npm run format #
|
|
72
|
+
npm run lint # OxLint only
|
|
73
|
+
npm run format # OxFmt format only
|
|
67
74
|
```
|
|
68
75
|
|
|
69
76
|
## Testing
|
|
@@ -84,33 +91,64 @@ npm run generate-types
|
|
|
84
91
|
|
|
85
92
|
## Tech Stack
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
| Technology | Version | Description |
|
|
95
|
+
|------------|---------|-------------|
|
|
96
|
+
| Nuxt | 4.2.x | Vue 3 meta-framework with SSR |
|
|
97
|
+
| TypeScript | 5.9.x | Strict type checking |
|
|
98
|
+
| Tailwind CSS | 4.1.x | Utility-first CSS (Vite plugin) |
|
|
99
|
+
| NuxtUI | 4.3.x | Component library with dark mode |
|
|
100
|
+
| Pinia | 0.11.x | State management |
|
|
101
|
+
| Better Auth | 1.4.x | Authentication framework |
|
|
102
|
+
| Playwright | 1.57.x | E2E testing |
|
|
103
|
+
| @hey-api/client-fetch | 0.13.x | Type-safe API client |
|
|
104
|
+
| Valibot | 1.2.x | Schema validation |
|
|
95
105
|
|
|
96
106
|
## Key Features
|
|
97
107
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
### Authentication (Better Auth)
|
|
109
|
+
|
|
110
|
+
- Email/password authentication with client-side SHA256 password hashing
|
|
111
|
+
- Two-factor authentication (2FA/TOTP) with backup codes
|
|
112
|
+
- Passkey/WebAuthn support
|
|
113
|
+
- Password reset flow
|
|
114
|
+
- Pre-built pages: login, register, forgot-password, reset-password, 2fa
|
|
115
|
+
- Route middleware: `auth.global.ts`, `admin.global.ts`, `guest.global.ts`
|
|
116
|
+
|
|
117
|
+
### UI & Styling
|
|
118
|
+
|
|
119
|
+
- NuxtUI v4 component library
|
|
120
|
+
- Dark/light mode support
|
|
121
|
+
- Transition components (Fade, Slide, FadeScale)
|
|
122
|
+
- Modal components with `useOverlay` pattern
|
|
123
|
+
|
|
124
|
+
### SEO & Analytics
|
|
125
|
+
|
|
126
|
+
- Sitemap generation (`@nuxtjs/seo`)
|
|
127
|
+
- robots.txt configuration
|
|
128
|
+
- OG image generation
|
|
129
|
+
- Plausible Analytics integration
|
|
130
|
+
|
|
131
|
+
### File Upload
|
|
132
|
+
|
|
133
|
+
- TUS resumable uploads (`tus-js-client`)
|
|
134
|
+
- Pre-built `TusFileUpload.vue` component
|
|
135
|
+
- Progress tracking and error handling
|
|
136
|
+
|
|
137
|
+
### Developer Experience
|
|
138
|
+
|
|
139
|
+
- OxLint for fast linting
|
|
140
|
+
- OxFmt for code formatting
|
|
141
|
+
- Auto-generated API client from OpenAPI
|
|
142
|
+
- Bug reporting to Linear (dev only via `@lenne.tech/bug.lt`)
|
|
143
|
+
- VueUse composition utilities
|
|
144
|
+
- dayjs for date/time handling
|
|
108
145
|
|
|
109
146
|
## Environment Variables
|
|
110
147
|
|
|
111
148
|
Create a `.env` file with the following variables:
|
|
112
149
|
|
|
113
150
|
```env
|
|
151
|
+
# Required
|
|
114
152
|
SITE_URL=http://localhost:3001
|
|
115
153
|
API_URL=http://localhost:3000
|
|
116
154
|
APP_ENV=development
|
|
@@ -124,7 +162,7 @@ WEB_PUSH_KEY= # Web push notifications
|
|
|
124
162
|
LINEAR_API_KEY= # Bug reporting
|
|
125
163
|
LINEAR_TEAM_NAME= # Bug reporting
|
|
126
164
|
LINEAR_PROJECT_NAME= # Bug reporting
|
|
127
|
-
API_SCHEMA=../api/schema.gql #
|
|
165
|
+
API_SCHEMA=../api/schema.gql # OpenAPI schema path
|
|
128
166
|
STORAGE_PREFIX=base-dev # Local storage prefix
|
|
129
167
|
```
|
|
130
168
|
|
|
@@ -132,25 +170,34 @@ STORAGE_PREFIX=base-dev # Local storage prefix
|
|
|
132
170
|
|
|
133
171
|
```
|
|
134
172
|
app/
|
|
135
|
-
├── assets/
|
|
173
|
+
├── assets/css/ # Tailwind CSS styles
|
|
136
174
|
├── components/ # Vue components (auto-imported)
|
|
175
|
+
│ ├── Modal/ # Modal components
|
|
176
|
+
│ ├── Transition/ # Transition animations
|
|
177
|
+
│ └── Upload/ # File upload components
|
|
137
178
|
├── composables/ # Composables (auto-imported)
|
|
138
|
-
├──
|
|
139
|
-
├──
|
|
179
|
+
│ ├── use-better-auth.ts # Auth session helpers
|
|
180
|
+
│ ├── use-file.ts # File utilities
|
|
181
|
+
│ ├── use-share.ts # Share API
|
|
182
|
+
│ └── use-tus-upload.ts # TUS upload logic
|
|
183
|
+
├── interfaces/ # TypeScript interfaces
|
|
184
|
+
├── layouts/ # Nuxt layouts (default, slim)
|
|
185
|
+
├── lib/ # Auth client configuration
|
|
186
|
+
├── middleware/ # Route guards (auth, admin, guest)
|
|
140
187
|
├── pages/ # File-based routing
|
|
188
|
+
│ ├── auth/ # Authentication pages
|
|
189
|
+
│ └── app/ # Protected app pages
|
|
190
|
+
├── utils/ # Utility functions
|
|
141
191
|
└── app.config.ts # NuxtUI configuration
|
|
142
192
|
|
|
143
193
|
docs/ # Dev-only documentation layer
|
|
144
194
|
tests/ # Playwright E2E tests
|
|
145
195
|
```
|
|
146
196
|
|
|
147
|
-
## Development Guidelines
|
|
148
|
-
|
|
149
|
-
For detailed coding standards and architecture information, see [CLAUDE.md](./CLAUDE.md).
|
|
150
|
-
|
|
151
197
|
## Documentation
|
|
152
198
|
|
|
153
199
|
- [Nuxt Documentation](https://nuxt.com/docs)
|
|
154
200
|
- [NuxtUI Documentation](https://ui.nuxt.com)
|
|
201
|
+
- [Better Auth Documentation](https://www.better-auth.com)
|
|
155
202
|
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
|
156
203
|
- [Vue 3 Documentation](https://vuejs.org)
|
|
@@ -1,25 +1,192 @@
|
|
|
1
1
|
import { authClient } from '~/lib/auth-client';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* User type for Better Auth session
|
|
5
|
+
*/
|
|
6
|
+
interface BetterAuthUser {
|
|
7
|
+
email: string;
|
|
8
|
+
emailVerified?: boolean;
|
|
9
|
+
id: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
role?: string;
|
|
12
|
+
twoFactorEnabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Stored auth state (persisted in cookie for SSR compatibility)
|
|
17
|
+
*/
|
|
18
|
+
interface StoredAuthState {
|
|
19
|
+
user: BetterAuthUser | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Better Auth composable with client-side state management
|
|
24
|
+
*
|
|
25
|
+
* This composable manages auth state using:
|
|
26
|
+
* 1. Client-side state stored in a cookie (for SSR compatibility)
|
|
27
|
+
* 2. Better Auth's session endpoint as a validation check
|
|
28
|
+
*
|
|
29
|
+
* The state is populated after login and cleared on logout.
|
|
30
|
+
*/
|
|
3
31
|
export function useBetterAuth() {
|
|
4
|
-
|
|
32
|
+
// Use useCookie for SSR-compatible persistent state
|
|
33
|
+
const authState = useCookie<StoredAuthState>('auth-state', {
|
|
34
|
+
default: () => ({ user: null }),
|
|
35
|
+
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
36
|
+
sameSite: 'lax',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Loading state
|
|
40
|
+
const isLoading = ref<boolean>(false);
|
|
5
41
|
|
|
6
|
-
|
|
42
|
+
// Computed properties based on stored state
|
|
43
|
+
const user = computed<BetterAuthUser | null>(() => authState.value?.user ?? null);
|
|
7
44
|
const isAuthenticated = computed<boolean>(() => !!user.value);
|
|
8
45
|
const isAdmin = computed<boolean>(() => user.value?.role === 'admin');
|
|
9
46
|
const is2FAEnabled = computed<boolean>(() => user.value?.twoFactorEnabled ?? false);
|
|
10
|
-
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set user data after successful login/signup
|
|
50
|
+
*/
|
|
51
|
+
function setUser(userData: BetterAuthUser | null): void {
|
|
52
|
+
authState.value = { user: userData };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Clear user data on logout
|
|
57
|
+
*/
|
|
58
|
+
function clearUser(): void {
|
|
59
|
+
authState.value = { user: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate session with backend (called on app init)
|
|
64
|
+
* If session is invalid, clear the stored state
|
|
65
|
+
*/
|
|
66
|
+
async function validateSession(): Promise<boolean> {
|
|
67
|
+
try {
|
|
68
|
+
// Try to get session from Better Auth
|
|
69
|
+
const session = authClient.useSession();
|
|
70
|
+
|
|
71
|
+
// Wait for session to load
|
|
72
|
+
if (session.value.isPending) {
|
|
73
|
+
await new Promise((resolve) => {
|
|
74
|
+
const unwatch = watch(
|
|
75
|
+
() => session.value.isPending,
|
|
76
|
+
(isPending) => {
|
|
77
|
+
if (!isPending) {
|
|
78
|
+
unwatch();
|
|
79
|
+
resolve(true);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{ immediate: true },
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If session has user data, update our state
|
|
88
|
+
if (session.value.data?.user) {
|
|
89
|
+
setUser(session.value.data.user as BetterAuthUser);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Session not found - check if we have a stored token cookie
|
|
94
|
+
// If we have auth-state but no session, it might be a mismatch
|
|
95
|
+
// For now, trust the stored state if token cookie exists
|
|
96
|
+
const tokenCookie = useCookie('token');
|
|
97
|
+
if (tokenCookie.value && authState.value?.user) {
|
|
98
|
+
// We have both token and stored user - trust it
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// No valid session found - clear state
|
|
103
|
+
if (authState.value?.user) {
|
|
104
|
+
clearUser();
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.debug('Session validation failed:', error);
|
|
109
|
+
return !!authState.value?.user;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Sign in with email and password
|
|
115
|
+
*/
|
|
116
|
+
const signIn = {
|
|
117
|
+
...authClient.signIn,
|
|
118
|
+
email: async (params: { email: string; password: string; rememberMe?: boolean }, options?: any) => {
|
|
119
|
+
isLoading.value = true;
|
|
120
|
+
try {
|
|
121
|
+
const result = await authClient.signIn.email(params, options);
|
|
122
|
+
|
|
123
|
+
// Check for successful response with user data
|
|
124
|
+
if (result && 'user' in result && result.user) {
|
|
125
|
+
setUser(result.user as BetterAuthUser);
|
|
126
|
+
} else if (result && 'data' in result && result.data?.user) {
|
|
127
|
+
setUser(result.data.user as BetterAuthUser);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
} finally {
|
|
132
|
+
isLoading.value = false;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Sign up with email and password
|
|
139
|
+
*/
|
|
140
|
+
const signUp = {
|
|
141
|
+
...authClient.signUp,
|
|
142
|
+
email: async (params: { email: string; name: string; password: string }, options?: any) => {
|
|
143
|
+
isLoading.value = true;
|
|
144
|
+
try {
|
|
145
|
+
const result = await authClient.signUp.email(params, options);
|
|
146
|
+
|
|
147
|
+
// Check for successful response with user data
|
|
148
|
+
if (result && 'user' in result && result.user) {
|
|
149
|
+
setUser(result.user as BetterAuthUser);
|
|
150
|
+
} else if (result && 'data' in result && result.data?.user) {
|
|
151
|
+
setUser(result.data.user as BetterAuthUser);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
} finally {
|
|
156
|
+
isLoading.value = false;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Sign out
|
|
163
|
+
*/
|
|
164
|
+
const signOut = async (options?: any) => {
|
|
165
|
+
isLoading.value = true;
|
|
166
|
+
try {
|
|
167
|
+
const result = await authClient.signOut(options);
|
|
168
|
+
// Clear user data on logout
|
|
169
|
+
clearUser();
|
|
170
|
+
return result;
|
|
171
|
+
} finally {
|
|
172
|
+
isLoading.value = false;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
11
175
|
|
|
12
176
|
return {
|
|
177
|
+
changePassword: authClient.changePassword,
|
|
178
|
+
clearUser,
|
|
13
179
|
is2FAEnabled,
|
|
14
180
|
isAdmin,
|
|
15
181
|
isAuthenticated,
|
|
16
|
-
isLoading,
|
|
182
|
+
isLoading: computed(() => isLoading.value),
|
|
17
183
|
passkey: authClient.passkey,
|
|
18
|
-
|
|
19
|
-
signIn
|
|
20
|
-
signOut
|
|
21
|
-
signUp
|
|
184
|
+
setUser,
|
|
185
|
+
signIn,
|
|
186
|
+
signOut,
|
|
187
|
+
signUp,
|
|
22
188
|
twoFactor: authClient.twoFactor,
|
|
23
189
|
user,
|
|
190
|
+
validateSession,
|
|
24
191
|
};
|
|
25
192
|
}
|
|
@@ -34,102 +34,190 @@ export interface AuthResponse {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
baseURL
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Configuration options for the auth client factory
|
|
39
|
+
* All options have sensible defaults for nest-server compatibility
|
|
40
|
+
*/
|
|
41
|
+
export interface AuthClientConfig {
|
|
42
|
+
/** API base URL (default: from env or http://localhost:3000) */
|
|
43
|
+
baseURL?: string;
|
|
44
|
+
/** Auth API base path (default: '/iam' - must match nest-server betterAuth.basePath) */
|
|
45
|
+
basePath?: string;
|
|
46
|
+
/** 2FA redirect path (default: '/auth/2fa') */
|
|
47
|
+
twoFactorRedirectPath?: string;
|
|
48
|
+
/** Enable admin plugin (default: true) */
|
|
49
|
+
enableAdmin?: boolean;
|
|
50
|
+
/** Enable 2FA plugin (default: true) */
|
|
51
|
+
enableTwoFactor?: boolean;
|
|
52
|
+
/** Enable passkey plugin (default: true) */
|
|
53
|
+
enablePasskey?: boolean;
|
|
54
|
+
}
|
|
54
55
|
|
|
55
56
|
// =============================================================================
|
|
56
|
-
// Auth Client
|
|
57
|
+
// Auth Client Factory
|
|
57
58
|
// =============================================================================
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
*
|
|
61
|
+
* Creates a configured Better-Auth client with password hashing
|
|
62
|
+
*
|
|
63
|
+
* This factory function allows creating auth clients with custom configuration,
|
|
64
|
+
* making it reusable across different projects.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Default configuration (works with nest-server defaults)
|
|
69
|
+
* const authClient = createBetterAuthClient();
|
|
70
|
+
*
|
|
71
|
+
* // Custom configuration
|
|
72
|
+
* const authClient = createBetterAuthClient({
|
|
73
|
+
* baseURL: 'https://api.example.com',
|
|
74
|
+
* basePath: '/auth',
|
|
75
|
+
* twoFactorRedirectPath: '/login/2fa',
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
61
78
|
*
|
|
62
79
|
* SECURITY: Passwords are hashed with SHA256 client-side to prevent
|
|
63
80
|
* plain text password transmission over the network.
|
|
64
|
-
*
|
|
65
|
-
* The server's normalizePasswordForIam() detects SHA256 hashes (64 hex chars)
|
|
66
|
-
* and processes them correctly.
|
|
67
81
|
*/
|
|
68
|
-
export
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
export function createBetterAuthClient(config: AuthClientConfig = {}) {
|
|
83
|
+
const {
|
|
84
|
+
baseURL = import.meta.env?.VITE_API_URL || process.env.API_URL || 'http://localhost:3000',
|
|
85
|
+
basePath = '/iam',
|
|
86
|
+
twoFactorRedirectPath = '/auth/2fa',
|
|
87
|
+
enableAdmin = true,
|
|
88
|
+
enableTwoFactor = true,
|
|
89
|
+
enablePasskey = true,
|
|
90
|
+
} = config;
|
|
91
|
+
|
|
92
|
+
// Build plugins array based on configuration
|
|
93
|
+
const plugins: any[] = [];
|
|
94
|
+
|
|
95
|
+
if (enableAdmin) {
|
|
96
|
+
plugins.push(adminClient());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (enableTwoFactor) {
|
|
100
|
+
plugins.push(
|
|
101
|
+
twoFactorClient({
|
|
102
|
+
onTwoFactorRedirect() {
|
|
103
|
+
navigateTo(twoFactorRedirectPath);
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (enablePasskey) {
|
|
110
|
+
plugins.push(passkeyClient());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create base client with configuration
|
|
114
|
+
const baseClient = createAuthClient({
|
|
115
|
+
basePath,
|
|
116
|
+
baseURL,
|
|
117
|
+
fetchOptions: {
|
|
118
|
+
credentials: 'include', // Required for cross-origin cookie handling
|
|
97
119
|
},
|
|
98
|
-
|
|
120
|
+
plugins,
|
|
121
|
+
});
|
|
99
122
|
|
|
100
|
-
//
|
|
101
|
-
|
|
123
|
+
// Return extended client with password hashing
|
|
124
|
+
return {
|
|
125
|
+
// Spread all base client properties and methods
|
|
126
|
+
...baseClient,
|
|
127
|
+
|
|
128
|
+
// Explicitly pass through methods not captured by spread operator
|
|
129
|
+
useSession: baseClient.useSession,
|
|
130
|
+
passkey: (baseClient as any).passkey,
|
|
131
|
+
admin: (baseClient as any).admin,
|
|
132
|
+
$Infer: baseClient.$Infer,
|
|
133
|
+
$fetch: baseClient.$fetch,
|
|
134
|
+
$store: baseClient.$store,
|
|
102
135
|
|
|
103
|
-
// Override signUp to hash password
|
|
104
|
-
signUp: {
|
|
105
|
-
...baseClient.signUp,
|
|
106
136
|
/**
|
|
107
|
-
*
|
|
137
|
+
* Change password for an authenticated user (both passwords are hashed)
|
|
108
138
|
*/
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
return baseClient.
|
|
139
|
+
changePassword: async (params: { currentPassword: string; newPassword: string }, options?: any) => {
|
|
140
|
+
const [hashedCurrent, hashedNew] = await Promise.all([sha256(params.currentPassword), sha256(params.newPassword)]);
|
|
141
|
+
return baseClient.changePassword?.({ currentPassword: hashedCurrent, newPassword: hashedNew }, options);
|
|
112
142
|
},
|
|
113
|
-
},
|
|
114
143
|
|
|
115
|
-
// Override twoFactor to hash passwords
|
|
116
|
-
twoFactor: {
|
|
117
|
-
...baseClient.twoFactor,
|
|
118
144
|
/**
|
|
119
|
-
*
|
|
145
|
+
* Reset password with token (new password is hashed before sending)
|
|
120
146
|
*/
|
|
121
|
-
|
|
122
|
-
const hashedPassword = await sha256(params.
|
|
123
|
-
return baseClient.
|
|
147
|
+
resetPassword: async (params: { newPassword: string; token: string }, options?: any) => {
|
|
148
|
+
const hashedPassword = await sha256(params.newPassword);
|
|
149
|
+
return baseClient.resetPassword?.({ newPassword: hashedPassword, token: params.token }, options);
|
|
124
150
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
|
|
152
|
+
// Override signIn to hash password (keep passkey method from plugin)
|
|
153
|
+
signIn: {
|
|
154
|
+
...baseClient.signIn,
|
|
155
|
+
/**
|
|
156
|
+
* Sign in with email and password (password is hashed before sending)
|
|
157
|
+
*/
|
|
158
|
+
email: async (params: { email: string; password: string; rememberMe?: boolean }, options?: any) => {
|
|
159
|
+
const hashedPassword = await sha256(params.password);
|
|
160
|
+
return baseClient.signIn.email({ ...params, password: hashedPassword }, options);
|
|
161
|
+
},
|
|
162
|
+
/**
|
|
163
|
+
* Sign in with passkey (pass through to base client - provided by passkeyClient plugin)
|
|
164
|
+
* @see https://www.better-auth.com/docs/plugins/passkey
|
|
165
|
+
*/
|
|
166
|
+
passkey: (baseClient.signIn as any).passkey,
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// Explicitly pass through signOut (not captured by spread operator)
|
|
170
|
+
signOut: baseClient.signOut,
|
|
171
|
+
|
|
172
|
+
// Override signUp to hash password
|
|
173
|
+
signUp: {
|
|
174
|
+
...baseClient.signUp,
|
|
175
|
+
/**
|
|
176
|
+
* Sign up with email and password (password is hashed before sending)
|
|
177
|
+
*/
|
|
178
|
+
email: async (params: { email: string; name: string; password: string }, options?: any) => {
|
|
179
|
+
const hashedPassword = await sha256(params.password);
|
|
180
|
+
return baseClient.signUp.email({ ...params, password: hashedPassword }, options);
|
|
181
|
+
},
|
|
131
182
|
},
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
183
|
|
|
135
|
-
|
|
184
|
+
// Override twoFactor to hash passwords (provided by twoFactorClient plugin)
|
|
185
|
+
twoFactor: {
|
|
186
|
+
...(baseClient as any).twoFactor,
|
|
187
|
+
/**
|
|
188
|
+
* Disable 2FA (password is hashed before sending)
|
|
189
|
+
*/
|
|
190
|
+
disable: async (params: { password: string }, options?: any) => {
|
|
191
|
+
const hashedPassword = await sha256(params.password);
|
|
192
|
+
return (baseClient as any).twoFactor.disable({ password: hashedPassword }, options);
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Enable 2FA (password is hashed before sending)
|
|
196
|
+
*/
|
|
197
|
+
enable: async (params: { password: string }, options?: any) => {
|
|
198
|
+
const hashedPassword = await sha256(params.password);
|
|
199
|
+
return (baseClient as any).twoFactor.enable({ password: hashedPassword }, options);
|
|
200
|
+
},
|
|
201
|
+
/**
|
|
202
|
+
* Verify TOTP code (pass through to base client)
|
|
203
|
+
*/
|
|
204
|
+
verifyTotp: (baseClient as any).twoFactor.verifyTotp,
|
|
205
|
+
/**
|
|
206
|
+
* Verify backup code (pass through to base client)
|
|
207
|
+
*/
|
|
208
|
+
verifyBackupCode: (baseClient as any).twoFactor.verifyBackupCode,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// Default Auth Client Instance
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Default auth client instance with standard nest-server configuration
|
|
219
|
+
* Use createBetterAuthClient() for custom configuration
|
|
220
|
+
*/
|
|
221
|
+
export const authClient = createBetterAuthClient();
|
|
222
|
+
|
|
223
|
+
export type AuthClient = ReturnType<typeof createBetterAuthClient>;
|
|
@@ -13,6 +13,7 @@ import { authClient } from '~/lib/auth-client';
|
|
|
13
13
|
// Composables
|
|
14
14
|
// ============================================================================
|
|
15
15
|
const toast = useToast();
|
|
16
|
+
const { setUser, validateSession } = useBetterAuth();
|
|
16
17
|
|
|
17
18
|
// ============================================================================
|
|
18
19
|
// Page Meta
|
|
@@ -28,6 +29,9 @@ const loading = ref<boolean>(false);
|
|
|
28
29
|
const useBackupCode = ref<boolean>(false);
|
|
29
30
|
const trustDevice = ref<boolean>(false);
|
|
30
31
|
|
|
32
|
+
// Form state for UForm
|
|
33
|
+
const formState = reactive({ code: '' });
|
|
34
|
+
|
|
31
35
|
const schema = v.object({
|
|
32
36
|
code: v.pipe(v.string('Code ist erforderlich'), v.minLength(6, 'Code muss mindestens 6 Zeichen haben')),
|
|
33
37
|
});
|
|
@@ -41,35 +45,46 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
|
|
|
41
45
|
loading.value = true;
|
|
42
46
|
|
|
43
47
|
try {
|
|
48
|
+
let result: any;
|
|
49
|
+
|
|
44
50
|
if (useBackupCode.value) {
|
|
45
|
-
|
|
51
|
+
result = await authClient.twoFactor.verifyBackupCode({
|
|
46
52
|
code: payload.data.code,
|
|
47
53
|
});
|
|
48
54
|
|
|
49
|
-
if (error) {
|
|
55
|
+
if (result.error) {
|
|
50
56
|
toast.add({
|
|
51
57
|
color: 'error',
|
|
52
|
-
description: error.message || 'Backup-Code ungültig',
|
|
58
|
+
description: result.error.message || 'Backup-Code ungültig',
|
|
53
59
|
title: 'Fehler',
|
|
54
60
|
});
|
|
55
61
|
return;
|
|
56
62
|
}
|
|
57
63
|
} else {
|
|
58
|
-
|
|
64
|
+
result = await authClient.twoFactor.verifyTotp({
|
|
59
65
|
code: payload.data.code,
|
|
60
66
|
trustDevice: trustDevice.value,
|
|
61
67
|
});
|
|
62
68
|
|
|
63
|
-
if (error) {
|
|
69
|
+
if (result.error) {
|
|
64
70
|
toast.add({
|
|
65
71
|
color: 'error',
|
|
66
|
-
description: error.message || 'Code ungültig',
|
|
72
|
+
description: result.error.message || 'Code ungültig',
|
|
67
73
|
title: 'Fehler',
|
|
68
74
|
});
|
|
69
75
|
return;
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
// Update auth state with user data from response
|
|
80
|
+
const userData = result?.data?.user || result?.user;
|
|
81
|
+
if (userData) {
|
|
82
|
+
setUser(userData);
|
|
83
|
+
} else {
|
|
84
|
+
// Fallback: validate session to get user data
|
|
85
|
+
await validateSession();
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
await navigateTo('/app');
|
|
74
89
|
} finally {
|
|
75
90
|
loading.value = false;
|
|
@@ -92,10 +107,10 @@ function toggleBackupCode(): void {
|
|
|
92
107
|
</p>
|
|
93
108
|
</div>
|
|
94
109
|
|
|
95
|
-
<UForm :schema="schema" class="flex flex-col gap-4" @submit="onSubmit">
|
|
110
|
+
<UForm :schema="schema" :state="formState" class="flex flex-col gap-4" @submit="onSubmit">
|
|
96
111
|
<UFormField :label="useBackupCode ? 'Backup-Code' : 'Authentifizierungscode'" name="code">
|
|
97
112
|
<UInput
|
|
98
|
-
|
|
113
|
+
v-model="formState.code"
|
|
99
114
|
:placeholder="useBackupCode ? 'Backup-Code eingeben' : '000000'"
|
|
100
115
|
size="lg"
|
|
101
116
|
class="text-center font-mono text-lg tracking-widest"
|
|
@@ -13,6 +13,7 @@ import { authClient } from '~/lib/auth-client';
|
|
|
13
13
|
// Composables
|
|
14
14
|
// ============================================================================
|
|
15
15
|
const toast = useToast();
|
|
16
|
+
const { signIn, setUser, isLoading, validateSession } = useBetterAuth();
|
|
16
17
|
|
|
17
18
|
// ============================================================================
|
|
18
19
|
// Page Meta
|
|
@@ -51,22 +52,53 @@ const schema = v.object({
|
|
|
51
52
|
|
|
52
53
|
type Schema = InferOutput<typeof schema>;
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Handle passkey authentication
|
|
57
|
+
* Uses official Better Auth signIn.passkey() method
|
|
58
|
+
* @see https://www.better-auth.com/docs/plugins/passkey
|
|
59
|
+
*/
|
|
54
60
|
async function onPasskeyLogin(): Promise<void> {
|
|
55
61
|
passkeyLoading.value = true;
|
|
56
62
|
|
|
57
63
|
try {
|
|
58
|
-
|
|
64
|
+
// Use official Better Auth client method
|
|
65
|
+
// This calls: GET /passkey/generate-authenticate-options → POST /passkey/verify-authentication
|
|
66
|
+
const result = await authClient.signIn.passkey();
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
// Check for error in response
|
|
69
|
+
if (result.error) {
|
|
61
70
|
toast.add({
|
|
62
71
|
color: 'error',
|
|
63
|
-
description: error.message || 'Passkey-Anmeldung fehlgeschlagen',
|
|
72
|
+
description: result.error.message || 'Passkey-Anmeldung fehlgeschlagen',
|
|
64
73
|
title: 'Fehler',
|
|
65
74
|
});
|
|
66
75
|
return;
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
// Update auth state with user data if available
|
|
79
|
+
if (result.data?.user) {
|
|
80
|
+
setUser(result.data.user as any);
|
|
81
|
+
} else if (result.data?.session) {
|
|
82
|
+
// Passkey auth returns session without user - fetch user via session validation
|
|
83
|
+
await validateSession();
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
await navigateTo('/app');
|
|
87
|
+
} catch (err: unknown) {
|
|
88
|
+
// Handle WebAuthn-specific errors
|
|
89
|
+
if (err instanceof Error && err.name === 'NotAllowedError') {
|
|
90
|
+
toast.add({
|
|
91
|
+
color: 'error',
|
|
92
|
+
description: 'Passkey-Authentifizierung wurde abgebrochen',
|
|
93
|
+
title: 'Fehler',
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
toast.add({
|
|
98
|
+
color: 'error',
|
|
99
|
+
description: err instanceof Error ? err.message : 'Passkey-Anmeldung fehlgeschlagen',
|
|
100
|
+
title: 'Fehler',
|
|
101
|
+
});
|
|
70
102
|
} finally {
|
|
71
103
|
passkeyLoading.value = false;
|
|
72
104
|
}
|
|
@@ -79,21 +111,48 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
|
|
|
79
111
|
loading.value = true;
|
|
80
112
|
|
|
81
113
|
try {
|
|
82
|
-
const
|
|
114
|
+
const result = await signIn.email({
|
|
83
115
|
email: payload.data.email,
|
|
84
116
|
password: payload.data.password,
|
|
85
117
|
});
|
|
86
118
|
|
|
87
|
-
|
|
119
|
+
// Check for error in response
|
|
120
|
+
if ('error' in result && result.error) {
|
|
88
121
|
toast.add({
|
|
89
122
|
color: 'error',
|
|
90
|
-
description: error.message || 'Anmeldung fehlgeschlagen',
|
|
123
|
+
description: (result.error as { message?: string }).message || 'Anmeldung fehlgeschlagen',
|
|
91
124
|
title: 'Fehler',
|
|
92
125
|
});
|
|
93
126
|
return;
|
|
94
127
|
}
|
|
95
128
|
|
|
96
|
-
|
|
129
|
+
// Check if 2FA is required
|
|
130
|
+
const resultData = 'data' in result ? result.data : result;
|
|
131
|
+
if (resultData && 'twoFactorRedirect' in resultData && resultData.twoFactorRedirect) {
|
|
132
|
+
// Redirect to 2FA page
|
|
133
|
+
await navigateTo('/auth/2fa');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if login was successful (user data in response)
|
|
138
|
+
const userData = 'user' in result ? result.user : ('data' in result ? result.data?.user : null);
|
|
139
|
+
if (userData) {
|
|
140
|
+
// Auth state is already stored by useBetterAuth
|
|
141
|
+
// Navigate to app
|
|
142
|
+
await navigateTo('/app');
|
|
143
|
+
} else {
|
|
144
|
+
toast.add({
|
|
145
|
+
color: 'error',
|
|
146
|
+
description: 'Anmeldung fehlgeschlagen - keine Benutzerdaten erhalten',
|
|
147
|
+
title: 'Fehler',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
toast.add({
|
|
152
|
+
color: 'error',
|
|
153
|
+
description: 'Ein unerwarteter Fehler ist aufgetreten',
|
|
154
|
+
title: 'Fehler',
|
|
155
|
+
});
|
|
97
156
|
} finally {
|
|
98
157
|
loading.value = false;
|
|
99
158
|
}
|
package/package.json
CHANGED