1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-16 11:21:12 +00:00

feat: add qrcode representation of one time link (#424) (#436)

Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
Co-authored-by: Kyle Mendell <kmendell@outlook.com>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Grégory Paul
2025-04-14 15:16:46 +02:00
committed by GitHub
parent 57cb8f8795
commit abf17f6211
9 changed files with 5643 additions and 5285 deletions

View File

@@ -1,12 +1,12 @@
{ {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "0.45.0", "version": "0.46.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "0.45.0", "version": "0.46.0",
"dependencies": { "dependencies": {
"@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/browser": "^13.1.0",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
@@ -17,6 +17,7 @@
"jose": "^5.9.6", "jose": "^5.9.6",
"lucide-svelte": "^0.487.0", "lucide-svelte": "^0.487.0",
"mode-watcher": "^0.5.1", "mode-watcher": "^0.5.1",
"qrcode": "^1.5.4",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.23.1", "sveltekit-superforms": "^2.23.1",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
@@ -35,6 +36,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/node": "^22.10.10", "@types/node": "^22.10.10",
"@types/qrcode": "^1.5.5",
"bits-ui": "^0.22.0", "bits-ui": "^0.22.0",
"cmdk-sv": "^0.0.19", "cmdk-sv": "^0.0.19",
"eslint": "^9.19.0", "eslint": "^9.19.0",
@@ -1725,6 +1727,15 @@
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
} }
}, },
"node_modules/@types/qrcode": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
"integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -2036,11 +2047,19 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
}, },
@@ -2237,6 +2256,17 @@
"validator": "^13.9.0" "validator": "^13.9.0"
} }
}, },
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2296,7 +2326,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
}, },
@@ -2307,8 +2336,7 @@
"node_modules/color-name": { "node_modules/color-name": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"dev": true
}, },
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
@@ -2439,6 +2467,15 @@
} }
} }
}, },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dedent": { "node_modules/dedent": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz",
@@ -2501,6 +2538,12 @@
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==" "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="
}, },
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dlv": { "node_modules/dlv": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -2516,6 +2559,12 @@
"fast-check": "^3.23.1" "fast-check": "^3.23.1"
} }
}, },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.0", "version": "5.18.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
@@ -3080,6 +3129,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -3223,6 +3281,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": { "node_modules/is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -3858,6 +3925,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3874,7 +3950,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -3941,6 +4016,15 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@@ -4227,6 +4311,22 @@
], ],
"optional": true "optional": true
}, },
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -4276,6 +4376,21 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -4398,6 +4513,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-cookie-parser": { "node_modules/set-cookie-parser": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
@@ -4476,6 +4597,32 @@
"kysely": "*" "kysely": "*"
} }
}, },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -5128,6 +5275,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -5137,6 +5290,26 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
@@ -5150,6 +5323,102 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yargs-parser/node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/yargs/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yargs/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -22,6 +22,7 @@
"jose": "^5.9.6", "jose": "^5.9.6",
"lucide-svelte": "^0.487.0", "lucide-svelte": "^0.487.0",
"mode-watcher": "^0.5.1", "mode-watcher": "^0.5.1",
"qrcode": "^1.5.4",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.23.1", "sveltekit-superforms": "^2.23.1",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
@@ -40,6 +41,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/node": "^22.10.10", "@types/node": "^22.10.10",
"@types/qrcode": "^1.5.5",
"bits-ui": "^0.22.0", "bits-ui": "^0.22.0",
"cmdk-sv": "^0.0.19", "cmdk-sv": "^0.0.19",
"eslint": "^9.19.0", "eslint": "^9.19.0",

View File

