create-nuxt-base 2.2.1 → 2.2.2
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 +2 -0
- package/nuxt-base-template/package-lock.json +170 -12
- package/nuxt-base-template/package.json +7 -6
- package/nuxt-base-template/tests/e2e/auth-feature-order.spec.ts +379 -0
- package/nuxt-base-template/tests/e2e/auth-lifecycle.spec.ts +713 -0
- package/package.json +1 -1
- package/nuxt-base-template/tests/e2e/auth.spec.ts +0 -467
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
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.2.2](https://github.com/lenneTech/nuxt-base-starter/compare/v2.2.1...v2.2.2) (2026-02-04)
|
|
6
|
+
|
|
5
7
|
### [2.2.1](https://github.com/lenneTech/nuxt-base-starter/compare/v2.2.0...v2.2.1) (2026-02-04)
|
|
6
8
|
|
|
7
9
|
|
|
@@ -33,20 +33,21 @@
|
|
|
33
33
|
"@tailwindcss/vite": "4.1.18",
|
|
34
34
|
"@types/node": "25.0.6",
|
|
35
35
|
"@types/qrcode": "1.5.6",
|
|
36
|
-
"@vitejs/plugin-vue": "
|
|
37
|
-
"@vue/test-utils": "
|
|
36
|
+
"@vitejs/plugin-vue": "6.0.3",
|
|
37
|
+
"@vue/test-utils": "2.4.6",
|
|
38
38
|
"dayjs-nuxt": "2.1.11",
|
|
39
|
-
"happy-dom": "
|
|
39
|
+
"happy-dom": "20.3.7",
|
|
40
40
|
"jsdom": "27.4.0",
|
|
41
|
-
"lint-staged": "
|
|
41
|
+
"lint-staged": "16.2.7",
|
|
42
|
+
"mongodb": "7.1.0",
|
|
42
43
|
"nuxt": "4.2.2",
|
|
43
44
|
"oxfmt": "latest",
|
|
44
45
|
"oxlint": "latest",
|
|
45
46
|
"rimraf": "6.1.2",
|
|
46
|
-
"simple-git-hooks": "
|
|
47
|
+
"simple-git-hooks": "2.13.1",
|
|
47
48
|
"tailwindcss": "4.1.18",
|
|
48
49
|
"typescript": "5.9.3",
|
|
49
|
-
"vitest": "
|
|
50
|
+
"vitest": "3.2.4"
|
|
50
51
|
},
|
|
51
52
|
"engines": {
|
|
52
53
|
"node": ">=22",
|
|
@@ -1860,7 +1861,9 @@
|
|
|
1860
1861
|
}
|
|
1861
1862
|
},
|
|
1862
1863
|
"node_modules/@isaacs/brace-expansion": {
|
|
1863
|
-
"version": "5.0.
|
|
1864
|
+
"version": "5.0.1",
|
|
1865
|
+
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
|
1866
|
+
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
|
1864
1867
|
"license": "MIT",
|
|
1865
1868
|
"dependencies": {
|
|
1866
1869
|
"@isaacs/balanced-match": "^4.0.1"
|
|
@@ -2433,6 +2436,16 @@
|
|
|
2433
2436
|
"node": ">=18"
|
|
2434
2437
|
}
|
|
2435
2438
|
},
|
|
2439
|
+
"node_modules/@mongodb-js/saslprep": {
|
|
2440
|
+
"version": "1.4.5",
|
|
2441
|
+
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz",
|
|
2442
|
+
"integrity": "sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==",
|
|
2443
|
+
"devOptional": true,
|
|
2444
|
+
"license": "MIT",
|
|
2445
|
+
"dependencies": {
|
|
2446
|
+
"sparse-bitfield": "^3.0.3"
|
|
2447
|
+
}
|
|
2448
|
+
},
|
|
2436
2449
|
"node_modules/@napi-rs/wasm-runtime": {
|
|
2437
2450
|
"version": "1.1.1",
|
|
2438
2451
|
"license": "MIT",
|
|
@@ -7216,6 +7229,13 @@
|
|
|
7216
7229
|
"version": "0.0.21",
|
|
7217
7230
|
"license": "MIT"
|
|
7218
7231
|
},
|
|
7232
|
+
"node_modules/@types/webidl-conversions": {
|
|
7233
|
+
"version": "7.0.3",
|
|
7234
|
+
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
|
7235
|
+
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
|
7236
|
+
"devOptional": true,
|
|
7237
|
+
"license": "MIT"
|
|
7238
|
+
},
|
|
7219
7239
|
"node_modules/@types/whatwg-mimetype": {
|
|
7220
7240
|
"version": "3.0.2",
|
|
7221
7241
|
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
|
@@ -7223,6 +7243,16 @@
|
|
|
7223
7243
|
"dev": true,
|
|
7224
7244
|
"license": "MIT"
|
|
7225
7245
|
},
|
|
7246
|
+
"node_modules/@types/whatwg-url": {
|
|
7247
|
+
"version": "13.0.0",
|
|
7248
|
+
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
|
|
7249
|
+
"integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
|
|
7250
|
+
"devOptional": true,
|
|
7251
|
+
"license": "MIT",
|
|
7252
|
+
"dependencies": {
|
|
7253
|
+
"@types/webidl-conversions": "*"
|
|
7254
|
+
}
|
|
7255
|
+
},
|
|
7226
7256
|
"node_modules/@types/ws": {
|
|
7227
7257
|
"version": "8.18.1",
|
|
7228
7258
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
|
@@ -8499,6 +8529,16 @@
|
|
|
8499
8529
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
|
8500
8530
|
}
|
|
8501
8531
|
},
|
|
8532
|
+
"node_modules/bson": {
|
|
8533
|
+
"version": "7.2.0",
|
|
8534
|
+
"resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
|
|
8535
|
+
"integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
|
|
8536
|
+
"devOptional": true,
|
|
8537
|
+
"license": "Apache-2.0",
|
|
8538
|
+
"engines": {
|
|
8539
|
+
"node": ">=20.19.0"
|
|
8540
|
+
}
|
|
8541
|
+
},
|
|
8502
8542
|
"node_modules/buffer": {
|
|
8503
8543
|
"version": "6.0.3",
|
|
8504
8544
|
"funding": [
|
|
@@ -9957,7 +9997,9 @@
|
|
|
9957
9997
|
}
|
|
9958
9998
|
},
|
|
9959
9999
|
"node_modules/fast-xml-parser": {
|
|
9960
|
-
"version": "5.3.
|
|
10000
|
+
"version": "5.3.4",
|
|
10001
|
+
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz",
|
|
10002
|
+
"integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==",
|
|
9961
10003
|
"dev": true,
|
|
9962
10004
|
"funding": [
|
|
9963
10005
|
{
|
|
@@ -11929,6 +11971,13 @@
|
|
|
11929
11971
|
"version": "2.0.0",
|
|
11930
11972
|
"license": "MIT"
|
|
11931
11973
|
},
|
|
11974
|
+
"node_modules/memory-pager": {
|
|
11975
|
+
"version": "1.5.0",
|
|
11976
|
+
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
|
11977
|
+
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
|
11978
|
+
"devOptional": true,
|
|
11979
|
+
"license": "MIT"
|
|
11980
|
+
},
|
|
11932
11981
|
"node_modules/merge-stream": {
|
|
11933
11982
|
"version": "2.0.0",
|
|
11934
11983
|
"license": "MIT"
|
|
@@ -12083,6 +12132,105 @@
|
|
|
12083
12132
|
"version": "4.6.7",
|
|
12084
12133
|
"license": "MIT"
|
|
12085
12134
|
},
|
|
12135
|
+
"node_modules/mongodb": {
|
|
12136
|
+
"version": "7.1.0",
|
|
12137
|
+
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.0.tgz",
|
|
12138
|
+
"integrity": "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==",
|
|
12139
|
+
"devOptional": true,
|
|
12140
|
+
"license": "Apache-2.0",
|
|
12141
|
+
"peer": true,
|
|
12142
|
+
"dependencies": {
|
|
12143
|
+
"@mongodb-js/saslprep": "^1.3.0",
|
|
12144
|
+
"bson": "^7.1.1",
|
|
12145
|
+
"mongodb-connection-string-url": "^7.0.0"
|
|
12146
|
+
},
|
|
12147
|
+
"engines": {
|
|
12148
|
+
"node": ">=20.19.0"
|
|
12149
|
+
},
|
|
12150
|
+
"peerDependencies": {
|
|
12151
|
+
"@aws-sdk/credential-providers": "^3.806.0",
|
|
12152
|
+
"@mongodb-js/zstd": "^7.0.0",
|
|
12153
|
+
"gcp-metadata": "^7.0.1",
|
|
12154
|
+
"kerberos": "^7.0.0",
|
|
12155
|
+
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
|
|
12156
|
+
"snappy": "^7.3.2",
|
|
12157
|
+
"socks": "^2.8.6"
|
|
12158
|
+
},
|
|
12159
|
+
"peerDependenciesMeta": {
|
|
12160
|
+
"@aws-sdk/credential-providers": {
|
|
12161
|
+
"optional": true
|
|
12162
|
+
},
|
|
12163
|
+
"@mongodb-js/zstd": {
|
|
12164
|
+
"optional": true
|
|
12165
|
+
},
|
|
12166
|
+
"gcp-metadata": {
|
|
12167
|
+
"optional": true
|
|
12168
|
+
},
|
|
12169
|
+
"kerberos": {
|
|
12170
|
+
"optional": true
|
|
12171
|
+
},
|
|
12172
|
+
"mongodb-client-encryption": {
|
|
12173
|
+
"optional": true
|
|
12174
|
+
},
|
|
12175
|
+
"snappy": {
|
|
12176
|
+
"optional": true
|
|
12177
|
+
},
|
|
12178
|
+
"socks": {
|
|
12179
|
+
"optional": true
|
|
12180
|
+
}
|
|
12181
|
+
}
|
|
12182
|
+
},
|
|
12183
|
+
"node_modules/mongodb-connection-string-url": {
|
|
12184
|
+
"version": "7.0.1",
|
|
12185
|
+
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
|
|
12186
|
+
"integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
|
|
12187
|
+
"devOptional": true,
|
|
12188
|
+
"license": "Apache-2.0",
|
|
12189
|
+
"dependencies": {
|
|
12190
|
+
"@types/whatwg-url": "^13.0.0",
|
|
12191
|
+
"whatwg-url": "^14.1.0"
|
|
12192
|
+
},
|
|
12193
|
+
"engines": {
|
|
12194
|
+
"node": ">=20.19.0"
|
|
12195
|
+
}
|
|
12196
|
+
},
|
|
12197
|
+
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
|
12198
|
+
"version": "5.1.1",
|
|
12199
|
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
|
12200
|
+
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
|
12201
|
+
"devOptional": true,
|
|
12202
|
+
"license": "MIT",
|
|
12203
|
+
"dependencies": {
|
|
12204
|
+
"punycode": "^2.3.1"
|
|
12205
|
+
},
|
|
12206
|
+
"engines": {
|
|
12207
|
+
"node": ">=18"
|
|
12208
|
+
}
|
|
12209
|
+
},
|
|
12210
|
+
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
|
12211
|
+
"version": "7.0.0",
|
|
12212
|
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
|
12213
|
+
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
|
12214
|
+
"devOptional": true,
|
|
12215
|
+
"license": "BSD-2-Clause",
|
|
12216
|
+
"engines": {
|
|
12217
|
+
"node": ">=12"
|
|
12218
|
+
}
|
|
12219
|
+
},
|
|
12220
|
+
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
|
12221
|
+
"version": "14.2.0",
|
|
12222
|
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
|
12223
|
+
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
|
12224
|
+
"devOptional": true,
|
|
12225
|
+
"license": "MIT",
|
|
12226
|
+
"dependencies": {
|
|
12227
|
+
"tr46": "^5.1.0",
|
|
12228
|
+
"webidl-conversions": "^7.0.0"
|
|
12229
|
+
},
|
|
12230
|
+
"engines": {
|
|
12231
|
+
"node": ">=18"
|
|
12232
|
+
}
|
|
12233
|
+
},
|
|
12086
12234
|
"node_modules/motion-dom": {
|
|
12087
12235
|
"version": "12.24.11",
|
|
12088
12236
|
"license": "MIT",
|
|
@@ -14625,7 +14773,7 @@
|
|
|
14625
14773
|
},
|
|
14626
14774
|
"node_modules/punycode": {
|
|
14627
14775
|
"version": "2.3.1",
|
|
14628
|
-
"
|
|
14776
|
+
"devOptional": true,
|
|
14629
14777
|
"license": "MIT",
|
|
14630
14778
|
"engines": {
|
|
14631
14779
|
"node": ">=6"
|
|
@@ -15578,6 +15726,16 @@
|
|
|
15578
15726
|
"node": ">=0.10.0"
|
|
15579
15727
|
}
|
|
15580
15728
|
},
|
|
15729
|
+
"node_modules/sparse-bitfield": {
|
|
15730
|
+
"version": "3.0.3",
|
|
15731
|
+
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
|
15732
|
+
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
|
15733
|
+
"devOptional": true,
|
|
15734
|
+
"license": "MIT",
|
|
15735
|
+
"dependencies": {
|
|
15736
|
+
"memory-pager": "^1.0.2"
|
|
15737
|
+
}
|
|
15738
|
+
},
|
|
15581
15739
|
"node_modules/speakingurl": {
|
|
15582
15740
|
"version": "14.0.1",
|
|
15583
15741
|
"license": "BSD-3-Clause",
|
|
@@ -15885,9 +16043,9 @@
|
|
|
15885
16043
|
}
|
|
15886
16044
|
},
|
|
15887
16045
|
"node_modules/tar": {
|
|
15888
|
-
"version": "7.5.
|
|
15889
|
-
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.
|
|
15890
|
-
"integrity": "sha512-
|
|
16046
|
+
"version": "7.5.7",
|
|
16047
|
+
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
|
16048
|
+
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
|
15891
16049
|
"license": "BlueOak-1.0.0",
|
|
15892
16050
|
"dependencies": {
|
|
15893
16051
|
"@isaacs/fs-minipass": "^4.0.0",
|
|
@@ -69,20 +69,21 @@
|
|
|
69
69
|
"@tailwindcss/vite": "4.1.18",
|
|
70
70
|
"@types/node": "25.0.6",
|
|
71
71
|
"@types/qrcode": "1.5.6",
|
|
72
|
-
"@vitejs/plugin-vue": "
|
|
73
|
-
"@vue/test-utils": "
|
|
72
|
+
"@vitejs/plugin-vue": "6.0.3",
|
|
73
|
+
"@vue/test-utils": "2.4.6",
|
|
74
74
|
"dayjs-nuxt": "2.1.11",
|
|
75
|
-
"happy-dom": "
|
|
75
|
+
"happy-dom": "20.3.7",
|
|
76
76
|
"jsdom": "27.4.0",
|
|
77
|
-
"lint-staged": "
|
|
77
|
+
"lint-staged": "16.2.7",
|
|
78
|
+
"mongodb": "7.1.0",
|
|
78
79
|
"nuxt": "4.2.2",
|
|
79
80
|
"oxfmt": "latest",
|
|
80
81
|
"oxlint": "latest",
|
|
81
82
|
"rimraf": "6.1.2",
|
|
82
|
-
"simple-git-hooks": "
|
|
83
|
+
"simple-git-hooks": "2.13.1",
|
|
83
84
|
"tailwindcss": "4.1.18",
|
|
84
85
|
"typescript": "5.9.3",
|
|
85
|
-
"vitest": "
|
|
86
|
+
"vitest": "3.2.4"
|
|
86
87
|
},
|
|
87
88
|
"engines": {
|
|
88
89
|
"node": ">=22",
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { expect, test } from '@nuxt/test-utils/playwright';
|
|
2
|
+
import type { Page } from '@playwright/test';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import {
|
|
5
|
+
extractTOTPSecret,
|
|
6
|
+
fillInput,
|
|
7
|
+
generateTestUser,
|
|
8
|
+
generateTOTP,
|
|
9
|
+
gotoAndWaitForHydration,
|
|
10
|
+
waitForURLAndHydration,
|
|
11
|
+
} from '@lenne.tech/nuxt-extensions/testing';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Authentication E2E Tests - Feature Ordering & Error Translations
|
|
15
|
+
*
|
|
16
|
+
* Tests:
|
|
17
|
+
* - Test 1: Register → 2FA → Passkey (without logout) - order independence
|
|
18
|
+
* - Test 2: Register → Passkey → 2FA (without logout) - order independence
|
|
19
|
+
* - Test 3: Error Translations (German error messages, i18n endpoint)
|
|
20
|
+
*
|
|
21
|
+
* Automatically detects backend configuration via /iam/features.
|
|
22
|
+
*
|
|
23
|
+
* Requirements:
|
|
24
|
+
* - API: nest-server-starter OR nest-server running on port 3000
|
|
25
|
+
* (stdout redirected to /tmp/nest-server.log or NEST_SERVER_LOG)
|
|
26
|
+
* - Frontend: nuxt-base-starter running on port 3001
|
|
27
|
+
*
|
|
28
|
+
* See auth-lifecycle.spec.ts for full documentation on backend options,
|
|
29
|
+
* configuration scenarios, and how to run against all 4 configurations.
|
|
30
|
+
*
|
|
31
|
+
* Run: npx playwright test tests/e2e/auth-feature-order.spec.ts
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Types
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
interface Features {
|
|
39
|
+
emailVerification: boolean;
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
jwt: boolean;
|
|
42
|
+
passkey: boolean;
|
|
43
|
+
signUpChecks: boolean;
|
|
44
|
+
twoFactor: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Constants
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
const API_BASE = 'http://localhost:3000';
|
|
52
|
+
const FRONTEND_BASE = 'http://localhost:3001';
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Helpers
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract email verification token from backend server logs.
|
|
60
|
+
* The nest-server logs: [EMAIL VERIFICATION] User: <email>, URL: ...?token=<jwt>
|
|
61
|
+
*/
|
|
62
|
+
function getVerificationTokenFromLog(email: string): string | null {
|
|
63
|
+
const logPath = process.env.NEST_SERVER_LOG || '/tmp/nest-server.log';
|
|
64
|
+
try {
|
|
65
|
+
const log = fs.readFileSync(logPath, 'utf-8');
|
|
66
|
+
const regex = new RegExp(`\\[EMAIL VERIFICATION\\] User: ${email.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}, URL: .+?token=([^&\\s]+)`);
|
|
67
|
+
const match = log.match(regex);
|
|
68
|
+
return match?.[1] ?? null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Register a new user via UI.
|
|
76
|
+
* Adapts to current configuration (terms checkbox, email verification).
|
|
77
|
+
*/
|
|
78
|
+
async function registerUser(
|
|
79
|
+
page: Page,
|
|
80
|
+
user: { email: string; password: string; name: string },
|
|
81
|
+
features: Features,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
await gotoAndWaitForHydration(page, '/auth/register');
|
|
84
|
+
await page.locator('input[name="name"]').waitFor({ state: 'visible', timeout: 10000 });
|
|
85
|
+
|
|
86
|
+
await fillInput(page, 'input[name="name"]', user.name);
|
|
87
|
+
await fillInput(page, 'input[name="email"]', user.email);
|
|
88
|
+
await fillInput(page, 'input[name="password"]', user.password);
|
|
89
|
+
await fillInput(page, 'input[name="confirmPassword"]', user.password);
|
|
90
|
+
|
|
91
|
+
// Accept terms if signUpChecks is enabled
|
|
92
|
+
if (features.signUpChecks) {
|
|
93
|
+
const termsCheckbox = page.getByRole('checkbox', { name: /akzeptiere die AGB/i });
|
|
94
|
+
await termsCheckbox.waitFor({ state: 'visible', timeout: 5000 });
|
|
95
|
+
await termsCheckbox.check();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await page.getByRole('button', { name: 'Konto erstellen' }).click();
|
|
99
|
+
|
|
100
|
+
if (features.emailVerification) {
|
|
101
|
+
// Wait for redirect to verify-email page
|
|
102
|
+
await waitForURLAndHydration(page, /\/auth\/verify-email/, { timeout: 15000 });
|
|
103
|
+
|
|
104
|
+
// Extract token from backend logs and verify email
|
|
105
|
+
let token: string | null = null;
|
|
106
|
+
for (let i = 0; i < 10; i++) {
|
|
107
|
+
token = getVerificationTokenFromLog(user.email);
|
|
108
|
+
if (token) break;
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
110
|
+
}
|
|
111
|
+
expect(token, 'Verification token not found in server logs').not.toBeNull();
|
|
112
|
+
|
|
113
|
+
await gotoAndWaitForHydration(page, `/auth/verify-email?token=${token}`);
|
|
114
|
+
await expect(page.getByRole('heading', { name: 'E-Mail bestätigt' })).toBeVisible({ timeout: 15000 });
|
|
115
|
+
|
|
116
|
+
// Login after verification
|
|
117
|
+
await page.getByRole('link', { name: 'Jetzt anmelden' }).click();
|
|
118
|
+
await waitForURLAndHydration(page, /\/auth\/login/, { timeout: 10000 });
|
|
119
|
+
await loginWithEmail(page, user.email, user.password);
|
|
120
|
+
await waitForURLAndHydration(page, /\/app/, { timeout: 15000 });
|
|
121
|
+
} else {
|
|
122
|
+
// Wait for passkey prompt and dismiss it
|
|
123
|
+
const laterButton = page.getByRole('button', { name: 'Später einrichten' });
|
|
124
|
+
await laterButton.waitFor({ state: 'visible', timeout: 10000 });
|
|
125
|
+
await laterButton.click();
|
|
126
|
+
await waitForURLAndHydration(page, /\/app/, { timeout: 15000 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Login with email and password via UI
|
|
132
|
+
*/
|
|
133
|
+
async function loginWithEmail(page: Page, email: string, password: string): Promise<void> {
|
|
134
|
+
await gotoAndWaitForHydration(page, '/auth/login');
|
|
135
|
+
await page.locator('input[name="email"]').waitFor({ state: 'visible', timeout: 10000 });
|
|
136
|
+
await fillInput(page, 'input[name="email"]', email);
|
|
137
|
+
await fillInput(page, 'input[name="password"]', password);
|
|
138
|
+
await page.getByRole('button', { name: 'Anmelden', exact: true }).click();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Enable 2FA and return the TOTP secret.
|
|
143
|
+
* Uses network interception to extract the TOTP URI (QR code is SVG via v-html).
|
|
144
|
+
*/
|
|
145
|
+
async function enable2FA(page: Page, password: string): Promise<string> {
|
|
146
|
+
await gotoAndWaitForHydration(page, '/app/settings/security');
|
|
147
|
+
|
|
148
|
+
const enableButton = page.getByRole('button', { name: '2FA aktivieren' });
|
|
149
|
+
await enableButton.waitFor({ state: 'visible', timeout: 10000 });
|
|
150
|
+
|
|
151
|
+
const passwordInput = page.locator('input[type="password"]');
|
|
152
|
+
await passwordInput.click();
|
|
153
|
+
await page.keyboard.type(password, { delay: 5 });
|
|
154
|
+
|
|
155
|
+
// Intercept the 2FA enable response to extract TOTP URI
|
|
156
|
+
const responsePromise = page.waitForResponse(
|
|
157
|
+
resp => resp.url().includes('/two-factor/enable') && resp.status() === 200,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await enableButton.click();
|
|
161
|
+
|
|
162
|
+
const response = await responsePromise;
|
|
163
|
+
const responseBody = await response.json();
|
|
164
|
+
const totpUri = responseBody.totpURI || responseBody.data?.totpURI;
|
|
165
|
+
expect(totpUri, '2FA enable response should contain totpURI').toBeTruthy();
|
|
166
|
+
|
|
167
|
+
const secret = extractTOTPSecret(totpUri);
|
|
168
|
+
expect(secret).not.toBeNull();
|
|
169
|
+
|
|
170
|
+
// Wait for QR code SVG to render
|
|
171
|
+
await page.locator('.bg-white svg').waitFor({ state: 'visible', timeout: 10000 });
|
|
172
|
+
|
|
173
|
+
// Verify TOTP
|
|
174
|
+
const totpCode = generateTOTP(secret!);
|
|
175
|
+
await fillInput(page, 'input[placeholder="000000"]', totpCode);
|
|
176
|
+
await page.getByRole('button', { name: 'Verifizieren' }).click();
|
|
177
|
+
|
|
178
|
+
// Close backup codes dialog
|
|
179
|
+
await expect(page.getByRole('heading', { name: 'Backup-Codes' })).toBeVisible({ timeout: 10000 });
|
|
180
|
+
await page.keyboard.press('Escape');
|
|
181
|
+
|
|
182
|
+
return secret!;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Cleanup Virtual Authenticator
|
|
187
|
+
*/
|
|
188
|
+
async function cleanupAuthenticator(cdpSession: any, authenticatorId: string): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
await cdpSession.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
|
|
191
|
+
await cdpSession.send('WebAuthn.disable');
|
|
192
|
+
} catch {
|
|
193
|
+
// Ignore cleanup errors
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// API Availability & Feature Detection
|
|
199
|
+
// =============================================================================
|
|
200
|
+
|
|
201
|
+
let apiAvailable = false;
|
|
202
|
+
let features: Features | null = null;
|
|
203
|
+
|
|
204
|
+
test.beforeAll(async ({ request }) => {
|
|
205
|
+
let frontendAvailable = false;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const apiResponse = await request.get(`${API_BASE}/`);
|
|
209
|
+
apiAvailable = apiResponse.ok();
|
|
210
|
+
} catch {
|
|
211
|
+
apiAvailable = false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const frontendResponse = await request.get(`${FRONTEND_BASE}/`);
|
|
216
|
+
frontendAvailable = frontendResponse.ok();
|
|
217
|
+
} catch {
|
|
218
|
+
frontendAvailable = false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!apiAvailable || !frontendAvailable) {
|
|
222
|
+
console.error('');
|
|
223
|
+
console.error('╔══════════════════════════════════════════════════════════════╗');
|
|
224
|
+
console.error('║ E2E TESTS REQUIRE RUNNING SERVERS ║');
|
|
225
|
+
console.error('╠══════════════════════════════════════════════════════════════╣');
|
|
226
|
+
console.error(`║ ${apiAvailable ? '✓' : '✗'} API Server (localhost:3000) ║`);
|
|
227
|
+
console.error(`║ ${frontendAvailable ? '✓' : '✗'} Frontend (localhost:3001) ║`);
|
|
228
|
+
console.error('╚══════════════════════════════════════════════════════════════╝');
|
|
229
|
+
apiAvailable = false;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
apiAvailable = true;
|
|
234
|
+
|
|
235
|
+
// Detect backend configuration
|
|
236
|
+
try {
|
|
237
|
+
const featuresResponse = await request.get(`${API_BASE}/iam/features`);
|
|
238
|
+
features = await featuresResponse.json() as Features;
|
|
239
|
+
} catch {
|
|
240
|
+
features = {
|
|
241
|
+
emailVerification: true,
|
|
242
|
+
enabled: true,
|
|
243
|
+
jwt: false,
|
|
244
|
+
passkey: true,
|
|
245
|
+
signUpChecks: true,
|
|
246
|
+
twoFactor: true,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// =============================================================================
|
|
252
|
+
// Test 1: Register -> 2FA -> Passkey (without logout)
|
|
253
|
+
// =============================================================================
|
|
254
|
+
|
|
255
|
+
test.describe.serial('Test 1: Register -> 2FA -> Passkey (no logout)', () => {
|
|
256
|
+
const testUser = generateTestUser('2fa-then-passkey');
|
|
257
|
+
|
|
258
|
+
test('Register, enable 2FA, then add Passkey without logout', async ({ page, context }) => {
|
|
259
|
+
test.skip(!apiAvailable, 'Servers not running');
|
|
260
|
+
|
|
261
|
+
// Register (adapts to config)
|
|
262
|
+
await registerUser(page, testUser, features!);
|
|
263
|
+
|
|
264
|
+
// Enable 2FA
|
|
265
|
+
await enable2FA(page, testUser.password);
|
|
266
|
+
|
|
267
|
+
// Add Passkey
|
|
268
|
+
const cdpSession = await context.newCDPSession(page);
|
|
269
|
+
await cdpSession.send('WebAuthn.enable');
|
|
270
|
+
|
|
271
|
+
const { authenticatorId } = await cdpSession.send(
|
|
272
|
+
'WebAuthn.addVirtualAuthenticator',
|
|
273
|
+
{
|
|
274
|
+
options: {
|
|
275
|
+
protocol: 'ctap2',
|
|
276
|
+
transport: 'internal',
|
|
277
|
+
hasResidentKey: true,
|
|
278
|
+
hasUserVerification: true,
|
|
279
|
+
isUserVerified: true,
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await gotoAndWaitForHydration(page, '/app/settings/security');
|
|
286
|
+
await page.getByRole('button', { name: 'Passkey hinzufügen' }).click();
|
|
287
|
+
await page.getByPlaceholder('Name für den Passkey').fill('After-2FA-Passkey');
|
|
288
|
+
await page.getByRole('button', { name: 'Hinzufügen' }).click();
|
|
289
|
+
|
|
290
|
+
await expect(page.getByText('After-2FA-Passkey')).toBeVisible({ timeout: 15000 });
|
|
291
|
+
await expect(page.getByText('2FA ist aktiviert')).toBeVisible();
|
|
292
|
+
} finally {
|
|
293
|
+
await cleanupAuthenticator(cdpSession, authenticatorId);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// =============================================================================
|
|
299
|
+
// Test 2: Register -> Passkey -> 2FA (without logout)
|
|
300
|
+
// =============================================================================
|
|
301
|
+
|
|
302
|
+
test.describe.serial('Test 2: Register -> Passkey -> 2FA (no logout)', () => {
|
|
303
|
+
const testUser = generateTestUser('passkey-then-2fa');
|
|
304
|
+
|
|
305
|
+
test('Register, add Passkey, then enable 2FA without logout', async ({ page, context }) => {
|
|
306
|
+
test.skip(!apiAvailable, 'Servers not running');
|
|
307
|
+
|
|
308
|
+
// Register (adapts to config)
|
|
309
|
+
await registerUser(page, testUser, features!);
|
|
310
|
+
|
|
311
|
+
// Add Passkey first
|
|
312
|
+
const cdpSession = await context.newCDPSession(page);
|
|
313
|
+
await cdpSession.send('WebAuthn.enable');
|
|
314
|
+
|
|
315
|
+
const { authenticatorId } = await cdpSession.send(
|
|
316
|
+
'WebAuthn.addVirtualAuthenticator',
|
|
317
|
+
{
|
|
318
|
+
options: {
|
|
319
|
+
protocol: 'ctap2',
|
|
320
|
+
transport: 'internal',
|
|
321
|
+
hasResidentKey: true,
|
|
322
|
+
hasUserVerification: true,
|
|
323
|
+
isUserVerified: true,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
await gotoAndWaitForHydration(page, '/app/settings/security');
|
|
330
|
+
await page.getByRole('button', { name: 'Passkey hinzufügen' }).click();
|
|
331
|
+
await page.getByPlaceholder('Name für den Passkey').fill('Before-2FA-Passkey');
|
|
332
|
+
await page.getByRole('button', { name: 'Hinzufügen' }).click();
|
|
333
|
+
|
|
334
|
+
await expect(page.getByText('Before-2FA-Passkey')).toBeVisible({ timeout: 15000 });
|
|
335
|
+
|
|
336
|
+
// Enable 2FA
|
|
337
|
+
await enable2FA(page, testUser.password);
|
|
338
|
+
|
|
339
|
+
// Verify both are active
|
|
340
|
+
await gotoAndWaitForHydration(page, '/app/settings/security');
|
|
341
|
+
await expect(page.getByText('2FA ist aktiviert')).toBeVisible();
|
|
342
|
+
await expect(page.getByText('Before-2FA-Passkey')).toBeVisible();
|
|
343
|
+
} finally {
|
|
344
|
+
await cleanupAuthenticator(cdpSession, authenticatorId);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// =============================================================================
|
|
350
|
+
// Test 3: Error Translations
|
|
351
|
+
// =============================================================================
|
|
352
|
+
|
|
353
|
+
test.describe('Test 3: Error Translations', () => {
|
|
354
|
+
test('3.1 Invalid credentials shows German error message', async ({ page }) => {
|
|
355
|
+
test.skip(!apiAvailable, 'Servers not running');
|
|
356
|
+
|
|
357
|
+
await loginWithEmail(page, 'invalid@test.com', 'WrongPassword123!');
|
|
358
|
+
|
|
359
|
+
const toast = page.locator('li[role="alert"]');
|
|
360
|
+
await expect(toast).toBeVisible({ timeout: 10000 });
|
|
361
|
+
await expect(toast).toContainText('Ungültige Anmeldedaten');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('3.2 Error translations are loaded from backend', async ({ page }) => {
|
|
365
|
+
test.skip(!apiAvailable, 'Servers not running');
|
|
366
|
+
|
|
367
|
+
await gotoAndWaitForHydration(page, '/auth/login');
|
|
368
|
+
|
|
369
|
+
const response = await page.request.get(`${FRONTEND_BASE}/api/i18n/errors/de`);
|
|
370
|
+
expect([200, 304]).toContain(response.status());
|
|
371
|
+
|
|
372
|
+
if (response.status() === 200) {
|
|
373
|
+
const data = await response.json();
|
|
374
|
+
expect(data).toHaveProperty('errors');
|
|
375
|
+
expect(data.errors).toHaveProperty('LTNS_0010');
|
|
376
|
+
console.info(` Error translations loaded: ${Object.keys(data.errors).length} codes`);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|