安全加固 + 结构清理:修注入/串写/竞态,DOMPurify 上线,后端补事务与 handler 测试
后端: - ORDER BY 白名单(sanitizeOrder)堵住 ?order= SQL 注入,补回归测试 - 登录限速(每 IP 10 次失败/10 分钟 429)、TLS/反代下 Secure cookie、NewAPI 构造器 - Delete/setTags/MergeTags/DeleteTag 包事务;Archive 去 500 篇上限 - 列表接口裁剪:不传 content_md,长文 content_html 截 600,新增 content_len;health 探 DB 前端: - EditorView 路由复用串写修复(RouterView :key + sync watch 回写原文章) - v-html 出口统一过 DOMPurify(sanitizeHtml),stripTags 改 DOMParser - 列表竞态防护(Home/Tag/Posts 请求序号)、TagView 分页修复 - 侧栏接口 30s 缓存去重;one:unauthorized 监听器泄漏修复 - 删 styles.css 498 行重复块;移除 tailwind/marked/vue-tsc 死依赖;CommandPalette a11y 语义
This commit is contained in:
1 parent
c762f06cd7
commit
dd2994189a
25 files changed
+539
-1411
No files matched your search
@@ -37,7 +37,11 @@ const tabs = [
|
||||
{ label: '短文', value: 'short' }
|
||||
]
|
||||
|
||||
// 快速翻页/切标签时旧响应可能后到并覆盖新结果,用递增序号只采纳最新一次
|
||||
let seq = 0
|
||||
|
||||
async function load() {
|
||||
const my = ++seq
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
@@ -47,13 +51,15 @@ async function load() {
|
||||
page: page.value,
|
||||
size: size.value
|
||||
})
|
||||
if (my !== seq) return
|
||||
items.value = data.items || []
|
||||
total.value = data.total || 0
|
||||
} catch (e) {
|
||||
if (my !== seq) return
|
||||
error.value = e.message || '加载失败'
|
||||
items.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (my === seq) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import LeftNav from '../components/LeftNav.vue'
|
||||
import RightRail from '../components/RightRail.vue'
|
||||
import { publicApi } from '../api'
|
||||
import { site, applyDocTitle } from '../site'
|
||||
import { formatDate, minutesLabel } from '../utils'
|
||||
import { formatDate, minutesLabel, sanitizeHtml } from '../utils'
|
||||
|
||||
const route = useRoute()
|
||||
const post = ref(null)
|
||||
@@ -65,7 +65,7 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp
|
||||
<p v-if="!isShort && post.summary" class="lede">{{ post.summary }}</p>
|
||||
</header>
|
||||
|
||||
<div class="prose" :class="{ 'prose-short': isShort }" v-html="post.content_html"></div>
|
||||
<div class="prose" :class="{ 'prose-short': isShort }" v-html="sanitizeHtml(post.content_html)"></div>
|
||||
|
||||
<footer class="foot">
|
||||
<div v-if="post.tags && post.tags.length" class="tags">
|
||||
|
||||
@@ -15,28 +15,32 @@ const error = ref('')
|
||||
const size = 20
|
||||
|
||||
const name = computed(() => route.params.slug)
|
||||
const page = computed(() => Number(route.query.page || 1))
|
||||
|
||||
// 快速翻页时旧响应可能后到并覆盖新结果,用递增序号只采纳最新一次
|
||||
let seq = 0
|
||||
|
||||
async function load() {
|
||||
const my = ++seq
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await publicApi.posts({ tag: name.value, page: 1, size })
|
||||
const data = await publicApi.posts({ tag: name.value, page: page.value, size })
|
||||
if (my !== seq) return
|
||||
items.value = data.items || []
|
||||
total.value = data.total || 0
|
||||
} catch (e) {
|
||||
if (my !== seq) return
|
||||
error.value = e.message || '加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (my === seq) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
watch(name, load)
|
||||
watch([name, page], load)
|
||||
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / size)))
|
||||
const page = computed(() => Number(route.query.page || 1))
|
||||
|
||||
watch(page, load)
|
||||
|
||||
applyDocTitle('标签')
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user