diff --git a/package-lock.json b/package-lock.json index f0febae..6c32156 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vitepress-cms", "version": "0.1.0", "dependencies": { + "vitepress": "^1.6.4", "vue": "^3.5.0" }, "devDependencies": { @@ -17,6 +18,246 @@ "vue-tsc": "^2.1.0" } }, + "node_modules/@algolia/abtesting": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.20.0.tgz", + "integrity": "sha512-5CqkS592H3+24b6H6CQ2RVpphdmAuIElZzv0Hngqo/ZEZpUZJ+KGLcueBhx33fv2wYBXyuuvskG5aQ7Ti+lR0g==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.54.0.tgz", + "integrity": "sha512-IXnH1x3DBsPQA4/FRO0Pvkfy79tKy5Qr+ugAV9jdcGkpzHc476D0WV1MFJ+pZxtbts0xh2JqzUVmYEqA6LkOpA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.54.0.tgz", + "integrity": "sha512-pBpRqm1wpE0GnGy2rNLk9rjDn0Le4iywNRtnAWblLeqfjxpWKg8lWnk7nmSoTShFO31sz2jatXXzxK2lz8ipbA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.54.0.tgz", + "integrity": "sha512-WbuwRUlFvSOsuxqTDjSSmgusuF5KFt+oFPzobvPDvodra6EWnVwUXjz0elkNSsnsIlZGtcXlX3LhxkO7rF90jw==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.54.0.tgz", + "integrity": "sha512-/HNLVi3kPI+JhO59WbglLjPM2c4uECU+x4gk1iADseKtE5eYqJ/RJ+FIwM8xzPFKhFJaw+8hVq4lkd0nn8HDDg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.54.0.tgz", + "integrity": "sha512-6TolyyDRumIKeLGBGSFAZsSIJ8hrm6NFCGR1jO7pQTiOtSWgAIxcFE5/JRpZ3g+unG4OkNOFy+I0dUEquIDbig==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.54.0.tgz", + "integrity": "sha512-AxW2MLBhjBtGX5kIZrVLO2SP2vRkZxJw5qHGxnQJfLcYhA04mFNP0fWRtHshlcVnDk9M8L9lwfr2lGpf3Er+hw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.54.0.tgz", + "integrity": "sha512-ngdgVGp05lJzUyA+sUzr0MDZ7AMtANcJpwIzq4ZsfpZL5B3S7A4XYfMcU2sECZc3bx3ysOhYcdbbaTjc3ve0WQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.54.0.tgz", + "integrity": "sha512-hee59Z7FgZ6/13FYL05ANPwRJY1pfvIlrwC8eBZYdiRFXTJVvf94IyTWOxLqRVpbseDP6eQQTW+PT7/DxTZIng==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.54.0.tgz", + "integrity": "sha512-diCjZVbIO7Pzw98tEKqLWIAgmQBI3Zt1sHsXyAPNGZgn32derpIXTnjjpJbZl+uAhSSznd6SfxFGdC9uYdN1TA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.54.0.tgz", + "integrity": "sha512-6zeslypRAGWDgVJEJYAPuqmyquHvnw4MQwG+XXdrw5dTNDjXYIcCJdQQcCY06xPF9tUvmzy/1vHiH9QxhgwuOQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.54.0.tgz", + "integrity": "sha512-iHZax214LPXd7XizQ4BNnTsegl8f3IeKm8JcrmSNZ/5x1rZ5xLkbG/anltAWtLpoRNnfpr4Z80YdYeVVPpx6wQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.54.0.tgz", + "integrity": "sha512-YKtuG5YwPxZ+kkfd4HmUO7Z9aICPUSMlHslzKTmtMMhxGnetxEqGj9T/v2r/PdcuOUC5oW0CHe8akJk8cpS3gQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.54.0.tgz", + "integrity": "sha512-zyZDJ4WS5TnjZZ5pqywTBFO9olW7QMtY2kf2dbLnu+UTzfc9ri/HGf27jRN2NTbX9FcRPxSqPqzUhF/BZIx0VA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -63,6 +304,54 @@ "node": ">=6.9.0" } }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -70,7 +359,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -87,7 +375,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -104,7 +391,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -121,7 +407,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -138,7 +423,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -155,7 +439,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -172,7 +455,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -189,7 +471,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -206,7 +487,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -223,7 +503,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -240,7 +519,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -257,7 +535,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -274,7 +551,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -291,7 +567,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -308,7 +583,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -325,7 +599,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -342,7 +615,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -359,7 +631,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -376,7 +647,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -393,7 +663,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -410,7 +679,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -427,7 +695,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -444,7 +711,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -454,6 +720,21 @@ "node": ">=12" } }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.86", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.86.tgz", + "integrity": "sha512-t3jck5qPQuK1qy+bRn9eCoDQhIB7XSazKz1Fjp8hcan3XOAsTI5Mq/s3F0ekOKSvMQqkVORYK6ns6o6T9f5EMA==", + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -467,7 +748,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -481,7 +761,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -495,7 +774,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -509,7 +787,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -523,7 +800,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -537,7 +813,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -551,7 +826,6 @@ "cpu": [ "arm" ], - "dev": true, "libc": [ "glibc" ], @@ -568,7 +842,6 @@ "cpu": [ "arm" ], - "dev": true, "libc": [ "musl" ], @@ -585,7 +858,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "glibc" ], @@ -602,7 +874,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "musl" ], @@ -619,7 +890,6 @@ "cpu": [ "loong64" ], - "dev": true, "libc": [ "glibc" ], @@ -636,7 +906,6 @@ "cpu": [ "loong64" ], - "dev": true, "libc": [ "musl" ], @@ -653,7 +922,6 @@ "cpu": [ "ppc64" ], - "dev": true, "libc": [ "glibc" ], @@ -670,7 +938,6 @@ "cpu": [ "ppc64" ], - "dev": true, "libc": [ "musl" ], @@ -687,7 +954,6 @@ "cpu": [ "riscv64" ], - "dev": true, "libc": [ "glibc" ], @@ -704,7 +970,6 @@ "cpu": [ "riscv64" ], - "dev": true, "libc": [ "musl" ], @@ -721,7 +986,6 @@ "cpu": [ "s390x" ], - "dev": true, "libc": [ "glibc" ], @@ -738,7 +1002,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "glibc" ], @@ -755,7 +1018,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "musl" ], @@ -772,7 +1034,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -786,7 +1047,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -800,7 +1060,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -814,7 +1073,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -828,7 +1086,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -842,25 +1099,159 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", - "dev": true, "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" @@ -960,6 +1351,39 @@ "he": "^1.2.0" } }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/language-core": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", @@ -1035,6 +1459,133 @@ "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", "license": "MIT" }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.54.0.tgz", + "integrity": "sha512-APAX4ajIOgsmYoUlGe++oNZkSTBgmXYM4maHC0OxC+Yo7xkaKQElV0ATZYCZA7jzrSJX1OBiqEs7mk+ZxXgYqA==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.20.0", + "@algolia/client-abtesting": "5.54.0", + "@algolia/client-analytics": "5.54.0", + "@algolia/client-common": "5.54.0", + "@algolia/client-insights": "5.54.0", + "@algolia/client-personalization": "5.54.0", + "@algolia/client-query-suggestions": "5.54.0", + "@algolia/client-search": "5.54.0", + "@algolia/ingestion": "1.54.0", + "@algolia/monitoring": "1.54.0", + "@algolia/recommend": "5.54.0", + "@algolia/requester-browser-xhr": "5.54.0", + "@algolia/requester-fetch": "5.54.0", + "@algolia/requester-node-http": "5.54.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/alien-signals": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", @@ -1049,6 +1600,15 @@ "dev": true, "license": "MIT" }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/brace-expansion": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", @@ -1059,6 +1619,61 @@ "balanced-match": "^1.0.0" } }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1072,6 +1687,34 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -1088,7 +1731,6 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1129,11 +1771,19 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1144,6 +1794,42 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1154,6 +1840,34 @@ "he": "bin/he" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1163,6 +1877,122 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -1179,6 +2009,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -1204,6 +2046,17 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -1211,6 +2064,12 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1245,11 +2104,60 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.61.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -1290,6 +2198,29 @@ "fsevents": "~2.3.2" } }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1299,6 +2230,67 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1313,11 +2305,106 @@ "node": ">=14.17" } }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -1373,6 +2460,47 @@ } } }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -1417,6 +2545,16 @@ "peerDependencies": { "typescript": ">=5.0.0" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 696772c..d9e2399 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "preview": "vite preview --host 0.0.0.0" }, "dependencies": { + "vitepress": "^1.6.4", "vue": "^3.5.0" }, "devDependencies": { diff --git a/server/auth.mjs b/server/auth.mjs index c0567f6..90ebc17 100644 --- a/server/auth.mjs +++ b/server/auth.mjs @@ -1,9 +1,10 @@ import { db } from "./db.mjs"; import { json, readBody } from "./http.mjs"; import { clearSessionCookie, createSession, deleteSession, getSession, parseCookies, sessionCookie } from "./session.mjs"; -import { verifyPassword } from "./security.mjs"; +import { hashPassword, verifyPassword } from "./security.mjs"; +import { hasPermission, publicPermissions } from "./permissions.mjs"; -function publicUser(user) { +export function publicUser(user) { if (!user) return null; return { @@ -12,6 +13,12 @@ function publicUser(user) { name: user.name, avatarUrl: user.avatar_url, role: user.role, + status: user.status || "active", + emailVerified: Boolean(user.email_verified), + createdAt: user.created_at, + updatedAt: user.updated_at, + lastLoginAt: user.last_login_at, + permissions: publicPermissions(user), }; } @@ -33,12 +40,49 @@ export function requireCmsUser(request, response) { return session.user; } +export function requirePermission(request, response, permission) { + const user = requireCmsUser(request, response); + + if (!user) return undefined; + + if (!hasPermission(user, permission)) { + json(response, 403, { error: "Permission denied" }); + return undefined; + } + + return user; +} + +function settingEnabled(key, fallback = false) { + const row = db.prepare("SELECT value FROM system_settings WHERE key = ?").get(key); + return (row?.value ?? String(fallback)) === "true"; +} + +function validateEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + async function handleMe(request, response) { const { session } = getRequestSession(request); json(response, 200, { user: session?.user ?? null }); } +async function handleOptions(request, response) { + json(response, 200, { + options: { + allowEmailLogin: settingEnabled("auth.allow_email_login", true), + allowGitHubLogin: settingEnabled("auth.allow_github_login", true), + allowRegistration: settingEnabled("auth.allow_registration", false), + }, + }); +} + async function handleLogin(request, response) { + if (!settingEnabled("auth.allow_email_login", true)) { + json(response, 403, { error: "Email login is disabled" }); + return; + } + const body = JSON.parse(await readBody(request) || "{}"); const email = String(body.email || "").trim().toLowerCase(); const password = String(body.password || ""); @@ -55,7 +99,15 @@ async function handleLogin(request, response) { return; } - const sessionUser = publicUser(user); + if ((user.status || "active") !== "active") { + json(response, 403, { error: "This account is disabled" }); + return; + } + + db.prepare("UPDATE users SET last_login_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(user.id); + const nextUser = db.prepare("SELECT * FROM users WHERE id = ?").get(user.id); + + const sessionUser = publicUser(nextUser); const sessionId = createSession({ user: sessionUser, }); @@ -63,12 +115,116 @@ async function handleLogin(request, response) { json(response, 200, { user: sessionUser }, { "Set-Cookie": sessionCookie(sessionId) }); } +async function handleRegister(request, response) { + if (!settingEnabled("auth.allow_registration", false)) { + json(response, 403, { error: "Registration is disabled" }); + return; + } + + const body = JSON.parse((await readBody(request)) || "{}"); + const email = String(body.email || "").trim().toLowerCase(); + const name = String(body.name || "").trim(); + const password = String(body.password || ""); + + if (!validateEmail(email)) { + json(response, 400, { error: "A valid email is required" }); + return; + } + + if (!name) { + json(response, 400, { error: "Name is required" }); + return; + } + + if (password.length < 8) { + json(response, 400, { error: "Password must be at least 8 characters" }); + return; + } + + const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(email); + if (existing) { + json(response, 409, { error: "Email already exists" }); + return; + } + + const result = db + .prepare("INSERT INTO users (email, password_hash, name, role, email_verified) VALUES (?, ?, ?, 'user', 0)") + .run(email, hashPassword(password), name); + + const user = publicUser(db.prepare("SELECT * FROM users WHERE id = ?").get(result.lastInsertRowid)); + const sessionId = createSession({ user }); + + json(response, 201, { user }, { "Set-Cookie": sessionCookie(sessionId) }); +} + async function handleLogout(request, response) { const { sessionId } = getRequestSession(request); deleteSession(sessionId); json(response, 200, { ok: true }, { "Set-Cookie": clearSessionCookie() }); } +async function handleUpdateProfile(request, response) { + const { sessionId, session } = getRequestSession(request); + + if (!session?.user?.id) { + json(response, 401, { error: "Not signed in" }); + return; + } + + const body = JSON.parse((await readBody(request)) || "{}"); + const name = String(body.name || "").trim(); + + if (!name) { + json(response, 400, { error: "Name is required" }); + return; + } + + db.prepare("UPDATE users SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(name, session.user.id); + const user = db.prepare("SELECT * FROM users WHERE id = ?").get(session.user.id); + const sessionUser = publicUser(user); + + delete session.user; + const nextSession = { + ...session, + user: sessionUser, + }; + deleteSession(sessionId); + const nextSessionId = createSession(nextSession); + + json(response, 200, { user: sessionUser }, { "Set-Cookie": sessionCookie(nextSessionId) }); +} + +async function handleChangePassword(request, response) { + const { session } = getRequestSession(request); + + if (!session?.user?.id) { + json(response, 401, { error: "Not signed in" }); + return; + } + + const body = JSON.parse((await readBody(request)) || "{}"); + const currentPassword = String(body.currentPassword || ""); + const newPassword = String(body.newPassword || ""); + const user = db.prepare("SELECT * FROM users WHERE id = ?").get(session.user.id); + + if (newPassword.length < 8) { + json(response, 400, { error: "New password must be at least 8 characters" }); + return; + } + + if (user.password_hash && !verifyPassword(currentPassword, user.password_hash)) { + json(response, 403, { error: "Current password is incorrect" }); + return; + } + + db.prepare("UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run( + hashPassword(newPassword), + session.user.id, + ); + + json(response, 200, { ok: true }); +} + export async function handleAuthApi(request, response, url) { try { if (url.pathname === "/api/auth/me" && request.method === "GET") { @@ -76,16 +232,36 @@ export async function handleAuthApi(request, response, url) { return true; } + if (url.pathname === "/api/auth/options" && request.method === "GET") { + await handleOptions(request, response); + return true; + } + if (url.pathname === "/api/auth/login" && request.method === "POST") { await handleLogin(request, response); return true; } + if (url.pathname === "/api/auth/register" && request.method === "POST") { + await handleRegister(request, response); + return true; + } + if (url.pathname === "/api/auth/logout" && request.method === "POST") { await handleLogout(request, response); return true; } + if (url.pathname === "/api/auth/profile" && request.method === "PATCH") { + await handleUpdateProfile(request, response); + return true; + } + + if (url.pathname === "/api/auth/password" && request.method === "POST") { + await handleChangePassword(request, response); + return true; + } + return false; } catch (error) { json(response, 500, { diff --git a/server/db.mjs b/server/db.mjs index ad0cf90..77c0e7e 100644 --- a/server/db.mjs +++ b/server/db.mjs @@ -67,6 +67,16 @@ export function migrate() { updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); `); + + const userColumns = db.prepare("PRAGMA table_info(users)").all().map((column) => column.name); + + if (!userColumns.includes("status")) { + db.exec("ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active'"); + } + + if (!userColumns.includes("last_login_at")) { + db.exec("ALTER TABLE users ADD COLUMN last_login_at TEXT"); + } } export function getSetting(key, fallback = "") { diff --git a/server/github.mjs b/server/github.mjs index 04f9154..9684fb9 100644 --- a/server/github.mjs +++ b/server/github.mjs @@ -1,7 +1,10 @@ import { randomBytes } from "node:crypto"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { spawn } from "node:child_process"; import { request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { db, getSetting } from "./db.mjs"; import { json, readBody, redirect } from "./http.mjs"; import { getRequestSession } from "./auth.mjs"; @@ -37,7 +40,7 @@ function requestTextWithPowerShell(targetUrl, init = {}) { ).toString("base64"); const script = ` $ErrorActionPreference = 'Stop' -$payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($args[0])) | ConvertFrom-Json +$payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:VITEPRESS_CMS_PS_PAYLOAD)) | ConvertFrom-Json $headers = @{} if ($payload.headers) { foreach ($property in $payload.headers.PSObject.Properties) { @@ -81,7 +84,11 @@ try { $result | ConvertTo-Json -Compress -Depth 5 `; const encodedCommand = Buffer.from(script, "utf16le").toString("base64"); - const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand, payload], { + const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand], { + env: { + ...process.env, + VITEPRESS_CMS_PS_PAYLOAD: payload, + }, windowsHide: true, }); const stdout = []; @@ -281,6 +288,11 @@ function publicCmsUser(user) { name: user.name, avatarUrl: user.avatar_url, role: user.role, + status: user.status || "active", + emailVerified: Boolean(user.email_verified), + createdAt: user.created_at, + updatedAt: user.updated_at, + lastLoginAt: user.last_login_at, }; } @@ -392,8 +404,14 @@ async function handleCallback(request, response, url) { const user = userResponse.payload ?? {}; const { session: currentSession } = getRequestSession(request); const cmsUser = findOrCreateGithubUser(user, tokenPayload.access_token, currentSession); + if ((cmsUser.status || "active") !== "active") { + throw new Error("This account is disabled"); + } + + db.prepare("UPDATE users SET last_login_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(cmsUser.id); + const nextCmsUser = db.prepare("SELECT * FROM users WHERE id = ?").get(cmsUser.id); const sessionId = createSession({ - user: publicCmsUser(cmsUser), + user: publicCmsUser(nextCmsUser), github: { accessToken: tokenPayload.access_token, login: user.login, @@ -489,6 +507,10 @@ function normalizeSiteRoot(siteRoot = "") { return siteRoot.trim().replace(/^\/+|\/+$/g, ""); } +function getVitePressSiteRoot(siteRoot = "") { + return normalizeSiteRoot(siteRoot) || "docs"; +} + function toRepoPath(siteRoot, path) { const normalizedRoot = normalizeSiteRoot(siteRoot); return normalizedRoot ? `${normalizedRoot}/${path}` : path; @@ -508,10 +530,11 @@ function encodeBase64Content(content) { } function packageJsonTemplate(siteRoot) { - const root = normalizeSiteRoot(siteRoot) || "."; + const root = getVitePressSiteRoot(siteRoot); return `${JSON.stringify( { + type: "module", scripts: { "docs:dev": `vitepress dev ${root}`, "docs:build": `vitepress build ${root}`, @@ -527,43 +550,100 @@ function packageJsonTemplate(siteRoot) { `; } -function siteConfigTemplate() { - return `import { defineConfig } from "vitepress"; +function mergePackageJsonContent(existingSource, siteRoot) { + const root = getVitePressSiteRoot(siteRoot); + let packageJson; -export default defineConfig({ - title: "VitePress Site", - description: "Created by VitePress-CMS", - lastUpdated: true, - themeConfig: { - nav: [ - { text: "首页", link: "/" }, - { text: "指南", link: "/guide/getting-started" }, - ], - sidebar: { - "/guide/": [ - { - text: "指南", - items: [ - { text: "快速开始", link: "/guide/getting-started" }, - ], - }, - ], - }, - search: { - provider: "local", - }, - footer: { - message: "Powered by VitePress-CMS", - copyright: "Copyright 2026", - }, - }, -}); -`; + try { + packageJson = JSON.parse(existingSource); + } catch { + throw new Error("package.json is not valid JSON"); + } + + packageJson.type = "module"; + packageJson.scripts = { + ...packageJson.scripts, + "docs:dev": `vitepress dev ${root}`, + "docs:build": `vitepress build ${root}`, + "docs:preview": `vitepress preview ${root}`, + }; + packageJson.devDependencies = { + ...packageJson.devDependencies, + vitepress: packageJson.devDependencies?.vitepress || "^1.6.4", + }; + + return `${JSON.stringify(packageJson, null, 2)}\n`; +} + +async function loadVitePressScaffold() { + try { + const module = await import("vitepress"); + return module.scaffold; + } catch { + const module = await import("../test-vitepress/node_modules/vitepress/dist/node/index.js"); + return module.scaffold; + } +} + +async function createOfficialVitePressFiles(siteRoot) { + const tempRoot = mkdtempSync(join(tmpdir(), "vitepress-cms-init-")); + const cwd = process.cwd(); + + try { + const scaffold = await loadVitePressScaffold(); + writeFileSync(join(tempRoot, "package.json"), packageJsonTemplate(siteRoot), "utf-8"); + process.chdir(tempRoot); + scaffold({ + root: siteRoot, + title: "VitePress Site", + description: "Created by VitePress-CMS", + theme: "default theme", + useTs: true, + injectNpmScripts: true, + }); + + const configPath = [ + toRepoPath(siteRoot, ".vitepress/config.ts"), + toRepoPath(siteRoot, ".vitepress/config.mts"), + toRepoPath(siteRoot, ".vitepress/config.mjs"), + toRepoPath(siteRoot, ".vitepress/config.js"), + ].find((path) => { + try { + readFileSync(join(tempRoot, path), "utf-8"); + return true; + } catch { + return false; + } + }); + + if (!configPath) { + throw new Error("VitePress official scaffold did not generate a config file"); + } + + const filePaths = [ + "package.json", + toRepoPath(siteRoot, "index.md"), + toRepoPath(siteRoot, "api-examples.md"), + toRepoPath(siteRoot, "markdown-examples.md"), + configPath, + ]; + + return filePaths.map((path) => ({ + path, + content: + path === "package.json" + ? mergePackageJsonContent(readFileSync(join(tempRoot, path), "utf-8"), siteRoot) + : readFileSync(join(tempRoot, path), "utf-8"), + })); + } finally { + process.chdir(cwd); + rmSync(tempRoot, { recursive: true, force: true }); + } } function deployWorkflowTemplate(siteRoot, branch) { - const root = normalizeSiteRoot(siteRoot); - const outputDir = root ? `${root}/.vitepress/dist` : ".vitepress/dist"; + const root = getVitePressSiteRoot(siteRoot); + const outputDir = `${root}/.vitepress/dist`; return `name: Deploy VitePress site to Pages @@ -579,7 +659,7 @@ permissions: concurrency: group: pages - cancel-in-progress: false + cancel-in-progress: true jobs: build: @@ -603,44 +683,16 @@ jobs: runs-on: ubuntu-latest steps: - id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 `; } -function initTemplateFiles(siteRoot, branch) { +async function initTemplateFiles(siteRoot, branch) { + const root = getVitePressSiteRoot(siteRoot); + const files = await createOfficialVitePressFiles(root); + return [ - { - path: "package.json", - content: packageJsonTemplate(siteRoot), - }, - { - path: toRepoPath(siteRoot, ".vitepress/config.ts"), - content: siteConfigTemplate(), - }, - { - path: toRepoPath(siteRoot, "index.md"), - content: `--- -title: 首页 -description: VitePress-CMS 初始化页面 ---- - -# VitePress Site - -这个站点由 VitePress-CMS 初始化。 -`, - }, - { - path: toRepoPath(siteRoot, "guide/getting-started.md"), - content: `--- -title: 快速开始 -description: 使用 VitePress-CMS 管理内容 ---- - -# 快速开始 - -这里是你的第一篇指南文档。 -`, - }, + ...files, { path: ".github/workflows/deploy.yml", content: deployWorkflowTemplate(siteRoot, branch), @@ -759,7 +811,7 @@ function getSiteQuery(url) { const owner = url.searchParams.get("owner"); const repo = url.searchParams.get("repo"); const branch = url.searchParams.get("branch") || "main"; - const siteRoot = url.searchParams.get("siteRoot") || ""; + const siteRoot = getVitePressSiteRoot(url.searchParams.get("siteRoot") || ""); if (!owner || !repo) { throw new Error("owner and repo are required"); @@ -789,6 +841,28 @@ async function createRepoPathFile(session, site, file) { try { const existing = await getRepoPathContent(session, site, file.path); + if (file.path === "package.json") { + const encodedPath = file.path.split("/").map(encodeURIComponent).join("/"); + const result = await githubFetch(session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message: "docs: update package metadata from VitePress-CMS", + content: encodeBase64Content(mergePackageJsonContent(decodeBase64Content(existing.content), site.siteRoot)), + branch: site.branch, + sha: existing.sha, + }), + }); + + return { + path: file.path, + status: "updated", + sha: result.content.sha, + }; + } + if (file.path.startsWith(".github/workflows/")) { const encodedPath = file.path.split("/").map(encodeURIComponent).join("/"); const result = await githubFetch(session, `/repos/${site.owner}/${site.repo}/contents/${encodedPath}`, { @@ -891,11 +965,11 @@ async function handleInitSite(request, response) { owner: String(site.owner), repo: String(site.repo), branch: String(site.branch || "main"), - siteRoot: normalizeSiteRoot(String(site.siteRoot || "")), + siteRoot: getVitePressSiteRoot(String(site.siteRoot || "")), }; const files = []; - for (const file of initTemplateFiles(normalizedSite.siteRoot, normalizedSite.branch)) { + for (const file of await initTemplateFiles(normalizedSite.siteRoot, normalizedSite.branch)) { files.push(await createRepoPathFile(session, normalizedSite, file)); } diff --git a/server/index.mjs b/server/index.mjs index 96b96d9..dec179a 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -6,6 +6,8 @@ import { bootstrap } from "./bootstrap.mjs"; import { handleAuthApi } from "./auth.mjs"; import { handleGithubApi } from "./github.mjs"; import { handleSettingsApi } from "./settings.mjs"; +import { handleUsersApi } from "./users.mjs"; +import { handleProjectsApi } from "./projects.mjs"; const isProduction = process.env.NODE_ENV === "production"; const port = Number(process.env.PORT || 5173); @@ -64,6 +66,14 @@ const server = createServer(async (request, response) => { return; } + if (await handleUsersApi(request, response, url)) { + return; + } + + if (await handleProjectsApi(request, response, url)) { + return; + } + if (await handleGithubApi(request, response, url)) { return; } diff --git a/server/permissions.mjs b/server/permissions.mjs new file mode 100644 index 0000000..e19b484 --- /dev/null +++ b/server/permissions.mjs @@ -0,0 +1,31 @@ +export const permissions = { + settingsManage: "settings.manage", + usersManage: "users.manage", + projectsManage: "projects.manage", + contentEdit: "content.edit", + contentPublish: "content.publish", + contentView: "content.view", +}; + +const rolePermissions = { + system_admin: Object.values(permissions), + admin: [ + permissions.usersManage, + permissions.projectsManage, + permissions.contentEdit, + permissions.contentPublish, + permissions.contentView, + ], + editor: [permissions.contentEdit, permissions.contentPublish, permissions.contentView], + viewer: [permissions.contentView], + user: [permissions.contentView], +}; + +export function hasPermission(user, permission) { + if (!user) return false; + return Boolean(rolePermissions[user.role]?.includes(permission)); +} + +export function publicPermissions(user) { + return rolePermissions[user?.role] ?? []; +} diff --git a/server/projects.mjs b/server/projects.mjs new file mode 100644 index 0000000..56a9e76 --- /dev/null +++ b/server/projects.mjs @@ -0,0 +1,343 @@ +import { db } from "./db.mjs"; +import { requireCmsUser } from "./auth.mjs"; +import { json, readBody } from "./http.mjs"; + +const projectRoles = ["owner", "admin", "editor", "viewer"]; +const projectManagers = ["owner", "admin"]; + +function publicProject(row) { + return { + id: row.id, + name: row.name, + githubOwner: row.github_owner, + githubRepo: row.github_repo, + githubBranch: row.github_branch, + siteRoot: row.site_root, + createdAt: row.created_at, + updatedAt: row.updated_at, + ownerUserId: row.owner_user_id, + }; +} + +function publicMember(row) { + return { + id: row.id, + projectId: row.project_id, + userId: row.user_id, + role: row.role, + name: row.name, + email: row.email, + avatarUrl: row.avatar_url, + status: row.status || "active", + createdAt: row.created_at, + }; +} + +function getProject(projectId) { + return db.prepare("SELECT * FROM projects WHERE id = ?").get(projectId); +} + +function hasGlobalProjectAccess(user) { + return user.role === "system_admin" || user.role === "admin"; +} + +function getProjectMember(projectId, userId) { + return db.prepare("SELECT * FROM project_members WHERE project_id = ? AND user_id = ?").get(projectId, userId); +} + +function canViewProject(user, projectId) { + return hasGlobalProjectAccess(user) || Boolean(getProjectMember(projectId, user.id)); +} + +function canManageProject(user, projectId) { + if (hasGlobalProjectAccess(user)) return true; + const member = getProjectMember(projectId, user.id); + return Boolean(member && projectManagers.includes(member.role)); +} + +function requireProjectView(request, response, projectId) { + const user = requireCmsUser(request, response); + if (!user) return undefined; + + if (!canViewProject(user, projectId)) { + json(response, 403, { error: "Permission denied" }); + return undefined; + } + + return user; +} + +function requireProjectManage(request, response, projectId) { + const user = requireCmsUser(request, response); + if (!user) return undefined; + + if (!canManageProject(user, projectId)) { + json(response, 403, { error: "Only project owners and admins can manage this project" }); + return undefined; + } + + return user; +} + +function listMembers(projectId) { + return db + .prepare(` + SELECT project_members.*, users.name, users.email, users.avatar_url, users.status + FROM project_members + INNER JOIN users ON users.id = project_members.user_id + WHERE project_members.project_id = ? + ORDER BY + CASE project_members.role + WHEN 'owner' THEN 1 + WHEN 'admin' THEN 2 + WHEN 'editor' THEN 3 + ELSE 4 + END, + project_members.created_at ASC + `) + .all(projectId) + .map(publicMember); +} + +function listProjects(user) { + if (user.role === "system_admin" || user.role === "admin") { + return db.prepare("SELECT * FROM projects ORDER BY updated_at DESC, id DESC").all().map(publicProject); + } + + return db + .prepare(` + SELECT projects.* + FROM projects + INNER JOIN project_members ON project_members.project_id = projects.id + WHERE project_members.user_id = ? + ORDER BY projects.updated_at DESC, projects.id DESC + `) + .all(user.id) + .map(publicProject); +} + +async function handleListProjects(request, response) { + const user = requireCmsUser(request, response); + if (!user) return; + + json(response, 200, { projects: listProjects(user) }); +} + +async function handleCreateProject(request, response) { + const user = requireCmsUser(request, response); + if (!user) return; + + const body = JSON.parse((await readBody(request)) || "{}"); + const name = String(body.name || "").trim(); + + if (!name) { + json(response, 400, { error: "Project name is required" }); + return; + } + + const result = db + .prepare(` + INSERT INTO projects (owner_user_id, name, github_owner, github_repo, github_branch, site_root) + VALUES (?, ?, ?, ?, ?, ?) + `) + .run( + user.id, + name, + String(body.githubOwner || "").trim() || null, + String(body.githubRepo || "").trim() || null, + String(body.githubBranch || "main").trim() || "main", + String(body.siteRoot || "docs").trim().replace(/^\/+|\/+$/g, "") || "docs", + ); + + db.prepare("INSERT OR IGNORE INTO project_members (project_id, user_id, role) VALUES (?, ?, 'owner')").run( + result.lastInsertRowid, + user.id, + ); + + const project = db.prepare("SELECT * FROM projects WHERE id = ?").get(result.lastInsertRowid); + json(response, 201, { project: publicProject(project) }); +} + +async function handleUpdateProject(request, response, projectId) { + const project = db.prepare("SELECT * FROM projects WHERE id = ?").get(projectId); + if (!project) { + json(response, 404, { error: "Project not found" }); + return; + } + + const user = requireProjectManage(request, response, projectId); + if (!user) return; + + const body = JSON.parse((await readBody(request)) || "{}"); + db.prepare(` + UPDATE projects + SET name = ?, + github_owner = ?, + github_repo = ?, + github_branch = ?, + site_root = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `).run( + String(body.name || project.name).trim(), + String(body.githubOwner ?? project.github_owner ?? "").trim() || null, + String(body.githubRepo ?? project.github_repo ?? "").trim() || null, + String(body.githubBranch || project.github_branch || "main").trim() || "main", + String(body.siteRoot || project.site_root || "docs").trim().replace(/^\/+|\/+$/g, "") || "docs", + projectId, + ); + + json(response, 200, { project: publicProject(db.prepare("SELECT * FROM projects WHERE id = ?").get(projectId)) }); +} + +async function handleListMembers(request, response, projectId) { + if (!getProject(projectId)) { + json(response, 404, { error: "Project not found" }); + return; + } + + const user = requireProjectView(request, response, projectId); + if (!user) return; + + json(response, 200, { members: listMembers(projectId) }); +} + +async function handleAddMember(request, response, projectId) { + if (!getProject(projectId)) { + json(response, 404, { error: "Project not found" }); + return; + } + + const user = requireProjectManage(request, response, projectId); + if (!user) return; + + const body = JSON.parse((await readBody(request)) || "{}"); + const email = String(body.email || "").trim().toLowerCase(); + const role = String(body.role || "editor"); + + if (!projectRoles.includes(role)) { + json(response, 400, { error: "Invalid member role" }); + return; + } + + const target = db.prepare("SELECT * FROM users WHERE email = ?").get(email); + if (!target) { + json(response, 404, { error: "User not found" }); + return; + } + + db.prepare(` + INSERT INTO project_members (project_id, user_id, role) + VALUES (?, ?, ?) + ON CONFLICT(project_id, user_id) DO UPDATE SET role = excluded.role + `).run(projectId, target.id, role); + + json(response, 200, { members: listMembers(projectId) }); +} + +async function handleUpdateMember(request, response, projectId, memberId) { + const member = db.prepare("SELECT * FROM project_members WHERE project_id = ? AND id = ?").get(projectId, memberId); + if (!member) { + json(response, 404, { error: "Member not found" }); + return; + } + + const user = requireProjectManage(request, response, projectId); + if (!user) return; + + const body = JSON.parse((await readBody(request)) || "{}"); + const role = String(body.role || member.role); + + if (!projectRoles.includes(role)) { + json(response, 400, { error: "Invalid member role" }); + return; + } + + if (member.role === "owner" && role !== "owner") { + const ownerCount = db + .prepare("SELECT COUNT(*) AS count FROM project_members WHERE project_id = ? AND role = 'owner'") + .get(projectId).count; + + if (ownerCount <= 1) { + json(response, 400, { error: "At least one project owner is required" }); + return; + } + } + + db.prepare("UPDATE project_members SET role = ? WHERE id = ?").run(role, memberId); + json(response, 200, { members: listMembers(projectId) }); +} + +async function handleRemoveMember(request, response, projectId, memberId) { + const member = db.prepare("SELECT * FROM project_members WHERE project_id = ? AND id = ?").get(projectId, memberId); + if (!member) { + json(response, 404, { error: "Member not found" }); + return; + } + + const user = requireProjectManage(request, response, projectId); + if (!user) return; + + if (member.role === "owner") { + const ownerCount = db + .prepare("SELECT COUNT(*) AS count FROM project_members WHERE project_id = ? AND role = 'owner'") + .get(projectId).count; + + if (ownerCount <= 1) { + json(response, 400, { error: "At least one project owner is required" }); + return; + } + } + + db.prepare("DELETE FROM project_members WHERE id = ?").run(memberId); + json(response, 200, { members: listMembers(projectId) }); +} + +export async function handleProjectsApi(request, response, url) { + try { + if (url.pathname === "/api/projects" && request.method === "GET") { + await handleListProjects(request, response); + return true; + } + + if (url.pathname === "/api/projects" && request.method === "POST") { + await handleCreateProject(request, response); + return true; + } + + const match = url.pathname.match(/^\/api\/projects\/(\d+)$/); + if (match && request.method === "PATCH") { + await handleUpdateProject(request, response, Number(match[1])); + return true; + } + + const membersMatch = url.pathname.match(/^\/api\/projects\/(\d+)\/members$/); + if (membersMatch && request.method === "GET") { + await handleListMembers(request, response, Number(membersMatch[1])); + return true; + } + + if (membersMatch && request.method === "POST") { + await handleAddMember(request, response, Number(membersMatch[1])); + return true; + } + + const memberMatch = url.pathname.match(/^\/api\/projects\/(\d+)\/members\/(\d+)$/); + if (memberMatch && request.method === "PATCH") { + await handleUpdateMember(request, response, Number(memberMatch[1]), Number(memberMatch[2])); + return true; + } + + if (memberMatch && request.method === "DELETE") { + await handleRemoveMember(request, response, Number(memberMatch[1]), Number(memberMatch[2])); + return true; + } + + return false; + } catch (error) { + json(response, 500, { + error: error instanceof Error ? error.message : "Unknown projects API error", + }); + return true; + } +} diff --git a/server/settings.mjs b/server/settings.mjs index 929295a..3573b23 100644 --- a/server/settings.mjs +++ b/server/settings.mjs @@ -1,18 +1,64 @@ +import net from "node:net"; +import tls from "node:tls"; import { db, setSetting } from "./db.mjs"; -import { requireCmsUser } from "./auth.mjs"; +import { requirePermission } from "./auth.mjs"; import { json, readBody } from "./http.mjs"; +import { permissions } from "./permissions.mjs"; -function requireAdmin(request, response) { - const user = requireCmsUser(request, response); +function getSettingValue(key, fallback = "") { + return db.prepare("SELECT value FROM system_settings WHERE key = ?").get(key)?.value ?? fallback; +} - if (!user) return undefined; +function testSmtpConnection({ host, port }) { + return new Promise((resolve, reject) => { + const socketFactory = Number(port) === 465 ? tls.connect : net.connect; + const socket = socketFactory({ + host, + port: Number(port), + servername: host, + timeout: 10000, + }); + let buffer = ""; - if (user.role !== "system_admin") { - json(response, 403, { error: "System admin permission is required" }); - return undefined; - } + const finish = (result) => { + socket.removeAllListeners(); + socket.end(); + resolve(result); + }; - return user; + socket.on("connect", () => { + if (Number(port) === 465) { + socket.write("EHLO vitepress-cms.local\r\n"); + } + }); + + socket.on("secureConnect", () => { + socket.write("EHLO vitepress-cms.local\r\n"); + }); + + socket.on("data", (chunk) => { + buffer += chunk.toString("utf-8"); + + if (buffer.startsWith("220")) { + socket.write("EHLO vitepress-cms.local\r\n"); + } + + if (buffer.includes("\n250 ") || buffer.includes("\n250-") || buffer.startsWith("250")) { + socket.write("QUIT\r\n"); + finish({ + ok: true, + greeting: buffer.split(/\r?\n/).filter(Boolean).slice(0, 3).join(" / "), + }); + } + }); + + socket.on("timeout", () => { + socket.destroy(); + reject(new Error("SMTP connection timed out")); + }); + + socket.on("error", reject); + }); } function publicSetting(row) { @@ -25,7 +71,7 @@ function publicSetting(row) { } async function handleGetSettings(request, response) { - if (!requireAdmin(request, response)) return; + if (!requirePermission(request, response, permissions.settingsManage)) return; const rows = db .prepare("SELECT key, value, encrypted, updated_at FROM system_settings ORDER BY key") @@ -35,7 +81,7 @@ async function handleGetSettings(request, response) { } async function handleSaveSettings(request, response) { - if (!requireAdmin(request, response)) return; + if (!requirePermission(request, response, permissions.settingsManage)) return; const body = JSON.parse(await readBody(request) || "{}"); const settings = Array.isArray(body.settings) ? body.settings : []; @@ -52,6 +98,21 @@ async function handleSaveSettings(request, response) { json(response, 200, { ok: true }); } +async function handleTestSmtp(request, response) { + if (!requirePermission(request, response, permissions.settingsManage)) return; + + const host = getSettingValue("smtp.host"); + const port = getSettingValue("smtp.port", "587"); + + if (!host) { + json(response, 400, { error: "SMTP host is required" }); + return; + } + + const result = await testSmtpConnection({ host, port }); + json(response, 200, result); +} + export async function handleSettingsApi(request, response, url) { try { if (url.pathname === "/api/admin/settings" && request.method === "GET") { @@ -64,6 +125,11 @@ export async function handleSettingsApi(request, response, url) { return true; } + if (url.pathname === "/api/admin/settings/test-smtp" && request.method === "POST") { + await handleTestSmtp(request, response); + return true; + } + return false; } catch (error) { json(response, 500, { diff --git a/server/users.mjs b/server/users.mjs new file mode 100644 index 0000000..33d1462 --- /dev/null +++ b/server/users.mjs @@ -0,0 +1,143 @@ +import { db } from "./db.mjs"; +import { requirePermission, publicUser } from "./auth.mjs"; +import { json, readBody } from "./http.mjs"; +import { permissions } from "./permissions.mjs"; +import { hashPassword } from "./security.mjs"; + +const roles = new Set(["system_admin", "admin", "editor", "viewer", "user"]); +const statuses = new Set(["active", "disabled"]); + +function listUsers() { + return db + .prepare(` + SELECT id, email, name, avatar_url, role, status, email_verified, created_at, updated_at, last_login_at + FROM users + ORDER BY created_at DESC, id DESC + `) + .all() + .map(publicUser); +} + +function validateEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +async function handleListUsers(request, response) { + if (!requirePermission(request, response, permissions.usersManage)) return; + json(response, 200, { users: listUsers() }); +} + +async function handleCreateUser(request, response) { + if (!requirePermission(request, response, permissions.usersManage)) return; + + const body = JSON.parse((await readBody(request)) || "{}"); + const email = String(body.email || "").trim().toLowerCase(); + const name = String(body.name || "").trim(); + const password = String(body.password || ""); + const role = String(body.role || "user"); + + if (!validateEmail(email)) { + json(response, 400, { error: "A valid email is required" }); + return; + } + + if (!name) { + json(response, 400, { error: "Name is required" }); + return; + } + + if (password.length < 8) { + json(response, 400, { error: "Password must be at least 8 characters" }); + return; + } + + if (!roles.has(role)) { + json(response, 400, { error: "Invalid role" }); + return; + } + + const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(email); + if (existing) { + json(response, 409, { error: "Email already exists" }); + return; + } + + const result = db + .prepare("INSERT INTO users (email, password_hash, name, role, email_verified) VALUES (?, ?, ?, ?, 1)") + .run(email, hashPassword(password), name, role); + + json(response, 201, { user: publicUser(db.prepare("SELECT * FROM users WHERE id = ?").get(result.lastInsertRowid)) }); +} + +async function handleUpdateUser(request, response, userId) { + const actor = requirePermission(request, response, permissions.usersManage); + if (!actor) return; + + const target = db.prepare("SELECT * FROM users WHERE id = ?").get(userId); + if (!target) { + json(response, 404, { error: "User not found" }); + return; + } + + const body = JSON.parse((await readBody(request)) || "{}"); + const nextRole = body.role === undefined ? target.role : String(body.role); + const nextStatus = body.status === undefined ? target.status || "active" : String(body.status); + + if (!roles.has(nextRole)) { + json(response, 400, { error: "Invalid role" }); + return; + } + + if (!statuses.has(nextStatus)) { + json(response, 400, { error: "Invalid status" }); + return; + } + + if (Number(actor.id) === Number(userId) && nextStatus !== "active") { + json(response, 400, { error: "You cannot disable your own account" }); + return; + } + + if (target.role === "system_admin" && nextRole !== "system_admin") { + const adminCount = db.prepare("SELECT COUNT(*) AS count FROM users WHERE role = 'system_admin'").get().count; + if (adminCount <= 1) { + json(response, 400, { error: "At least one system admin is required" }); + return; + } + } + + db.prepare("UPDATE users SET role = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run( + nextRole, + nextStatus, + userId, + ); + + json(response, 200, { user: publicUser(db.prepare("SELECT * FROM users WHERE id = ?").get(userId)) }); +} + +export async function handleUsersApi(request, response, url) { + try { + if (url.pathname === "/api/admin/users" && request.method === "GET") { + await handleListUsers(request, response); + return true; + } + + if (url.pathname === "/api/admin/users" && request.method === "POST") { + await handleCreateUser(request, response); + return true; + } + + const match = url.pathname.match(/^\/api\/admin\/users\/(\d+)$/); + if (match && request.method === "PATCH") { + await handleUpdateUser(request, response, Number(match[1])); + return true; + } + + return false; + } catch (error) { + json(response, 500, { + error: error instanceof Error ? error.message : "Unknown users API error", + }); + return true; + } +} diff --git a/src/App.vue b/src/App.vue index a3cc1de..2d1e203 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,7 @@ diff --git a/src/components/ProjectPanel.vue b/src/components/ProjectPanel.vue new file mode 100644 index 0000000..bdb3fad --- /dev/null +++ b/src/components/ProjectPanel.vue @@ -0,0 +1,199 @@ + + + diff --git a/src/components/SystemSettingsPanel.vue b/src/components/SystemSettingsPanel.vue index b7d2040..6c784c2 100644 --- a/src/components/SystemSettingsPanel.vue +++ b/src/components/SystemSettingsPanel.vue @@ -5,10 +5,13 @@ import type { SystemSetting } from "../types"; const props = defineProps<{ settings: SystemSetting[]; saveState: "idle" | "saving" | "saved" | "error"; + smtpState: "idle" | "testing" | "success" | "error"; + smtpMessage: string; }>(); defineEmits<{ saveSettings: [settings: SystemSetting[]]; + testSmtp: []; }>(); const draft = reactive>({}); @@ -126,6 +129,14 @@ function isBooleanKey(key: string) { :type="getSetting(key).encrypted ? 'password' : 'text'" /> + diff --git a/src/components/UserManagementPanel.vue b/src/components/UserManagementPanel.vue new file mode 100644 index 0000000..9f28a96 --- /dev/null +++ b/src/components/UserManagementPanel.vue @@ -0,0 +1,150 @@ + + + diff --git a/src/styles.css b/src/styles.css index 0fc0813..47774e8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -498,6 +498,16 @@ label { font-weight: 700; } +.helper-text.is-error { + color: #a64038; + font-weight: 700; +} + +.helper-text.is-success { + color: var(--accent-strong); + font-weight: 700; +} + .empty-state { color: var(--muted); } @@ -543,6 +553,8 @@ label { .two-column, .publish-layout, .account-layout, +.account-grid, +.project-layout, .connect-layout { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -563,6 +575,10 @@ label { margin-top: 14px; } +.single-column { + grid-template-columns: 1fr; +} + .wide-field { grid-column: 1 / -1; } @@ -614,10 +630,163 @@ label { margin-bottom: 16px; } +.profile-card { + display: grid; + grid-template-columns: 46px minmax(0, 1fr); + gap: 12px; + align-items: center; + margin-top: 14px; + padding: 12px; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface-soft); +} + +.profile-card strong, +.profile-card small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.profile-card small { + color: var(--muted); +} + +.project-list { + display: grid; + gap: 8px; + margin-top: 14px; +} + +.project-item { + display: grid; + gap: 4px; + width: 100%; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface-soft); + padding: 12px; + color: var(--ink); + text-align: left; +} + +.project-item.is-active { + border-color: #b9d8d4; + background: var(--accent-soft); +} + +.project-item span { + overflow: hidden; + color: var(--muted); + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.member-editor { + display: grid; + grid-template-columns: minmax(220px, 1fr) minmax(140px, 0.4fr) auto; + gap: 12px; + align-items: end; + margin-top: 14px; +} + +.member-list { + display: grid; + gap: 8px; + margin-top: 14px; +} + +.member-row { + display: grid; + grid-template-columns: minmax(220px, 1fr) minmax(140px, 0.4fr) auto; + gap: 12px; + align-items: center; + border-top: 1px solid var(--line); + padding: 10px 0; +} + .account-logout { width: 100%; } +.user-table { + display: grid; + gap: 8px; + margin-top: 14px; +} + +.user-create-card { + margin-bottom: 16px; +} + +.user-create-grid { + display: grid; + grid-template-columns: minmax(180px, 1fr) minmax(160px, 0.8fr) minmax(160px, 0.8fr) minmax(140px, 0.7fr) auto; + gap: 12px; + align-items: end; + margin-top: 14px; +} + +.user-table-head, +.user-table-row { + display: grid; + grid-template-columns: minmax(220px, 1.6fr) minmax(150px, 0.8fr) minmax(90px, 0.5fr) minmax(160px, 0.9fr) minmax(90px, 0.4fr); + gap: 10px; + align-items: center; +} + +.user-table-head { + color: var(--muted); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.user-table-row { + padding: 10px 0; + border-top: 1px solid var(--line); +} + +.user-identity { + display: grid; + grid-template-columns: 38px minmax(0, 1fr); + gap: 10px; + align-items: center; +} + +.user-identity strong, +.user-identity small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-identity small { + color: var(--muted); +} + +.status-pill { + display: inline-flex; + width: fit-content; + min-height: 28px; + align-items: center; + border-radius: 999px; + padding: 0 10px; + background: var(--accent-soft); + color: var(--accent-strong); + font-size: 12px; + font-weight: 800; +} + +.status-pill.is-disabled { + background: #f4e4e2; + color: #9a3d36; +} + .source-summary { display: grid; gap: 12px; @@ -747,12 +916,23 @@ label { .two-column, .publish-layout, .account-layout, + .account-grid, + .project-layout, .connect-layout, .settings-grid, .frontmatter-grid, - .form-grid { + .form-grid, + .user-create-grid, + .user-table-head, + .user-table-row, + .member-editor, + .member-row { grid-template-columns: 1fr; } + + .user-table-head { + display: none; + } } @media (max-width: 640px) { diff --git a/src/types.ts b/src/types.ts index 06da7b2..c9cd1ab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -export type PanelKey = "account" | "admin" | "connect" | "content" | "structure" | "config" | "publish"; +export type PanelKey = "account" | "projects" | "users" | "admin" | "connect" | "content" | "structure" | "config" | "publish"; export type EditorMode = "write" | "preview"; export type DataSource = "local" | "github"; @@ -57,7 +57,19 @@ export interface CmsUser { email?: string; name: string; avatarUrl?: string; - role: "system_admin" | "user" | string; + role: "system_admin" | "admin" | "editor" | "viewer" | "user" | string; + status?: "active" | "disabled" | string; + emailVerified?: boolean; + createdAt?: string; + updatedAt?: string; + lastLoginAt?: string; + permissions?: string[]; +} + +export interface AuthOptions { + allowEmailLogin: boolean; + allowGitHubLogin: boolean; + allowRegistration: boolean; } export interface GitHubRepository { @@ -70,6 +82,30 @@ export interface GitHubRepository { updatedAt: string; } +export interface CmsProject { + id: number; + name: string; + githubOwner?: string; + githubRepo?: string; + githubBranch: string; + siteRoot: string; + ownerUserId: number; + createdAt: string; + updatedAt: string; +} + +export interface ProjectMember { + id: number; + projectId: number; + userId: number; + role: "owner" | "admin" | "editor" | "viewer" | string; + name: string; + email?: string; + avatarUrl?: string; + status?: string; + createdAt: string; +} + export interface SystemSetting { key: string; value: string; diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts new file mode 100644 index 0000000..825ee6c --- /dev/null +++ b/src/utils/permissions.ts @@ -0,0 +1,31 @@ +import type { CmsUser } from "../types"; + +export const permissions = { + settingsManage: "settings.manage", + usersManage: "users.manage", + projectsManage: "projects.manage", + contentEdit: "content.edit", + contentPublish: "content.publish", + contentView: "content.view", +} as const; + +export type Permission = (typeof permissions)[keyof typeof permissions]; + +const rolePermissions: Record = { + system_admin: Object.values(permissions), + admin: [ + permissions.usersManage, + permissions.projectsManage, + permissions.contentEdit, + permissions.contentPublish, + permissions.contentView, + ], + editor: [permissions.contentEdit, permissions.contentPublish, permissions.contentView], + viewer: [permissions.contentView], + user: [permissions.contentView], +}; + +export function hasPermission(user: CmsUser | null | undefined, permission: Permission) { + if (!user) return false; + return Boolean(user.permissions?.includes(permission) || rolePermissions[user.role]?.includes(permission)); +}