create-nuxt-base 1.2.0 → 2.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/nuxt-base-template/.github/workflows/test.yml +90 -0
  3. package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +2 -1
  4. package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +7 -7
  5. package/nuxt-base-template/app/interfaces/user.interface.ts +5 -12
  6. package/nuxt-base-template/app/layouts/default.vue +1 -1
  7. package/nuxt-base-template/app/middleware/admin.global.ts +2 -2
  8. package/nuxt-base-template/app/middleware/auth.global.ts +2 -2
  9. package/nuxt-base-template/app/middleware/guest.global.ts +2 -2
  10. package/nuxt-base-template/app/pages/app/index.vue +1 -1
  11. package/nuxt-base-template/app/pages/app/settings/security.vue +54 -43
  12. package/nuxt-base-template/app/pages/auth/2fa.vue +2 -3
  13. package/nuxt-base-template/app/pages/auth/forgot-password.vue +2 -1
  14. package/nuxt-base-template/app/pages/auth/login.vue +6 -4
  15. package/nuxt-base-template/app/pages/auth/register.vue +85 -61
  16. package/nuxt-base-template/app/pages/auth/reset-password.vue +2 -1
  17. package/nuxt-base-template/docs/pages/docs.vue +1 -1
  18. package/nuxt-base-template/nuxt.config.ts +50 -1
  19. package/nuxt-base-template/package-lock.json +1311 -2920
  20. package/nuxt-base-template/package.json +27 -2
  21. package/nuxt-base-template/playwright.config.ts +1 -1
  22. package/nuxt-base-template/tests/e2e/auth.spec.ts +467 -0
  23. package/nuxt-base-template/tests/unit/auth/auth.spec.ts +439 -0
  24. package/nuxt-base-template/tests/unit/auth/error-translation.spec.ts +279 -0
  25. package/nuxt-base-template/tests/unit/mocks/auth-client.mock.ts +165 -0
  26. package/nuxt-base-template/tests/unit/mocks/nuxt-imports.ts +105 -0
  27. package/nuxt-base-template/tests/unit/setup.ts +56 -0
  28. package/nuxt-base-template/vitest.config.ts +25 -0
  29. package/package.json +1 -1
  30. package/nuxt-base-template/app/components/Transition/TransitionFade.vue +0 -27
  31. package/nuxt-base-template/app/components/Transition/TransitionFadeScale.vue +0 -27
  32. package/nuxt-base-template/app/components/Transition/TransitionSlide.vue +0 -12
  33. package/nuxt-base-template/app/components/Transition/TransitionSlideBottom.vue +0 -12
  34. package/nuxt-base-template/app/components/Transition/TransitionSlideRevert.vue +0 -12
  35. package/nuxt-base-template/app/composables/use-better-auth.ts +0 -597
  36. package/nuxt-base-template/app/composables/use-file.ts +0 -71
  37. package/nuxt-base-template/app/composables/use-share.ts +0 -38
  38. package/nuxt-base-template/app/composables/use-tus-upload.ts +0 -278
  39. package/nuxt-base-template/app/composables/use-tw.ts +0 -1
  40. package/nuxt-base-template/app/interfaces/upload.interface.ts +0 -58
  41. package/nuxt-base-template/app/lib/auth-client.ts +0 -229
  42. package/nuxt-base-template/app/lib/auth-state.ts +0 -206
  43. package/nuxt-base-template/app/plugins/auth-interceptor.client.ts +0 -151
  44. package/nuxt-base-template/app/utils/crypto.ts +0 -44
  45. package/nuxt-base-template/tests/iam.spec.ts +0 -247
  46. /package/nuxt-base-template/tests/{init.spec.ts → e2e/init.spec.ts} +0 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Authentication Unit Tests
