Compare commits
	
		
			2 Commits
		
	
	
		
			081f3f609e
			...
			f1867e7916
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f1867e7916 | |||
| 0486c0d0e5 | 
| @@ -13,6 +13,7 @@ | ||||
|         "cfturnstile-vue3": "^2.0.0", | ||||
|         "pinia": "^3.0.3", | ||||
|         "tailwindcss": "^4.1.11", | ||||
|         "tus-js-client": "^4.3.1", | ||||
|         "vue": "^3.5.17", | ||||
|         "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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-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-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=="], | ||||
|  | ||||
| @@ -571,6 +578,8 @@ | ||||
|  | ||||
|     "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-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._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.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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], | ||||
| @@ -781,6 +814,8 @@ | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], | ||||
|  | ||||
|     "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" href="/favicon.ico" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Solarpass</title> | ||||
|     <title>Solar Network Drive</title> | ||||
|     <app-data /> | ||||
|   </head> | ||||
|   <body> | ||||
|   | ||||
| @@ -24,6 +24,7 @@ | ||||
|     "cfturnstile-vue3": "^2.0.0", | ||||
|     "pinia": "^3.0.3", | ||||
|     "tailwindcss": "^4.1.11", | ||||
|     "tus-js-client": "^4.3.1", | ||||
|     "vue": "^3.5.17", | ||||
|     "vue-router": "^4.5.1" | ||||
|   }, | ||||
|   | ||||
| @@ -27,18 +27,14 @@ import { NLayout, NLayoutHeader, NLayoutContent, NButton, NDropdown, NIcon } fro | ||||
| import { | ||||
|   LogInOutlined, | ||||
|   PersonAddAlt1Outlined, | ||||
|   LogOutOutlined, | ||||
|   PersonOutlineRound, | ||||
| } from '@vicons/material' | ||||
| import { useUserStore } from '@/stores/user' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { useServicesStore } from '@/stores/services' | ||||
|  | ||||
| const userStore = useUserStore() | ||||
| const route = useRoute() | ||||
| const router = useRouter() | ||||
|  | ||||
| // Initialize user state on component mount | ||||
| userStore.initialize() | ||||
|  | ||||
| const hideUserMenu = computed(() => { | ||||
|   return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string) | ||||
| @@ -71,31 +67,22 @@ const userOptions = computed(() => [ | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(PersonOutlineRound), | ||||
|       }), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Logout', | ||||
|     key: 'logout', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(LogOutOutlined), | ||||
|       }), | ||||
|   }, | ||||
|   } | ||||
| ]) | ||||
|  | ||||
| const servicesStore = useServicesStore() | ||||
|  | ||||
| function handleGuestMenuSelect(key: string) { | ||||
|   if (key === 'login') { | ||||
|     router.push('/login') | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'login')!, '_blank') | ||||
|   } else if (key === 'create-account') { | ||||
|     router.push('/create-account') | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'create-account')!, '_blank') | ||||
|   } | ||||
| } | ||||
|  | ||||
| function handleUserMenuSelect(key: string) { | ||||
|   if (key === 'logout') { | ||||
|     userStore.logout() | ||||
|     router.push('/login') | ||||
|   } else if (key === 'profile') { | ||||
|     router.push('/accounts/me') // Assuming you have a profile page | ||||
|   if (key === 'profile') { | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank') | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { NGlobalStyle, NConfigProvider, NMessageProvider, lightTheme, darkTheme | ||||
| import { usePreferredDark } from '@vueuse/core' | ||||
| import { useUserStore } from './stores/user' | ||||
| import { onMounted } from 'vue' | ||||
| import { useServicesStore } from './stores/services' | ||||
|  | ||||
| const themeOverrides = { | ||||
|   common: { | ||||
| @@ -20,9 +21,13 @@ const themeOverrides = { | ||||
| const isDark = usePreferredDark() | ||||
|  | ||||
| const userStore = useUserStore() | ||||
| const servicesStore = useServicesStore() | ||||
|  | ||||
| onMounted(() => { | ||||
|   userStore.initialize() | ||||
|  | ||||
|   userStore.fetchUser() | ||||
|   servicesStore.fetchServices() | ||||
| }) | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,11 @@ const router = createRouter({ | ||||
|       path: '/', | ||||
|       name: 'index', | ||||
|       component: () => import('../views/index.vue') | ||||
|     }, | ||||
|     { | ||||
|       path: '/files', | ||||
|       name: 'files', | ||||
|       component: () => import('../views/files.vue'), | ||||
|     } | ||||
|   ] | ||||
| }) | ||||
|   | ||||
| @@ -1,3 +1,27 @@ | ||||
| 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 | ||||
|     error.value = null | ||||
|     try { | ||||
|       const response = await fetch('/api/accounts/me', { | ||||
|       const response = await fetch('/cgi/id/accounts/me', { | ||||
|         credentials: 'include', | ||||
|       }) | ||||
|  | ||||
| @@ -43,8 +43,24 @@ export const useUserStore = defineStore('user', () => { | ||||
|     // router.push('/login') | ||||
|   } | ||||
|  | ||||
|   async function initialize() { | ||||
|     await fetchUser() | ||||
|   function initialize() { | ||||
|     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 { | ||||
|   | ||||
							
								
								
									
										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> | ||||
|   <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> | ||||
|         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> | ||||
|       <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> | ||||
|     </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"> | ||||
|         <span v-if="version == null">Loading...</span> | ||||
| @@ -19,8 +69,25 @@ | ||||
| </template> | ||||
|  | ||||
| <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 { UploadOutlined } from '@vicons/material' | ||||
| import { useUserStore } from '@/stores/user' | ||||
|  | ||||
| import * as tus from 'tus-js-client' | ||||
|  | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const version = ref<any>(null) | ||||
|  | ||||
| @@ -30,8 +97,62 @@ async function fetchVersion() { | ||||
| } | ||||
|  | ||||
| onMounted(() => fetchVersion()) | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| /* Add any specific styles here if needed, though Tailwind should handle most. */ | ||||
| </style> | ||||
| const modeAdvanced = ref(false) | ||||
|  | ||||
| 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: { | ||||
|     proxy: { | ||||
|       '/api': { | ||||
|         target: 'http://localhost:5216', | ||||
|         target: 'http://localhost:5090', | ||||
|         changeOrigin: true, | ||||
|       }, | ||||
|       '/cgi': { | ||||
|         target: 'http://localhost:5216', | ||||
|         target: 'http://localhost:5090', | ||||
|         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"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("FileMeta") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("file_meta"); | ||||
|  | ||||
| @@ -69,6 +68,10 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .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"); | ||||
| @@ -189,6 +192,14 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .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") | ||||
| @@ -208,6 +219,18 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .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") | ||||
|   | ||||
| @@ -33,8 +33,8 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|  | ||||
|     [MaxLength(1024)] public string Name { get; set; } = string.Empty; | ||||
|     [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>? UserMeta { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object?>? FileMeta { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object?>? UserMeta { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; | ||||
|     [MaxLength(256)] public string? MimeType { 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 bool HasCompression { get; set; } = false; | ||||
|     public bool HasThumbnail { get; set; } = false; | ||||
|     public bool IsEncrypted { get; set; } = false; | ||||
|  | ||||
|     [JsonIgnore] public FilePool? Pool { 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 string Id { get; set; } = string.Empty; | ||||
|     public string Label { get; set; } = string.Empty; | ||||
|     public string Region { get; set; } = string.Empty; | ||||
|     public string Bucket { 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; | ||||
|     [Column(TypeName = "jsonb")] public RemoteStorageConfig StorageConfig { 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}"; | ||||
| } | ||||
| @@ -102,16 +102,32 @@ public class FileService( | ||||
|     public async Task<CloudFile> ProcessNewFileAsync( | ||||
|         Account account, | ||||
|         string fileId, | ||||
|         string filePool, | ||||
|         Stream stream, | ||||
|         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 fileSize = stream.Length; | ||||
|         var hash = await HashFileAsync(stream, fileSize: fileSize); | ||||
|         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 | ||||
|         { | ||||
|             Id = fileId, | ||||
| @@ -119,7 +135,8 @@ public class FileService( | ||||
|             MimeType = contentType, | ||||
|             Size = fileSize, | ||||
|             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); | ||||
| @@ -143,6 +160,7 @@ public class FileService( | ||||
|         } | ||||
|  | ||||
|         // Extract metadata on the current thread for a faster initial response | ||||
|         if (!pool.NoMetadata) | ||||
|             await ExtractMetadataAsync(file, ogFilePath, stream); | ||||
|  | ||||
|         db.Files.Add(file); | ||||
| @@ -150,7 +168,7 @@ public class FileService( | ||||
|  | ||||
|         // Offload optimization (image conversion, thumbnailing) and uploading to a background task | ||||
|         _ = Task.Run(() => | ||||
|             ProcessAndUploadInBackgroundAsync(file.Id, file.StorageId, contentType, ogFilePath, stream)); | ||||
|             ProcessAndUploadInBackgroundAsync(file.Id, filePool, file.StorageId, contentType, ogFilePath, stream)); | ||||
|  | ||||
|         return file; | ||||
|     } | ||||
| @@ -258,9 +276,18 @@ public class FileService( | ||||
|     /// <summary> | ||||
|     /// Handles file optimization (image compression, video thumbnailing) and uploads to remote storage in the background. | ||||
|     /// </summary> | ||||
|     private async Task ProcessAndUploadInBackgroundAsync(string fileId, string storageId, string contentType, | ||||
|         string originalFilePath, Stream stream) | ||||
|     private async Task ProcessAndUploadInBackgroundAsync( | ||||
|         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 | ||||
|         using var scope = scopeFactory.CreateScope(); | ||||
|         var nfs = scope.ServiceProvider.GetRequiredService<FileService>(); | ||||
| @@ -275,6 +302,7 @@ public class FileService( | ||||
|         { | ||||
|             logger.LogInformation("Processing file {FileId} in background...", fileId); | ||||
|  | ||||
|             if (!pool.NoOptimization) | ||||
|                 switch (contentType.Split('/')[0]) | ||||
|                 { | ||||
|                     case "image" when !AnimatedImageTypes.Contains(contentType): | ||||
| @@ -337,12 +365,13 @@ public class FileService( | ||||
|                         uploads.Add((originalFilePath, string.Empty, contentType, false)); | ||||
|                         break; | ||||
|                 } | ||||
|             else uploads.Add((originalFilePath, string.Empty, contentType, false)); | ||||
|  | ||||
|             logger.LogInformation("Optimized file {FileId}, now uploading...", fileId); | ||||
|  | ||||
|             if (uploads.Count > 0) | ||||
|             { | ||||
|                 var destPool = Guid.Parse(configuration.GetValue<string>("Storage:PreferredRemote")!); | ||||
|                 var destPool = Guid.Parse(remoteId!); | ||||
|                 var uploadTasks = uploads.Select(item => | ||||
|                     nfs.UploadFileToRemoteAsync(storageId, destPool, item.FilePath, item.Suffix, item.ContentType, | ||||
|                         item.SelfDestruct) | ||||
|   | ||||
| @@ -44,6 +44,10 @@ public abstract class TusService | ||||
|                     if (!allowed.HasPermission) | ||||
|                         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 => | ||||
|             { | ||||
| @@ -62,8 +66,22 @@ public abstract class TusService | ||||
|  | ||||
|                 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 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(); | ||||
|                 var jsonOptions = finalScope.ServiceProvider.GetRequiredService<IOptions<JsonOptions>>().Value | ||||
| @@ -80,7 +98,7 @@ public abstract class TusService | ||||
|                 if (gatewayUrl is not null) | ||||
|                     eventContext.SetUploadUrl(new Uri(gatewayUrl + "/drive/tus/" + eventContext.FileId)); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
|             }, | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -1,7 +1,6 @@ | ||||
| { | ||||
|   "Debug": true, | ||||
|   "BaseUrl": "http://localhost:5071", | ||||
|   "GatewayUrl": "http://localhost:5094", | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
| @@ -42,7 +41,7 @@ | ||||
|     "StorePath": "Uploads" | ||||
|   }, | ||||
|   "Storage": { | ||||
|     "PreferredRemote": "minio", | ||||
|     "PreferredRemote": "2adceae3-981a-4564-9b8d-5d71a211c873", | ||||
|     "Remote": [ | ||||
|       { | ||||
|         "Id": "minio", | ||||
|   | ||||
| @@ -37,9 +37,6 @@ const userStore = useUserStore() | ||||
| const route = useRoute() | ||||
| const router = useRouter() | ||||
|  | ||||
| // Initialize user state on component mount | ||||
| userStore.initialize() | ||||
|  | ||||
| const hideUserMenu = computed(() => { | ||||
|   return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string) | ||||
| }) | ||||
|   | ||||
| @@ -42,8 +42,8 @@ router.beforeEach(async (to, from, next) => { | ||||
|   const userStore = useUserStore() | ||||
|  | ||||
|   // Initialize user state if not already initialized | ||||
|   if (!userStore.user && localStorage.getItem('authToken')) { | ||||
|     await userStore.initialize() | ||||
|   if (!userStore.user) { | ||||
|     await userStore.fetchUser(false) | ||||
|   } | ||||
|  | ||||
|   if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) { | ||||
|   | ||||
| @@ -1,3 +1,22 @@ | ||||
| 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 { ref, computed } from 'vue' | ||||
| import { ref, computed, watch } from 'vue' | ||||
|  | ||||
| export const useUserStore = defineStore('user', () => { | ||||
|   // State | ||||
| @@ -11,19 +11,13 @@ export const useUserStore = defineStore('user', () => { | ||||
|   const isAuthenticated = computed(() => !!user.value) | ||||
|  | ||||
|   // Actions | ||||
|   async function fetchUser() { | ||||
|     const token = localStorage.getItem('authToken') | ||||
|     if (!token) { | ||||
|       return // No token, no need to fetch | ||||
|     } | ||||
|  | ||||
|   async function fetchUser(reload = true) { | ||||
|     if (!reload && user.value) return // Skip fetching if already loaded and not forced to | ||||
|     isLoading.value = true | ||||
|     error.value = null | ||||
|     try { | ||||
|       const response = await fetch('/api/accounts/me', { | ||||
|         headers: { | ||||
|           'Authorization': `Bearer ${token}` | ||||
|         } | ||||
|         credentials: 'include', | ||||
|       }) | ||||
|  | ||||
|       if (!response.ok) { | ||||
| @@ -50,9 +44,21 @@ export const useUserStore = defineStore('user', () => { | ||||
|     // router.push('/login') | ||||
|   } | ||||
|  | ||||
|   async function initialize() { | ||||
|     await fetchUser() | ||||
|   } | ||||
|   watch( | ||||
|     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 { | ||||
|     user, | ||||
| @@ -61,6 +67,5 @@ export const useUserStore = defineStore('user', () => { | ||||
|     isAuthenticated, | ||||
|     fetchUser, | ||||
|     logout, | ||||
|     initialize | ||||
|   } | ||||
| }) | ||||
| @@ -32,7 +32,3 @@ async function fetchVersion() { | ||||
|  | ||||
| onMounted(() => fetchVersion()) | ||||
| </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 result = await fp.get() | ||||
|   deviceId.value = result.visitorId | ||||
|   localStorage.setItem('deviceId', deviceId.value) | ||||
| }) | ||||
|  | ||||
| const selectedFactor = computed(() => { | ||||
| @@ -214,7 +213,6 @@ async function exchangeToken() { | ||||
|     } | ||||
|  | ||||
|     const { token } = await response.json() | ||||
|     localStorage.setItem('authToken', token) | ||||
|     await userStore.fetchUser() | ||||
|  | ||||
|     const redirectUri = route.query.redirect_uri as string | ||||
|   | ||||
| @@ -29,11 +29,11 @@ export default defineConfig({ | ||||
|   server: { | ||||
|     proxy: { | ||||
|       '/api': { | ||||
|         target: 'http://localhost:5090', | ||||
|         target: 'http://localhost:5216', | ||||
|         changeOrigin: true, | ||||
|       }, | ||||
|       '/cgi': { | ||||
|         target: 'http://localhost:5090', | ||||
|         target: 'http://localhost:5216', | ||||
|         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"> | ||||
| 	<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_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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user