fix(security): complete security and delivery compliance remediation
This commit is contained in:
+7
-3
@@ -1,10 +1,14 @@
|
||||
# Stage 1: Build
|
||||
FROM node:18-alpine AS build
|
||||
ARG NODE_BASE_IMAGE=node:20-alpine
|
||||
ARG NGINX_BASE_IMAGE=nginx:alpine
|
||||
FROM ${NODE_BASE_IMAGE} AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
ARG NPM_CONFIG_REGISTRY
|
||||
RUN if [ -n "$NPM_CONFIG_REGISTRY" ]; then npm config set registry "$NPM_CONFIG_REGISTRY"; fi \
|
||||
&& npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -12,7 +16,7 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Serve with nginx
|
||||
FROM nginx:alpine AS serve
|
||||
FROM ${NGINX_BASE_IMAGE} AS serve
|
||||
|
||||
# Remove default nginx config
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -20,6 +20,35 @@ server {
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Keep the browser and API on the same origin in production. This must be
|
||||
# evaluated before the SPA fallback, otherwise /api/* returns index.html.
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_connect_timeout 15s;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /docs {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /openapi.json {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# SPA fallback - all routes serve index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
@@ -19,15 +19,34 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { marked, Renderer } from 'marked'
|
||||
import request from '../utils/request'
|
||||
|
||||
const content = ref('')
|
||||
const loading = ref(true)
|
||||
|
||||
const safeLink = (href) => {
|
||||
try {
|
||||
const url = new URL(href, window.location.origin)
|
||||
return ['http:', 'https:', 'mailto:'].includes(url.protocol) ? url.href : ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const safeRenderer = new Renderer()
|
||||
safeRenderer.html = () => ''
|
||||
safeRenderer.image = ({ text }) => text
|
||||
safeRenderer.link = function ({ href, tokens }) {
|
||||
const text = this.parser.parseInline(tokens)
|
||||
const safeHref = safeLink(href)
|
||||
if (!safeHref) return text
|
||||
return `<a href="${safeHref}" rel="noopener noreferrer" target="_blank">${text}</a>`
|
||||
}
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
if (!content.value) return ''
|
||||
return marked.parse(content.value)
|
||||
return marked.parse(content.value, { renderer: safeRenderer })
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
<div v-else-if="error">
|
||||
<p>登录失败: {{ error }}</p>
|
||||
<el-button @click="$router.push('/login')">返回登录</el-button>
|
||||
<el-button type="info" @click="showDetails = !showDetails">详细信息</el-button>
|
||||
<pre v-if="showDetails" class="error-details">{{ errorDetails }}</pre>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -26,8 +24,6 @@ const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const errorDetails = ref('')
|
||||
const showDetails = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
const code = new URLSearchParams(window.location.search).get('code')
|
||||
@@ -44,8 +40,7 @@ onMounted(async () => {
|
||||
authStore.setToken(data.access_token)
|
||||
router.push('/dashboard')
|
||||
} catch (err) {
|
||||
error.value = err.message || '登录失败'
|
||||
errorDetails.value = err.response?.data?.detail || JSON.stringify(err, null, 2)
|
||||
error.value = err.response?.data?.detail || '登录失败,请稍后重试'
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
@@ -58,16 +53,4 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.error-details {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user