create-nuxt-base 2.0.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
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
+ ## [2.1.0](https://github.com/lenneTech/nuxt-base-starter/compare/v2.0.0...v2.1.0) (2026-01-24)
6
+
7
+
8
+ ### Features
9
+
10
+ * **tests, i18n:** add test infrastructure and error translation integration ([f8aff01](https://github.com/lenneTech/nuxt-base-starter/commit/f8aff01b5c763ea54cee270e10a1a29e2bece867))
11
+
5
12
  ## [2.0.0](https://github.com/lenneTech/nuxt-base-starter/compare/v1.2.0...v2.0.0) (2026-01-24)
6
13
 
7
14
 
@@ -0,0 +1,90 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ pull_request:
7
+ branches: [main, develop]
8
+
9
+ jobs:
10
+ unit-tests:
11
+ name: Unit Tests
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout repository
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup Node.js
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '22'
22
+ cache: 'npm'
23
+
24
+ - name: Install dependencies
25
+ run: npm ci
26
+
27
+ - name: Run unit tests
28
+ run: npm run test:unit
29
+
30
+ - name: Run unit tests with coverage
31
+ run: npm run test:unit:coverage
32
+
33
+ - name: Upload coverage report
34
+ uses: actions/upload-artifact@v4
35
+ if: always()
36
+ with:
37
+ name: coverage-report
38
+ path: coverage/
39
+ retention-days: 7
40
+
41
+ lint:
42
+ name: Lint
43
+ runs-on: ubuntu-latest
44
+
45
+ steps:
46
+ - name: Checkout repository
47
+ uses: actions/checkout@v4
48
+
49
+ - name: Setup Node.js
50
+ uses: actions/setup-node@v4
51
+ with:
52
+ node-version: '22'
53
+ cache: 'npm'
54
+
55
+ - name: Install dependencies
56
+ run: npm ci
57
+
58
+ - name: Run linter
59
+ run: npm run lint
60
+
61
+ - name: Check formatting
62
+ run: npm run format:check
63
+
64
+ build:
65
+ name: Build
66
+ runs-on: ubuntu-latest
67
+ needs: [unit-tests, lint]
68
+
69
+ steps:
70
+ - name: Checkout repository
71
+ uses: actions/checkout@v4
72
+
73
+ - name: Setup Node.js
74
+ uses: actions/setup-node@v4
75
+ with:
76
+ node-version: '22'
77
+ cache: 'npm'
78
+
79
+ - name: Install dependencies
80
+ run: npm ci
81
+
82
+ - name: Build application
83
+ run: npm run build
84
+
85
+ - name: Upload build artifacts
86
+ uses: actions/upload-artifact@v4
87
+ with:
88
+ name: build
89
+ path: .output/
90
+ retention-days: 7
@@ -2,11 +2,6 @@
2
2
  // ============================================================================
3
3
  // Imports
4
4
  // ============================================================================
5
- import type { FormSubmitEvent } from '@nuxt/ui';
6
- import type { InferOutput } from 'valibot';
7
-
8
- import * as v from 'valibot';
9
-
10
5
  import ModalBackupCodes from '~/components/Modal/ModalBackupCodes.vue';
11
6
 
12
7
  // ============================================================================
@@ -39,21 +34,10 @@ const passkeyLoading = ref<boolean>(false);
39
34
  const newPasskeyName = ref<string>('');
40
35
  const showAddPasskey = ref<boolean>(false);
41
36
 
42
- // Form states for UForm (required for proper data binding)
43
- const enable2FAForm = reactive({ password: '' });
44
- const disable2FAForm = reactive({ password: '' });
45
- const totpForm = reactive({ code: '' });
46
-
47
- const passwordSchema = v.object({
48
- password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(1, 'Passwort ist erforderlich')),
49
- });
50
-
51
- const totpSchema = v.object({
52
- code: v.pipe(v.string('Code ist erforderlich'), v.length(6, 'Code muss 6 Ziffern haben')),
53
- });
54
-
55
- type PasswordSchema = InferOutput<typeof passwordSchema>;
56
- type TotpSchema = InferOutput<typeof totpSchema>;
37
+ // Form states - using reactive for direct v-model binding
38
+ const enable2FAPassword = ref('');
39
+ const disable2FAPassword = ref('');
40
+ const totpCode = ref('');
57
41
 
58
42
  // ============================================================================
59
43
  // Lifecycle Hooks
@@ -130,12 +114,21 @@ async function deletePasskey(id: string): Promise<void> {
130
114
  }
131
115
  }
