Compare commits
	
		
			3 Commits
		
	
	
		
			192ea0fcdd
			...
			7d3236550c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7d3236550c | |||
| adf62fb42b | |||
| 14c6913af7 | 
| @@ -228,13 +228,33 @@ public abstract class TusService | ||||
|                         ); | ||||
|                         rejected = true; | ||||
|                     } | ||||
|                     else if (!policy.AcceptTypes.Contains(contentType)) | ||||
|                     else | ||||
|                     { | ||||
|                         eventContext.FailRequest( | ||||
|                             HttpStatusCode.Forbidden, | ||||
|                             $"Content type {contentType} is not allowed by the pool's policy" | ||||
|                         ); | ||||
|                         rejected = true; | ||||
|                         var foundMatch = false; | ||||
|                         foreach (var acceptType in policy.AcceptTypes) | ||||
|                         { | ||||
|                             if (acceptType.EndsWith("/*", StringComparison.OrdinalIgnoreCase)) | ||||
|                             { | ||||
|                                 var type = acceptType[..^2]; | ||||
|                                 if (!contentType.StartsWith($"{type}/", StringComparison.OrdinalIgnoreCase)) continue; | ||||
|                                 foundMatch = true; | ||||
|                                 break; | ||||
|                             } | ||||
|                             else if (acceptType.Equals(contentType, StringComparison.OrdinalIgnoreCase)) | ||||
|                             { | ||||
|                                 foundMatch = true; | ||||
|                                 break; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         if (!foundMatch) | ||||
|                         { | ||||
|                             eventContext.FailRequest( | ||||
|                                 HttpStatusCode.Forbidden, | ||||
|                                 $"Content type {contentType} is not allowed by the pool's policy" | ||||
|                             ); | ||||
|                             rejected = true; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -57,7 +57,7 @@ const connectionsAddable = computed(() => | ||||
| ) | ||||
|  | ||||
| function connectionAdd(provider: string) { | ||||
|   window.open(`/api/auth/login/${provider}`, '_blank') | ||||
|   window.open(`/api/auth/login/${provider}`,"","width=400,height=600"); | ||||
| } | ||||
|  | ||||
| async function fetchConnections() { | ||||
|   | ||||
							
								
								
									
										9
									
								
								DysonNetwork.Sphere/Client/.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								DysonNetwork.Sphere/Client/.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] | ||||
| charset = utf-8 | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
| insert_final_newline = true | ||||
| trim_trailing_whitespace = true | ||||
|  | ||||
| end_of_line = lf | ||||
| max_line_length = 100 | ||||
							
								
								
									
										1
									
								
								DysonNetwork.Sphere/Client/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Sphere/Client/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| * text=auto eol=lf | ||||
							
								
								
									
										31
									
								
								DysonNetwork.Sphere/Client/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								DysonNetwork.Sphere/Client/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| node_modules | ||||
| **/node_modules/highlight.js/ | ||||
| .DS_Store | ||||
| dist | ||||
| dist-ssr | ||||
| coverage | ||||
| *.local | ||||
|  | ||||
| /cypress/videos/ | ||||
| /cypress/screenshots/ | ||||
|  | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .idea | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
|  | ||||
| *.tsbuildinfo | ||||
							
								
								
									
										6
									
								
								DysonNetwork.Sphere/Client/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								DysonNetwork.Sphere/Client/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/prettierrc", | ||||
|   "semi": false, | ||||
|   "singleQuote": true, | ||||
|   "printWidth": 100 | ||||
| } | ||||
							
								
								
									
										9
									
								
								DysonNetwork.Sphere/Client/.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								DysonNetwork.Sphere/Client/.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "recommendations": [ | ||||
|     "Vue.volar", | ||||
|     "dbaeumer.vscode-eslint", | ||||
|     "EditorConfig.EditorConfig", | ||||
|     "oxc.oxc-vscode", | ||||
|     "esbenp.prettier-vscode" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										1
									
								
								DysonNetwork.Sphere/Client/env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Sphere/Client/env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /// <reference types="vite/client" /> | ||||
							
								
								
									
										31
									
								
								DysonNetwork.Sphere/Client/eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								DysonNetwork.Sphere/Client/eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { globalIgnores } from 'eslint/config' | ||||
| import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' | ||||
| import pluginVue from 'eslint-plugin-vue' | ||||
| import pluginOxlint from 'eslint-plugin-oxlint' | ||||
| import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' | ||||
|  | ||||
| // To allow more languages other than `ts` in `.vue` files, uncomment the following lines: | ||||
| // import { configureVueProject } from '@vue/eslint-config-typescript' | ||||
| // configureVueProject({ scriptLangs: ['ts', 'tsx'] }) | ||||
| // More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup | ||||
|  | ||||
| export default defineConfigWithVueTs( | ||||
|   { | ||||
|     name: 'app/files-to-lint', | ||||
|     files: ['**/*.{ts,mts,tsx,vue}'], | ||||
|   }, | ||||
|  | ||||
|   globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), | ||||
|  | ||||
|   pluginVue.configs['flat/essential'], | ||||
|   vueTsConfigs.recommended, | ||||
|   ...pluginOxlint.configs['flat/recommended'], | ||||
|   { | ||||
|     rules: { | ||||
|       'vue/multi-word-component-names': 'off', | ||||
|       '@typescript-eslint/no-explicit-any': 'off', | ||||
|       '@typescript-eslint/ban-ts-comment': 'off', | ||||
|     }, | ||||
|   }, | ||||
|   skipFormatting, | ||||
| ) | ||||
							
								
								
									
										14
									
								
								DysonNetwork.Sphere/Client/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								DysonNetwork.Sphere/Client/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| <!doctype html> | ||||
| <html lang=""> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" href="/favicon.png" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Solar Network</title> | ||||
|     <app-data /> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/main.ts"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										58
									
								
								DysonNetwork.Sphere/Client/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								DysonNetwork.Sphere/Client/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| { | ||||
|   "name": "@solar-network/sphere", | ||||
|   "version": "0.0.0", | ||||
|   "private": true, | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "run-p type-check \"build-only {@}\" --", | ||||
|     "preview": "vite preview", | ||||
|     "build-only": "vite build", | ||||
|     "type-check": "vue-tsc --build", | ||||
|     "lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore", | ||||
|     "lint:eslint": "eslint . --fix", | ||||
|     "lint": "run-s lint:*", | ||||
|     "format": "prettier --write src/" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@fingerprintjs/fingerprintjs": "^4.6.2", | ||||
|     "@fontsource-variable/nunito": "^5.2.6", | ||||
|     "@hcaptcha/vue3-hcaptcha": "^1.3.0", | ||||
|     "@tailwindcss/typography": "^0.5.16", | ||||
|     "@tailwindcss/vite": "^4.1.11", | ||||
|     "@vueuse/core": "^13.5.0", | ||||
|     "aspnet-prerendering": "^3.0.1", | ||||
|     "cfturnstile-vue3": "^2.0.0", | ||||
|     "chart.js": "^4.5.0", | ||||
|     "dayjs": "^1.11.13", | ||||
|     "marked": "^16.1.1", | ||||
|     "pinia": "^3.0.3", | ||||
|     "tailwindcss": "^4.1.11", | ||||
|     "tus-js-client": "^4.3.1", | ||||
|     "vue": "^3.5.17", | ||||
|     "vue-chartjs": "^5.3.2", | ||||
|     "vue-router": "^4.5.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tsconfig/node22": "^22.0.2", | ||||
|     "@types/node": "^22.16.4", | ||||
|     "@vicons/material": "^0.13.0", | ||||
|     "@vitejs/plugin-vue": "^6.0.0", | ||||
|     "@vitejs/plugin-vue-jsx": "^5.0.1", | ||||
|     "@vue/eslint-config-prettier": "^10.2.0", | ||||
|     "@vue/eslint-config-typescript": "^14.6.0", | ||||
|     "@vue/tsconfig": "^0.7.0", | ||||
|     "eslint": "^9.31.0", | ||||
|     "eslint-plugin-oxlint": "~1.1.0", | ||||
|     "eslint-plugin-vue": "~10.2.0", | ||||
|     "jiti": "^2.4.2", | ||||
|     "naive-ui": "^2.42.0", | ||||
|     "npm-run-all2": "^8.0.4", | ||||
|     "oxlint": "~1.1.0", | ||||
|     "prettier": "3.5.3", | ||||
|     "typescript": "~5.8.3", | ||||
|     "vite": "npm:rolldown-vite@latest", | ||||
|     "vite-plugin-vue-devtools": "^7.7.7", | ||||
|     "vue-tsc": "^2.2.12" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								DysonNetwork.Sphere/Client/public/favicon.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								DysonNetwork.Sphere/Client/public/favicon.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 70 KiB | 
							
								
								
									
										
											BIN
										
									
								
								DysonNetwork.Sphere/Client/public/image-broken.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								DysonNetwork.Sphere/Client/public/image-broken.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 38 KiB | 
							
								
								
									
										10
									
								
								DysonNetwork.Sphere/Client/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								DysonNetwork.Sphere/Client/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| @import "tailwindcss"; | ||||
| @plugin "@tailwindcss/typography"; | ||||
|  | ||||
| @layer theme, base, components, utilities; | ||||
|  | ||||
| @layer base { | ||||
|   body { | ||||
|     font-family: 'Nunito Variable', sans-serif; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										18
									
								
								DysonNetwork.Sphere/Client/src/components/AttachmentItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								DysonNetwork.Sphere/Client/src/components/AttachmentItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| <template> | ||||
|   <n-image v-if="itemType == 'image'" :src="remoteSource" class="rounded-md"> | ||||
|     <template #error> | ||||
|       <img src="/image-broken.jpg" class="w-32 h-32 rounded-md" /> | ||||
|     </template> | ||||
|   </n-image> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { NImage } from 'naive-ui' | ||||
| import { computed } from 'vue' | ||||
|  | ||||
| const props = defineProps<{ item: any }>() | ||||
|  | ||||
| const itemType = computed(() => props.item.mime_type.split('/')[0] ?? 'unknown') | ||||
|  | ||||
| const remoteSource = computed(() => `/cgi/drive/files/${props.item.id}`) | ||||
| </script> | ||||
							
								
								
									
										34
									
								
								DysonNetwork.Sphere/Client/src/components/PostHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								DysonNetwork.Sphere/Client/src/components/PostHeader.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <template> | ||||
|   <div class="flex gap-3 items-center"> | ||||
|     <n-avatar round :size="40" :src="publisherAvatar" /> | ||||
|     <div class="flex-grow-1 flex flex-col"> | ||||
|       <p class="flex gap-1 items-baseline"> | ||||
|         <span class="font-bold">{{ props.item.publisher.nick }}</span> | ||||
|         <span class="text-xs">@{{ props.item.publisher.name }}</span> | ||||
|       </p> | ||||
|       <p class="text-xs flex gap-1"> | ||||
|         <span>{{ dayjs(props.item.created_at).utc().fromNow() }}</span> | ||||
|         <span class="font-bold">·</span> | ||||
|         <span>{{ new Date(props.item.created_at).toLocaleString() }}</span> | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { NAvatar } from 'naive-ui' | ||||
| import { computed } from 'vue' | ||||
|  | ||||
| import dayjs from 'dayjs' | ||||
| import relativeTime from 'dayjs/plugin/relativeTime' | ||||
| import utc from 'dayjs/plugin/utc' | ||||
|  | ||||
| dayjs.extend(utc) | ||||
| dayjs.extend(relativeTime) | ||||
|  | ||||
| const props = defineProps<{ item: any }>() | ||||
|  | ||||
| const publisherAvatar = computed(() => | ||||
|   props.item.publisher.picture ? `/cgi/drive/files/${props.item.publisher.picture.id}` : undefined, | ||||
| ) | ||||
| </script> | ||||
							
								
								
									
										53
									
								
								DysonNetwork.Sphere/Client/src/components/PostItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								DysonNetwork.Sphere/Client/src/components/PostItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| <template> | ||||
|   <n-card> | ||||
|     <div class="flex flex-col gap-3"> | ||||
|       <post-header :item="props.item" /> | ||||
|  | ||||
|       <div v-if="props.item.title || props.item.description"> | ||||
|         <h2 class="text-lg" v-if="props.item.title">{{ props.item.title }}</h2> | ||||
|         <p class="text-sm" v-if="props.item.description"> | ||||
|           {{ props.item.description }} | ||||
|         </p> | ||||
|       </div> | ||||
|  | ||||
|       <article v-if="htmlContent" class="prose prose-sm dark:prose-invert prose-slate prose-p:m-0"> | ||||
|         <div v-html="htmlContent"></div> | ||||
|       </article> | ||||
|  | ||||
|       <div v-if="props.item.attachments"> | ||||
|         <n-image-group> | ||||
|           <n-space> | ||||
|             <attachment-item | ||||
|               v-for="attachment in props.item.attachments" | ||||
|               :key="attachment.id" | ||||
|               :item="attachment" | ||||
|             /> | ||||
|           </n-space> | ||||
|         </n-image-group> | ||||
|       </div> | ||||
|     </div> | ||||
|   </n-card> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { NCard, NImageGroup, NSpace } from 'naive-ui' | ||||
| import { ref, watch } from 'vue' | ||||
| import { Marked } from 'marked' | ||||
|  | ||||
| import PostHeader from './PostHeader.vue' | ||||
| import AttachmentItem from './AttachmentItem.vue' | ||||
|  | ||||
| const props = defineProps<{ item: any }>() | ||||
|  | ||||
| const marked = new Marked() | ||||
|  | ||||
| const htmlContent = ref<string>('') | ||||
|  | ||||
| watch( | ||||
|   props.item, | ||||
|   async (value) => { | ||||
|     if (value.content) htmlContent.value = await marked.parse(value.content) | ||||
|   }, | ||||
|   { immediate: true, deep: true }, | ||||
| ) | ||||
| </script> | ||||
							
								
								
									
										115
									
								
								DysonNetwork.Sphere/Client/src/layouts/default.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								DysonNetwork.Sphere/Client/src/layouts/default.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| <template> | ||||
|   <n-layout> | ||||
|     <n-layout-header class="border-b-1 flex justify-between items-center"> | ||||
|       <router-link to="/" class="text-lg font-bold">Solar Network</router-link> | ||||
|       <div v-if="!hideUserMenu"> | ||||
|         <n-dropdown | ||||
|           v-if="!userStore.isAuthenticated" | ||||
|           :options="guestOptions" | ||||
|           @select="handleGuestMenuSelect" | ||||
|         > | ||||
|           <n-button>Account</n-button> | ||||
|         </n-dropdown> | ||||
|         <n-dropdown v-else :options="userOptions" @select="handleUserMenuSelect" type="primary"> | ||||
|           <n-button>{{ userStore.user.nick }}</n-button> | ||||
|         </n-dropdown> | ||||
|       </div> | ||||
|     </n-layout-header> | ||||
|     <n-layout-content embedded> | ||||
|       <router-view /> | ||||
|     </n-layout-content> | ||||
|   </n-layout> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed, h } from 'vue' | ||||
| import { NLayout, NLayoutHeader, NLayoutContent, NButton, NDropdown, NIcon } from 'naive-ui' | ||||
| import { | ||||
|   LogInOutlined, | ||||
|   PersonAddAlt1Outlined, | ||||
|   PersonOutlineRound, | ||||
|   DataUsageRound, | ||||
| } from '@vicons/material' | ||||
| import { useUserStore } from '@/stores/user' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { useServicesStore } from '@/stores/services' | ||||
|  | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const router = useRouter() | ||||
| const route = useRoute() | ||||
|  | ||||
| const hideUserMenu = computed(() => { | ||||
|   return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string) | ||||
| }) | ||||
|  | ||||
| const guestOptions = [ | ||||
|   { | ||||
|     label: 'Login', | ||||
|     key: 'login', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(LogInOutlined), | ||||
|       }), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Create Account', | ||||
|     key: 'create-account', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(PersonAddAlt1Outlined), | ||||
|       }), | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const userOptions = computed(() => [ | ||||
|   { | ||||
|     label: 'Dashboard', | ||||
|     key: 'dashboardUsage', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(DataUsageRound), | ||||
|       }), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Profile', | ||||
|     key: 'profile', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(PersonOutlineRound), | ||||
|       }), | ||||
|   }, | ||||
| ]) | ||||
|  | ||||
| const servicesStore = useServicesStore() | ||||
|  | ||||
| function handleGuestMenuSelect(key: string) { | ||||
|   if (key === 'login') { | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'login')!, '_blank') | ||||
|   } else if (key === 'create-account') { | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'create-account')!, '_blank') | ||||
|   } | ||||
| } | ||||
|  | ||||
| function handleUserMenuSelect(key: string) { | ||||
|   if (key === 'profile') { | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank') | ||||
|   } else { | ||||
|     router.push({ name: key }) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .n-layout-header { | ||||
|   padding: 8px 24px; | ||||
|   border-color: var(--n-border-color); | ||||
|   height: 57px; /* Fixed height */ | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .n-layout-content { | ||||
|   height: calc(100vh - 57px); /* Adjust based on header height */ | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										16
									
								
								DysonNetwork.Sphere/Client/src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								DysonNetwork.Sphere/Client/src/main.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import '@fontsource-variable/nunito'; | ||||
|  | ||||
| import './assets/main.css' | ||||
|  | ||||
| import { createApp } from 'vue' | ||||
| import { createPinia } from 'pinia' | ||||
|  | ||||
| import Root from './root.vue' | ||||
| import router from './router' | ||||
|  | ||||
| const app = createApp(Root) | ||||
|  | ||||
| app.use(createPinia()) | ||||
| app.use(router) | ||||
|  | ||||
| app.mount('#app') | ||||
							
								
								
									
										55
									
								
								DysonNetwork.Sphere/Client/src/root.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								DysonNetwork.Sphere/Client/src/root.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| <script setup lang="ts"> | ||||
| import LayoutDefault from './layouts/default.vue' | ||||
|  | ||||
| import { RouterView } from 'vue-router' | ||||
| import { | ||||
|   NGlobalStyle, | ||||
|   NConfigProvider, | ||||
|   NMessageProvider, | ||||
|   NDialogProvider, | ||||
|   NLoadingBarProvider, | ||||
|   lightTheme, | ||||
|   darkTheme, | ||||
| } from 'naive-ui' | ||||
| import { usePreferredDark } from '@vueuse/core' | ||||
| import { useUserStore } from './stores/user' | ||||
| import { onMounted } from 'vue' | ||||
| import { useServicesStore } from './stores/services' | ||||
|  | ||||
| const themeOverrides = { | ||||
|   common: { | ||||
|     fontFamily: 'Nunito Variable, v-sans, ui-system, -apple-system, sans-serif', | ||||
|     primaryColor: '#7D80BAFF', | ||||
|     primaryColorHover: '#9294C5FF', | ||||
|     primaryColorPressed: '#575B9DFF', | ||||
|     primaryColorSuppl: '#6B6FC1FF', | ||||
|   }, | ||||
| } | ||||
|  | ||||
| const isDark = usePreferredDark() | ||||
|  | ||||
| const userStore = useUserStore() | ||||
| const servicesStore = useServicesStore() | ||||
|  | ||||
| onMounted(() => { | ||||
|   userStore.initialize() | ||||
|  | ||||
|   userStore.fetchUser() | ||||
|   servicesStore.fetchServices() | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme"> | ||||
|     <n-global-style /> | ||||
|     <n-loading-bar-provider> | ||||
|       <n-dialog-provider> | ||||
|         <n-message-provider placement="bottom"> | ||||
|           <layout-default> | ||||
|             <router-view /> | ||||
|           </layout-default> | ||||
|         </n-message-provider> | ||||
|       </n-dialog-provider> | ||||
|     </n-loading-bar-provider> | ||||
|   </n-config-provider> | ||||
| </template> | ||||
							
								
								
									
										39
									
								
								DysonNetwork.Sphere/Client/src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								DysonNetwork.Sphere/Client/src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import { createRouter, createWebHistory } from 'vue-router' | ||||
| import { useUserStore } from '@/stores/user' | ||||
| import { useServicesStore } from '@/stores/services' | ||||
|  | ||||
| const router = createRouter({ | ||||
|   history: createWebHistory(import.meta.env.BASE_URL), | ||||
|   routes: [ | ||||
|     { | ||||
|       path: '/', | ||||
|       name: 'index', | ||||
|       component: () => import('../views/index.vue'), | ||||
|     }, | ||||
|   ], | ||||
| }) | ||||
|  | ||||
| router.beforeEach(async (to, from, next) => { | ||||
|   const userStore = useUserStore() | ||||
|   const servicesStore = useServicesStore() | ||||
|  | ||||
|   // Initialize user state if not already initialized | ||||
|   if (!userStore.user) { | ||||
|     await userStore.fetchUser() | ||||
|   } | ||||
|  | ||||
|   if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) { | ||||
|     window.open( | ||||
|       servicesStore.getSerivceUrl( | ||||
|         'DysonNetwork.Pass', | ||||
|         'login?redirect=' + encodeURIComponent(window.location.href), | ||||
|       )!, | ||||
|       '_blank', | ||||
|     ) | ||||
|     next('/') | ||||
|   } else { | ||||
|     next() | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default router | ||||
							
								
								
									
										27
									
								
								DysonNetwork.Sphere/Client/src/stores/services.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								DysonNetwork.Sphere/Client/src/stores/services.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { defineStore } from 'pinia' | ||||
| import { ref } from 'vue' | ||||
|  | ||||
| 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 { | ||||
|     const baseUrl = services.value[serviceName] || null | ||||
|     return baseUrl ? `${baseUrl}/${parts.join('/')}` : null | ||||
|   } | ||||
|  | ||||
|   return { services, fetchServices, getSerivceUrl } | ||||
| }) | ||||
							
								
								
									
										65
									
								
								DysonNetwork.Sphere/Client/src/stores/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								DysonNetwork.Sphere/Client/src/stores/user.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| import { defineStore } from 'pinia' | ||||
| import { ref, computed } from 'vue' | ||||
|  | ||||
| export const useUserStore = defineStore('user', () => { | ||||
|   // State | ||||
|   const user = ref<any>(null) | ||||
|   const isLoading = ref(false) | ||||
|   const error = ref<string | null>(null) | ||||
|  | ||||
|   // Getters | ||||
|   const isAuthenticated = computed(() => !!user.value) | ||||
|  | ||||
|   // Actions | ||||
|   async function fetchUser(reload = true) { | ||||
|     if (!reload && user.value) return | ||||
|     isLoading.value = true | ||||
|     error.value = null | ||||
|     try { | ||||
|       const response = await fetch('/cgi/id/accounts/me', { | ||||
|         credentials: 'include', | ||||
|       }) | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         // If the token is invalid, clear it and the user state | ||||
|         throw new Error('Failed to fetch user information.') | ||||
|       } | ||||
|  | ||||
|       user.value = await response.json() | ||||
|     } catch (e: any) { | ||||
|       error.value = e.message | ||||
|       user.value = null // Clear user data on error | ||||
|     } finally { | ||||
|       isLoading.value = false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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 { | ||||
|     user, | ||||
|     isLoading, | ||||
|     error, | ||||
|     isAuthenticated, | ||||
|     fetchUser, | ||||
|     initialize, | ||||
|   } | ||||
| }) | ||||
							
								
								
									
										67
									
								
								DysonNetwork.Sphere/Client/src/views/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								DysonNetwork.Sphere/Client/src/views/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| <template> | ||||
|   <div class="h-full max-w-5xl container mx-auto px-8"> | ||||
|     <n-grid cols="1 l:5" responsive="screen" :x-gap="16"> | ||||
|       <n-gi span="3"> | ||||
|         <n-infinite-scroll style="height: calc(100vh - 57px)" :distance="10" @load="fetchActivites"> | ||||
|           <div v-for="activity in activites" :key="activity.id" class="mt-4"> | ||||
|             <post-item v-if="activity.type == 'posts.new'" :item="activity.data" /> | ||||
|           </div> | ||||
|         </n-infinite-scroll> | ||||
|       </n-gi> | ||||
|       <n-gi span="2" class="max-lg:order-first"> | ||||
|         <n-card class="w-full mt-4" title="About"> | ||||
|           <p>Welcome to the <b>Solar Network</b></p> | ||||
|           <p>The open social network. Friendly to everyone.</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-gi> | ||||
|     </n-grid> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { NCard, NInfiniteScroll, NGrid, NGi } from 'naive-ui' | ||||
| import { computed, onMounted, ref } from 'vue' | ||||
| import { useUserStore } from '@/stores/user' | ||||
|  | ||||
| import PostItem from '@/components/PostItem.vue' | ||||
|  | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const version = ref<any>(null) | ||||
| async function fetchVersion() { | ||||
|   const resp = await fetch('/api/version') | ||||
|   version.value = await resp.json() | ||||
| } | ||||
| onMounted(() => fetchVersion()) | ||||
|  | ||||
| const loading = ref(false) | ||||
|  | ||||
| const activites = ref<any[]>([]) | ||||
| const activitesLast = computed( | ||||
|   () => | ||||
|     activites.value.sort( | ||||
|       (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), | ||||
|     )[0], | ||||
| ) | ||||
|  | ||||
| async function fetchActivites() { | ||||
|   loading.value = true | ||||
|   const resp = await fetch( | ||||
|     activitesLast.value == null | ||||
|       ? '/api/activities' | ||||
|       : `/api/activities?cursor=${new Date(activitesLast.value.created_at).toISOString()}`, | ||||
|   ) | ||||
|   activites.value.push(...(await resp.json())) | ||||
|   loading.value = false | ||||
| } | ||||
| onMounted(() => fetchActivites()) | ||||
| </script> | ||||
							
								
								
									
										16
									
								
								DysonNetwork.Sphere/Client/src/views/not-found.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								DysonNetwork.Sphere/Client/src/views/not-found.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| <template> | ||||
|   <section class="h-full flex items-center justify-center"> | ||||
|     <n-result status="404" title="404" description="Page not found"> | ||||
|       <template #footer> | ||||
|         <n-button @click="router.push('/')">Go to Home</n-button> | ||||
|       </template> | ||||
|     </n-result> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { NResult, NButton } from 'naive-ui' | ||||
| import { useRouter } from 'vue-router'; | ||||
|  | ||||
| const router = useRouter() | ||||
| </script> | ||||
							
								
								
									
										94
									
								
								DysonNetwork.Sphere/Client/src/views/secure.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								DysonNetwork.Sphere/Client/src/views/secure.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| export async function downloadAndDecryptFile( | ||||
|   url: string, | ||||
|   password: string, | ||||
|   fileName: 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 = fileName | ||||
|   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) | ||||
| } | ||||
							
								
								
									
										12
									
								
								DysonNetwork.Sphere/Client/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								DysonNetwork.Sphere/Client/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| { | ||||
|   "extends": "@vue/tsconfig/tsconfig.dom.json", | ||||
|   "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./**/*.d.ts"], | ||||
|   "exclude": ["src/**/__tests__/*"], | ||||
|   "compilerOptions": { | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||||
|  | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								DysonNetwork.Sphere/Client/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								DysonNetwork.Sphere/Client/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "files": [], | ||||
|   "references": [ | ||||
|     { | ||||
|       "path": "./tsconfig.node.json" | ||||
|     }, | ||||
|     { | ||||
|       "path": "./tsconfig.app.json" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										19
									
								
								DysonNetwork.Sphere/Client/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								DysonNetwork.Sphere/Client/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| { | ||||
|   "extends": "@tsconfig/node22/tsconfig.json", | ||||
|   "include": [ | ||||
|     "vite.config.*", | ||||
|     "vitest.config.*", | ||||
|     "cypress.config.*", | ||||
|     "nightwatch.conf.*", | ||||
|     "playwright.config.*", | ||||
|     "eslint.config.*" | ||||
|   ], | ||||
|   "compilerOptions": { | ||||
|     "noEmit": true, | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", | ||||
|  | ||||
|     "module": "ESNext", | ||||
|     "moduleResolution": "Bundler", | ||||
|     "types": ["node"] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										32
									
								
								DysonNetwork.Sphere/Client/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								DysonNetwork.Sphere/Client/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import { fileURLToPath, URL } from 'node:url' | ||||
|  | ||||
| import { defineConfig } from 'vite' | ||||
| import vue from '@vitejs/plugin-vue' | ||||
| import vueJsx from '@vitejs/plugin-vue-jsx' | ||||
| import vueDevTools from 'vite-plugin-vue-devtools' | ||||
| import tailwindcss from '@tailwindcss/vite' | ||||
|  | ||||
| process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' | ||||
|  | ||||
| // https://vite.dev/config/ | ||||
| export default defineConfig({ | ||||
|   base: '/', | ||||
|   plugins: [vue(), vueJsx(), vueDevTools(), tailwindcss()], | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       '@': fileURLToPath(new URL('./src', import.meta.url)), | ||||
|     }, | ||||
|   }, | ||||
|   server: { | ||||
|     proxy: { | ||||
|       '/api': { | ||||
|         target: 'http://localhost:5071', | ||||
|         changeOrigin: true, | ||||
|       }, | ||||
|       '/cgi': { | ||||
|         target: 'http://localhost:5071', | ||||
|         changeOrigin: true, | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| @@ -1,9 +1,28 @@ | ||||
| # Stage 1: Base runtime image | ||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base | ||||
| USER $APP_UID | ||||
| WORKDIR /app | ||||
| EXPOSE 8080 | ||||
| EXPOSE 8081 | ||||
|  | ||||
| # Stage 2: Build SPA | ||||
| FROM node:22-alpine AS spa-builder | ||||
| WORKDIR /src | ||||
|  | ||||
| # Copy package files for SPA | ||||
| COPY ["DysonNetwork.Sphere/Client/package.json", "DysonNetwork.Sphere/Client/package-lock.json*", "./Client/"] | ||||
|  | ||||
| # Install SPA dependencies | ||||
| WORKDIR /src/Client | ||||
| RUN npm install | ||||
|  | ||||
| # Copy SPA source | ||||
| COPY ["DysonNetwork.Sphere/Client/", "./"] | ||||
|  | ||||
| # Build SPA | ||||
| RUN npm run build | ||||
|  | ||||
| # Stage 3: Build .NET application | ||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| WORKDIR /src | ||||
| @@ -16,9 +35,12 @@ COPY ["DysonNetwork.Shared/DysonNetwork.Shared.csproj", "DysonNetwork.Shared/"] | ||||
| # Restore packages | ||||
| RUN dotnet restore "DysonNetwork.Sphere/DysonNetwork.Sphere.csproj" | ||||
|  | ||||
| # Copy everything except Pass project's config files | ||||
| # Copy everything else and build | ||||
| COPY . . | ||||
|  | ||||
| # Copy built SPA to wwwroot | ||||
| COPY --from=spa-builder /src/Client/dist /src/DysonNetwork.Sphere/wwwroot/dist | ||||
|  | ||||
| # Remove Pass project's config files to prevent conflicts | ||||
| RUN rm -f /src/DysonNetwork.Pass/appsettings*.json /src/DysonNetwork.Pass/version.json | ||||
|  | ||||
| @@ -26,15 +48,23 @@ RUN rm -f /src/DysonNetwork.Pass/appsettings*.json /src/DysonNetwork.Pass/versio | ||||
| RUN mkdir -p /src/DysonNetwork.Sphere/bin/Release/net9.0/zh-hans \ | ||||
|     && mkdir -p /src/DysonNetwork.Pass/bin/Release/net9.0/zh-hans | ||||
|  | ||||
| # Build the application | ||||
| WORKDIR "/src/DysonNetwork.Sphere" | ||||
| RUN dotnet build "DysonNetwork.Sphere.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||||
| RUN dotnet build "DysonNetwork.Sphere.csproj" -c $BUILD_CONFIGURATION -o /app/build \ | ||||
|     -p:TypeScriptCompileBlocked=true \ | ||||
|     -p:UseRazorBuildServer=false | ||||
|  | ||||
| # Stage 4: Publish | ||||
| FROM build AS publish | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| # Ensure the target directory for localized resources exists | ||||
| RUN mkdir -p /app/publish/zh-Hans | ||||
| RUN dotnet publish "./DysonNetwork.Sphere.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | ||||
| RUN dotnet publish "DysonNetwork.Sphere.csproj" -c $BUILD_CONFIGURATION -o /app/publish \ | ||||
|     -p:TypeScriptCompileBlocked=true \ | ||||
|     -p:UseRazorBuildServer=false \ | ||||
|     /p:UseAppHost=false | ||||
|  | ||||
| # Final stage: Runtime | ||||
| FROM base AS final | ||||
| WORKDIR /app | ||||
| COPY --from=publish /app/publish . | ||||
|   | ||||
| @@ -45,4 +45,6 @@ using (var scope = app.Services.CreateScope()) | ||||
| // Configure application middleware pipeline | ||||
| app.ConfigureAppMiddleware(builder.Configuration); | ||||
|  | ||||
| app.MapGatewayProxy(); | ||||
|  | ||||
| app.Run(); | ||||
| @@ -52,13 +52,15 @@ public static class ServiceCollectionExtensions | ||||
|         { | ||||
|             options.DataAnnotationLocalizerProvider = (type, factory) => | ||||
|                 factory.Create(typeof(SharedResource)); | ||||
|         }).ConfigureApplicationPartManager(opts => | ||||
|         { | ||||
|             var mockingPart = opts.ApplicationParts.FirstOrDefault(a => a.Name == "DysonNetwork.Pass"); | ||||
|             if (mockingPart != null) | ||||
|                 opts.ApplicationParts.Remove(mockingPart); | ||||
|         }); | ||||
|         services.AddRazorPages(); | ||||
|  | ||||
|         services.AddGrpc(options => | ||||
|         { | ||||
|             options.EnableDetailedErrors = true; | ||||
|         }); | ||||
|         services.AddGrpc(options => { options.EnableDetailedErrors = true; }); | ||||
|  | ||||
|         services.Configure<RequestLocalizationOptions>(options => | ||||
|         { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user