diff --git a/package.json b/package.json index 7fb86d6..8aa245c 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "ssh": "ssh -L 6379:localhost:6379 light ", "ssh:sky": "ssh -L 6379:172.21.32.13:6379 sky", "dev:lib": "turbo run dev:lib", + "build:lib": "turbo run build", "dev:oss": "turbo run dev:lib --filter=@kevisual/oss" }, "keywords": [], @@ -37,7 +38,7 @@ "dependencies": { "@kevisual/local-app-manager": "0.1.9", "@kevisual/router": "0.0.9", - "@kevisual/use-config": "^1.0.9", + "@kevisual/use-config": "^1.0.10", "@types/semver": "^7.5.8", "archiver": "^7.0.1", "crypto-js": "^4.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 031c4e6..c4643cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,13 +15,13 @@ importers: dependencies: '@kevisual/local-app-manager': specifier: 0.1.9 - version: 0.1.9(@kevisual/router@0.0.9)(@kevisual/types@0.0.6)(@kevisual/use-config@1.0.9)(pm2@6.0.5) + version: 0.1.9(@kevisual/router@0.0.9)(@kevisual/types@0.0.6)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(pm2@6.0.5) '@kevisual/router': specifier: 0.0.9 version: 0.0.9 '@kevisual/use-config': - specifier: ^1.0.9 - version: 1.0.9 + specifier: ^1.0.10 + version: 1.0.10(dotenv@16.4.7) '@types/semver': specifier: ^7.5.8 version: 7.5.8 @@ -189,16 +189,16 @@ importers: specifier: ^0.0.9 version: 0.0.9 '@kevisual/use-config': - specifier: ^1.0.9 - version: 1.0.9 + specifier: ^1.0.10 + version: 1.0.10(dotenv@16.4.7) ioredis: specifier: ^5.6.0 version: 5.6.0 nanoid: - specifier: ^5.1.2 - version: 5.1.3 + specifier: ^5.1.5 + version: 5.1.5 pg: - specifier: ^8.13.3 + specifier: ^8.14.1 version: 8.14.1 sequelize: specifier: ^6.37.6 @@ -215,22 +215,22 @@ importers: version: 0.0.6 '@rollup/plugin-alias': specifier: ^5.1.1 - version: 5.1.1(rollup@4.35.0) + version: 5.1.1(rollup@4.36.0) '@rollup/plugin-commonjs': - specifier: ^28.0.2 - version: 28.0.3(rollup@4.35.0) + specifier: ^28.0.3 + version: 28.0.3(rollup@4.36.0) '@rollup/plugin-json': specifier: ^6.1.0 - version: 6.1.0(rollup@4.35.0) + version: 6.1.0(rollup@4.36.0) '@rollup/plugin-node-resolve': - specifier: ^16.0.0 - version: 16.0.1(rollup@4.35.0) + specifier: ^16.0.1 + version: 16.0.1(rollup@4.36.0) '@rollup/plugin-replace': specifier: ^6.0.2 - version: 6.0.2(rollup@4.35.0) + version: 6.0.2(rollup@4.36.0) '@rollup/plugin-typescript': specifier: ^12.1.2 - version: 12.1.2(rollup@4.35.0)(tslib@2.8.1)(typescript@5.8.2) + version: 12.1.2(rollup@4.36.0)(tslib@2.8.1)(typescript@5.8.2) '@types/archiver': specifier: ^6.0.3 version: 6.0.3 @@ -247,11 +247,11 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^22.13.9 - version: 22.13.10 + specifier: ^22.13.11 + version: 22.13.11 '@types/react': - specifier: ^19.0.10 - version: 19.0.10 + specifier: ^19.0.12 + version: 19.0.12 '@types/uuid': specifier: ^10.0.0 version: 10.0.0 @@ -268,17 +268,17 @@ importers: specifier: latest version: 6.0.1 rollup: - specifier: ^4.34.9 - version: 4.35.0 + specifier: ^4.36.0 + version: 4.36.0 rollup-plugin-copy: specifier: ^3.5.0 version: 3.5.0 rollup-plugin-dts: - specifier: ^6.1.1 - version: 6.1.1(rollup@4.35.0)(typescript@5.8.2) + specifier: ^6.2.1 + version: 6.2.1(rollup@4.36.0)(typescript@5.8.2) rollup-plugin-esbuild: specifier: ^6.2.1 - version: 6.2.1(esbuild@0.25.0)(rollup@4.35.0) + version: 6.2.1(esbuild@0.25.0)(rollup@4.36.0) tape: specifier: ^5.9.0 version: 5.9.0 @@ -516,8 +516,10 @@ packages: '@kevisual/types@0.0.6': resolution: {integrity: sha512-7yxe1QmuC5g7lI/1Hm+zXly8if0z+ZqGM1SVOVv2VNRwRAVYBJDc365zWCCfRwE+5YaB2daWTe5zBOU4EkltkQ==} - '@kevisual/use-config@1.0.9': - resolution: {integrity: sha512-lJz98WWL178QUaf/rkM9feMm0aUnYd6ikm3ma/9Zi/K2QNrxbTRAgMGkCggUalAES8IbUvEsg+Q+Y2RaPLxCmw==} + '@kevisual/use-config@1.0.10': + resolution: {integrity: sha512-fH2B4BnR4+OjR3PzAegF8H9RJpyFZu6BnVDyfvSSZavZMurufkJ949jizoRde+bNAHff/PRcpa5EZg2imZNf1g==} + peerDependencies: + dotenv: ^16.4.7 '@ljharb/resumer@0.1.3': resolution: {integrity: sha512-d+tsDgfkj9X5QTriqM4lKesCkMMJC3IrbPKHvayP00ELx2axdXvDfWkqjxrLXIzGcQzmj7VAUT1wopqARTvafw==} @@ -624,191 +626,96 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.35.0': - resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==} - cpu: [arm] - os: [android] - '@rollup/rollup-android-arm-eabi@4.36.0': resolution: {integrity: sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.35.0': - resolution: {integrity: sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==} - cpu: [arm64] - os: [android] - '@rollup/rollup-android-arm64@4.36.0': resolution: {integrity: sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.35.0': - resolution: {integrity: sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-arm64@4.36.0': resolution: {integrity: sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.35.0': - resolution: {integrity: sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==} - cpu: [x64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.36.0': resolution: {integrity: sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.35.0': - resolution: {integrity: sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==} - cpu: [arm64] - os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.36.0': resolution: {integrity: sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.35.0': - resolution: {integrity: sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==} - cpu: [x64] - os: [freebsd] - '@rollup/rollup-freebsd-x64@4.36.0': resolution: {integrity: sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.35.0': - resolution: {integrity: sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==} - cpu: [arm] - os: [linux] - '@rollup/rollup-linux-arm-gnueabihf@4.36.0': resolution: {integrity: sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.35.0': - resolution: {integrity: sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==} - cpu: [arm] - os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.36.0': resolution: {integrity: sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.35.0': - resolution: {integrity: sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==} - cpu: [arm64] - os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.36.0': resolution: {integrity: sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.35.0': - resolution: {integrity: sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==} - cpu: [arm64] - os: [linux] - '@rollup/rollup-linux-arm64-musl@4.36.0': resolution: {integrity: sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.35.0': - resolution: {integrity: sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==} - cpu: [loong64] - os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.36.0': resolution: {integrity: sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': - resolution: {integrity: sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==} - cpu: [ppc64] - os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': resolution: {integrity: sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.35.0': - resolution: {integrity: sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==} - cpu: [riscv64] - os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.36.0': resolution: {integrity: sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.35.0': - resolution: {integrity: sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==} - cpu: [s390x] - os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.36.0': resolution: {integrity: sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.35.0': - resolution: {integrity: sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==} - cpu: [x64] - os: [linux] - '@rollup/rollup-linux-x64-gnu@4.36.0': resolution: {integrity: sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.35.0': - resolution: {integrity: sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==} - cpu: [x64] - os: [linux] - '@rollup/rollup-linux-x64-musl@4.36.0': resolution: {integrity: sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.35.0': - resolution: {integrity: sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==} - cpu: [arm64] - os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.36.0': resolution: {integrity: sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.35.0': - resolution: {integrity: sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==} - cpu: [ia32] - os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.36.0': resolution: {integrity: sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.35.0': - resolution: {integrity: sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-msvc@4.36.0': resolution: {integrity: sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==} cpu: [x64] @@ -865,15 +772,9 @@ packages: '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} - '@types/node@22.13.10': - resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==} - '@types/node@22.13.11': resolution: {integrity: sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g==} - '@types/react@19.0.10': - resolution: {integrity: sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==} - '@types/react@19.0.12': resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} @@ -1955,11 +1856,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@5.1.3: - resolution: {integrity: sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==} - engines: {node: ^18 || >=20} - hasBin: true - nanoid@5.1.5: resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} engines: {node: ^18 || >=20} @@ -2302,13 +2198,6 @@ packages: resolution: {integrity: sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==} engines: {node: '>=8.3'} - rollup-plugin-dts@6.1.1: - resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} - engines: {node: '>=16'} - peerDependencies: - rollup: ^3.29.4 || ^4 - typescript: ^4.5 || ^5.0 - rollup-plugin-dts@6.2.1: resolution: {integrity: sha512-sR3CxYUl7i2CHa0O7bA45mCrgADyAQ0tVtGSqi3yvH28M+eg1+g5d7kQ9hLvEz5dorK3XVsH5L2jwHLQf72DzA==} engines: {node: '>=16'} @@ -2323,11 +2212,6 @@ packages: esbuild: '>=0.18.0' rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 - rollup@4.35.0: - resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - rollup@4.36.0: resolution: {integrity: sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3018,11 +2902,11 @@ snapshots: dependencies: eventemitter3: 5.0.1 - '@kevisual/local-app-manager@0.1.9(@kevisual/router@0.0.9)(@kevisual/types@0.0.6)(@kevisual/use-config@1.0.9)(pm2@6.0.5)': + '@kevisual/local-app-manager@0.1.9(@kevisual/router@0.0.9)(@kevisual/types@0.0.6)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(pm2@6.0.5)': dependencies: '@kevisual/router': 0.0.9 '@kevisual/types': 0.0.6 - '@kevisual/use-config': 1.0.9 + '@kevisual/use-config': 1.0.10(dotenv@16.4.7) pm2: 6.0.5 '@kevisual/router@0.0.9': @@ -3036,9 +2920,10 @@ snapshots: '@kevisual/types@0.0.6': {} - '@kevisual/use-config@1.0.9': + '@kevisual/use-config@1.0.10(dotenv@16.4.7)': dependencies: '@kevisual/load': 0.0.4 + dotenv: 16.4.7 '@ljharb/resumer@0.1.3': dependencies: @@ -3114,26 +2999,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@rollup/plugin-alias@5.1.1(rollup@4.35.0)': - optionalDependencies: - rollup: 4.35.0 - '@rollup/plugin-alias@5.1.1(rollup@4.36.0)': optionalDependencies: rollup: 4.36.0 - '@rollup/plugin-commonjs@28.0.3(rollup@4.35.0)': - dependencies: - '@rollup/pluginutils': 5.1.2(rollup@4.35.0) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.3.0(picomatch@4.0.2) - is-reference: 1.2.1 - magic-string: 0.30.11 - picomatch: 4.0.2 - optionalDependencies: - rollup: 4.35.0 - '@rollup/plugin-commonjs@28.0.3(rollup@4.36.0)': dependencies: '@rollup/pluginutils': 5.1.2(rollup@4.36.0) @@ -3146,28 +3015,12 @@ snapshots: optionalDependencies: rollup: 4.36.0 - '@rollup/plugin-json@6.1.0(rollup@4.35.0)': - dependencies: - '@rollup/pluginutils': 5.1.2(rollup@4.35.0) - optionalDependencies: - rollup: 4.35.0 - '@rollup/plugin-json@6.1.0(rollup@4.36.0)': dependencies: '@rollup/pluginutils': 5.1.2(rollup@4.36.0) optionalDependencies: rollup: 4.36.0 - '@rollup/plugin-node-resolve@16.0.1(rollup@4.35.0)': - dependencies: - '@rollup/pluginutils': 5.1.2(rollup@4.35.0) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.8 - optionalDependencies: - rollup: 4.35.0 - '@rollup/plugin-node-resolve@16.0.1(rollup@4.36.0)': dependencies: '@rollup/pluginutils': 5.1.2(rollup@4.36.0) @@ -3178,13 +3031,6 @@ snapshots: optionalDependencies: rollup: 4.36.0 - '@rollup/plugin-replace@6.0.2(rollup@4.35.0)': - dependencies: - '@rollup/pluginutils': 5.1.2(rollup@4.35.0) - magic-string: 0.30.11 - optionalDependencies: - rollup: 4.35.0 - '@rollup/plugin-replace@6.0.2(rollup@4.36.0)': dependencies: '@rollup/pluginutils': 5.1.2(rollup@4.36.0) @@ -3192,15 +3038,6 @@ snapshots: optionalDependencies: rollup: 4.36.0 - '@rollup/plugin-typescript@12.1.2(rollup@4.35.0)(tslib@2.8.1)(typescript@5.8.2)': - dependencies: - '@rollup/pluginutils': 5.1.2(rollup@4.35.0) - resolve: 1.22.8 - typescript: 5.8.2 - optionalDependencies: - rollup: 4.35.0 - tslib: 2.8.1 - '@rollup/plugin-typescript@12.1.2(rollup@4.36.0)(tslib@2.8.1)(typescript@5.8.2)': dependencies: '@rollup/pluginutils': 5.1.2(rollup@4.36.0) @@ -3210,14 +3047,6 @@ snapshots: rollup: 4.36.0 tslib: 2.8.1 - '@rollup/pluginutils@5.1.2(rollup@4.35.0)': - dependencies: - '@types/estree': 1.0.6 - estree-walker: 2.0.2 - picomatch: 4.0.2 - optionalDependencies: - rollup: 4.35.0 - '@rollup/pluginutils@5.1.2(rollup@4.36.0)': dependencies: '@types/estree': 1.0.6 @@ -3226,117 +3055,60 @@ snapshots: optionalDependencies: rollup: 4.36.0 - '@rollup/rollup-android-arm-eabi@4.35.0': - optional: true - '@rollup/rollup-android-arm-eabi@4.36.0': optional: true - '@rollup/rollup-android-arm64@4.35.0': - optional: true - '@rollup/rollup-android-arm64@4.36.0': optional: true - '@rollup/rollup-darwin-arm64@4.35.0': - optional: true - '@rollup/rollup-darwin-arm64@4.36.0': optional: true - '@rollup/rollup-darwin-x64@4.35.0': - optional: true - '@rollup/rollup-darwin-x64@4.36.0': optional: true - '@rollup/rollup-freebsd-arm64@4.35.0': - optional: true - '@rollup/rollup-freebsd-arm64@4.36.0': optional: true - '@rollup/rollup-freebsd-x64@4.35.0': - optional: true - '@rollup/rollup-freebsd-x64@4.36.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.35.0': - optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.36.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.35.0': - optional: true - '@rollup/rollup-linux-arm-musleabihf@4.36.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.35.0': - optional: true - '@rollup/rollup-linux-arm64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.35.0': - optional: true - '@rollup/rollup-linux-arm64-musl@4.36.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.35.0': - optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.35.0': - optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.35.0': - optional: true - '@rollup/rollup-linux-riscv64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.35.0': - optional: true - '@rollup/rollup-linux-s390x-gnu@4.36.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.35.0': - optional: true - '@rollup/rollup-linux-x64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-x64-musl@4.35.0': - optional: true - '@rollup/rollup-linux-x64-musl@4.36.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.35.0': - optional: true - '@rollup/rollup-win32-arm64-msvc@4.36.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.35.0': - optional: true - '@rollup/rollup-win32-ia32-msvc@4.36.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.35.0': - optional: true - '@rollup/rollup-win32-x64-msvc@4.36.0': optional: true @@ -3394,18 +3166,10 @@ snapshots: dependencies: '@types/node': 22.13.11 - '@types/node@22.13.10': - dependencies: - undici-types: 6.20.0 - '@types/node@22.13.11': dependencies: undici-types: 6.20.0 - '@types/react@19.0.10': - dependencies: - csstype: 3.1.3 - '@types/react@19.0.12': dependencies: csstype: 3.1.3 @@ -4579,8 +4343,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@5.1.3: {} - nanoid@5.1.5: {} needle@2.4.0: @@ -4969,14 +4731,6 @@ snapshots: globby: 10.0.1 is-plain-object: 3.0.1 - rollup-plugin-dts@6.1.1(rollup@4.35.0)(typescript@5.8.2): - dependencies: - magic-string: 0.30.11 - rollup: 4.35.0 - typescript: 5.8.2 - optionalDependencies: - '@babel/code-frame': 7.26.2 - rollup-plugin-dts@6.2.1(rollup@4.36.0)(typescript@5.8.2): dependencies: magic-string: 0.30.17 @@ -4985,17 +4739,6 @@ snapshots: optionalDependencies: '@babel/code-frame': 7.26.2 - rollup-plugin-esbuild@6.2.1(esbuild@0.25.0)(rollup@4.35.0): - dependencies: - debug: 4.4.0 - es-module-lexer: 1.6.0 - esbuild: 0.25.0 - get-tsconfig: 4.10.0 - rollup: 4.35.0 - unplugin-utils: 0.2.4 - transitivePeerDependencies: - - supports-color - rollup-plugin-esbuild@6.2.1(esbuild@0.25.0)(rollup@4.36.0): dependencies: debug: 4.4.0 @@ -5007,31 +4750,6 @@ snapshots: transitivePeerDependencies: - supports-color - rollup@4.35.0: - dependencies: - '@types/estree': 1.0.6 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.35.0 - '@rollup/rollup-android-arm64': 4.35.0 - '@rollup/rollup-darwin-arm64': 4.35.0 - '@rollup/rollup-darwin-x64': 4.35.0 - '@rollup/rollup-freebsd-arm64': 4.35.0 - '@rollup/rollup-freebsd-x64': 4.35.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.35.0 - '@rollup/rollup-linux-arm-musleabihf': 4.35.0 - '@rollup/rollup-linux-arm64-gnu': 4.35.0 - '@rollup/rollup-linux-arm64-musl': 4.35.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.35.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.35.0 - '@rollup/rollup-linux-riscv64-gnu': 4.35.0 - '@rollup/rollup-linux-s390x-gnu': 4.35.0 - '@rollup/rollup-linux-x64-gnu': 4.35.0 - '@rollup/rollup-linux-x64-musl': 4.35.0 - '@rollup/rollup-win32-arm64-msvc': 4.35.0 - '@rollup/rollup-win32-ia32-msvc': 4.35.0 - '@rollup/rollup-win32-x64-msvc': 4.35.0 - fsevents: 2.3.3 - rollup@4.36.0: dependencies: '@types/estree': 1.0.6 diff --git a/src/app.ts b/src/app.ts index 9f3fc7a..9a75c89 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,10 +5,18 @@ import * as minioLib from './modules/minio.ts'; import * as sequelizeLib from './modules/sequelize.ts'; import { useContextKey, useContext } from '@kevisual/use-config/context'; import { SimpleRouter } from '@kevisual/router/simple'; - +import { OssBase } from '@kevisual/oss/services'; useConfig(); export const router = useContextKey('router', () => new SimpleRouter()); - +export const oss = useContextKey( + 'oss', + () => + new OssBase({ + client: minioLib.minioClient, + bucketName: minioLib.bucketName, + prefix: '', + }), +); export const redis = useContextKey('redis', () => redisLib.redis); export const redisPublisher = useContextKey('redisPublisher', () => redisLib.redisPublisher); export const redisSubscriber = useContextKey('redisSubscriber', () => redisLib.redisSubscriber); diff --git a/src/route.ts b/src/route.ts index 9309dda..657f3c6 100644 --- a/src/route.ts +++ b/src/route.ts @@ -1,8 +1,64 @@ import './routes/index.ts'; import { app } from './app.ts'; +import type { App } from '@kevisual/router'; import { User } from './models/user.ts'; -import { addAuth } from '@kevisual/code-center-module/models'; +// import { addAuth } from '@kevisual/code-center-module/models'; +// addAuth(app); +import { createCookie, getSomeInfoFromReq } from './routes/user/me.ts'; +/** + * 添加auth中间件, 用于验证token + * 添加 id: auth 必须需要user成功 + * 添加 id: auth-can 可以不需要user成功,有则赋值 + * + * @param app + */ +export const addAuth = (app: App) => { + app + .route({ + path: 'auth', + id: 'auth', + }) + .define(async (ctx) => { + const token = ctx.query.token; + if (!token) { + app.throw(401, 'Token is required'); + } + const user = await User.getOauthUser(token); + if (!user) { + app.throw(401, 'Token is invalid'); + } + const someInfo = getSomeInfoFromReq(ctx); + if (someInfo.isBrowser && !ctx.req?.cookies?.['token']) { + createCookie({ accessToken: token }, ctx); + } + ctx.state.tokenUser = user; + }) + .addTo(app); + + app + .route({ + path: 'auth', + key: 'can', + id: 'auth-can', + }) + .define(async (ctx) => { + if (ctx.query?.token) { + const token = ctx.query.token; + const user = await User.getOauthUser(token); + if (token) { + ctx.state.tokenUser = user; + const someInfo = getSomeInfoFromReq(ctx); + if (someInfo.isBrowser && !ctx.req?.cookies?.['token']) { + createCookie({ accessToken: token }, ctx); + } + } else { + ctx.state.tokenUser = null; + } + } + }) + .addTo(app); +}; addAuth(app); app @@ -53,6 +109,7 @@ app if (!tokenUser) { ctx.throw(401, 'No User For authorized'); } + try { const user = await User.findOne({ where: { diff --git a/src/routes/config/check.ts b/src/routes/config/check.ts new file mode 100644 index 0000000..18b44c9 --- /dev/null +++ b/src/routes/config/check.ts @@ -0,0 +1,78 @@ +import { app } from '@/app.ts'; +import { ConfigModel } from './models/model.ts'; +import { oss } from '@/app.ts'; +import { ConfigOssService } from '@kevisual/oss/services'; +import { Op } from 'sequelize'; + +app + .route({ + path: 'config', + key: 'detect', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const owner = tokenUser.username; + const configOss = ConfigOssService.fromBase({ + oss, + opts: { + owner, + }, + }); + const { list, keys, keyEtagMap } = await configOss.getList(); + const configList = await ConfigModel.findAll({ + where: { + key: { + [Op.in]: keys, + }, + uid: tokenUser.id, + }, + }); + const needUpdateList = list.filter((item) => { + const key = item.key; + const hash = keyEtagMap.get(key); + const config = configList.find((item) => item.key === key); + if (!config) { + return true; + } + return config?.hash !== hash; + }); + const keyDataMap = await configOss.getObjectList(needUpdateList.map((item) => item.key)); + const updateList = []; + for (const [key, json] of keyDataMap.entries()) { + const keyETag = keyEtagMap.get(key); + const configData = keyDataMap.get(key); + if (keyETag && configData) { + const [config, created] = await ConfigModel.findOrCreate({ + where: { + key, + uid: tokenUser.id, + }, + defaults: { + key, + title: key, + description: `从${key}:${keyETag} 同步而来`, + uid: tokenUser.id, + hash: keyETag, + data: configData, + }, + }); + if (!created) { + await config.update( + { + hash: keyETag, + data: json, + }, + { + fields: ['hash', 'data'], + }, + ); + } + updateList.push(config); + } + } + ctx.body = { + updateList, + }; + }) + .addTo(app); diff --git a/src/routes/config/config-key.ts b/src/routes/config/config-key.ts new file mode 100644 index 0000000..96a7b4f --- /dev/null +++ b/src/routes/config/config-key.ts @@ -0,0 +1,45 @@ +import { app } from '@/app.ts'; +import { ConfigModel } from './models/model.ts'; +import { ShareConfigService } from './services/share.ts'; +import { oss } from '@/app.ts'; +import { ConfigOssService } from '@kevisual/oss/services'; +import { User } from '@/models/user.ts'; +import { defaultKeys } from './models/default-keys.ts'; +app + .route({ + path: 'config', + key: 'defaultConfig', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { configKey } = ctx.query; + if (!configKey) { + ctx.throw(400, 'configKey is required'); + } + const user = new User(); + user.setTokenUser(tokenUser); + const isAdmin = await user.hasUser('admin'); + const usersConfig = ['upload.json', 'workspace.json', 'ai.json']; + const adminConfig = ['vip.json']; + const configs = [...usersConfig, ...(isAdmin ? adminConfig : [])]; + if (!configs.includes(configKey)) { + ctx.throw(400, 'configKey is invalid'); + } + const defaultConfig = defaultKeys.find((item) => item.key === configKey); + + const [config, created] = await ConfigModel.findOrCreate({ + where: { + key: configKey, + uid: tokenUser.id, + }, + defaults: { + key: configKey, + uid: tokenUser.id, + data: defaultConfig?.data, + }, + }); + + ctx.body = config; + }) + .addTo(app); diff --git a/src/routes/config/index.ts b/src/routes/config/index.ts index 7374005..2739274 100644 --- a/src/routes/config/index.ts +++ b/src/routes/config/index.ts @@ -1,3 +1,5 @@ import './list.ts'; import './upload-config.ts'; -import './share-config.ts'; \ No newline at end of file +import './share-config.ts'; +import './check.ts'; +import './config-key.ts'; diff --git a/src/routes/config/list.ts b/src/routes/config/list.ts index 22b04d6..d22c17d 100644 --- a/src/routes/config/list.ts +++ b/src/routes/config/list.ts @@ -1,6 +1,9 @@ import { app } from '@/app.ts'; import { ConfigModel } from './models/model.ts'; import { ShareConfigService } from './services/share.ts'; +import { oss } from '@/app.ts'; +import { ConfigOssService } from '@kevisual/oss/services'; + app .route({ path: 'config', @@ -31,56 +34,57 @@ app const tokernUser = ctx.state.tokenUser; const tuid = tokernUser.id; const { id, data, ...rest } = ctx.query?.data || {}; + let config: ConfigModel; if (id) { - const config = await ConfigModel.findByPk(id); + config = await ConfigModel.findByPk(id); let keyIsChange = false; if (rest?.key) { keyIsChange = rest.key !== config?.key; } - if (config && config.uid === tuid) { - if (keyIsChange) { - const key = rest.key; - const keyConfig = await ConfigModel.findOne({ - where: { - key, - uid: tuid, - }, - }); - if (keyConfig && keyConfig.id !== id) { - ctx.throw(403, 'key is already exists'); - } - } - await config.update({ - data: { - ...config.data, - ...data, - }, - ...rest, - }); - if (config.data?.permission?.share === 'public') { - await ShareConfigService.expireShareConfig(config.key, tokernUser.username); - } - ctx.body = config; - } else { + if (!config || config.uid !== tuid) { ctx.throw(403, 'no permission'); } + if (keyIsChange) { + const key = rest.key; + const keyConfig = await ConfigModel.findOne({ + where: { + key, + uid: tuid, + }, + }); + if (keyConfig && keyConfig.id !== id) { + ctx.throw(403, 'key is already exists'); + } + } + await config.update({ + data: { + ...config.data, + ...data, + }, + ...rest, + }); + if (config.data?.permission?.share === 'public') { + await ShareConfigService.expireShareConfig(config.key, tokernUser.username); + } + ctx.body = config; } else if (rest?.key) { // id 不存在,key存在,则属于更新,key不能重复 const key = rest.key; - const keyConfig = await ConfigModel.findOne({ + config = await ConfigModel.findOne({ where: { key, uid: tuid, }, }); - if (keyConfig) { - await keyConfig.update({ - data: { ...keyConfig.data, ...data }, + if (config) { + await config.update({ + data: { ...config.data, ...data }, ...rest, }); - ctx.body = keyConfig; + ctx.body = config; } else { - const config = await ConfigModel.create({ + // 根据key创建一个配置 + config = await ConfigModel.create({ key, ...rest, data: data, @@ -89,15 +93,33 @@ app ctx.body = config; } } - if (id || rest?.key) return; + const key = config?.key; + const ossConfig = ConfigOssService.fromBase({ + oss, + opts: { + owner: tokernUser.username, + }, + }); + if (ossConfig.isEndWithJson(key)) { + const data = config.data; + const hash = ossConfig.hash(data); + if (config.hash !== hash) { + config.hash = hash; + await config.save({ + fields: ['hash'], + }); + await ossConfig.putJsonObject(key, data); + } + } + if (config) return; - // id和key不存在。创建一个新的配置 - const config = await ConfigModel.create({ + // id和key不存在。创建一个新的配置, 而且没有id的 + const newConfig = await ConfigModel.create({ ...rest, data: data, uid: tuid, }); - ctx.body = config; + ctx.body = newConfig; }) .addTo(app); @@ -154,6 +176,18 @@ app }, }); if (config && config.uid === tuid) { + const key = config.key; + const ossConfig = ConfigOssService.fromBase({ + oss, + opts: { + owner: tokernUser.username, + }, + }); + if (ossConfig.isEndWithJson(key)) { + try { + await ossConfig.deleteObject(key); + } catch (e) {} + } await config.destroy(); } else { ctx.throw(403, 'no permission'); diff --git a/src/routes/config/models/default-keys.ts b/src/routes/config/models/default-keys.ts new file mode 100644 index 0000000..e3362ea --- /dev/null +++ b/src/routes/config/models/default-keys.ts @@ -0,0 +1,18 @@ +export const defaultKeys = [ + { + key: 'upload.json', + data: { key: 'upload', version: '1.0.0' }, + }, + { + key: 'workspace.json', + data: { key: 'workspace', version: '1.0.0' }, + }, + { + key: 'ai.json', + data: { key: 'ai', version: '1.0.0' }, + }, + { + key: 'vip.json', + data: { key: 'vip', version: '1.0.0' }, + }, +]; diff --git a/src/routes/config/models/model.ts b/src/routes/config/models/model.ts index 6e23f32..bad57ba 100644 --- a/src/routes/config/models/model.ts +++ b/src/routes/config/models/model.ts @@ -25,6 +25,7 @@ export class ConfigModel extends Model { declare key: string; declare data: ConfigData; // files declare uid: string; + declare hash: string; /** * 获取用户配置 * @param key 配置key diff --git a/src/routes/mark/list.ts b/src/routes/mark/list.ts new file mode 100644 index 0000000..272827b --- /dev/null +++ b/src/routes/mark/list.ts @@ -0,0 +1,227 @@ +import { app } from '@/app.ts'; +import { MarkModel } from './model.ts'; +import { MarkServices } from './services/mark.ts'; +import dayjs from 'dayjs'; + +app + .route({ + path: 'mark', + key: 'list', + description: 'mark list.', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + ctx.body = await MarkServices.getList({ + uid: tokenUser.id, + query: ctx.query, + queryType: 'simple', + }); + }) + .addTo(app); + +app + .route({ + path: 'mark', + key: 'getVersion', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query; + if (id) { + const markModel = await MarkModel.findByPk(id); + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + ctx.body = { + version: Number(markModel.version), + updatedAt: markModel.updatedAt, + createdAt: markModel.createdAt, + id: markModel.id, + }; + } else { + const [markModel, created] = await MarkModel.findOrCreate({ + where: { + uid: tokenUser.id, + puid: tokenUser.uid, + title: dayjs().format('YYYY-MM-DD'), + }, + defaults: { + title: dayjs().format('YYYY-MM-DD'), + uid: tokenUser.id, + markType: 'wallnote', + tags: ['daily'], + }, + }); + ctx.body = { + version: Number(markModel.version), + updatedAt: markModel.updatedAt, + createdAt: markModel.createdAt, + id: markModel.id, + created: created, + }; + } + }) + .addTo(app); + +app + .route({ + path: 'mark', + key: 'get', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query; + if (id) { + const markModel = await MarkModel.findByPk(id); + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + ctx.body = markModel; + } else { + // id 不存在,获取当天的title为 日期的一条数据 + const [markModel, created] = await MarkModel.findOrCreate({ + where: { + uid: tokenUser.id, + puid: tokenUser.uid, + title: dayjs().format('YYYY-MM-DD'), + }, + defaults: { + title: dayjs().format('YYYY-MM-DD'), + uid: tokenUser.id, + markType: 'wallnote', + tags: ['daily'], + uname: tokenUser.username, + puid: tokenUser.uid, + version: 1, + }, + }); + ctx.body = markModel; + } + }) + .addTo(app); + +app + .route({ + path: 'mark', + key: 'update', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id, ...data } = ctx.query.data || {}; + let markModel: MarkModel; + if (id) { + markModel = await MarkModel.findByPk(id); + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + const version = Number(markModel.version) + 1; + await markModel.update({ ...markModel.data, ...data, version }); + } else { + markModel = await MarkModel.create({ + ...data, + uname: tokenUser.username, + uid: tokenUser.id, + puid: tokenUser.uid, + }); + } + ctx.body = markModel; + }) + .addTo(app); +app + .route({ + path: 'mark', + key: 'updateNode', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const operate = ctx.query.operate || 'update'; + const { id, node } = ctx.query.data || {}; + const markModel = await MarkModel.findByPk(id); + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + await MarkModel.updateJsonNode(id, node, { operate }); + ctx.body = markModel; + }) + .addTo(app); +app + .route({ + path: 'mark', + key: 'updateNodes', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id, nodeOperateList } = ctx.query.data || {}; + const markModel = await MarkModel.findByPk(id); + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + if (!nodeOperateList || !Array.isArray(nodeOperateList) || nodeOperateList.length === 0) { + ctx.throw(400, 'nodeOperateList is required'); + } + if (nodeOperateList.some((node) => !node.node)) { + ctx.throw(400, 'nodeOperateList node is required'); + } + const newmark = await MarkModel.updateJsonNodes(id, nodeOperateList); + ctx.body = newmark; + }) + .addTo(app); + +app + .route({ + path: 'mark', + key: 'delete', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query; + const markModel = await MarkModel.findByPk(id); + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + await markModel.destroy(); + ctx.body = markModel; + }) + .addTo(app); + +app + .route({ path: 'mark', key: 'getMenu', description: '获取菜单', middleware: ['auth'] }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { rows, count } = await MarkModel.findAndCountAll({ + where: { + uid: tokenUser.id, + }, + attributes: ['id', 'title', 'summary', 'tags', 'thumbnail', 'link', 'createdAt', 'updatedAt'], + }); + ctx.body = { + list: rows, + total: count, + }; + }) + .addTo(app); diff --git a/src/routes/mark/model.ts b/src/routes/mark/model.ts new file mode 100644 index 0000000..822b3bf --- /dev/null +++ b/src/routes/mark/model.ts @@ -0,0 +1,319 @@ +import { useContextKey } from '@kevisual/use-config/context'; +import { nanoid, customAlphabet } from 'nanoid'; +import { DataTypes, Model, ModelAttributes } from 'sequelize'; +import type { Sequelize } from 'sequelize'; +export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); +export type Mark = Partial>; +export type MarkData = { + md?: string; // markdown + mdList?: string[]; // markdown list + type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file + data?: any; + push?: boolean; // 是否推送到elasticsearch + pushTime?: Date; // 推送时间 + summary?: string; // 摘要 + nodes?: MarkDataNode[]; // 节点 + [key: string]: any; +}; +export type MarkFile = { + id: string; + name: string; + url: string; + size: number; + type: 'self' | 'data' | 'generate'; // generate为生成文件 + query: string; // 'data.nodes[id].content'; + hash: string; + fileKey: string; // 文件的名称, 唯一 +}; +export type MarkDataNode = { + id?: string; + [key: string]: any; +}; +export type MarkConfig = { + [key: string]: any; +}; +export type MarkAuth = { + [key: string]: any; +}; +/** + * 隐秘内容 + * auth + * config + * + */ +export class MarkModel extends Model { + declare id: string; + declare title: string; // 标题,可以ai生成 + declare description: string; // 描述,可以ai生成 + declare cover: string; // 封面,可以ai生成 + declare thumbnail: string; // 缩略图 + + declare markType: string; // markdown | json | html | image | video | audio | code | link | file + declare link: string; // 访问链接 + declare tags: string[]; // 标签 + declare summary: string; // 摘要, description的简化版 + declare data: MarkData; // 数据 + + declare uid: string; // 操作用户的id + declare puid: string; // 父级用户的id, 真实用户 + declare config: MarkConfig; // mark属于一定不会暴露的内容。 + + declare fileList: MarkFile[]; // 文件管理 + declare uname: string; // 用户的名称, 或者着别名 + + declare createdAt: Date; + declare updatedAt: Date; + declare version: number; + /** + * 加锁更新data中的node的节点,通过node的id + * @param param0 + */ + static async updateJsonNode(id: string, node: MarkDataNode, opts?: { operate?: 'update' | 'delete'; Model?: any; sequelize?: Sequelize }) { + const sequelize = opts?.sequelize || (await useContextKey('sequelize')); + const transaction = await sequelize.transaction(); // 开启事务 + const operate = opts.operate || 'update'; + const isUpdate = operate === 'update'; + const Model = opts.Model || MarkModel; + try { + // 1. 获取当前的 JSONB 字段值(加锁) + const mark = await Model.findByPk(id, { + transaction, + lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 + }); + if (!mark) { + throw new Error('Mark not found'); + } + // 2. 修改特定的数组元素 + const data = mark.data as MarkData; + const items = data.nodes; + if (!node.id) { + node.id = random(12); + } + + // 找到要更新的元素 + const itemIndex = items.findIndex((item) => item.id === node.id); + if (itemIndex === -1) { + isUpdate && items.push(node); + } else { + if (isUpdate) { + items[itemIndex] = node; + } else { + items.splice(itemIndex, 1); + } + } + const version = Number(mark.version) + 1; + // 4. 更新 JSONB 字段 + const result = await mark.update( + { + data: { + ...data, + nodes: items, + }, + version, + }, + { transaction }, + ); + + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + static async updateJsonNodes(id: string, nodes: { node: MarkDataNode; operate?: 'update' | 'delete' }[], opts?: { Model?: any; sequelize?: Sequelize }) { + const sequelize = opts?.sequelize || (await useContextKey('sequelize')); + const transaction = await sequelize.transaction(); // 开启事务 + const Model = opts?.Model || MarkModel; + try { + const mark = await Model.findByPk(id, { + transaction, + lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 + }); + if (!mark) { + throw new Error('Mark not found'); + } + const data = mark.data as MarkData; + const _nodes = data.nodes || []; + // 过滤不在nodes中的节点 + const blankNodes = nodes.filter((node) => !_nodes.find((n) => n.id === node.node.id)).map((node) => node.node); + // 更新或删除节点 + const newNodes = _nodes + .map((node) => { + const nodeOperate = nodes.find((n) => n.node.id === node.id); + if (nodeOperate) { + if (nodeOperate.operate === 'delete') { + return null; + } + return nodeOperate.node; + } + return node; + }) + .filter((node) => node !== null); + const version = Number(mark.version) + 1; + const result = await mark.update( + { + data: { + ...data, + nodes: [...blankNodes, ...newNodes], + }, + version, + }, + { transaction }, + ); + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + static async updateData(id: string, data: MarkData, opts: { Model?: any; sequelize?: Sequelize }) { + const sequelize = opts.sequelize || (await useContextKey('sequelize')); + const transaction = await sequelize.transaction(); // 开启事务 + const Model = opts.Model || MarkModel; + const mark = await Model.findByPk(id, { + transaction, + lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 + }); + if (!mark) { + throw new Error('Mark not found'); + } + const version = Number(mark.version) + 1; + const result = await mark.update( + { + ...mark.data, + ...data, + data: { + ...mark.data, + ...data, + }, + version, + }, + { transaction }, + ); + await transaction.commit(); + return result; + } + static async createNew(data: any, opts: { Model?: any; sequelize?: Sequelize }) { + const sequelize = opts.sequelize || (await useContextKey('sequelize')); + const transaction = await sequelize.transaction(); // 开启事务 + const Model = opts.Model || MarkModel; + const result = await Model.create({ ...data, version: 1 }, { transaction }); + await transaction.commit(); + return result; + } +} +export type MarkInitOpts = { + tableName: string; + sequelize?: Sequelize; + callInit?: (attribute: ModelAttributes) => ModelAttributes; + Model?: T; +}; +export type Opts = { + sync?: boolean; + alter?: boolean; + logging?: boolean; + force?: boolean; +}; +export const MarkMInit = async (opts: MarkInitOpts, sync?: Opts) => { + const sequelize = await useContextKey('sequelize'); + opts.sequelize = opts.sequelize || sequelize; + const { callInit, Model, ...optsRest } = opts; + const modelAttribute = { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + comment: 'id', + }, + title: { + type: DataTypes.TEXT, + defaultValue: '', + }, + markType: { + type: DataTypes.TEXT, + defaultValue: 'md', // markdown | json | html | image | video | audio | code | link | file + comment: '类型', + }, + description: { + type: DataTypes.TEXT, + defaultValue: '', + }, + cover: { + type: DataTypes.TEXT, + defaultValue: '', + comment: '封面', + }, + thumbnail: { + type: DataTypes.TEXT, + defaultValue: '', + comment: '缩略图', + }, + link: { + type: DataTypes.TEXT, + defaultValue: '', + comment: '链接', + }, + tags: { + type: DataTypes.JSONB, + defaultValue: [], + }, + summary: { + type: DataTypes.TEXT, + defaultValue: '', + comment: '摘要', + }, + config: { + type: DataTypes.JSONB, + defaultValue: {}, + }, + data: { + type: DataTypes.JSONB, + defaultValue: {}, + }, + fileList: { + type: DataTypes.JSONB, + defaultValue: [], + }, + uname: { + type: DataTypes.STRING, + defaultValue: '', + comment: '用户的名称, 更新后的用户的名称', + }, + version: { + type: DataTypes.INTEGER, // 更新刷新版本,多人协作 + defaultValue: 1, + }, + uid: { + type: DataTypes.UUID, + allowNull: true, + }, + puid: { + type: DataTypes.UUID, + allowNull: true, + }, + }; + const InitModel = Model || MarkModel; + // @ts-ignore + InitModel.init(callInit ? callInit(modelAttribute) : modelAttribute, { + sequelize, + paranoid: true, + ...optsRest, + }); + console.log('MarkModel init', optsRest); + if (sync && sync.sync) { + const { sync: _, ...rest } = sync; + console.log('MarkModel sync', rest); + MarkModel.sync({ alter: true, logging: false, ...rest }).catch((e) => { + console.error('MarkModel sync', e); + }); + } +}; + +export const markModelInit = MarkMInit; + +export const syncMarkModel = async (sync?: Opts) => { + const sequelize = await useContextKey('sequelize'); + await MarkMInit({ sequelize, tableName: 'micro_mark' }, sync); +}; diff --git a/src/routes/mark/services/mark.ts b/src/routes/mark/services/mark.ts new file mode 100644 index 0000000..646445a --- /dev/null +++ b/src/routes/mark/services/mark.ts @@ -0,0 +1,58 @@ +import { FindAttributeOptions, Op } from 'sequelize'; +import { MarkModel } from '../model.ts'; + +export class MarkServices { + static getList = async (opts: { + /** 查询用户的 */ + uid?: string; + query?: { + page?: number; + pageSize?: number; + search?: string; + markType?: string; + sort?: string; + }; + /** + * 查询类型 + * simple: 简单查询 默认 + */ + queryType?: string; + }) => { + const { uid, query } = opts; + const { page = 1, pageSize = 999, search, sort = 'DESC' } = query; + const searchWhere = search + ? { + [Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { summary: { [Op.like]: `%${search}%` } }], + } + : {}; + if (opts.query?.markType) { + searchWhere['markType'] = opts.query.markType; + } + const attributes: FindAttributeOptions = { + exclude: [], + }; + const queryType = opts.queryType || 'simple'; + if (queryType === 'simple') { + // attributes.include = ['id', 'title', 'link', 'summary', 'thumbnail', 'markType', 'tags', 'uid', 'share', 'uname']; + attributes.exclude = ['data', 'config', 'cover', 'description']; + } + const { rows, count } = await MarkModel.findAndCountAll({ + where: { + uid: uid, + ...searchWhere, + }, + order: [['updatedAt', sort]], + attributes: attributes, + limit: pageSize, + offset: (page - 1) * pageSize, + }); + return { + pagination: { + current: page, + pageSize, + total: count, + }, + list: rows, + }; + }; +} diff --git a/src/routes/user/me.ts b/src/routes/user/me.ts index 8b546fa..fe14e7c 100644 --- a/src/routes/user/me.ts +++ b/src/routes/user/me.ts @@ -12,8 +12,10 @@ export const createCookie = (token: any, ctx: any) => { if (!domain) { return; } - if (ctx.res.cookie) { - ctx.res.cookie('token', token.token, { + const browser = ctx.req.headers['user-agent']; + const isBrowser = browser.includes('Mozilla'); // 浏览器 + if (isBrowser && ctx.res.cookie) { + ctx.res.cookie('token', token.accessToken || token?.token, { maxAge: 7 * 24 * 60 * 60 * 1000, // 过期时间, 设置7天 domain, sameSite: 'lax', @@ -21,7 +23,48 @@ export const createCookie = (token: any, ctx: any) => { }); } }; -const clearCookie = (ctx: any) => { +export type ReqHeaders = { + host: string; + 'x-forwarded-for': string; + 'x-real-ip': string; + 'sec-ch-ua': string; // 浏览器 + 'sec-ch-ua-mobile': string; // 移动设备 + 'sec-ch-ua-platform': string; // 平台 + 'sec-ch-ua-arch': string; // 架构 + 'sec-ch-ua-bitness': string; // 位数 + 'sec-ch-ua-full-version': string; // 完整版本 + 'sec-ch-ua-full-version-list': string; // 完整版本列表 + 'sec-fetch-dest': string; // 目标 + 'sec-fetch-mode': string; // 模式 + 'sec-fetch-site': string; // 站点 + 'sec-fetch-user': string; // 用户 + 'upgrade-insecure-requests': string; // 升级不安全请求 + 'user-agent': string; // 用户代理 + accept: string; // 接受 + 'accept-language': string; // 接受语言 + 'accept-encoding': string; // 接受编码 + 'cache-control': string; // 缓存控制 + pragma: string; // 预先 + expires: string; // 过期 + connection: string; // 连接 + cookie: string; // 饼干 +}; +export const getSomeInfoFromReq = (ctx: any) => { + const headers = ctx.req.headers as ReqHeaders; + const userAgent = headers['user-agent']; + const host = headers['host']; + const ip = headers['x-forwarded-for'] || ctx.req.connection.remoteAddress; + console.log('req headers', headers); + return { + 'user-agent': userAgent, + browser: userAgent, + isBrowser: userAgent.includes('Mozilla'), + host, + ip, + headers, + }; +}; +export const clearCookie = (ctx: any) => { if (!domain) { return; } @@ -87,7 +130,12 @@ app ctx.throw(500, 'Password error'); } user.expireOrgs(); - const token = await user.createToken(null, loginType); + const someInfo = getSomeInfoFromReq(ctx); + const token = await user.createToken(null, loginType, { + ip: someInfo.ip, + browser: someInfo['user-agent'], + host: someInfo.host, + }); createCookie(token, ctx); ctx.body = token; }) diff --git a/src/routes/user/web-login.ts b/src/routes/user/web-login.ts index 48ac70d..89aadbd 100644 --- a/src/routes/user/web-login.ts +++ b/src/routes/user/web-login.ts @@ -1,30 +1,72 @@ import { app } from '@/app.ts'; import { User } from '@/models/user.ts'; import MD5 from 'crypto-js/md5.js'; - +import { authCan } from '@kevisual/code-center-module/models'; import jsonwebtoken from 'jsonwebtoken'; -// const tokenData: Record = {}; import { redis } from '@/app.ts'; -import { createCookie } from './me.ts'; +import { createCookie, clearCookie } from './me.ts'; app .route({ path: 'user', key: 'webLogin', - middleware: ['auth'], + middleware: [authCan], }) .define(async (ctx) => { const tokenUser = ctx.state.tokenUser; + const token = ctx.query.token; const { loginToken, sign, randomId } = ctx.query || {}; + const setErrorLoginTokenRedis = async (loginToken: string) => { + await redis.set(loginToken, JSON.stringify({}), 'EX', 2 * 60); // 2分钟 + }; + if (!tokenUser) { + if (token) { + console.log('web-login, token', ' run clearCookie', token, tokenUser); + // clearCookie(ctx); + } else { + // const message = 'token is expired, please login in web page. '; + } + try { + ctx.res.setHeader('Content-Type', 'text/html'); + const createRedirectHtml = () => { + const reqUrl = ctx.req.url; + return ` + + +

login with web page

+ ${reqUrl} + + + + `; + }; + ctx.res.end(createRedirectHtml()); + } catch (e) { + await setErrorLoginTokenRedis(loginToken); + ctx.throw(400, 'token is expired and redirect error'); + } + + return; + } if (!loginToken) { + await setErrorLoginTokenRedis(loginToken); ctx.throw(400, 'loginToken is required'); } if (!sign) { + await setErrorLoginTokenRedis(loginToken); ctx.throw(400, 'sign is required'); } if (!randomId) { + await setErrorLoginTokenRedis(loginToken); ctx.throw(400, 'randomId is required'); } const tokenSecret = 'xiao' + randomId; @@ -32,16 +74,19 @@ app try { payload = jsonwebtoken.verify(loginToken, tokenSecret); } catch (e) { + await setErrorLoginTokenRedis(loginToken); ctx.throw(400, 'loginToken error'); } const { timestamp } = payload; const checkSign = MD5(`${tokenSecret}${timestamp}`).toString(); if (sign !== checkSign) { + await setErrorLoginTokenRedis(loginToken); ctx.throw(400, 'sign error'); } const user = await User.findByPk(tokenUser.id); if (!user) { + await setErrorLoginTokenRedis(loginToken); ctx.throw(400, 'user not found'); } const data = await user.createToken(null, 'plugin', { loginWith: 'cli' }); @@ -63,11 +108,16 @@ app // const data = tokenData[loginToken]; const data = await redis.get(loginToken); if (data) { - ctx.body = JSON.parse(data); - await redis.expire(loginToken, 3600); + const token = JSON.parse(data); + if (token.accessToken) { + ctx.body = token; + createCookie(token, ctx); + } else { + ctx.throw(500, 'Checked error Failed, login failed, please login again'); + } + await redis.expire(loginToken, 2 * 60); // 2分钟 } else { ctx.throw(400, 'Checked Failed'); } - createCookie(data, ctx); }) .addTo(app); diff --git a/src/scripts/sync-mark.ts b/src/scripts/sync-mark.ts new file mode 100644 index 0000000..89e31e9 --- /dev/null +++ b/src/scripts/sync-mark.ts @@ -0,0 +1,46 @@ +import { useContextKey } from '@kevisual/use-config/context'; +import { sequelize } from '../modules/sequelize.ts'; +import { MarkModel, syncMarkModel } from '../routes/mark/model.ts'; +export const sequelize2 = useContextKey('sequelize', () => sequelize); + +const main = async () => { + // 把所有markmodel的表的source字段的类型改为jsonb + // const marks = await MarkModel.findAll(); + // const mark = marks[0]; + + // for (const mark of marks) { + // if (mark.source) { + // try { + // await MarkModel.update({ source: {} }, { where: { id: mark.id } }); + // } catch (e) { + // console.error('update source error:', e); + // } + // } + // } + console.log('update source success'); + // await MarkModel.sync({ alter: true, logging: true }).catch((e) => { + // console.error('MarkModel.sync error:', e); + // }); + await syncMarkModel({ alter: true, logging: true, sync: true }); +}; + +main(); + +const sql = `ALTER TABLE "micro_mark" ALTER COLUMN "source" DROP NOT NULL;ALTER TABLE "micro_mark" ALTER COLUMN "source" SET DEFAULT '{}';ALTER TABLE "micro_mark" ALTER COLUMN "source" TYPE JSONB ; COMMENT ON COLUMN "micro_mark"."source" IS '需要的数据的来源,作为一个备注使用。';`; + +// sequelize +/** + * 失败 + */ +const runSql = async () => { + sequelize + .query(sql) + .then(() => { + console.log('update source success'); + }) + .catch((e) => { + console.error('update source error:', e); + }); +}; + +// runSql(); diff --git a/submodules/oss b/submodules/oss index 5563ded..68332c9 160000 --- a/submodules/oss +++ b/submodules/oss @@ -1 +1 @@ -Subproject commit 5563ded0a19d60c34cce6272e272c6405bcab304 +Subproject commit 68332c9c8dd74751ee3b1a6944984ee02313f929 diff --git a/submodules/permission b/submodules/permission index bc6df19..f8e2f74 160000 --- a/submodules/permission +++ b/submodules/permission @@ -1 +1 @@ -Subproject commit bc6df19c9c5365b7950929ebe1be9fbb7224c670 +Subproject commit f8e2f74d5e4ac68e2a78db66b25246074f2675bd