132
116
 
133
- async function disable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void> {
117
+ async function disable2FA(): Promise<void> {
118
+ if (!disable2FAPassword.value) {
119
+ toast.add({
120
+ color: 'error',
121
+ description: 'Passwort ist erforderlich',
122
+ title: 'Validierungsfehler',
123
+ });
124
+ return;
125
+ }
126
+
134
127
  loading.value = true;
135
128
 
136
129
  try {
137
130
  const { error } = await authClient.twoFactor.disable({
138
- password: payload.data.password,
131
+ password: disable2FAPassword.value,
139
132
  });
140
133
 
141
134
  if (error) {
@@ -159,18 +152,27 @@ async function disable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<voi
159
152
  });
160
153
 
161
154
  show2FADisable.value = false;
162
- disable2FAForm.password = '';
155
+ disable2FAPassword.value = '';
163
156
  } finally {
164
157
  loading.value = false;
165
158
  }
166
159
  }
167
160
 
168
- async function enable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void> {
161
+ async function enable2FA(): Promise<void> {
162
+ if (!enable2FAPassword.value) {
163
+ toast.add({
164
+ color: 'error',
165
+ description: 'Passwort ist erforderlich',
166
+ title: 'Validierungsfehler',
167
+ });
168
+ return;
169
+ }
170
+
169
171
  loading.value = true;
170
172
 
171
173
  try {
172
174
  const { data, error } = await authClient.twoFactor.enable({
173
- password: payload.data.password,
175
+ password: enable2FAPassword.value,
174
176
  });
175
177
 
176
178
  if (error) {
@@ -185,7 +187,7 @@ async function enable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void
185
187
  totpUri.value = data?.totpURI ?? '';
186
188
  backupCodes.value = data?.backupCodes ?? [];
187
189
  showTotpSetup.value = true;
188
- enable2FAForm.password = '';
190
+ enable2FAPassword.value = '';
189
191
  } finally {
190
192
  loading.value = false;
191
193
  }
@@ -218,12 +220,21 @@ async function openBackupCodesModal(codes: string[] = []): Promise<void> {
218
220
  await modal.open();
219
221
  }
220
222
 
221
- async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
223
+ async function verifyTotp(): Promise<void> {
224
+ if (!totpCode.value || totpCode.value.length !== 6) {
225
+ toast.add({
226
+ color: 'error',
227
+ description: 'Code muss 6 Ziffern haben',
228
+ title: 'Validierungsfehler',
229
+ });
230
+ return;
231
+ }
232
+
222
233
  loading.value = true;
223
234
 
224
235
  try {
225
236
  const { error } = await authClient.twoFactor.verifyTotp({
226
- code: payload.data.code,
237
+ code: totpCode.value,
227
238
  });
228
239
 
229
240
  if (error) {
@@ -247,7 +258,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
247
258
  });
248
259
 
249
260
  showTotpSetup.value = false;
250
- totpForm.code = '';
261
+ totpCode.value = '';
251
262
  await openBackupCodesModal(backupCodes.value);
252
263
  } finally {
253
264
  loading.value = false;
@@ -288,12 +299,12 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
288
299
  </div>
289
300
 
290
301
  <template v-if="!is2FAEnabled && !showTotpSetup">
291
- <UForm :schema="passwordSchema" :state="enable2FAForm" class="space-y-4" @submit="enable2FA">
292
- <UFormField label="Passwort bestätigen" name="password">
293
- <UInput v-model="enable2FAForm.password" type="password" placeholder="Dein Passwort" />
302
+ <form class="space-y-4" @submit.prevent="enable2FA">
303
+ <UFormField label="Passwort bestätigen" name="password" class="w-full">
304
+ <UInput v-model="enable2FAPassword" type="password" placeholder="Dein Passwort" class="w-full" />
294
305
  </UFormField>
295
306
  <UButton type="submit" :loading="loading"> 2FA aktivieren </UButton>
296
- </UForm>
307
+ </form>
297
308
  </template>
298
309
 
299
310
  <template v-if="showTotpSetup">
@@ -304,15 +315,15 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
304
315
  <img v-if="totpUri" :src="`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(totpUri)}`" alt="TOTP QR Code" class="rounded-lg" />
305
316
  </div>
306
317
 
307
- <UForm :schema="totpSchema" :state="totpForm" class="space-y-4" @submit="verifyTotp">
308
- <UFormField label="Verifizierungscode" name="code">
309
- <UInput v-model="totpForm.code" placeholder="000000" class="text-center font-mono" />
318
+ <form class="space-y-4" @submit.prevent="verifyTotp">
319
+ <UFormField label="Verifizierungscode" name="code" class="w-full">
320
+ <UInput v-model="totpCode" placeholder="000000" class="w-full text-center font-mono" />
310
321
  </UFormField>
311
322
  <div class="flex gap-2">
312
323
  <UButton type="submit" :loading="loading"> Verifizieren </UButton>
313
324
  <UButton variant="outline" color="neutral" @click="showTotpSetup = false"> Abbrechen </UButton>
314
325
  </div>
315
- </UForm>
326
+ </form>
316
327
  </div>
317
328
  </template>
318
329
 
@@ -324,16 +335,16 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
324
335
  </template>
325
336
 
326
337
  <template v-if="show2FADisable">
327
- <UForm :schema="passwordSchema" :state="disable2FAForm" class="space-y-4" @submit="disable2FA">
338
+ <form class="space-y-4" @submit.prevent="disable2FA">
328
339
  <UAlert color="warning" icon="i-lucide-alert-triangle"> 2FA zu deaktivieren verringert die Sicherheit deines Kontos. </UAlert>
329
- <UFormField label="Passwort bestätigen" name="password">
330
- <UInput v-model="disable2FAForm.password" type="password" placeholder="Dein Passwort" />
340
+ <UFormField label="Passwort bestätigen" name="password" class="w-full">
341
+ <UInput v-model="disable2FAPassword" type="password" placeholder="Dein Passwort" class="w-full" />
331
342
  </UFormField>
332
343
  <div class="flex gap-2">
333
344
  <UButton type="submit" color="error" :loading="loading"> 2FA deaktivieren </UButton>
334
345
  <UButton variant="outline" color="neutral" @click="show2FADisable = false"> Abbrechen </UButton>
335
346
  </div>
336
- </UForm>
347
+ </form>
337
348
  </template>
338
349
  </div>
339
350
  </UCard>
@@ -12,6 +12,7 @@ import * as v from 'valibot';
12
12
  // ============================================================================
13
13
  const toast = useToast();
14
14
  const { signIn, setUser, isLoading, validateSession, authenticateWithPasskey } = useLtAuth();
15
+ const { translateError } = useLtErrorTranslation();
15
16
 
16
17
  // ============================================================================
17
18
  // Page Meta
@@ -114,10 +115,11 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
114
115
 
115
116
  // Check for error in response
116
117
  if ('error' in result && result.error) {
118
+ const errorMessage = (result.error as { message?: string }).message || 'Anmeldung fehlgeschlagen';
117
119
  toast.add({
118
120
  color: 'error',
119
- description: (result.error as { message?: string }).message || 'Anmeldung fehlgeschlagen',
120
- title: 'Fehler',
121
+ description: translateError(errorMessage),
122
+ title: 'Anmeldung fehlgeschlagen',
121
123
  });
122
124
  return;
123
125
  }
@@ -2,9 +2,6 @@
2
2
  // ============================================================================
3
3
  // Imports
4
4
  // ============================================================================
5
- import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui';
6
- import type { InferOutput } from 'valibot';
7
-
8
5
  import * as v from 'valibot';
9
6
 
10
7
  // ============================================================================
@@ -27,36 +24,13 @@ const loading = ref<boolean>(false);
27
24
  const showPasskeyPrompt = ref<boolean>(false);
28
25
  const passkeyLoading = ref<boolean>(false);
29
26
 
30
- const fields: AuthFormField[] = [
31
- {
32
- label: 'Name',
33
- name: 'name',
34
- placeholder: 'Name eingeben',
35
- required: true,
36
- type: 'text',
37
- },
38
- {
39
- label: 'E-Mail',
40
- name: 'email',
41
- placeholder: 'E-Mail eingeben',
42
- required: true,
43
- type: 'email',
44
- },
45
- {
46
- label: 'Passwort',
47
- name: 'password',
48
- placeholder: 'Passwort eingeben',
49
- required: true,
50
- type: 'password',
51
- },
52
- {
53
- label: 'Passwort bestätigen',
54
- name: 'confirmPassword',
55
- placeholder: 'Passwort wiederholen',
56
- required: true,
57
- type: 'password',
58
- },
59
- ];
27
+ // Form state - using refs for direct v-model binding
28
+ const formState = reactive({
29
+ name: '',
30
+ email: '',
31
+ password: '',
32
+ confirmPassword: '',
33
+ });
60
34
 
61
35
  const schema = v.pipe(
62
36
  v.object({
@@ -71,20 +45,30 @@ const schema = v.pipe(
71
45
  ),
72
46
  );
73
47
 
74
- type Schema = InferOutput<typeof schema>;
75
-
76
48
  // ============================================================================
77
49
  // Functions
78
50
  // ============================================================================
79
- async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
51
+ async function onSubmit(): Promise<void> {
52
+ // Validate
53
+ const result = v.safeParse(schema, formState);
54
+ if (!result.success) {
55
+ const firstError = result.issues[0];
56
+ toast.add({
57
+ color: 'error',
58
+ description: firstError.message,
59
+ title: 'Validierungsfehler',
60
+ });
61
+ return;
62
+ }
63
+
80
64
  loading.value = true;
81
65
 
82
66
  try {
83
67
  // Step 1: Sign up
84
68
  const signUpResult = await signUp.email({
85
- email: payload.data.email,
86
- name: payload.data.name,
87
- password: payload.data.password,
69
+ email: formState.email,
70
+ name: formState.name,
71
+ password: formState.password,
88
72
  });
89
73
 
90
74
  const signUpError = 'error' in signUpResult ? signUpResult.error : null;
@@ -100,8 +84,8 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
100
84
 
101
85
  // Step 2: Sign in to create session (required for passkey registration)
102
86
  const signInResult = await signIn.email({
103
- email: payload.data.email,
104
- password: payload.data.password,
87
+ email: formState.email,
88
+ password: formState.password,
105
89
  });
106
90
 
107
91
  const signInError = 'error' in signInResult ? signInResult.error : null;
@@ -168,25 +152,65 @@ async function skipPasskey(): Promise<void> {
168
152
  <template>
169
153
  <UPageCard class="w-md" variant="naked">
170
154
  <template v-if="!showPasskeyPrompt">
171
- <UAuthForm
172
- :schema="schema"
173
- title="Registrieren"
174
- icon="i-lucide-user-plus"
175
- :fields="fields"
176
- :loading="loading"
177
- :submit="{
178
- label: 'Konto erstellen',
179
- block: true,
180
- }"
181
- @submit="onSubmit"
182
- >
183
- <template #footer>
184
- <p class="text-center text-sm text-muted">
185
- Bereits ein Konto?
186
- <ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
187
- </p>
188
- </template>
189
- </UAuthForm>
155
+ <div class="flex flex-col items-center gap-4">
156
+ <UIcon name="i-lucide-user-plus" class="size-12 text-primary" />
157
+ <h1 class="text-2xl font-semibold">Registrieren</h1>
158
+ </div>
159
+
160
+ <form class="mt-6 flex flex-col gap-4" @submit.prevent="onSubmit">
161
+ <UFormField label="Name" name="name" required class="w-full">
162
+ <UInput
163
+ v-model="formState.name"
164
+ name="name"
165
+ type="text"
166
+ placeholder="Name eingeben"
167
+ autocomplete="name"
168
+ class="w-full"
169
+ />
170
+ </UFormField>
171
+
172
+ <UFormField label="E-Mail" name="email" required class="w-full">
173
+ <UInput
174
+ v-model="formState.email"
175
+ name="email"
176
+ type="email"
177
+ placeholder="E-Mail eingeben"
178
+ autocomplete="email"
179
+ class="w-full"
180
+ />
181
+ </UFormField>
182
+
183
+ <UFormField label="Passwort" name="password" required class="w-full">
184
+ <UInput
185
+ v-model="formState.password"
186
+ name="password"
187
+ type="password"
188
+ placeholder="Passwort eingeben"
189
+ autocomplete="new-password"
190
+ class="w-full"
191
+ />
192
+ </UFormField>
193
+
194
+ <UFormField label="Passwort bestätigen" name="confirmPassword" required class="w-full">
195
+ <UInput
196
+ v-model="formState.confirmPassword"
197
+ name="confirmPassword"
198
+ type="password"
199
+ placeholder="Passwort wiederholen"
200
+ autocomplete="new-password"
201
+ class="w-full"
202
+ />
203
+ </UFormField>
204
+
205
+ <UButton type="submit" block :loading="loading">
206
+ Konto erstellen
207
+ </UButton>
208
+ </form>
209
+
210
+ <p class="mt-4 text-center text-sm text-muted">
211
+ Bereits ein Konto?
212
+ <ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
213
+ </p>
190
214
  </template>
191
215
 
192
216
  <template v-else>
@@ -73,6 +73,10 @@ export default defineNuxtConfig({
73
73
  ltExtensions: {
74
74
  auth: {
75
75
  enabled: true,
76
+ // baseURL is used in production mode for cross-origin API requests
77
+ // In dev mode, Nuxt proxy is used (baseURL is ignored, requests go through /api/iam)
78
+ // In production, requests go directly to baseURL + basePath (e.g., https://api.example.com/iam)
79
+ baseURL: process.env.API_URL || 'http://localhost:3000',
76
80
  basePath: '/iam',
77
81
  loginPath: '/auth/login',
78
82
  twoFactorRedirectPath: '/auth/2fa',
@@ -184,11 +188,19 @@ export default defineNuxtConfig({
184
188
  plugins: [tailwindcss()],
185
189
  server: {
186
190
  proxy: {
191
+ // IAM proxy via /api prefix (nuxt-extensions adds /api in dev mode)
192
+ // Must be before /api to match more specifically
193
+ '/api/iam': {
194
+ target: 'http://localhost:3000',
195
+ changeOrigin: true,
196
+ rewrite: (path) => path.replace(/^\/api/, ''),
197
+ },
198
+ // API proxy - no rewrite, backend expects /api/... paths
187
199
  '/api': {
188
200
  target: 'http://localhost:3000',
189
201
  changeOrigin: true,
190
- rewrite: (path: string) => path.replace(/^\/api/, ''),
191
202
  },
203
+ // IAM proxy for direct BetterAuth endpoints (SSR mode)
192
204
  '/iam': {
193
205
  target: 'http://localhost:3000',
194
206
  changeOrigin: true,