Harden auth, CSRF, and email log UX

This commit is contained in:
2025-12-26 19:05:20 +08:00
parent 3214cbbd91
commit f90b0a4f11
47 changed files with 583 additions and 198 deletions

View File

@@ -1206,23 +1206,24 @@
}
},
"node_modules/element-plus": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.12.0.tgz",
"integrity": "sha512-M9YLSn2np9OnqrSKWsiXvGe3qnF8pd94+TScsHj1aTMCD+nSEvucXermf807qNt6hOP040le0e5Aft7E9ZfHmA==",
"version": "2.11.3",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.3.tgz",
"integrity": "sha512-769xsjLR4B9Vf9cl5PDXnwTEdmFJvMgAkYtthdJKPhjVjU3hdAwTJ+gXKiO+PUyo2KWFwOYKZd4Ywh6PHfkbJg==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.2",
"@element-plus/icons-vue": "^2.3.1",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.1.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.19",
"dayjs": "^1.11.13",
"escape-html": "^1.0.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.3",
"lodash-unified": "^1.0.2",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0"
},
@@ -1329,6 +1330,12 @@
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",

View File

@@ -12,12 +12,30 @@ function toastErrorOnce(key, message, minIntervalMs = 1500) {
ElMessage.error(message)
}
function getCookie(name) {
const escaped = String(name || '').replace(/([.*+?^${}()|[\]\\])/g, '\\$1')
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`))
return match ? decodeURIComponent(match[1]) : ''
}
export const api = axios.create({
baseURL: '/yuyx/api',
timeout: 30_000,
withCredentials: true,
})
api.interceptors.request.use((config) => {
const method = String(config?.method || 'GET').toUpperCase()
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
const token = getCookie('csrf_token')
if (token) {
config.headers = config.headers || {}
config.headers['X-CSRF-Token'] = token
}
}
return config
})
api.interceptors.response.use(
(response) => response,
(error) => {

View File

@@ -477,6 +477,12 @@ function emailTypeLabel(type) {
return map[type] || type
}
function emailLogUserLabel(row) {
if (row?.username && row?.user_id) return `${row.username} (#${row.user_id})`
if (row?.user_id) return `用户#${row.user_id}`
return '系统'
}
async function loadEmailStats() {
emailStatsLoading.value = true
try {
@@ -709,6 +715,11 @@ onMounted(refreshAll)
<el-table :data="emailLogs" v-loading="emailLogsLoading" style="width: 100%">
<el-table-column prop="created_at" label="时间" width="180" />
<el-table-column prop="email_to" label="收件人" min-width="180" />
<el-table-column label="来源用户" min-width="160">
<template #default="{ row }">
<span class="ellipsis" :title="emailLogUserLabel(row)">{{ emailLogUserLabel(row) }}</span>
</template>
</el-table-column>
<el-table-column label="类型" width="120">
<template #default="{ row }">{{ emailTypeLabel(row.email_type) }}</template>
</el-table-column>