🎉 Initial Commit for the Sphere webpage
This commit is contained in:
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
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||||
USER $APP_UID
|
USER $APP_UID
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
EXPOSE 8081
|
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
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@@ -16,9 +35,12 @@ COPY ["DysonNetwork.Shared/DysonNetwork.Shared.csproj", "DysonNetwork.Shared/"]
|
|||||||
# Restore packages
|
# Restore packages
|
||||||
RUN dotnet restore "DysonNetwork.Sphere/DysonNetwork.Sphere.csproj"
|
RUN dotnet restore "DysonNetwork.Sphere/DysonNetwork.Sphere.csproj"
|
||||||
|
|
||||||
# Copy everything except Pass project's config files
|
# Copy everything else and build
|
||||||
COPY . .
|
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
|
# Remove Pass project's config files to prevent conflicts
|
||||||
RUN rm -f /src/DysonNetwork.Pass/appsettings*.json /src/DysonNetwork.Pass/version.json
|
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 \
|
RUN mkdir -p /src/DysonNetwork.Sphere/bin/Release/net9.0/zh-hans \
|
||||||
&& mkdir -p /src/DysonNetwork.Pass/bin/Release/net9.0/zh-hans
|
&& mkdir -p /src/DysonNetwork.Pass/bin/Release/net9.0/zh-hans
|
||||||
|
|
||||||
|
# Build the application
|
||||||
WORKDIR "/src/DysonNetwork.Sphere"
|
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
|
FROM build AS publish
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
# Ensure the target directory for localized resources exists
|
# Ensure the target directory for localized resources exists
|
||||||
RUN mkdir -p /app/publish/zh-Hans
|
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
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=publish /app/publish .
|
||||||
|
@@ -45,4 +45,6 @@ using (var scope = app.Services.CreateScope())
|
|||||||
// Configure application middleware pipeline
|
// Configure application middleware pipeline
|
||||||
app.ConfigureAppMiddleware(builder.Configuration);
|
app.ConfigureAppMiddleware(builder.Configuration);
|
||||||
|
|
||||||
|
app.MapGatewayProxy();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
@@ -52,13 +52,15 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
options.DataAnnotationLocalizerProvider = (type, factory) =>
|
options.DataAnnotationLocalizerProvider = (type, factory) =>
|
||||||
factory.Create(typeof(SharedResource));
|
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.AddRazorPages();
|
||||||
|
|
||||||
services.AddGrpc(options =>
|
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
|
||||||
{
|
|
||||||
options.EnableDetailedErrors = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.Configure<RequestLocalizationOptions>(options =>
|
services.Configure<RequestLocalizationOptions>(options =>
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user