diff --git a/.env.release.draft b/.env.release.draft index 8a0c28e2..ede15f1c 100644 --- a/.env.release.draft +++ b/.env.release.draft @@ -1,11 +1,11 @@ # SkillHub production draft for bare-metal deployment. -# Copy this to .env.release and replace every value marked TODO. +# Copy this to .env.dev.release and replace every value marked TODO. # # Recommended workflow: -# 1. cp .env.release.draft .env.release +# 1. cp .env.dev.release.draft .env.dev.release # 2. Edit TODO values # 3. make validate-release-config -# 4. docker compose --env-file .env.release -f compose.release.yml up -d +# 4. docker compose --env-file .env.dev.release -f compose.release.yml up -d # Pin a released image tag in production. SKILLHUB_VERSION=v0.1.0-beta.5 diff --git a/.gitignore b/.gitignore index dcf12884..17e51625 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ yarn-error.log* .env.* !.env.example !.env.*.example +!web/.env.dev +!web/.env.fat +!web/.env.production .dev/ .memory/ *.local.json diff --git a/Makefile b/Makefile index 8f262ab5..08dcc199 100644 --- a/Makefile +++ b/Makefile @@ -225,9 +225,12 @@ web-deps: ## 确保前端依赖可用(本地开发优先复用现有 node_modu web-install-ci: ## 以 CI 方式安装前端依赖 cd web && CI=true pnpm install --frozen-lockfile -dev-web: ## 启动前端开发服务器 +local-web: ## 启动前端开发服务器 cd web && pnpm run dev +dev-web: ## 启动前端开发服务器 + cd web && pnpm run fat + build-frontend: web-deps ## 构建前端 cd web && pnpm run build diff --git a/README.md b/README.md index 2edab733..4c7c9bb6 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ curl -fsSL https://imageless.oss-cn-beijing.aliyuncs.com/runtime.sh | sh -s -- u 3. Start the stack with Docker Compose. ```bash -cp .env.release.example .env.release +cp .env.dev.release.example .env.dev.release ``` Recommended image tags: @@ -223,7 +223,7 @@ Start the runtime: ```bash make validate-release-config -docker compose --env-file .env.release -f compose.release.yml up -d +docker compose --env-file .env.dev.release -f compose.release.yml up -d ``` Then open: @@ -234,7 +234,7 @@ Then open: Stop it with: ```bash -docker compose --env-file .env.release -f compose.release.yml down +docker compose --env-file .env.dev.release -f compose.release.yml down ``` The runtime stack uses its own Compose project name, so it does not diff --git a/docs/09-deployment.md b/docs/09-deployment.md index 13b159ae..e45178b7 100644 --- a/docs/09-deployment.md +++ b/docs/09-deployment.md @@ -97,9 +97,9 @@ make dev-all-reset ### 5.1 启动 ```bash -cp .env.release.example .env.release +cp .env.dev.release.example .env.dev.release make validate-release-config -docker compose --env-file .env.release -f compose.release.yml up -d +docker compose --env-file .env.dev.release -f compose.release.yml up -d ``` 默认访问地址: diff --git a/document/docs/02-administration/deployment/single-machine.md b/document/docs/02-administration/deployment/single-machine.md index 8673a8fd..ad8a8303 100644 --- a/document/docs/02-administration/deployment/single-machine.md +++ b/document/docs/02-administration/deployment/single-machine.md @@ -23,16 +23,16 @@ git clone https://github.com/iflytek/skillhub.git cd skillhub # 2. 复制环境变量模板 -cp .env.release.example .env.release +cp .env.dev.release.example .env.dev.release # 3. 编辑配置 -# 修改 .env.release 中的配置项,特别是密码和公网地址 +# 修改 .env.dev.release 中的配置项,特别是密码和公网地址 # 4. 验证配置 make validate-release-config # 5. 启动服务 -docker compose --env-file .env.release -f compose.release.yml up -d +docker compose --env-file .env.dev.release -f compose.release.yml up -d ``` ## 配置说明 @@ -43,7 +43,7 @@ docker compose --env-file .env.release -f compose.release.yml up -d ```bash # 检查容器状态 -docker compose --env-file .env.release -f compose.release.yml ps +docker compose --env-file .env.dev.release -f compose.release.yml ps # 检查后端健康状态 curl -i http://127.0.0.1:8080/actuator/health diff --git a/document/i18n/en/docusaurus-plugin-content-docs/current/02-administration/deployment/single-machine.md b/document/i18n/en/docusaurus-plugin-content-docs/current/02-administration/deployment/single-machine.md index 711156b4..832a1da2 100644 --- a/document/i18n/en/docusaurus-plugin-content-docs/current/02-administration/deployment/single-machine.md +++ b/document/i18n/en/docusaurus-plugin-content-docs/current/02-administration/deployment/single-machine.md @@ -23,16 +23,16 @@ git clone https://github.com/iflytek/skillhub.git cd skillhub # 2. Copy environment variable template -cp .env.release.example .env.release +cp .env.dev.release.example .env.dev.release # 3. Edit configuration -# Modify configuration items in .env.release, especially passwords and public URLs +# Modify configuration items in .env.dev.release, especially passwords and public URLs # 4. Validate configuration make validate-release-config # 5. Start services -docker compose --env-file .env.release -f compose.release.yml up -d +docker compose --env-file .env.dev.release -f compose.release.yml up -d ``` ## Configuration @@ -43,7 +43,7 @@ See [Configuration](./configuration) documentation for details. ```bash # Check container status -docker compose --env-file .env.release -f compose.release.yml ps +docker compose --env-file .env.dev.release -f compose.release.yml ps # Check backend health curl -i http://127.0.0.1:8080/actuator/health diff --git a/scripts/validate-release-config.sh b/scripts/validate-release-config.sh index 1d4f9285..59abba4c 100755 --- a/scripts/validate-release-config.sh +++ b/scripts/validate-release-config.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -ENV_FILE="${1:-.env.release}" +ENV_FILE="${1:-.env.dev.release}" if [ ! -f "$ENV_FILE" ]; then echo "ERROR: env file not found: $ENV_FILE" >&2 diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/CorsConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/CorsConfig.java new file mode 100644 index 00000000..5534c348 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/CorsConfig.java @@ -0,0 +1,31 @@ +package com.iflytek.skillhub.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +public class CorsConfig { + + @Value("${skillhub.cors.allowed-origins:http://localhost:3000,http://localhost:3001}") + private List allowedOrigins; + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(allowedOrigins); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/server/skillhub-app/src/main/resources/application-dev.yml b/server/skillhub-app/src/main/resources/application-dev.yml index a9a05881..b3ef6d2e 100644 --- a/server/skillhub-app/src/main/resources/application-dev.yml +++ b/server/skillhub-app/src/main/resources/application-dev.yml @@ -63,6 +63,8 @@ spring: max-request-size: 100MB skillhub: + cors: + allowed-origins: ${SKILLHUB_CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001,http://sz-api.indusmind.local} auth: mock: enabled: ${SKILLHUB_AUTH_MOCK_ENABLED:false} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java index 276826ec..560be8d0 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java @@ -101,6 +101,8 @@ public class SecurityConfig { }; http + .cors(cors -> cors.configurationSource(http.getSharedObject(org.springframework.context.ApplicationContext.class) + .getBean(org.springframework.web.cors.CorsConfigurationSource.class))) .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(csrfHandler) diff --git a/web/.env.dev b/web/.env.dev new file mode 100644 index 00000000..a47415fe --- /dev/null +++ b/web/.env.dev @@ -0,0 +1,6 @@ +# 本地开发环境(pnpm run dev) +# API 走 vite proxy 代理到本地后端,无需设置 base URL +VITE_API_BASE_URL= +VITE_APP_BASE_URL= +VITE_BASE_PATH=/ +VITE_DEV_PROXY_TARGET=http://localhost:8000 diff --git a/web/.env.fat b/web/.env.fat new file mode 100644 index 00000000..62f19986 --- /dev/null +++ b/web/.env.fat @@ -0,0 +1,5 @@ +# FAT 环境(本地前端 + 远程后端联调) +VITE_API_BASE_URL=/skillhub-server +VITE_APP_BASE_URL= +VITE_BASE_PATH=/ +VITE_DEV_PROXY_TARGET=http://sz-api.indusmind.local diff --git a/web/.env.production b/web/.env.production new file mode 100644 index 00000000..bd28be5d --- /dev/null +++ b/web/.env.production @@ -0,0 +1,4 @@ +# 生产/云效部署环境(pnpm build) +VITE_API_BASE_URL=/skillhub-server/ +VITE_APP_BASE_URL=/skillhub-web/index.html +VITE_BASE_PATH=/skillhub-web/ diff --git a/web/Dockerfile b/web/Dockerfile index 28f88a42..96ab0cbf 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -4,7 +4,6 @@ WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . -ENV VITE_BASE_PATH=/skillhub-web/ RUN pnpm build FROM jsessh-registry.cn-shanghai.cr.aliyuncs.com/base-images/nginx-skillhub:alpine diff --git a/web/package.json b/web/package.json index 54660099..fefa2c1f 100644 --- a/web/package.json +++ b/web/package.json @@ -5,8 +5,10 @@ "type": "module", "scripts": { "install:ci": "pnpm install --frozen-lockfile", - "dev": "vite", + "fat": "vite --mode fat", + "dev": "vite --mode dev", "build": "tsc -b && vite build", + "build:production": "tsc -b && vite build --mode production", "preview": "vite preview", "test": "vitest run", "test:e2e": "playwright test", diff --git a/web/src/api/client.ts b/web/src/api/client.ts index bdda0e5f..dcb33c80 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -76,7 +76,8 @@ function getRuntimeConfig(): RuntimeConfig { } function getApiBaseUrl(): string { - return getRuntimeConfig().apiBaseUrl || '/skillhub-server/' + console.log('RuntimeConfig', getRuntimeConfig()) + return getRuntimeConfig().apiBaseUrl || import.meta.env.VITE_API_BASE_URL || '' } function parseBooleanFlag(value: string | undefined): boolean { diff --git a/web/src/bootstrap.ts b/web/src/bootstrap.ts index d00e3f6c..8172e9e0 100644 --- a/web/src/bootstrap.ts +++ b/web/src/bootstrap.ts @@ -17,8 +17,8 @@ async function loadRuntimeConfig() { function ensureRuntimeConfigFallback() { window.__SKILLHUB_RUNTIME_CONFIG__ ??= { - apiBaseUrl: '/skillhub-server/', - appBaseUrl: '/skillhub-web/', + apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '', + appBaseUrl: import.meta.env.VITE_APP_BASE_URL || '', authDirectEnabled: 'false', authDirectProvider: '', authSessionBootstrapEnabled: 'false', diff --git a/web/src/shared/components/user-menu.tsx b/web/src/shared/components/user-menu.tsx index 715b4b21..0d8714f6 100644 --- a/web/src/shared/components/user-menu.tsx +++ b/web/src/shared/components/user-menu.tsx @@ -78,7 +78,7 @@ export function UserMenu({ user, triggerClassName }: UserMenuProps) { // Always clear cache and redirect, even if API call fails clearSessionScopedQueries(queryClient) queryClient.setQueryData(['auth', 'me'], null) - window.location.href = '/skillhub-web/index.html' + window.location.href = import.meta.env.BASE_URL || '/' } } diff --git a/web/vite.config.ts b/web/vite.config.ts index caac3178..b45abaf9 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,30 +1,40 @@ -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import * as path from 'path' -export default defineConfig({ - base: process.env.VITE_BASE_PATH || '/', - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - port: 3000, - watch: { - usePolling: true, - interval: 150, - }, - proxy: { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true, - }, - '/oauth2': { - target: 'http://localhost:8080', - changeOrigin: true, +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + + const proxyTarget = env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080' + + return { + base: env.VITE_BASE_PATH || '/', + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), }, }, - }, + server: { + port: 3000, + watch: { + usePolling: true, + interval: 150, + }, + proxy: { + '/skillhub-server': { + target: proxyTarget, + changeOrigin: true, + }, + '/api': { + target: proxyTarget, + changeOrigin: true, + }, + '/oauth2': { + target: proxyTarget, + changeOrigin: true, + }, + }, + }, + } })