3
+ *
4
+ * These tests cover the same functionality as the E2E Playwright tests,
5
+ * but use mocks instead of real servers. They can run in CI/CD pipelines.
6
+ *
7
+ * Test Coverage:
8
+ * - User registration
9
+ * - Login with email/password
10
+ * - Logout
11
+ * - 2FA enable/disable/verify
12
+ * - Passkey management
13
+ * - Error translations
14
+ */
15
+
16
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
17
+ import {
18
+ createMockAuthClient,
19
+ mockUser,
20
+ mockTotpData,
21
+ mockPasskey,
22
+ resetMockAuthClient,
23
+ } from '../mocks/auth-client.mock';
24
+ import { resetCookies } from '../setup';
25
+
26
+ // ============================================================================
27
+ // Test Suite: Registration
28
+ // ============================================================================
29
+
30
+ describe('Registration', () => {
31
+ let authClient: ReturnType<typeof createMockAuthClient>;
32
+
33
+ beforeEach(() => {
34
+ authClient = createMockAuthClient();
35
+ resetCookies();
36
+ });
37
+
38
+ it('should register a new user with valid credentials', async () => {
39
+ const newUser = {
40
+ email: 'new@test.com',
41
+ password: 'SecurePass123!',
42
+ name: 'New User',
43
+ };
44
+
45
+ const result = await authClient.signUp.email(newUser);
46
+
47
+ expect(result.error).toBeNull();
48
+ expect(result.data).not.toBeNull();
49
+ expect(result.data?.user.email).toBe(newUser.email);
50
+ expect(result.data?.user.name).toBe(newUser.name);
51
+ expect(authClient.signUp.email).toHaveBeenCalledWith(newUser);
52
+ });
53
+
54
+ it('should have a session after registration', async () => {
55
+ await authClient.signUp.email({
56
+ email: 'new@test.com',
57
+ password: 'SecurePass123!',
58
+ name: 'New User',
59
+ });
60
+
61
+ const session = authClient._getSession();
62
+ expect(session).not.toBeNull();
63
+ expect(session?.user.email).toBe('new@test.com');
64
+ });
65
+ });
66
+
67
+ // ============================================================================
68
+ // Test Suite: Login
69
+ // ============================================================================
70
+
71
+ describe('Login', () => {
72
+ let authClient: ReturnType<typeof createMockAuthClient>;
73
+
74
+ beforeEach(() => {
75
+ authClient = createMockAuthClient();
76
+ resetCookies();
77
+ });
78
+
79
+ it('should login with valid email and password', async () => {
80
+ const credentials = {
81
+ email: 'test@example.com',
82
+ password: 'SecurePass123!',
83
+ };
84
+
85
+ const result = await authClient.signIn.email(credentials);
86
+
87
+ expect(result.error).toBeNull();
88
+ expect(result.data).not.toBeNull();
89
+ expect(result.data?.user.email).toBe(credentials.email);
90
+ });
91
+
92
+ it('should fail login with invalid credentials', async () => {
93
+ const credentials = {
94
+ email: 'invalid@test.com',
95
+ password: 'WrongPassword!',
96
+ };
97
+
98
+ const result = await authClient.signIn.email(credentials);
99
+
100
+ expect(result.error).not.toBeNull();
101
+ expect(result.error?.message).toBe('Ungültige Anmeldedaten');
102
+ expect(result.data).toBeNull();
103
+ });
104
+
105
+ it('should login with passkey', async () => {
106
+ const result = await authClient.signIn.passkey();
107
+
108
+ expect(result.error).toBeNull();
109
+ expect(result.data).not.toBeNull();
110
+ expect(result.data?.user).toBeDefined();
111
+ });
112
+ });
113
+
114
+ // ============================================================================
115
+ // Test Suite: Logout
116
+ // ============================================================================
117
+
118
+ describe('Logout', () => {
119
+ let authClient: ReturnType<typeof createMockAuthClient>;
120
+
121
+ beforeEach(async () => {
122
+ authClient = createMockAuthClient();
123
+ resetCookies();
124
+ // Login first
125
+ await authClient.signIn.email({
126
+ email: 'test@example.com',
127
+ password: 'SecurePass123!',
128
+ });
129
+ });
130
+
131
+ it('should have session before logout', () => {
132
+ const session = authClient._getSession();
133
+ expect(session).not.toBeNull();
134
+ });
135
+
136
+ it('should clear session after logout', async () => {
137
+ await authClient.signOut();
138
+
139
+ const session = authClient._getSession();
140
+ expect(session).toBeNull();
141
+ });
142
+
143
+ it('should call signOut method', async () => {
144
+ await authClient.signOut();
145
+ expect(authClient.signOut).toHaveBeenCalled();
146
+ });
147
+ });
148
+
149
+ // ============================================================================
150
+ // Test Suite: Two-Factor Authentication (2FA)
151
+ // ============================================================================
152
+
153
+ describe('Two-Factor Authentication', () => {
154
+ let authClient: ReturnType<typeof createMockAuthClient>;
155
+
156
+ beforeEach(async () => {
157
+ authClient = createMockAuthClient();
158
+ resetCookies();
159
+ // Login first
160
+ await authClient.signIn.email({
161
+ email: 'test@example.com',
162
+ password: 'SecurePass123!',
163
+ });
164
+ });
165
+
166
+ describe('Enable 2FA', () => {
167
+ it('should return TOTP URI and backup codes when enabling 2FA', async () => {
168
+ const result = await authClient.twoFactor.enable({ password: 'SecurePass123!' });
169
+
170
+ expect(result.error).toBeNull();
171
+ expect(result.data).not.toBeNull();
172
+ expect(result.data?.totpURI).toContain('otpauth://totp/');
173
+ expect(result.data?.backupCodes).toHaveLength(5);
174
+ });
175
+
176
+ it('should extract secret from TOTP URI', async () => {
177
+ const result = await authClient.twoFactor.enable({ password: 'SecurePass123!' });
178
+
179
+ const totpUri = result.data?.totpURI;
180
+ const secretMatch = totpUri?.match(/secret=([A-Z2-7]+)/i);
181
+ expect(secretMatch).not.toBeNull();
182
+ expect(secretMatch?.[1]).toBe('JBSWY3DPEHPK3PXP');
183
+ });
184
+ });
185
+
186
+ describe('Verify TOTP', () => {
187
+ it('should verify valid TOTP code', async () => {
188
+ const result = await authClient.twoFactor.verifyTotp({ code: '123456' });
189
+
190
+ expect(result.error).toBeNull();
191
+ expect(result.data).not.toBeNull();
192
+ });
193
+
194
+ it('should reject invalid TOTP code', async () => {
195
+ const result = await authClient.twoFactor.verifyTotp({ code: '000000' });
196
+
197
+ expect(result.error).not.toBeNull();
198
+ expect(result.error?.message).toBe('Ungültiger Code');
199
+ });
200
+
201
+ it('should set twoFactorEnabled after successful verification', async () => {
202
+ await authClient.twoFactor.verifyTotp({ code: '123456' });
203
+
204
+ const session = authClient._getSession();
205
+ expect(session?.user.twoFactorEnabled).toBe(true);
206
+ });
207
+ });
208
+
209
+ describe('Disable 2FA', () => {
210
+ it('should disable 2FA with password', async () => {
211
+ // First enable 2FA
212
+ await authClient.twoFactor.enable({ password: 'SecurePass123!' });
213
+ await authClient.twoFactor.verifyTotp({ code: '123456' });
214
+
215
+ // Then disable
216
+ const result = await authClient.twoFactor.disable({ password: 'SecurePass123!' });
217
+
218
+ expect(result.error).toBeNull();
219
+ const session = authClient._getSession();
220
+ expect(session?.user.twoFactorEnabled).toBe(false);
221
+ });
222
+ });
223
+
224
+ describe('Backup Codes', () => {
225
+ it('should generate backup codes', async () => {
226
+ const result = await authClient.twoFactor.generateBackupCodes({ password: 'SecurePass123!' });
227
+
228
+ expect(result.error).toBeNull();
229
+ expect(result.data?.backupCodes).toHaveLength(5);
230
+ });
231
+
232
+ it('should verify backup code', async () => {
233
+ const result = await authClient.twoFactor.verifyBackupCode({ code: '12345678' });
234
+
235
+ expect(result.error).toBeNull();
236
+ expect(result.data).not.toBeNull();
237
+ });
238
+ });
239
+ });
240
+
241
+ // ============================================================================
242
+ // Test Suite: Passkey Management
243
+ // ============================================================================
244
+
245
+ describe('Passkey Management', () => {
246
+ let authClient: ReturnType<typeof createMockAuthClient>;
247
+
248
+ beforeEach(async () => {
249
+ authClient = createMockAuthClient();
250
+ resetCookies();
251
+ // Login first
252
+ await authClient.signIn.email({
253
+ email: 'test@example.com',
254
+ password: 'SecurePass123!',
255
+ });
256
+ });
257
+
258
+ it('should add a new passkey', async () => {
259
+ const passkeyName = 'My Laptop';
260
+ const result = await authClient.passkey.addPasskey({ name: passkeyName });
261
+
262
+ expect(result.error).toBeNull();
263
+ expect(result.data).not.toBeNull();
264
+ expect(result.data?.name).toBe(passkeyName);
265
+ });
266
+
267
+ it('should list user passkeys', async () => {
268
+ const result = await authClient.passkey.listUserPasskeys();
269
+
270
+ expect(result.error).toBeNull();
271
+ expect(result.data).toBeInstanceOf(Array);
272
+ expect(result.data?.length).toBeGreaterThan(0);
273
+ });
274
+
275
+ it('should delete a passkey', async () => {
276
+ const result = await authClient.passkey.deletePasskey({ id: 'passkey-123' });
277
+
278
+ expect(result.error).toBeNull();
279
+ expect(result.data).toBe(true);
280
+ });
281
+ });
282
+
283
+ // ============================================================================
284
+ // Test Suite: Error Translations
285
+ // ============================================================================
286
+
287
+ describe('Error Translations', () => {
288
+ let authClient: ReturnType<typeof createMockAuthClient>;
289
+
290
+ beforeEach(() => {
291
+ authClient = createMockAuthClient();
292
+ resetCookies();
293
+ });
294
+
295
+ it('should return German error message for invalid credentials', async () => {
296
+ const result = await authClient.signIn.email({
297
+ email: 'invalid@test.com',
298
+ password: 'WrongPassword!',
299
+ });
300
+
301
+ expect(result.error?.message).toBe('Ungültige Anmeldedaten');
302
+ });
303
+
304
+ it('should return error code for backend errors', async () => {
305
+ const result = await authClient.signIn.email({
306
+ email: 'invalid@test.com',
307
+ password: 'WrongPassword!',
308
+ });
309
+
310
+ expect(result.error?.code).toBe('LTNS_0010');
311
+ });
312
+ });
313
+
314
+ // ============================================================================
315
+ // Test Suite: Complete Auth Flow
316
+ // ============================================================================
317
+
318
+ describe('Complete Auth Flow', () => {
319
+ let authClient: ReturnType<typeof createMockAuthClient>;
320
+
321
+ beforeEach(() => {
322
+ authClient = createMockAuthClient();
323
+ resetCookies();
324
+ });
325
+
326
+ it('should complete: Register -> Enable 2FA -> Add Passkey', async () => {
327
+ // 1. Register
328
+ const registerResult = await authClient.signUp.email({
329
+ email: 'flow@test.com',
330
+ password: 'SecurePass123!',
331
+ name: 'Flow Test',
332
+ });
333
+ expect(registerResult.error).toBeNull();
334
+ expect(authClient._getSession()).not.toBeNull();
335
+
336
+ // 2. Enable 2FA
337
+ const enable2FAResult = await authClient.twoFactor.enable({ password: 'SecurePass123!' });
338
+ expect(enable2FAResult.error).toBeNull();
339
+ expect(enable2FAResult.data?.totpURI).toBeDefined();
340
+
341
+ // 3. Verify TOTP
342
+ const verifyResult = await authClient.twoFactor.verifyTotp({ code: '123456' });
343
+ expect(verifyResult.error).toBeNull();
344
+ expect(authClient._getSession()?.user.twoFactorEnabled).toBe(true);
345
+
346
+ // 4. Add Passkey
347
+ const passkeyResult = await authClient.passkey.addPasskey({ name: 'Test Device' });
348
+ expect(passkeyResult.error).toBeNull();
349
+ expect(passkeyResult.data?.name).toBe('Test Device');
350
+ });
351
+
352
+ it('should complete: Register -> Add Passkey -> Enable 2FA', async () => {
353
+ // 1. Register
354
+ const registerResult = await authClient.signUp.email({
355
+ email: 'flow2@test.com',
356
+ password: 'SecurePass123!',
357
+ name: 'Flow Test 2',
358
+ });
359
+ expect(registerResult.error).toBeNull();
360
+
361
+ // 2. Add Passkey first
362
+ const passkeyResult = await authClient.passkey.addPasskey({ name: 'Early Passkey' });
363
+ expect(passkeyResult.error).toBeNull();
364
+
365
+ // 3. Then enable 2FA
366
+ const enable2FAResult = await authClient.twoFactor.enable({ password: 'SecurePass123!' });
367
+ expect(enable2FAResult.error).toBeNull();
368
+
369
+ // 4. Verify TOTP
370
+ const verifyResult = await authClient.twoFactor.verifyTotp({ code: '123456' });
371
+ expect(verifyResult.error).toBeNull();
372
+ expect(authClient._getSession()?.user.twoFactorEnabled).toBe(true);
373
+ });
374
+
375
+ it('should complete: Login -> Logout -> Login with 2FA', async () => {
376
+ // Setup: Register and enable 2FA
377
+ await authClient.signUp.email({
378
+ email: 'logout@test.com',
379
+ password: 'SecurePass123!',
380
+ name: 'Logout Test',
381
+ });
382
+ await authClient.twoFactor.enable({ password: 'SecurePass123!' });
383
+ await authClient.twoFactor.verifyTotp({ code: '123456' });
384
+
385
+ // 1. Logout
386
+ await authClient.signOut();
387
+ expect(authClient._getSession()).toBeNull();
388
+
389
+ // 2. Login again
390
+ const loginResult = await authClient.signIn.email({
391
+ email: 'logout@test.com',
392
+ password: 'SecurePass123!',
393
+ });
394
+ expect(loginResult.error).toBeNull();
395
+
396
+ // Note: In real flow, 2FA would be required here
397
+ // The mock simulates this by having twoFactorEnabled in the user object
398
+ });
399
+ });
400
+
401
+ // ============================================================================
402
+ // Test Suite: Session Management
403
+ // ============================================================================
404
+
405
+ describe('Session Management', () => {
406
+ let authClient: ReturnType<typeof createMockAuthClient>;
407
+
408
+ beforeEach(() => {
409
+ authClient = createMockAuthClient();
410
+ resetCookies();
411
+ });
412
+
413
+ it('should start with no session', () => {
414
+ const session = authClient._getSession();
415
+ expect(session).toBeNull();
416
+ });
417
+
418
+ it('should create session on login', async () => {
419
+ await authClient.signIn.email({
420
+ email: 'test@example.com',
421
+ password: 'SecurePass123!',
422
+ });
423
+
424
+ const session = authClient._getSession();
425
+ expect(session).not.toBeNull();
426
+ expect(session?.session.token).toBeDefined();
427
+ });
428
+
429
+ it('should preserve session data', async () => {
430
+ await authClient.signIn.email({
431
+ email: 'preserve@test.com',
432
+ password: 'SecurePass123!',
433
+ });
434
+
435
+ const session = authClient._getSession();
436
+ expect(session?.user.email).toBe('preserve@test.com');
437
+ expect(session?.session.id).toBe('session-123');
438
+ });
439
+ });