From 28040f44f41a5cae5168ea5ff74181621435530b Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Wed, 15 Apr 2026 12:47:53 +0100 Subject: [PATCH] refactor(board): atomic delete + query limit + composable cleanup Delete uses findOneAndDelete with author match (no TOCTOU window); existence check only runs on miss to distinguish 403 vs 404. Posts list capped at 200. Drop unused resolveTagChannel and refreshParams; route slack URL building through the composable's slackUrl helper. --- app/components/BoardPostCard.vue | 4 +++- app/composables/useBoardChannels.js | 14 ------------- app/composables/useBoardPosts.js | 16 ++++++--------- app/pages/board.vue | 5 ----- server/api/board/posts.get.js | 1 + server/api/board/posts/[id].delete.js | 15 +++++++------- tests/server/api/board-posts.test.js | 29 ++++++++++++--------------- 7 files changed, 30 insertions(+), 54 deletions(-) diff --git a/app/components/BoardPostCard.vue b/app/components/BoardPostCard.vue index 5f00f3f..ff6e9d4 100644 --- a/app/components/BoardPostCard.vue +++ b/app/components/BoardPostCard.vue @@ -86,6 +86,8 @@ const props = defineProps({ defineEmits(['edit', 'delete', 'confirm-delete', 'cancel-delete']) +const { slackUrl } = useBoardChannels() + const capitalizeAvatar = (str) => { if (str.toLowerCase() === 'wtf') return 'WTF' return str @@ -148,7 +150,7 @@ const slackLinks = computed(() => { .map((c) => ({ id: c.slackChannelId, name: c.slackChannelName || c.name || c.slackChannelId, - url: `https://gammaspace.slack.com/archives/${c.slackChannelId}`, + url: slackUrl(c.slackChannelId), })) }) diff --git a/app/composables/useBoardChannels.js b/app/composables/useBoardChannels.js index d499b77..03eb872 100644 --- a/app/composables/useBoardChannels.js +++ b/app/composables/useBoardChannels.js @@ -1,7 +1,3 @@ -/** - * Board Channels Composable - * Shared state + helpers for mapping board tags to Slack channels. - */ export function useBoardChannels() { const channels = useState('board.channels', () => []) @@ -11,15 +7,6 @@ export function useBoardChannels() { return channels.value } - function resolveTagChannel(tagSlugs = []) { - if (!tagSlugs?.length) return null - return ( - channels.value.find((channel) => - (channel.tagSlugs || []).some((slug) => tagSlugs.includes(slug)) - ) || null - ) - } - function slackUrl(channelId) { return `https://gammaspace.slack.com/archives/${channelId}` } @@ -27,7 +14,6 @@ export function useBoardChannels() { return { channels: readonly(channels), fetchChannels, - resolveTagChannel, slackUrl, } } diff --git a/app/composables/useBoardPosts.js b/app/composables/useBoardPosts.js index 3f0b0d9..987d6c8 100644 --- a/app/composables/useBoardPosts.js +++ b/app/composables/useBoardPosts.js @@ -1,7 +1,3 @@ -/** - * Board Posts Composable - * Shared state + CRUD for board posts. - */ export function useBoardPosts() { const posts = useState('board.posts', () => []) const loading = useState('board.loading', () => false) @@ -17,29 +13,29 @@ export function useBoardPosts() { } } - async function createPost(body, refreshParams = {}) { + async function createPost(body) { const created = await $fetch('/api/board/posts', { method: 'POST', body, }) - await fetchPosts(refreshParams) + await fetchPosts() return created } - async function updatePost(id, body, refreshParams = {}) { + async function updatePost(id, body) { const updated = await $fetch(`/api/board/posts/${id}`, { method: 'PATCH', body, }) - await fetchPosts(refreshParams) + await fetchPosts() return updated } - async function deletePost(id, refreshParams = {}) { + async function deletePost(id) { const result = await $fetch(`/api/board/posts/${id}`, { method: 'DELETE', }) - await fetchPosts(refreshParams) + await fetchPosts() return result } diff --git a/app/pages/board.vue b/app/pages/board.vue index 460c78b..6ee1af3 100644 --- a/app/pages/board.vue +++ b/app/pages/board.vue @@ -1,13 +1,11 @@