@@ -1,13 +1,16 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
import Qrcode from '$lib/components/qrcode/qrcode.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import Input from '$lib/components/ui/input/input.svelte';
import Label from '$lib/components/ui/label/label.svelte'; import Label from '$lib/components/ui/label/label.svelte';
import * as Select from '$lib/components/ui/select/index.js'; import * as Select from '$lib/components/ui/select/index.js';
import { Separator } from '$lib/components/ui/separator';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
import { axiosErrorToast } from '$lib/utils/error-util'; import { axiosErrorToast } from '$lib/utils/error-util';
import { mode } from 'mode-watcher';
let { let {
userId = $bindable() userId = $bindable()
@@ -18,6 +21,7 @@
const userService = new UserService(); const userService = new UserService();
let oneTimeLink: string | null = $state(null); let oneTimeLink: string | null = $state(null);
let code: string | null = $state(null);
let selectedExpiration: keyof typeof availableExpirations = $state(m.one_hour()); let selectedExpiration: keyof typeof availableExpirations = $state(m.one_hour());
let availableExpirations = { let availableExpirations = {
@@ -31,8 +35,8 @@
async function createOneTimeAccessToken() { async function createOneTimeAccessToken() {
try { try {
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000); const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
const token = await userService.createOneTimeAccessToken(expiration, userId!); code = await userService.createOneTimeAccessToken(expiration, userId!);
oneTimeLink = `${page.url.origin}/lc/${token}`; oneTimeLink = `${page.url.origin}/lc/${code}`;
} catch (e) { } catch (e) {
axiosErrorToast(e); axiosErrorToast(e);
} }
@@ -41,6 +45,7 @@
function onOpenChange(open: boolean) { function onOpenChange(open: boolean) {
if (!open) { if (!open) {
oneTimeLink = null; oneTimeLink = null;
code = null;
userId = null; userId = null;
} }
} }
@@ -54,6 +59,7 @@
>{m.create_a_login_code_to_sign_in_without_a_passkey_once()}</Dialog.Description >{m.create_a_login_code_to_sign_in_without_a_passkey_once()}</Dialog.Description
> >
</Dialog.Header> </Dialog.Header>
{#if oneTimeLink === null} {#if oneTimeLink === null}
<div> <div>
<Label for="expiration">{m.expiration()}</Label> <Label for="expiration">{m.expiration()}</Label>
@@ -65,7 +71,7 @@
onSelectedChange={(v) => onSelectedChange={(v) =>
(selectedExpiration = v!.value as keyof typeof availableExpirations)} (selectedExpiration = v!.value as keyof typeof availableExpirations)}
> >
<Select.Trigger class="h-9 "> <Select.Trigger class="h-9 w-full">
<Select.Value>{selectedExpiration}</Select.Value> <Select.Value>{selectedExpiration}</Select.Value>
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
@@ -75,12 +81,36 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> </div>
<Button onclick={() => createOneTimeAccessToken()} disabled={!selectedExpiration}> <Button
onclick={() => createOneTimeAccessToken()}
disabled={!selectedExpiration}
class="mt-2 w-full"
>
{m.generate_code()} {m.generate_code()}
</Button> </Button>
{:else} {:else}
<Label for="login-code" class="sr-only">{m.login_code()}</Label> <div class="flex flex-col items-center gap-2">
<Input id="login-code" value={oneTimeLink} readonly /> <CopyToClipboard value={code!}>
<p class="text-3xl font-semibold">{code}</p>
</CopyToClipboard>
<div class="text-muted-foreground my-2 flex items-center justify-center gap-3">
<Separator />
<p class="text-nowrap text-xs">{m.or_visit()}</p>
<Separator />
</div>
<Qrcode
class="mb-2"
value={oneTimeLink}
size={180}
color={$mode === 'dark' ? '#FFFFFF' : '#000000'}
backgroundColor={$mode === 'dark' ? '#000000' : '#FFFFFF'}
/>
<CopyToClipboard value={oneTimeLink!}>
<p data-testId="login-code-link">{oneTimeLink!}</p>
</CopyToClipboard>
</div>
{/if} {/if}
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { cn } from '$lib/utils/style';
import QRCode from 'qrcode';
import { onMount } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
let canvasEl: HTMLCanvasElement | null;
let {
value,
size = 200,
color = '#000000',
backgroundColor = '#FFFFFF',
...restProps
}: HTMLAttributes<HTMLCanvasElement> & {
value: string | null;
size?: number;
color?: string;
backgroundColor?: string;
} = $props();
onMount(() => {
if (value && canvasEl) {
// Convert "transparent" to a valid value for the QR code library
const lightColor = backgroundColor === 'transparent' ? '#00000000' : backgroundColor;
const options = {
width: size,
margin: 0,
color: {
dark: color,
light: lightColor
}
};
QRCode.toCanvas(canvasEl, value, options).catch((error: Error) => {
console.error('Error generating QR Code:', error);
});
}
});
</script>
<canvas {...restProps} bind:this={canvasEl} class={cn('rounded-lg', restProps.class)}></canvas>

View File

@@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte'; import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
import Qrcode from '$lib/components/qrcode/qrcode.svelte';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
import { axiosErrorToast } from '$lib/utils/error-util'; import { axiosErrorToast } from '$lib/utils/error-util';
import { mode } from 'mode-watcher';
let { let {
show = $bindable() show = $bindable()
@@ -16,13 +18,17 @@
const userService = new UserService(); const userService = new UserService();
let code: string | null = $state(null); let code: string | null = $state(null);
let loginCodeLink: string | null = $state(null);
$effect(() => { $effect(() => {
if (show) { if (show) {
const expiration = new Date(Date.now() + 15 * 60 * 1000); const expiration = new Date(Date.now() + 15 * 60 * 1000);
userService userService
.createOneTimeAccessToken(expiration, 'me') .createOneTimeAccessToken(expiration, 'me')
.then((c) => (code = c)) .then((c) => {
code = c;
loginCodeLink = page.url.origin + '/lc/' + code;
})
.catch((e) => axiosErrorToast(e)); .catch((e) => axiosErrorToast(e));
} }
}); });
@@ -48,16 +54,22 @@
<CopyToClipboard value={code!}> <CopyToClipboard value={code!}>
<p class="text-3xl font-semibold">{code}</p> <p class="text-3xl font-semibold">{code}</p>
</CopyToClipboard> </CopyToClipboard>
<div class="text-muted-foreground flex items-center justify-center gap-3"> <div class="text-muted-foreground my-2 flex items-center justify-center gap-3">
<Separator /> <Separator />
<p class="text-nowrap text-xs">{m.or_visit()}</p> <p class="text-nowrap text-xs">{m.or_visit()}</p>
<Separator /> <Separator />
</div> </div>
<div>
<CopyToClipboard value={page.url.origin + '/lc/' + code!}> <Qrcode
<p data-testId="login-code-link">{page.url.origin + '/lc/' + code!}</p> class="mb-2"
value={loginCodeLink}
size={180}
color={$mode === 'dark' ? '#FFFFFF' : '#000000'}
backgroundColor={$mode === 'dark' ? '#000000' : '#FFFFFF'}
/>
<CopyToClipboard value={loginCodeLink!}>
<p data-testId="login-code-link">{loginCodeLink!}</p>
</CopyToClipboard> </CopyToClipboard>
</div> </div>
</div>
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>

View File

@@ -3,14 +3,13 @@
import { openConfirmDialog } from '$lib/components/confirm-dialog/'; import { openConfirmDialog } from '$lib/components/confirm-dialog/';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Table from '$lib/components/ui/table'; import * as Table from '$lib/components/ui/table';
import { m } from '$lib/paraglide/messages';
import OIDCService from '$lib/services/oidc-service'; import OIDCService from '$lib/services/oidc-service';
import type { OidcClient } from '$lib/types/oidc.type'; import type { OidcClient } from '$lib/types/oidc.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import { axiosErrorToast } from '$lib/utils/error-util'; import { axiosErrorToast } from '$lib/utils/error-util';
import { LucidePencil, LucideTrash } from 'lucide-svelte'; import { LucidePencil, LucideTrash } from 'lucide-svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import OneTimeLinkModal from './client-secret.svelte';
import { m } from '$lib/paraglide/messages';
let { let {
clients = $bindable(), clients = $bindable(),
@@ -20,8 +19,6 @@
requestOptions: SearchPaginationSortRequest; requestOptions: SearchPaginationSortRequest;
} = $props(); } = $props();
let oneTimeLink = $state<string | null>(null);
const oidcService = new OIDCService(); const oidcService = new OIDCService();
async function deleteClient(client: OidcClient) { async function deleteClient(client: OidcClient) {
@@ -86,5 +83,3 @@
</Table.Cell> </Table.Cell>
{/snippet} {/snippet}
</AdvancedTable> </AdvancedTable>
<OneTimeLinkModal {oneTimeLink} />

View File

@@ -1,8 +1,8 @@
import test, { expect } from '@playwright/test'; import test, { expect } from '@playwright/test';
import { users } from './data'; import { users } from './data';
import authUtil from './utils/auth.util';
import { cleanupBackend } from './utils/cleanup.util'; import { cleanupBackend } from './utils/cleanup.util';
import passkeyUtil from './utils/passkey.util'; import passkeyUtil from './utils/passkey.util';
import authUtil from './utils/auth.util';
test.beforeEach(cleanupBackend); test.beforeEach(cleanupBackend);

View File

@@ -5,7 +5,7 @@ import { cleanupBackend } from './utils/cleanup.util';
test.describe('API Key Management', () => { test.describe('API Key Management', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await cleanupBackend() await cleanupBackend();
await page.goto('/settings/admin/api-keys'); await page.goto('/settings/admin/api-keys');
}); });

View File

@@ -50,11 +50,13 @@ test('Create user fails with already taken username', async ({ page }) => {
await expect(page.getByRole('status')).toHaveText('Username is already in use'); await expect(page.getByRole('status')).toHaveText('Username is already in use');
}); });
test('Create one time access token', async ({ page }) => { test('Create one time access token', async ({ page, context }) => {
await page.goto('/settings/admin/users'); await page.goto('/settings/admin/users');
await page await page
.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` }) .getByRole('row', {
name: `${users.craig.firstname} ${users.craig.lastname}`
})
.getByRole('button') .getByRole('button')
.click(); .click();
@@ -64,16 +66,20 @@ test('Create one time access token', async ({ page }) => {
await page.getByRole('option', { name: '12 hours' }).click(); await page.getByRole('option', { name: '12 hours' }).click();
await page.getByRole('button', { name: 'Generate Code' }).click(); await page.getByRole('button', { name: 'Generate Code' }).click();
await expect(page.getByRole('textbox', { name: 'Login Code' })).toHaveValue( const link = await page.getByTestId('login-code-link').textContent();
/http:\/\/localhost\/lc\/.*/ await context.clearCookies();
);
await page.goto(link!);
await page.waitForURL('/settings/account');
}); });
test('Delete user', async ({ page }) => { test('Delete user', async ({ page }) => {
await page.goto('/settings/admin/users'); await page.goto('/settings/admin/users');
await page await page
.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` }) .getByRole('row', {
name: `${users.craig.firstname} ${users.craig.lastname}`
})
.getByRole('button') .getByRole('button')
.click(); .click();
await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('menuitem', { name: 'Delete' }).click();
@@ -81,7 +87,9 @@ test('Delete user', async ({ page }) => {
await expect(page.getByRole('status')).toHaveText('User deleted successfully'); await expect(page.getByRole('status')).toHaveText('User deleted successfully');
await expect( await expect(
page.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` }) page.getByRole('row', {
name: `${users.craig.firstname} ${users.craig.lastname}`
})
).not.toBeVisible(); ).not.toBeVisible();
}); });