Compare commits
	
		
			2 Commits
		
	
	
		
			081f3f609e
			...
			f1867e7916
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f1867e7916 | |||
| 0486c0d0e5 | 
| @@ -13,6 +13,7 @@ | |||||||
|         "cfturnstile-vue3": "^2.0.0", |         "cfturnstile-vue3": "^2.0.0", | ||||||
|         "pinia": "^3.0.3", |         "pinia": "^3.0.3", | ||||||
|         "tailwindcss": "^4.1.11", |         "tailwindcss": "^4.1.11", | ||||||
|  |         "tus-js-client": "^4.3.1", | ||||||
|         "vue": "^3.5.17", |         "vue": "^3.5.17", | ||||||
|         "vue-router": "^4.5.1", |         "vue-router": "^4.5.1", | ||||||
|       }, |       }, | ||||||
| @@ -387,6 +388,8 @@ | |||||||
|  |  | ||||||
|     "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="], |     "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="], | ||||||
|  |  | ||||||
|  |     "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], | ||||||
|  |  | ||||||
|     "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], |     "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], | ||||||
|  |  | ||||||
|     "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], |     "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], | ||||||
| @@ -403,6 +406,8 @@ | |||||||
|  |  | ||||||
|     "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], |     "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], | ||||||
|  |  | ||||||
|  |     "combine-errors": ["combine-errors@3.0.3", "", { "dependencies": { "custom-error-instance": "2.1.1", "lodash.uniqby": "4.5.0" } }, "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q=="], | ||||||
|  |  | ||||||
|     "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], |     "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], | ||||||
|  |  | ||||||
|     "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], |     "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], | ||||||
| @@ -417,6 +422,8 @@ | |||||||
|  |  | ||||||
|     "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], |     "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], | ||||||
|  |  | ||||||
|  |     "custom-error-instance": ["custom-error-instance@2.1.1", "", {}, "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="], | ||||||
|  |  | ||||||
|     "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], |     "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], | ||||||
|  |  | ||||||
|     "date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="], |     "date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="], | ||||||
| @@ -557,7 +564,7 @@ | |||||||
|  |  | ||||||
|     "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], |     "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], | ||||||
|  |  | ||||||
|     "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], |     "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], | ||||||
|  |  | ||||||
|     "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], |     "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], | ||||||
|  |  | ||||||
| @@ -571,6 +578,8 @@ | |||||||
|  |  | ||||||
|     "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], |     "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], | ||||||
|  |  | ||||||
|  |     "js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="], | ||||||
|  |  | ||||||
|     "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], |     "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], | ||||||
|  |  | ||||||
|     "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], |     "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], | ||||||
| @@ -625,8 +634,24 @@ | |||||||
|  |  | ||||||
|     "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], |     "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], | ||||||
|  |  | ||||||
|  |     "lodash._baseiteratee": ["lodash._baseiteratee@4.7.0", "", { "dependencies": { "lodash._stringtopath": "~4.8.0" } }, "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ=="], | ||||||
|  |  | ||||||
|  |     "lodash._basetostring": ["lodash._basetostring@4.12.0", "", {}, "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw=="], | ||||||
|  |  | ||||||
|  |     "lodash._baseuniq": ["lodash._baseuniq@4.6.0", "", { "dependencies": { "lodash._createset": "~4.0.0", "lodash._root": "~3.0.0" } }, "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A=="], | ||||||
|  |  | ||||||
|  |     "lodash._createset": ["lodash._createset@4.0.3", "", {}, "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA=="], | ||||||
|  |  | ||||||
|  |     "lodash._root": ["lodash._root@3.0.1", "", {}, "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ=="], | ||||||
|  |  | ||||||
|  |     "lodash._stringtopath": ["lodash._stringtopath@4.8.0", "", { "dependencies": { "lodash._basetostring": "~4.12.0" } }, "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ=="], | ||||||
|  |  | ||||||
|     "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], |     "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], | ||||||
|  |  | ||||||
|  |     "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], | ||||||
|  |  | ||||||
|  |     "lodash.uniqby": ["lodash.uniqby@4.5.0", "", { "dependencies": { "lodash._baseiteratee": "~4.7.0", "lodash._baseuniq": "~4.6.0" } }, "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ=="], | ||||||
|  |  | ||||||
|     "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |     "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], | ||||||
|  |  | ||||||
|     "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], |     "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], | ||||||
| @@ -715,14 +740,22 @@ | |||||||
|  |  | ||||||
|     "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], |     "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], | ||||||
|  |  | ||||||
|  |     "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], | ||||||
|  |  | ||||||
|     "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], |     "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], | ||||||
|  |  | ||||||
|  |     "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], | ||||||
|  |  | ||||||
|     "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], |     "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], | ||||||
|  |  | ||||||
|     "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], |     "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], | ||||||
|  |  | ||||||
|  |     "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], | ||||||
|  |  | ||||||
|     "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], |     "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], | ||||||
|  |  | ||||||
|  |     "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], | ||||||
|  |  | ||||||
|     "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], |     "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], | ||||||
|  |  | ||||||
|     "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], |     "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], | ||||||
| @@ -781,6 +814,8 @@ | |||||||
|  |  | ||||||
|     "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], |     "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||||
|  |  | ||||||
|  |     "tus-js-client": ["tus-js-client@4.3.1", "", { "dependencies": { "buffer-from": "^1.1.2", "combine-errors": "^3.0.3", "is-stream": "^2.0.0", "js-base64": "^3.7.2", "lodash.throttle": "^4.1.1", "proper-lockfile": "^4.1.2", "url-parse": "^1.5.7" } }, "sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg=="], | ||||||
|  |  | ||||||
|     "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], |     "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], | ||||||
|  |  | ||||||
|     "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], |     "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], | ||||||
| @@ -797,6 +832,8 @@ | |||||||
|  |  | ||||||
|     "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], |     "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], | ||||||
|  |  | ||||||
|  |     "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], | ||||||
|  |  | ||||||
|     "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], |     "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], | ||||||
|  |  | ||||||
|     "vdirs": ["vdirs@0.1.8", "", { "dependencies": { "evtd": "^0.2.2" }, "peerDependencies": { "vue": "^3.0.11" } }, "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw=="], |     "vdirs": ["vdirs@0.1.8", "", { "dependencies": { "evtd": "^0.2.2" }, "peerDependencies": { "vue": "^3.0.11" } }, "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw=="], | ||||||
| @@ -879,8 +916,12 @@ | |||||||
|  |  | ||||||
|     "css-render/csstype": ["csstype@3.0.11", "", {}, "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="], |     "css-render/csstype": ["csstype@3.0.11", "", {}, "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="], | ||||||
|  |  | ||||||
|  |     "execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], | ||||||
|  |  | ||||||
|     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], |     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], | ||||||
|  |  | ||||||
|  |     "get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], | ||||||
|  |  | ||||||
|     "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], |     "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], | ||||||
|  |  | ||||||
|     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | ||||||
| @@ -889,6 +930,8 @@ | |||||||
|  |  | ||||||
|     "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], |     "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], | ||||||
|  |  | ||||||
|  |     "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], | ||||||
|  |  | ||||||
|     "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], |     "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], | ||||||
|  |  | ||||||
|     "vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], |     "vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|     <meta charset="UTF-8" /> |     <meta charset="UTF-8" /> | ||||||
|     <link rel="icon" href="/favicon.ico" /> |     <link rel="icon" href="/favicon.ico" /> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|     <title>Solarpass</title> |     <title>Solar Network Drive</title> | ||||||
|     <app-data /> |     <app-data /> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ | |||||||
|     "cfturnstile-vue3": "^2.0.0", |     "cfturnstile-vue3": "^2.0.0", | ||||||
|     "pinia": "^3.0.3", |     "pinia": "^3.0.3", | ||||||
|     "tailwindcss": "^4.1.11", |     "tailwindcss": "^4.1.11", | ||||||
|  |     "tus-js-client": "^4.3.1", | ||||||
|     "vue": "^3.5.17", |     "vue": "^3.5.17", | ||||||
|     "vue-router": "^4.5.1" |     "vue-router": "^4.5.1" | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -27,18 +27,14 @@ import { NLayout, NLayoutHeader, NLayoutContent, NButton, NDropdown, NIcon } fro | |||||||
| import { | import { | ||||||
|   LogInOutlined, |   LogInOutlined, | ||||||
|   PersonAddAlt1Outlined, |   PersonAddAlt1Outlined, | ||||||
|   LogOutOutlined, |  | ||||||
|   PersonOutlineRound, |   PersonOutlineRound, | ||||||
| } from '@vicons/material' | } from '@vicons/material' | ||||||
| import { useUserStore } from '@/stores/user' | import { useUserStore } from '@/stores/user' | ||||||
| import { useRoute, useRouter } from 'vue-router' | import { useRoute, useRouter } from 'vue-router' | ||||||
|  | import { useServicesStore } from '@/stores/services' | ||||||
|  |  | ||||||
| const userStore = useUserStore() | const userStore = useUserStore() | ||||||
| const route = useRoute() | const route = useRoute() | ||||||
| const router = useRouter() |  | ||||||
|  |  | ||||||
| // Initialize user state on component mount |  | ||||||
| userStore.initialize() |  | ||||||
|  |  | ||||||
| const hideUserMenu = computed(() => { | const hideUserMenu = computed(() => { | ||||||
|   return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string) |   return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string) | ||||||
| @@ -71,31 +67,22 @@ const userOptions = computed(() => [ | |||||||
|       h(NIcon, null, { |       h(NIcon, null, { | ||||||
|         default: () => h(PersonOutlineRound), |         default: () => h(PersonOutlineRound), | ||||||
|       }), |       }), | ||||||
|   }, |   } | ||||||
|   { |  | ||||||
|     label: 'Logout', |  | ||||||
|     key: 'logout', |  | ||||||
|     icon: () => |  | ||||||
|       h(NIcon, null, { |  | ||||||
|         default: () => h(LogOutOutlined), |  | ||||||
|       }), |  | ||||||
|   }, |  | ||||||
| ]) | ]) | ||||||
|  |  | ||||||
|  | const servicesStore = useServicesStore() | ||||||
|  |  | ||||||
| function handleGuestMenuSelect(key: string) { | function handleGuestMenuSelect(key: string) { | ||||||
|   if (key === 'login') { |   if (key === 'login') { | ||||||
|     router.push('/login') |     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'login')!, '_blank') | ||||||
|   } else if (key === 'create-account') { |   } else if (key === 'create-account') { | ||||||
|     router.push('/create-account') |     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'create-account')!, '_blank') | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function handleUserMenuSelect(key: string) { | function handleUserMenuSelect(key: string) { | ||||||
|   if (key === 'logout') { |   if (key === 'profile') { | ||||||
|     userStore.logout() |     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank') | ||||||
|     router.push('/login') |  | ||||||
|   } else if (key === 'profile') { |  | ||||||
|     router.push('/accounts/me') // Assuming you have a profile page |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import { NGlobalStyle, NConfigProvider, NMessageProvider, lightTheme, darkTheme | |||||||
| import { usePreferredDark } from '@vueuse/core' | import { usePreferredDark } from '@vueuse/core' | ||||||
| import { useUserStore } from './stores/user' | import { useUserStore } from './stores/user' | ||||||
| import { onMounted } from 'vue' | import { onMounted } from 'vue' | ||||||
|  | import { useServicesStore } from './stores/services' | ||||||
|  |  | ||||||
| const themeOverrides = { | const themeOverrides = { | ||||||
|   common: { |   common: { | ||||||
| @@ -20,9 +21,13 @@ const themeOverrides = { | |||||||
| const isDark = usePreferredDark() | const isDark = usePreferredDark() | ||||||
|  |  | ||||||
| const userStore = useUserStore() | const userStore = useUserStore() | ||||||
|  | const servicesStore = useServicesStore() | ||||||
|  |  | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|  |   userStore.initialize() | ||||||
|  |  | ||||||
|   userStore.fetchUser() |   userStore.fetchUser() | ||||||
|  |   servicesStore.fetchServices() | ||||||
| }) | }) | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,6 +8,11 @@ const router = createRouter({ | |||||||
|       path: '/', |       path: '/', | ||||||
|       name: 'index', |       name: 'index', | ||||||
|       component: () => import('../views/index.vue') |       component: () => import('../views/index.vue') | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       path: '/files', | ||||||
|  |       name: 'files', | ||||||
|  |       component: () => import('../views/files.vue'), | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -1,3 +1,27 @@ | |||||||
| import { defineStore } from 'pinia' | import { defineStore } from 'pinia' | ||||||
|  | import { ref } from 'vue' | ||||||
|  |  | ||||||
| export const useServicesStore = defineStore('services', () => {}) | export const useServicesStore = defineStore('services', () => { | ||||||
|  |   const services = ref<Record<string, string>>({}) | ||||||
|  |  | ||||||
|  |   async function fetchServices() { | ||||||
|  |     try { | ||||||
|  |       const response = await fetch('/cgi/.well-known/services') | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error('Network response was not ok') | ||||||
|  |       } | ||||||
|  |       const data = await response.json() | ||||||
|  |       services.value = data | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Failed to fetch services:', error) | ||||||
|  |       services.value = {} | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getSerivceUrl(serviceName: string, ...parts: string[]): string | null { | ||||||
|  |     let baseUrl = services.value[serviceName] || null | ||||||
|  |     return baseUrl ? `${baseUrl}/${parts.join('/')}` : null | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { services, fetchServices, getSerivceUrl } | ||||||
|  | }) | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ export const useUserStore = defineStore('user', () => { | |||||||
|     isLoading.value = true |     isLoading.value = true | ||||||
|     error.value = null |     error.value = null | ||||||
|     try { |     try { | ||||||
|       const response = await fetch('/api/accounts/me', { |       const response = await fetch('/cgi/id/accounts/me', { | ||||||
|         credentials: 'include', |         credentials: 'include', | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
| @@ -43,8 +43,24 @@ export const useUserStore = defineStore('user', () => { | |||||||
|     // router.push('/login') |     // router.push('/login') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async function initialize() { |   function initialize() { | ||||||
|     await fetchUser() |     const allowedOrigin = import.meta.env.DEV ? window.location.origin : 'https://id.solian.app' | ||||||
|  |     window.addEventListener('message', (event) => { | ||||||
|  |       // IMPORTANT: Always check the origin of the message for security! | ||||||
|  |       // This prevents malicious scripts from sending fake login status updates. | ||||||
|  |       // Ensure event.origin exactly matches your identity service's origin. | ||||||
|  |       if (event.origin !== allowedOrigin) { | ||||||
|  |         console.warn(`[SYNC] Message received from unexpected origin: ${event.origin}. Ignoring.`) | ||||||
|  |         return // Ignore messages from unknown origins | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Check if the message is the type we're expecting | ||||||
|  |       if (event.data && event.data.type === 'DY:LOGIN_STATUS_CHANGE') { | ||||||
|  |         const { loggedIn } = event.data | ||||||
|  |         console.log(`[SYNC] Received login status change: ${loggedIn}`) | ||||||
|  |         fetchUser() // Re-fetch user data on login status change | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								DysonNetwork.Drive/Client/src/views/files.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								DysonNetwork.Drive/Client/src/views/files.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="h-full relative flex items-center justify-center"> | ||||||
|  |     <n-card class="max-w-lg" title="Download file"> | ||||||
|  |       <div class="flex flex-col gap-3" v-if="!progress"> | ||||||
|  |         <n-input placeholder="File ID" v-model:value="fileId" /> | ||||||
|  |         <n-input placeholder="Password" v-model:value="filePass" type="password" /> | ||||||
|  |         <n-button @click="downloadFile">Download</n-button> | ||||||
|  |       </div> | ||||||
|  |       <div v-else> | ||||||
|  |         <n-progress :percentage="progress" /> | ||||||
|  |       </div> | ||||||
|  |     </n-card> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { NCard, NInput, NButton, NProgress, useMessage } from 'naive-ui' | ||||||
|  | import { ref } from 'vue' | ||||||
|  |  | ||||||
|  | import { downloadAndDecryptFile } from './secure' | ||||||
|  |  | ||||||
|  | const filePass = ref<string>('') | ||||||
|  | const fileId = ref<string>('') | ||||||
|  |  | ||||||
|  | const progress = ref<number | undefined>(0) | ||||||
|  |  | ||||||
|  | const messageDisplay = useMessage() | ||||||
|  |  | ||||||
|  | function downloadFile() { | ||||||
|  |   downloadAndDecryptFile('/api/files/' + fileId.value, filePass.value, (p: number) => { | ||||||
|  |     progress.value = p * 100 | ||||||
|  |   }).catch((err) => { | ||||||
|  |     messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 }) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @@ -1,10 +1,60 @@ | |||||||
| <template> | <template> | ||||||
|   <section class="h-full relative flex items-center justify-center"> |   <section class="h-full relative flex items-center justify-center"> | ||||||
|     <n-card class="max-w-lg" title="About"> |     <n-card class="max-w-lg" title="About" v-if="!userStore.user"> | ||||||
|       <p>Welcome to the <b>Solar Drive</b></p> |       <p>Welcome to the <b>Solar Drive</b></p> | ||||||
|       <p> |       <p>We help you upload, collect, and share files with ease in mind.</p> | ||||||
|         We help you upload, collect, and share files with ease in mind. |       <p>To continue, login first.</p> | ||||||
|  |  | ||||||
|  |       <p class="mt-4 opacity-75 text-xs"> | ||||||
|  |         <span v-if="version == null">Loading...</span> | ||||||
|  |         <span v-else> | ||||||
|  |           v{{ version.version }} @ | ||||||
|  |           {{ version.commit.substring(0, 6) }} | ||||||
|  |           {{ version.updatedAt }} | ||||||
|  |         </span> | ||||||
|       </p> |       </p> | ||||||
|  |     </n-card> | ||||||
|  |     <n-card class="max-w-2xl" title="Upload to Solar Network" v-else> | ||||||
|  |       <template #header-extra> | ||||||
|  |         <div class="flex gap-2 items-center"> | ||||||
|  |           <p>Advance Mode</p> | ||||||
|  |           <n-switch v-model:value="modeAdvanced" size="small" /> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |  | ||||||
|  |       <div class="mb-3" v-if="modeAdvanced"> | ||||||
|  |         <n-input | ||||||
|  |           v-model:value="filePass" | ||||||
|  |           placeholder="Enter password to protect the file" | ||||||
|  |           clearable | ||||||
|  |           size="large" | ||||||
|  |           type="password" | ||||||
|  |           class="mb-2" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <n-upload | ||||||
|  |         multiple | ||||||
|  |         directory-dnd | ||||||
|  |         with-credentials | ||||||
|  |         show-preview-button | ||||||
|  |         list-type="image" | ||||||
|  |         :custom-request="customRequest" | ||||||
|  |         :create-thumbnail-url="createThumbnailUrl" | ||||||
|  |       > | ||||||
|  |         <n-upload-dragger> | ||||||
|  |           <div style="margin-bottom: 12px"> | ||||||
|  |             <n-icon size="48" :depth="3"> | ||||||
|  |               <upload-outlined /> | ||||||
|  |             </n-icon> | ||||||
|  |           </div> | ||||||
|  |           <n-text style="font-size: 16px"> Click or drag a file to this area to upload </n-text> | ||||||
|  |           <n-p depth="3" style="margin: 8px 0 0 0"> | ||||||
|  |             Strictly prohibit from uploading sensitive information. For example, your bank card PIN | ||||||
|  |             or your credit card expiry date. | ||||||
|  |           </n-p> | ||||||
|  |         </n-upload-dragger> | ||||||
|  |       </n-upload> | ||||||
|  |  | ||||||
|       <p class="mt-4 opacity-75 text-xs"> |       <p class="mt-4 opacity-75 text-xs"> | ||||||
|         <span v-if="version == null">Loading...</span> |         <span v-if="version == null">Loading...</span> | ||||||
| @@ -19,8 +69,25 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { NCard } from 'naive-ui' | import { | ||||||
|  |   NCard, | ||||||
|  |   NUpload, | ||||||
|  |   NUploadDragger, | ||||||
|  |   NIcon, | ||||||
|  |   NText, | ||||||
|  |   NP, | ||||||
|  |   NInput, | ||||||
|  |   NSwitch, | ||||||
|  |   type UploadCustomRequestOptions, | ||||||
|  |   type UploadSettledFileInfo, | ||||||
|  | } from 'naive-ui' | ||||||
| import { onMounted, ref } from 'vue' | import { onMounted, ref } from 'vue' | ||||||
|  | import { UploadOutlined } from '@vicons/material' | ||||||
|  | import { useUserStore } from '@/stores/user' | ||||||
|  |  | ||||||
|  | import * as tus from 'tus-js-client' | ||||||
|  |  | ||||||
|  | const userStore = useUserStore() | ||||||
|  |  | ||||||
| const version = ref<any>(null) | const version = ref<any>(null) | ||||||
|  |  | ||||||
| @@ -30,8 +97,62 @@ async function fetchVersion() { | |||||||
| } | } | ||||||
|  |  | ||||||
| onMounted(() => fetchVersion()) | onMounted(() => fetchVersion()) | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped> | const modeAdvanced = ref(false) | ||||||
| /* Add any specific styles here if needed, though Tailwind should handle most. */ |  | ||||||
| </style> | const filePass = ref<string>('') | ||||||
|  |  | ||||||
|  | function customRequest({ | ||||||
|  |   file, | ||||||
|  |   data, | ||||||
|  |   headers, | ||||||
|  |   withCredentials, | ||||||
|  |   action, | ||||||
|  |   onFinish, | ||||||
|  |   onError, | ||||||
|  |   onProgress, | ||||||
|  | }: UploadCustomRequestOptions) { | ||||||
|  |   const upload = new tus.Upload(file.file, { | ||||||
|  |     endpoint: '/api/tus', | ||||||
|  |     retryDelays: [0, 3000, 5000, 10000, 20000], | ||||||
|  |     metadata: { | ||||||
|  |       filename: file.name, | ||||||
|  |       filetype: file.type ?? 'application/octet-stream', | ||||||
|  |     }, | ||||||
|  |     headers: { | ||||||
|  |       'X-FilePass': filePass.value, | ||||||
|  |       ...headers, | ||||||
|  |     }, | ||||||
|  |     onError: function (error) { | ||||||
|  |       console.error('[DRIVE] Upload failed:', error) | ||||||
|  |       onError() | ||||||
|  |     }, | ||||||
|  |     onProgress: function (bytesUploaded, bytesTotal) { | ||||||
|  |       onProgress({ percent: (bytesUploaded / bytesTotal) * 100 }) | ||||||
|  |     }, | ||||||
|  |     onSuccess: function (payload) { | ||||||
|  |       const rawInfo = payload.lastResponse.getHeader('x-fileinfo') | ||||||
|  |       const jsonInfo = JSON.parse(rawInfo as string) | ||||||
|  |       console.log('[DRIVE] Upload successful: ', jsonInfo) | ||||||
|  |       file.url = `/api/files/${jsonInfo.id}` | ||||||
|  |       file.type = jsonInfo.mime_type | ||||||
|  |       onFinish() | ||||||
|  |     }, | ||||||
|  |     onBeforeRequest: function (req) { | ||||||
|  |       const xhr = req.getUnderlyingObject() | ||||||
|  |       xhr.withCredentials = withCredentials | ||||||
|  |     }, | ||||||
|  |   }) | ||||||
|  |   upload.findPreviousUploads().then(function (previousUploads) { | ||||||
|  |     if (previousUploads.length) { | ||||||
|  |       upload.resumeFromPreviousUpload(previousUploads[0]) | ||||||
|  |     } | ||||||
|  |     upload.start() | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function createThumbnailUrl(_file: File | null, fileInfo: UploadSettledFileInfo): string | undefined { | ||||||
|  |   if (!fileInfo) return undefined | ||||||
|  |   return fileInfo.url ?? undefined | ||||||
|  | } | ||||||
|  | </script> | ||||||
|   | |||||||
							
								
								
									
										92
									
								
								DysonNetwork.Drive/Client/src/views/secure.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								DysonNetwork.Drive/Client/src/views/secure.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | export async function downloadAndDecryptFile( | ||||||
|  |   url: string, | ||||||
|  |   password: string, | ||||||
|  |   onProgress?: (progress: number) => void | ||||||
|  | ): Promise<void> { | ||||||
|  |   const response = await fetch(url); | ||||||
|  |   if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`); | ||||||
|  |  | ||||||
|  |   const contentLength = +(response.headers.get('Content-Length') || 0); | ||||||
|  |   const reader = response.body!.getReader(); | ||||||
|  |   const chunks: Uint8Array[] = []; | ||||||
|  |   let received = 0; | ||||||
|  |  | ||||||
|  |   while (true) { | ||||||
|  |     const { done, value } = await reader.read(); | ||||||
|  |     if (done) break; | ||||||
|  |     if (value) { | ||||||
|  |       chunks.push(value); | ||||||
|  |       received += value.length; | ||||||
|  |       if (contentLength && onProgress) { | ||||||
|  |         onProgress(received / contentLength); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const fullBuffer = new Uint8Array(received); | ||||||
|  |   let offset = 0; | ||||||
|  |   for (const chunk of chunks) { | ||||||
|  |     fullBuffer.set(chunk, offset); | ||||||
|  |     offset += chunk.length; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const decryptedBytes = await decryptFile(fullBuffer, password); | ||||||
|  |  | ||||||
|  |   // Create a blob and trigger a download | ||||||
|  |   const blob = new Blob([decryptedBytes]); | ||||||
|  |   const downloadUrl = URL.createObjectURL(blob); | ||||||
|  |   const a = document.createElement('a'); | ||||||
|  |   a.href = downloadUrl; | ||||||
|  |   a.download = 'decrypted_file'; // You may allow customization | ||||||
|  |   document.body.appendChild(a); | ||||||
|  |   a.click(); | ||||||
|  |   a.remove(); | ||||||
|  |   URL.revokeObjectURL(downloadUrl); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function decryptFile( | ||||||
|  |   fileBuffer: Uint8Array, | ||||||
|  |   password: string | ||||||
|  | ): Promise<Uint8Array> { | ||||||
|  |   const salt = fileBuffer.slice(0, 16); | ||||||
|  |   const nonce = fileBuffer.slice(16, 28); | ||||||
|  |   const tag = fileBuffer.slice(28, 44); | ||||||
|  |   const ciphertext = fileBuffer.slice(44); | ||||||
|  |  | ||||||
|  |   const enc = new TextEncoder(); | ||||||
|  |   const keyMaterial = await crypto.subtle.importKey( | ||||||
|  |     'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey'] | ||||||
|  |   ); | ||||||
|  |   const key = await crypto.subtle.deriveKey( | ||||||
|  |     { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, | ||||||
|  |     keyMaterial, | ||||||
|  |     { name: 'AES-GCM', length: 256 }, | ||||||
|  |     false, | ||||||
|  |     ['decrypt'] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const fullCiphertext = new Uint8Array(ciphertext.length + tag.length); | ||||||
|  |   fullCiphertext.set(ciphertext); | ||||||
|  |   fullCiphertext.set(tag, ciphertext.length); | ||||||
|  |  | ||||||
|  |   let decrypted: ArrayBuffer; | ||||||
|  |   try { | ||||||
|  |     decrypted = await crypto.subtle.decrypt( | ||||||
|  |       { name: 'AES-GCM', iv: nonce, tagLength: 128 }, | ||||||
|  |       key, | ||||||
|  |       fullCiphertext | ||||||
|  |     ); | ||||||
|  |   } catch { | ||||||
|  |     throw new Error("Incorrect password or corrupted file."); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const magic = new TextEncoder().encode("DYSON1"); | ||||||
|  |   const decryptedBytes = new Uint8Array(decrypted); | ||||||
|  |   for (let i = 0; i < magic.length; i++) { | ||||||
|  |     if (decryptedBytes[i] !== magic[i]) { | ||||||
|  |       throw new Error("Incorrect password or corrupted file."); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return decryptedBytes.slice(magic.length); | ||||||
|  | } | ||||||
| @@ -29,11 +29,11 @@ export default defineConfig({ | |||||||
|   server: { |   server: { | ||||||
|     proxy: { |     proxy: { | ||||||
|       '/api': { |       '/api': { | ||||||
|         target: 'http://localhost:5216', |         target: 'http://localhost:5090', | ||||||
|         changeOrigin: true, |         changeOrigin: true, | ||||||
|       }, |       }, | ||||||
|       '/cgi': { |       '/cgi': { | ||||||
|         target: 'http://localhost:5216', |         target: 'http://localhost:5090', | ||||||
|         changeOrigin: true, |         changeOrigin: true, | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|   | |||||||
							
								
								
									
										257
									
								
								DysonNetwork.Drive/Migrations/20250725170254_AddCloudFileEncrypt.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								DysonNetwork.Drive/Migrations/20250725170254_AddCloudFileEncrypt.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,257 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Drive; | ||||||
|  | using DysonNetwork.Drive.Storage; | ||||||
|  | using DysonNetwork.Shared.Data; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDatabase))] | ||||||
|  |     [Migration("20250725170254_AddCloudFileEncrypt")] | ||||||
|  |     partial class AddCloudFileEncrypt | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "9.0.7") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|  |             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<string>("Id") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Description") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("description"); | ||||||
|  |  | ||||||
|  |                     b.Property<Dictionary<string, object>>("FileMeta") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("file_meta"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("HasCompression") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("has_compression"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("HasThumbnail") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("has_thumbnail"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Hash") | ||||||
|  |                         .HasMaxLength(256) | ||||||
|  |                         .HasColumnType("character varying(256)") | ||||||
|  |                         .HasColumnName("hash"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsEncrypted") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_encrypted"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsMarkedRecycle") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_marked_recycle"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("MimeType") | ||||||
|  |                         .HasMaxLength(256) | ||||||
|  |                         .HasColumnType("character varying(256)") | ||||||
|  |                         .HasColumnName("mime_type"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid?>("PoolId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("pool_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("sensitive_marks"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("Size") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("size"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("StorageId") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("storage_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("StorageUrl") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("storage_url"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("UploadedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("uploaded_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("UploadedTo") | ||||||
|  |                         .HasMaxLength(128) | ||||||
|  |                         .HasColumnType("character varying(128)") | ||||||
|  |                         .HasColumnName("uploaded_to"); | ||||||
|  |  | ||||||
|  |                     b.Property<Dictionary<string, object>>("UserMeta") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("user_meta"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_files"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("PoolId") | ||||||
|  |                         .HasDatabaseName("ix_files_pool_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("files", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("ExpiredAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("expired_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("FileId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("file_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("ResourceId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("resource_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Usage") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("usage"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_file_references"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("FileId") | ||||||
|  |                         .HasDatabaseName("ix_file_references_file_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("file_references", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<BillingConfig>("BillingConfig") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("billing_config"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("storage_config"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_pools"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("pools", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("PoolId") | ||||||
|  |                         .HasConstraintName("fk_files_pools_pool_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Pool"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("FileId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_file_references_files_file_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("File"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddCloudFileEncrypt : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<bool>( | ||||||
|  |                 name: "is_encrypted", | ||||||
|  |                 table: "files", | ||||||
|  |                 type: "boolean", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "is_encrypted", | ||||||
|  |                 table: "files"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										277
									
								
								DysonNetwork.Drive/Migrations/20250725183846_EnrichCloudPoolConfigure.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								DysonNetwork.Drive/Migrations/20250725183846_EnrichCloudPoolConfigure.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Drive; | ||||||
|  | using DysonNetwork.Drive.Storage; | ||||||
|  | using DysonNetwork.Shared.Data; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDatabase))] | ||||||
|  |     [Migration("20250725183846_EnrichCloudPoolConfigure")] | ||||||
|  |     partial class EnrichCloudPoolConfigure | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "9.0.7") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|  |             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<string>("Id") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Description") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("description"); | ||||||
|  |  | ||||||
|  |                     b.Property<Dictionary<string, object>>("FileMeta") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("file_meta"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("HasCompression") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("has_compression"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("HasThumbnail") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("has_thumbnail"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Hash") | ||||||
|  |                         .HasMaxLength(256) | ||||||
|  |                         .HasColumnType("character varying(256)") | ||||||
|  |                         .HasColumnName("hash"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsEncrypted") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_encrypted"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsMarkedRecycle") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_marked_recycle"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("MimeType") | ||||||
|  |                         .HasMaxLength(256) | ||||||
|  |                         .HasColumnType("character varying(256)") | ||||||
|  |                         .HasColumnName("mime_type"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid?>("PoolId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("pool_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("sensitive_marks"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("Size") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("size"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("StorageId") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("storage_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("StorageUrl") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("storage_url"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("UploadedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("uploaded_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("UploadedTo") | ||||||
|  |                         .HasMaxLength(128) | ||||||
|  |                         .HasColumnType("character varying(128)") | ||||||
|  |                         .HasColumnName("uploaded_to"); | ||||||
|  |  | ||||||
|  |                     b.Property<Dictionary<string, object>>("UserMeta") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("user_meta"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_files"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("PoolId") | ||||||
|  |                         .HasDatabaseName("ix_files_pool_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("files", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("ExpiredAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("expired_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("FileId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("file_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("ResourceId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("resource_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Usage") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("usage"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_file_references"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("FileId") | ||||||
|  |                         .HasDatabaseName("ix_file_references_file_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("file_references", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("AllowAnonymous") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("allow_anonymous"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("AllowEncryption") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("allow_encryption"); | ||||||
|  |  | ||||||
|  |                     b.Property<BillingConfig>("BillingConfig") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("billing_config"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("NoMetadata") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("no_metadata"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("NoOptimization") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("no_optimization"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("RequirePrivilege") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("require_privilege"); | ||||||
|  |  | ||||||
|  |                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("storage_config"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_pools"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("pools", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("PoolId") | ||||||
|  |                         .HasConstraintName("fk_files_pools_pool_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Pool"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("FileId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_file_references_files_file_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("File"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,73 @@ | |||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class EnrichCloudPoolConfigure : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<bool>( | ||||||
|  |                 name: "allow_anonymous", | ||||||
|  |                 table: "pools", | ||||||
|  |                 type: "boolean", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: false); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<bool>( | ||||||
|  |                 name: "allow_encryption", | ||||||
|  |                 table: "pools", | ||||||
|  |                 type: "boolean", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: false); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<bool>( | ||||||
|  |                 name: "no_metadata", | ||||||
|  |                 table: "pools", | ||||||
|  |                 type: "boolean", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: false); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<bool>( | ||||||
|  |                 name: "no_optimization", | ||||||
|  |                 table: "pools", | ||||||
|  |                 type: "boolean", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: false); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<int>( | ||||||
|  |                 name: "require_privilege", | ||||||
|  |                 table: "pools", | ||||||
|  |                 type: "integer", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: 0); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "allow_anonymous", | ||||||
|  |                 table: "pools"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "allow_encryption", | ||||||
|  |                 table: "pools"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "no_metadata", | ||||||
|  |                 table: "pools"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "no_optimization", | ||||||
|  |                 table: "pools"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "require_privilege", | ||||||
|  |                 table: "pools"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										276
									
								
								DysonNetwork.Drive/Migrations/20250725184107_NullableFileMeta.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										276
									
								
								DysonNetwork.Drive/Migrations/20250725184107_NullableFileMeta.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,276 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Drive; | ||||||
|  | using DysonNetwork.Drive.Storage; | ||||||
|  | using DysonNetwork.Shared.Data; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDatabase))] | ||||||
|  |     [Migration("20250725184107_NullableFileMeta")] | ||||||
|  |     partial class NullableFileMeta | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "9.0.7") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|  |             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<string>("Id") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Description") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("description"); | ||||||
|  |  | ||||||
|  |                     b.Property<Dictionary<string, object>>("FileMeta") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("file_meta"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("HasCompression") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("has_compression"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("HasThumbnail") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("has_thumbnail"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Hash") | ||||||
|  |                         .HasMaxLength(256) | ||||||
|  |                         .HasColumnType("character varying(256)") | ||||||
|  |                         .HasColumnName("hash"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsEncrypted") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_encrypted"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsMarkedRecycle") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_marked_recycle"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("MimeType") | ||||||
|  |                         .HasMaxLength(256) | ||||||
|  |                         .HasColumnType("character varying(256)") | ||||||
|  |                         .HasColumnName("mime_type"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid?>("PoolId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("pool_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("sensitive_marks"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("Size") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("size"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("StorageId") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("storage_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("StorageUrl") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("storage_url"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("UploadedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("uploaded_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("UploadedTo") | ||||||
|  |                         .HasMaxLength(128) | ||||||
|  |                         .HasColumnType("character varying(128)") | ||||||
|  |                         .HasColumnName("uploaded_to"); | ||||||
|  |  | ||||||
|  |                     b.Property<Dictionary<string, object>>("UserMeta") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("user_meta"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_files"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("PoolId") | ||||||
|  |                         .HasDatabaseName("ix_files_pool_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("files", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("ExpiredAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("expired_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("FileId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("file_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("ResourceId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("resource_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Usage") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("usage"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_file_references"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("FileId") | ||||||
|  |                         .HasDatabaseName("ix_file_references_file_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("file_references", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("AllowAnonymous") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("allow_anonymous"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("AllowEncryption") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("allow_encryption"); | ||||||
|  |  | ||||||
|  |                     b.Property<BillingConfig>("BillingConfig") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("billing_config"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("NoMetadata") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("no_metadata"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("NoOptimization") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("no_optimization"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("RequirePrivilege") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("require_privilege"); | ||||||
|  |  | ||||||
|  |                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("storage_config"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_pools"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("pools", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("PoolId") | ||||||
|  |                         .HasConstraintName("fk_files_pools_pool_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Pool"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("FileId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_file_references_files_file_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("File"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class NullableFileMeta : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AlterColumn<Dictionary<string, object>>( | ||||||
|  |                 name: "file_meta", | ||||||
|  |                 table: "files", | ||||||
|  |                 type: "jsonb", | ||||||
|  |                 nullable: true, | ||||||
|  |                 oldClrType: typeof(Dictionary<string, object>), | ||||||
|  |                 oldType: "jsonb"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AlterColumn<Dictionary<string, object>>( | ||||||
|  |                 name: "file_meta", | ||||||
|  |                 table: "files", | ||||||
|  |                 type: "jsonb", | ||||||
|  |                 nullable: false, | ||||||
|  |                 oldClrType: typeof(Dictionary<string, object>), | ||||||
|  |                 oldType: "jsonb", | ||||||
|  |                 oldNullable: true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -52,7 +52,6 @@ namespace DysonNetwork.Drive.Migrations | |||||||
|                         .HasColumnName("description"); |                         .HasColumnName("description"); | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("FileMeta") |                     b.Property<Dictionary<string, object>>("FileMeta") | ||||||
|                         .IsRequired() |  | ||||||
|                         .HasColumnType("jsonb") |                         .HasColumnType("jsonb") | ||||||
|                         .HasColumnName("file_meta"); |                         .HasColumnName("file_meta"); | ||||||
|  |  | ||||||
| @@ -69,6 +68,10 @@ namespace DysonNetwork.Drive.Migrations | |||||||
|                         .HasColumnType("character varying(256)") |                         .HasColumnType("character varying(256)") | ||||||
|                         .HasColumnName("hash"); |                         .HasColumnName("hash"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsEncrypted") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_encrypted"); | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsMarkedRecycle") |                     b.Property<bool>("IsMarkedRecycle") | ||||||
|                         .HasColumnType("boolean") |                         .HasColumnType("boolean") | ||||||
|                         .HasColumnName("is_marked_recycle"); |                         .HasColumnName("is_marked_recycle"); | ||||||
| @@ -189,6 +192,14 @@ namespace DysonNetwork.Drive.Migrations | |||||||
|                         .HasColumnType("uuid") |                         .HasColumnType("uuid") | ||||||
|                         .HasColumnName("id"); |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("AllowAnonymous") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("allow_anonymous"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("AllowEncryption") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("allow_encryption"); | ||||||
|  |  | ||||||
|                     b.Property<BillingConfig>("BillingConfig") |                     b.Property<BillingConfig>("BillingConfig") | ||||||
|                         .IsRequired() |                         .IsRequired() | ||||||
|                         .HasColumnType("jsonb") |                         .HasColumnType("jsonb") | ||||||
| @@ -208,6 +219,18 @@ namespace DysonNetwork.Drive.Migrations | |||||||
|                         .HasColumnType("character varying(1024)") |                         .HasColumnType("character varying(1024)") | ||||||
|                         .HasColumnName("name"); |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("NoMetadata") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("no_metadata"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("NoOptimization") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("no_optimization"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("RequirePrivilege") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("require_privilege"); | ||||||
|  |  | ||||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") |                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||||
|                         .IsRequired() |                         .IsRequired() | ||||||
|                         .HasColumnType("jsonb") |                         .HasColumnType("jsonb") | ||||||
|   | |||||||
| @@ -33,8 +33,8 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | |||||||
|  |  | ||||||
|     [MaxLength(1024)] public string Name { get; set; } = string.Empty; |     [MaxLength(1024)] public string Name { get; set; } = string.Empty; | ||||||
|     [MaxLength(4096)] public string? Description { get; set; } |     [MaxLength(4096)] public string? Description { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object?> FileMeta { get; set; } = null!; |     [Column(TypeName = "jsonb")] public Dictionary<string, object?>? FileMeta { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? UserMeta { get; set; } = null!; |     [Column(TypeName = "jsonb")] public Dictionary<string, object?>? UserMeta { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; |     [Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; | ||||||
|     [MaxLength(256)] public string? MimeType { get; set; } |     [MaxLength(256)] public string? MimeType { get; set; } | ||||||
|     [MaxLength(256)] public string? Hash { get; set; } |     [MaxLength(256)] public string? Hash { get; set; } | ||||||
| @@ -42,6 +42,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | |||||||
|     public Instant? UploadedAt { get; set; } |     public Instant? UploadedAt { get; set; } | ||||||
|     public bool HasCompression { get; set; } = false; |     public bool HasCompression { get; set; } = false; | ||||||
|     public bool HasThumbnail { get; set; } = false; |     public bool HasThumbnail { get; set; } = false; | ||||||
|  |     public bool IsEncrypted { get; set; } = false; | ||||||
|  |  | ||||||
|     [JsonIgnore] public FilePool? Pool { get; set; } |     [JsonIgnore] public FilePool? Pool { get; set; } | ||||||
|     public Guid? PoolId { get; set; } |     public Guid? PoolId { get; set; } | ||||||
|   | |||||||
							
								
								
									
										60
									
								
								DysonNetwork.Drive/Storage/FileEncryptor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								DysonNetwork.Drive/Storage/FileEncryptor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | using System.Security.Cryptography; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Storage; | ||||||
|  |  | ||||||
|  | public static class FileEncryptor | ||||||
|  | { | ||||||
|  |     public static void EncryptFile(string inputPath, string outputPath, string password) | ||||||
|  |     { | ||||||
|  |         var salt = RandomNumberGenerator.GetBytes(16); | ||||||
|  |         var key = DeriveKey(password, salt, 32); | ||||||
|  |         var nonce = RandomNumberGenerator.GetBytes(12); // For AES-GCM | ||||||
|  |  | ||||||
|  |         using var aes = new AesGcm(key, 16); // Specify 16-byte tag size explicitly | ||||||
|  |         var plaintext = File.ReadAllBytes(inputPath); | ||||||
|  |         var magic = "DYSON1"u8.ToArray(); | ||||||
|  |         var contentWithMagic = new byte[magic.Length + plaintext.Length]; | ||||||
|  |         Buffer.BlockCopy(magic, 0, contentWithMagic, 0, magic.Length); | ||||||
|  |         Buffer.BlockCopy(plaintext, 0, contentWithMagic, magic.Length, plaintext.Length); | ||||||
|  |  | ||||||
|  |         var ciphertext = new byte[contentWithMagic.Length]; | ||||||
|  |         var tag = new byte[16]; | ||||||
|  |         aes.Encrypt(nonce, contentWithMagic, ciphertext, tag); | ||||||
|  |  | ||||||
|  |         // Save as: [salt (16)][nonce (12)][tag (16)][ciphertext] | ||||||
|  |         using var fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write); | ||||||
|  |         fs.Write(salt); | ||||||
|  |         fs.Write(nonce); | ||||||
|  |         fs.Write(tag); | ||||||
|  |         fs.Write(ciphertext); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static void DecryptFile(string inputPath, string outputPath, string password) | ||||||
|  |     { | ||||||
|  |         var input = File.ReadAllBytes(inputPath); | ||||||
|  |  | ||||||
|  |         var salt = input[..16]; | ||||||
|  |         var nonce = input[16..28]; | ||||||
|  |         var tag = input[28..44]; | ||||||
|  |         var ciphertext = input[44..]; | ||||||
|  |  | ||||||
|  |         var key = DeriveKey(password, salt, 32); | ||||||
|  |         var decrypted = new byte[ciphertext.Length]; | ||||||
|  |  | ||||||
|  |         using var aes = new AesGcm(key, 16); // Specify 16-byte tag size explicitly | ||||||
|  |         aes.Decrypt(nonce, ciphertext, tag, decrypted); | ||||||
|  |  | ||||||
|  |         var magic = "DYSON1"u8.ToArray(); | ||||||
|  |         if (magic.Where((t, i) => decrypted[i] != t).Any()) | ||||||
|  |             throw new CryptographicException("Incorrect password or corrupted file."); | ||||||
|  |  | ||||||
|  |         var plaintext = decrypted[magic.Length..]; | ||||||
|  |         File.WriteAllBytes(outputPath, plaintext); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static byte[] DeriveKey(string password, byte[] salt, int keyBytes) | ||||||
|  |     { | ||||||
|  |         using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256); | ||||||
|  |         return pbkdf2.GetBytes(keyBytes); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -7,8 +7,6 @@ namespace DysonNetwork.Drive.Storage; | |||||||
|  |  | ||||||
| public class RemoteStorageConfig | public class RemoteStorageConfig | ||||||
| { | { | ||||||
|     public string Id { get; set; } = string.Empty; |  | ||||||
|     public string Label { get; set; } = string.Empty; |  | ||||||
|     public string Region { get; set; } = string.Empty; |     public string Region { get; set; } = string.Empty; | ||||||
|     public string Bucket { get; set; } = string.Empty; |     public string Bucket { get; set; } = string.Empty; | ||||||
|     public string Endpoint { get; set; } = string.Empty; |     public string Endpoint { get; set; } = string.Empty; | ||||||
| @@ -32,6 +30,11 @@ public class FilePool : ModelBase, IIdentifiedResource | |||||||
|     [MaxLength(1024)] public string Name { get; set; } = string.Empty; |     [MaxLength(1024)] public string Name { get; set; } = string.Empty; | ||||||
|     [Column(TypeName = "jsonb")] public RemoteStorageConfig StorageConfig { get; set; } = new(); |     [Column(TypeName = "jsonb")] public RemoteStorageConfig StorageConfig { get; set; } = new(); | ||||||
|     [Column(TypeName = "jsonb")] public BillingConfig BillingConfig { get; set; } = new(); |     [Column(TypeName = "jsonb")] public BillingConfig BillingConfig { get; set; } = new(); | ||||||
|  |     public bool NoOptimization { get; set; } = false; | ||||||
|  |     public bool NoMetadata { get; set; } = false; | ||||||
|  |     public bool AllowEncryption { get; set; } = true; | ||||||
|  |     public bool AllowAnonymous { get; set; } = true; | ||||||
|  |     public int RequirePrivilege { get; set; } = 0; | ||||||
|  |  | ||||||
|     public string ResourceIdentifier => $"file-pool/{Id}"; |     public string ResourceIdentifier => $"file-pool/{Id}"; | ||||||
| } | } | ||||||
| @@ -102,16 +102,32 @@ public class FileService( | |||||||
|     public async Task<CloudFile> ProcessNewFileAsync( |     public async Task<CloudFile> ProcessNewFileAsync( | ||||||
|         Account account, |         Account account, | ||||||
|         string fileId, |         string fileId, | ||||||
|  |         string filePool, | ||||||
|         Stream stream, |         Stream stream, | ||||||
|         string fileName, |         string fileName, | ||||||
|         string? contentType |         string? contentType, | ||||||
|  |         string? encryptPassword | ||||||
|     ) |     ) | ||||||
|     { |     { | ||||||
|  |         var pool = await GetPoolAsync(Guid.Parse(filePool)); | ||||||
|  |         if (pool is null) throw new InvalidOperationException("Pool not found"); | ||||||
|  |  | ||||||
|         var ogFilePath = Path.GetFullPath(Path.Join(configuration.GetValue<string>("Tus:StorePath"), fileId)); |         var ogFilePath = Path.GetFullPath(Path.Join(configuration.GetValue<string>("Tus:StorePath"), fileId)); | ||||||
|         var fileSize = stream.Length; |         var fileSize = stream.Length; | ||||||
|         var hash = await HashFileAsync(stream, fileSize: fileSize); |  | ||||||
|         contentType ??= !fileName.Contains('.') ? "application/octet-stream" : MimeTypes.GetMimeType(fileName); |         contentType ??= !fileName.Contains('.') ? "application/octet-stream" : MimeTypes.GetMimeType(fileName); | ||||||
|  |  | ||||||
|  |         if (!string.IsNullOrWhiteSpace(encryptPassword)) | ||||||
|  |         { | ||||||
|  |             if (!pool.AllowEncryption) throw new InvalidOperationException("Encryption is not allowed in this pool"); | ||||||
|  |             var encryptedPath = Path.Combine(Path.GetTempPath(), $"{fileId}.encrypted"); | ||||||
|  |             FileEncryptor.EncryptFile(ogFilePath, encryptedPath, encryptPassword); | ||||||
|  |             File.Delete(ogFilePath); // Delete original unencrypted | ||||||
|  |             File.Move(encryptedPath, ogFilePath); // Replace the original one with encrypted | ||||||
|  |             contentType = "application/octet-stream"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var hash = await HashFileAsync(stream, fileSize: fileSize); | ||||||
|  |  | ||||||
|         var file = new CloudFile |         var file = new CloudFile | ||||||
|         { |         { | ||||||
|             Id = fileId, |             Id = fileId, | ||||||
| @@ -119,7 +135,8 @@ public class FileService( | |||||||
|             MimeType = contentType, |             MimeType = contentType, | ||||||
|             Size = fileSize, |             Size = fileSize, | ||||||
|             Hash = hash, |             Hash = hash, | ||||||
|             AccountId = Guid.Parse(account.Id) |             AccountId = Guid.Parse(account.Id), | ||||||
|  |             IsEncrypted = !string.IsNullOrWhiteSpace(encryptPassword) | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         var existingFile = await db.Files.AsNoTracking().FirstOrDefaultAsync(f => f.Hash == hash); |         var existingFile = await db.Files.AsNoTracking().FirstOrDefaultAsync(f => f.Hash == hash); | ||||||
| @@ -143,6 +160,7 @@ public class FileService( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Extract metadata on the current thread for a faster initial response |         // Extract metadata on the current thread for a faster initial response | ||||||
|  |         if (!pool.NoMetadata) | ||||||
|             await ExtractMetadataAsync(file, ogFilePath, stream); |             await ExtractMetadataAsync(file, ogFilePath, stream); | ||||||
|  |  | ||||||
|         db.Files.Add(file); |         db.Files.Add(file); | ||||||
| @@ -150,7 +168,7 @@ public class FileService( | |||||||
|  |  | ||||||
|         // Offload optimization (image conversion, thumbnailing) and uploading to a background task |         // Offload optimization (image conversion, thumbnailing) and uploading to a background task | ||||||
|         _ = Task.Run(() => |         _ = Task.Run(() => | ||||||
|             ProcessAndUploadInBackgroundAsync(file.Id, file.StorageId, contentType, ogFilePath, stream)); |             ProcessAndUploadInBackgroundAsync(file.Id, filePool, file.StorageId, contentType, ogFilePath, stream)); | ||||||
|  |  | ||||||
|         return file; |         return file; | ||||||
|     } |     } | ||||||
| @@ -258,9 +276,18 @@ public class FileService( | |||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Handles file optimization (image compression, video thumbnailing) and uploads to remote storage in the background. |     /// Handles file optimization (image compression, video thumbnailing) and uploads to remote storage in the background. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     private async Task ProcessAndUploadInBackgroundAsync(string fileId, string storageId, string contentType, |     private async Task ProcessAndUploadInBackgroundAsync( | ||||||
|         string originalFilePath, Stream stream) |         string fileId, | ||||||
|  |         string remoteId, | ||||||
|  |         string storageId, | ||||||
|  |         string contentType, | ||||||
|  |         string originalFilePath, | ||||||
|  |         Stream stream | ||||||
|  |     ) | ||||||
|     { |     { | ||||||
|  |         var pool = await GetPoolAsync(Guid.Parse(remoteId)); | ||||||
|  |         if (pool is null) return; | ||||||
|  |  | ||||||
|         await using var bgStream = stream; // Ensure stream is disposed at the end of this task |         await using var bgStream = stream; // Ensure stream is disposed at the end of this task | ||||||
|         using var scope = scopeFactory.CreateScope(); |         using var scope = scopeFactory.CreateScope(); | ||||||
|         var nfs = scope.ServiceProvider.GetRequiredService<FileService>(); |         var nfs = scope.ServiceProvider.GetRequiredService<FileService>(); | ||||||
| @@ -275,6 +302,7 @@ public class FileService( | |||||||
|         { |         { | ||||||
|             logger.LogInformation("Processing file {FileId} in background...", fileId); |             logger.LogInformation("Processing file {FileId} in background...", fileId); | ||||||
|  |  | ||||||
|  |             if (!pool.NoOptimization) | ||||||
|                 switch (contentType.Split('/')[0]) |                 switch (contentType.Split('/')[0]) | ||||||
|                 { |                 { | ||||||
|                     case "image" when !AnimatedImageTypes.Contains(contentType): |                     case "image" when !AnimatedImageTypes.Contains(contentType): | ||||||
| @@ -337,12 +365,13 @@ public class FileService( | |||||||
|                         uploads.Add((originalFilePath, string.Empty, contentType, false)); |                         uploads.Add((originalFilePath, string.Empty, contentType, false)); | ||||||
|                         break; |                         break; | ||||||
|                 } |                 } | ||||||
|  |             else uploads.Add((originalFilePath, string.Empty, contentType, false)); | ||||||
|  |  | ||||||
|             logger.LogInformation("Optimized file {FileId}, now uploading...", fileId); |             logger.LogInformation("Optimized file {FileId}, now uploading...", fileId); | ||||||
|  |  | ||||||
|             if (uploads.Count > 0) |             if (uploads.Count > 0) | ||||||
|             { |             { | ||||||
|                 var destPool = Guid.Parse(configuration.GetValue<string>("Storage:PreferredRemote")!); |                 var destPool = Guid.Parse(remoteId!); | ||||||
|                 var uploadTasks = uploads.Select(item => |                 var uploadTasks = uploads.Select(item => | ||||||
|                     nfs.UploadFileToRemoteAsync(storageId, destPool, item.FilePath, item.Suffix, item.ContentType, |                     nfs.UploadFileToRemoteAsync(storageId, destPool, item.FilePath, item.Suffix, item.ContentType, | ||||||
|                         item.SelfDestruct) |                         item.SelfDestruct) | ||||||
|   | |||||||
| @@ -44,6 +44,10 @@ public abstract class TusService | |||||||
|                     if (!allowed.HasPermission) |                     if (!allowed.HasPermission) | ||||||
|                         eventContext.FailRequest(HttpStatusCode.Forbidden); |                         eventContext.FailRequest(HttpStatusCode.Forbidden); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 var filePool = httpContext.Request.Headers["X-FilePool"].FirstOrDefault(); | ||||||
|  |                 if (!string.IsNullOrEmpty(filePool) && !Guid.TryParse(filePool, out _)) | ||||||
|  |                     eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file pool id"); | ||||||
|             }, |             }, | ||||||
|             OnFileCompleteAsync = async eventContext => |             OnFileCompleteAsync = async eventContext => | ||||||
|             { |             { | ||||||
| @@ -62,8 +66,22 @@ public abstract class TusService | |||||||
|  |  | ||||||
|                 var fileStream = await file.GetContentAsync(eventContext.CancellationToken); |                 var fileStream = await file.GetContentAsync(eventContext.CancellationToken); | ||||||
|  |  | ||||||
|  |                 var filePool = httpContext.Request.Headers["X-FilePool"].FirstOrDefault(); | ||||||
|  |                 var encryptPassword = httpContext.Request.Headers["X-FilePass"].FirstOrDefault(); | ||||||
|  |  | ||||||
|  |                 if (string.IsNullOrEmpty(filePool)) | ||||||
|  |                     filePool = configuration["Storage:PreferredRemote"]; | ||||||
|  |  | ||||||
|                 var fileService = services.GetRequiredService<FileService>(); |                 var fileService = services.GetRequiredService<FileService>(); | ||||||
|                 var info = await fileService.ProcessNewFileAsync(user, file.Id, fileStream, fileName, contentType); |                 var info = await fileService.ProcessNewFileAsync( | ||||||
|  |                     user, | ||||||
|  |                     file.Id, | ||||||
|  |                     filePool, | ||||||
|  |                     fileStream, | ||||||
|  |                     fileName, | ||||||
|  |                     contentType, | ||||||
|  |                     encryptPassword | ||||||
|  |                 ); | ||||||
|  |  | ||||||
|                 using var finalScope = eventContext.HttpContext.RequestServices.CreateScope(); |                 using var finalScope = eventContext.HttpContext.RequestServices.CreateScope(); | ||||||
|                 var jsonOptions = finalScope.ServiceProvider.GetRequiredService<IOptions<JsonOptions>>().Value |                 var jsonOptions = finalScope.ServiceProvider.GetRequiredService<IOptions<JsonOptions>>().Value | ||||||
| @@ -80,7 +98,7 @@ public abstract class TusService | |||||||
|                 if (gatewayUrl is not null) |                 if (gatewayUrl is not null) | ||||||
|                     eventContext.SetUploadUrl(new Uri(gatewayUrl + "/drive/tus/" + eventContext.FileId)); |                     eventContext.SetUploadUrl(new Uri(gatewayUrl + "/drive/tus/" + eventContext.FileId)); | ||||||
|                 return Task.CompletedTask; |                 return Task.CompletedTask; | ||||||
|             } |             }, | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| { | { | ||||||
|   "Debug": true, |   "Debug": true, | ||||||
|   "BaseUrl": "http://localhost:5071", |   "BaseUrl": "http://localhost:5071", | ||||||
|   "GatewayUrl": "http://localhost:5094", |  | ||||||
|   "Logging": { |   "Logging": { | ||||||
|     "LogLevel": { |     "LogLevel": { | ||||||
|       "Default": "Information", |       "Default": "Information", | ||||||
| @@ -42,7 +41,7 @@ | |||||||
|     "StorePath": "Uploads" |     "StorePath": "Uploads" | ||||||
|   }, |   }, | ||||||
|   "Storage": { |   "Storage": { | ||||||
|     "PreferredRemote": "minio", |     "PreferredRemote": "2adceae3-981a-4564-9b8d-5d71a211c873", | ||||||
|     "Remote": [ |     "Remote": [ | ||||||
|       { |       { | ||||||
|         "Id": "minio", |         "Id": "minio", | ||||||
|   | |||||||
| @@ -37,9 +37,6 @@ const userStore = useUserStore() | |||||||
| const route = useRoute() | const route = useRoute() | ||||||
| const router = useRouter() | const router = useRouter() | ||||||
|  |  | ||||||
| // Initialize user state on component mount |  | ||||||
| userStore.initialize() |  | ||||||
|  |  | ||||||
| const hideUserMenu = computed(() => { | const hideUserMenu = computed(() => { | ||||||
|   return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string) |   return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -42,8 +42,8 @@ router.beforeEach(async (to, from, next) => { | |||||||
|   const userStore = useUserStore() |   const userStore = useUserStore() | ||||||
|  |  | ||||||
|   // Initialize user state if not already initialized |   // Initialize user state if not already initialized | ||||||
|   if (!userStore.user && localStorage.getItem('authToken')) { |   if (!userStore.user) { | ||||||
|     await userStore.initialize() |     await userStore.fetchUser(false) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) { |   if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) { | ||||||
|   | |||||||
| @@ -1,3 +1,22 @@ | |||||||
| import { defineStore } from 'pinia' | import { defineStore } from 'pinia' | ||||||
|  | import { ref } from 'vue' | ||||||
|  |  | ||||||
| export const useServicesStore = defineStore('services', () => {}) | export const useServicesStore = defineStore('services', () => { | ||||||
|  |   const services = ref<Record<string, string>>({}) | ||||||
|  |  | ||||||
|  |   async function fetchServices() { | ||||||
|  |     try { | ||||||
|  |       const response = await fetch('/cgi/.well-known/services') | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error('Network response was not ok') | ||||||
|  |       } | ||||||
|  |       const data = await response.json() | ||||||
|  |       services.value = data | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Failed to fetch services:', error) | ||||||
|  |       services.value = {} | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { services, fetchServices } | ||||||
|  | }) | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { defineStore } from 'pinia' | import { defineStore } from 'pinia' | ||||||
| import { ref, computed } from 'vue' | import { ref, computed, watch } from 'vue' | ||||||
|  |  | ||||||
| export const useUserStore = defineStore('user', () => { | export const useUserStore = defineStore('user', () => { | ||||||
|   // State |   // State | ||||||
| @@ -11,19 +11,13 @@ export const useUserStore = defineStore('user', () => { | |||||||
|   const isAuthenticated = computed(() => !!user.value) |   const isAuthenticated = computed(() => !!user.value) | ||||||
|  |  | ||||||
|   // Actions |   // Actions | ||||||
|   async function fetchUser() { |   async function fetchUser(reload = true) { | ||||||
|     const token = localStorage.getItem('authToken') |     if (!reload && user.value) return // Skip fetching if already loaded and not forced to | ||||||
|     if (!token) { |  | ||||||
|       return // No token, no need to fetch |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     isLoading.value = true |     isLoading.value = true | ||||||
|     error.value = null |     error.value = null | ||||||
|     try { |     try { | ||||||
|       const response = await fetch('/api/accounts/me', { |       const response = await fetch('/api/accounts/me', { | ||||||
|         headers: { |         credentials: 'include', | ||||||
|           'Authorization': `Bearer ${token}` |  | ||||||
|         } |  | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       if (!response.ok) { |       if (!response.ok) { | ||||||
| @@ -50,9 +44,21 @@ export const useUserStore = defineStore('user', () => { | |||||||
|     // router.push('/login') |     // router.push('/login') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async function initialize() { |   watch( | ||||||
|     await fetchUser() |     user, | ||||||
|   } |     (_) => { | ||||||
|  |       // Broadcast user changes to other subapps | ||||||
|  |       window.parent.postMessage( | ||||||
|  |         { | ||||||
|  |           type: 'DY:LOGIN_STATUS_CHANGE', | ||||||
|  |           data: user.value != null, | ||||||
|  |         }, | ||||||
|  |         '*', | ||||||
|  |       ) | ||||||
|  |       console.log(`[SYNC] Message sent to parent: Login status changed to ${status}`) | ||||||
|  |     }, | ||||||
|  |     { immediate: true, deep: true }, | ||||||
|  |   ) | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     user, |     user, | ||||||
| @@ -61,6 +67,5 @@ export const useUserStore = defineStore('user', () => { | |||||||
|     isAuthenticated, |     isAuthenticated, | ||||||
|     fetchUser, |     fetchUser, | ||||||
|     logout, |     logout, | ||||||
|     initialize |  | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
| @@ -32,7 +32,3 @@ async function fetchVersion() { | |||||||
|  |  | ||||||
| onMounted(() => fetchVersion()) | onMounted(() => fetchVersion()) | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scoped> |  | ||||||
| /* Add any specific styles here if needed, though Tailwind should handle most. */ |  | ||||||
| </style> |  | ||||||
|   | |||||||
| @@ -30,7 +30,6 @@ onMounted(async () => { | |||||||
|   const fp = await FingerprintJS.load() |   const fp = await FingerprintJS.load() | ||||||
|   const result = await fp.get() |   const result = await fp.get() | ||||||
|   deviceId.value = result.visitorId |   deviceId.value = result.visitorId | ||||||
|   localStorage.setItem('deviceId', deviceId.value) |  | ||||||
| }) | }) | ||||||
|  |  | ||||||
| const selectedFactor = computed(() => { | const selectedFactor = computed(() => { | ||||||
| @@ -214,7 +213,6 @@ async function exchangeToken() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const { token } = await response.json() |     const { token } = await response.json() | ||||||
|     localStorage.setItem('authToken', token) |  | ||||||
|     await userStore.fetchUser() |     await userStore.fetchUser() | ||||||
|  |  | ||||||
|     const redirectUri = route.query.redirect_uri as string |     const redirectUri = route.query.redirect_uri as string | ||||||
|   | |||||||
| @@ -29,11 +29,11 @@ export default defineConfig({ | |||||||
|   server: { |   server: { | ||||||
|     proxy: { |     proxy: { | ||||||
|       '/api': { |       '/api': { | ||||||
|         target: 'http://localhost:5090', |         target: 'http://localhost:5216', | ||||||
|         changeOrigin: true, |         changeOrigin: true, | ||||||
|       }, |       }, | ||||||
|       '/cgi': { |       '/cgi': { | ||||||
|         target: 'http://localhost:5090', |         target: 'http://localhost:5216', | ||||||
|         changeOrigin: true, |         changeOrigin: true, | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAccessToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fb370f448e9f5fca62da785172d83a214319335e27ac4d51840349c6dce15d68_003FAccessToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAccessToken_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fb370f448e9f5fca62da785172d83a214319335e27ac4d51840349c6dce15d68_003FAccessToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
|  | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAesGcm_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3d932a3ff98244208ca84309a75a7734243600_003F2c_003F1063867b_003FAesGcm_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAny_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003F67_003F87f868e3_003FAny_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAny_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003F67_003F87f868e3_003FAny_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSender_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003Fc5_003F2a1973a9_003FApnSender_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSender_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003Fc5_003F2a1973a9_003FApnSender_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
| 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSettings_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003F0f_003F51443844_003FApnSettings_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSettings_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003F0f_003F51443844_003FApnSettings_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user