refactor: extract escapeRegex and validateTagSlugs server utils
Deduplicate tag validation and regex escaping into shared auto-imported utils. Add tag validation to wiki patch/batch-tag routes. Remove duplicate tags field from event schema.
This commit is contained in:
parent
f585fabf21
commit
a516f172fb
11 changed files with 33 additions and 31 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import Event from "../../models/event.js";
|
import Event from "../../models/event.js";
|
||||||
import Tag from "../../models/tag.js";
|
|
||||||
import { connectDB } from "../../utils/mongoose.js";
|
import { connectDB } from "../../utils/mongoose.js";
|
||||||
import { requireAdmin } from "../../utils/auth.js";
|
import { requireAdmin } from "../../utils/auth.js";
|
||||||
import { validateBody } from "../../utils/validateBody.js";
|
import { validateBody } from "../../utils/validateBody.js";
|
||||||
|
|
@ -13,18 +12,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
await connectDB();
|
await connectDB();
|
||||||
|
|
||||||
// Validate tag slugs against Tag collection
|
await validateTagSlugs(body.tags);
|
||||||
if (body.tags && body.tags.length > 0) {
|
|
||||||
const foundTags = await Tag.find({ slug: { $in: body.tags } });
|
|
||||||
const foundSlugs = new Set(foundTags.map((t) => t.slug));
|
|
||||||
const invalid = body.tags.filter((s) => !foundSlugs.has(s));
|
|
||||||
if (invalid.length > 0) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
statusMessage: `Unknown tag slugs: ${invalid.join(", ")}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventData = {
|
const eventData = {
|
||||||
...body,
|
...body,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Event from '../../../models/event.js'
|
import Event from '../../../models/event.js'
|
||||||
import Tag from '../../../models/tag.js'
|
|
||||||
import { connectDB } from '../../../utils/mongoose.js'
|
import { connectDB } from '../../../utils/mongoose.js'
|
||||||
import { requireAdmin } from '../../../utils/auth.js'
|
import { requireAdmin } from '../../../utils/auth.js'
|
||||||
|
|
||||||
|
|
@ -12,18 +11,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
await connectDB()
|
await connectDB()
|
||||||
|
|
||||||
// Validate tag slugs against Tag collection
|
await validateTagSlugs(body.tags)
|
||||||
if (body.tags && body.tags.length > 0) {
|
|
||||||
const foundTags = await Tag.find({ slug: { $in: body.tags } })
|
|
||||||
const foundSlugs = new Set(foundTags.map(t => t.slug))
|
|
||||||
const invalid = body.tags.filter(s => !foundSlugs.has(s))
|
|
||||||
if (invalid.length > 0) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
statusMessage: `Unknown tag slugs: ${invalid.join(', ')}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
...body,
|
...body,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
await connectDB()
|
await connectDB()
|
||||||
|
|
||||||
|
await validateTagSlugs(body.tags)
|
||||||
|
|
||||||
const article = await WikiArticle.findByIdAndUpdate(
|
const article = await WikiArticle.findByIdAndUpdate(
|
||||||
id,
|
id,
|
||||||
{ tags: body.tags },
|
{ tags: body.tags },
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
await connectDB()
|
await connectDB()
|
||||||
|
|
||||||
|
await validateTagSlugs([...(body.addTags || []), ...(body.removeTags || [])])
|
||||||
|
|
||||||
const filter = body.articleIds
|
const filter = body.articleIds
|
||||||
? { _id: { $in: body.articleIds } }
|
? { _id: { $in: body.articleIds } }
|
||||||
: { collection: body.collection }
|
: { collection: body.collection }
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export default defineEventHandler(async (event) => {
|
||||||
filter.collection = collection
|
filter.collection = collection
|
||||||
}
|
}
|
||||||
if (search) {
|
if (search) {
|
||||||
filter.title = { $regex: search, $options: 'i' }
|
filter.title = { $regex: escapeRegex(search), $options: 'i' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const articles = await WikiArticle.find(filter)
|
const articles = await WikiArticle.find(filter)
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,7 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
// Escape regex metacharacters to prevent ReDoS
|
const escaped = escapeRegex(search);
|
||||||
const escaped = search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
||||||
andConditions.push({
|
andConditions.push({
|
||||||
$or: [
|
$or: [
|
||||||
{ name: { $regex: escaped, $options: "i" } },
|
{ name: { $regex: escaped, $options: "i" } },
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,6 @@ const eventSchema = new mongoose.Schema({
|
||||||
refundAmount: Number,
|
refundAmount: Number,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tags: [{ type: String }],
|
|
||||||
createdBy: { type: String, required: true },
|
createdBy: { type: String, required: true },
|
||||||
createdAt: { type: Date, default: Date.now },
|
createdAt: { type: Date, default: Date.now },
|
||||||
updatedAt: { type: Date, default: Date.now },
|
updatedAt: { type: Date, default: Date.now },
|
||||||
|
|
|
||||||
3
server/utils/escapeRegex.js
Normal file
3
server/utils/escapeRegex.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function escapeRegex(str) {
|
||||||
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
}
|
||||||
14
server/utils/validateTagSlugs.js
Normal file
14
server/utils/validateTagSlugs.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Tag from '../models/tag.js'
|
||||||
|
|
||||||
|
export async function validateTagSlugs(slugs) {
|
||||||
|
if (!slugs?.length) return
|
||||||
|
const foundTags = await Tag.find({ slug: { $in: slugs } })
|
||||||
|
const foundSlugs = new Set(foundTags.map(t => t.slug))
|
||||||
|
const invalid = slugs.filter(s => !foundSlugs.has(s))
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: `Unknown tag slugs: ${invalid.join(', ')}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ vi.mock('../../../server/utils/schemas.js', () => ({
|
||||||
|
|
||||||
import Tag from '../../../server/models/tag.js'
|
import Tag from '../../../server/models/tag.js'
|
||||||
import { validateBody } from '../../../server/utils/validateBody.js'
|
import { validateBody } from '../../../server/utils/validateBody.js'
|
||||||
|
import { validateTagSlugs } from '../../../server/utils/validateTagSlugs.js'
|
||||||
import createHandler from '../../../server/api/admin/events.post.js'
|
import createHandler from '../../../server/api/admin/events.post.js'
|
||||||
import updateHandler from '../../../server/api/admin/events/[id].put.js'
|
import updateHandler from '../../../server/api/admin/events/[id].put.js'
|
||||||
import { createMockEvent } from '../helpers/createMockEvent.js'
|
import { createMockEvent } from '../helpers/createMockEvent.js'
|
||||||
|
|
@ -51,8 +52,9 @@ const validEventBody = {
|
||||||
describe('Event tag validation', () => {
|
describe('Event tag validation', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
// Override the global validateBody stub (from setup.js) to use our mock
|
// Override global stubs (from setup.js) to use the real/mocked implementations
|
||||||
globalThis.validateBody = validateBody
|
globalThis.validateBody = validateBody
|
||||||
|
globalThis.validateTagSlugs = validateTagSlugs
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('create route (events.post)', () => {
|
describe('create route (events.post)', () => {
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,8 @@ vi.stubGlobal('requireAuth', vi.fn())
|
||||||
vi.stubGlobal('requireAdmin', vi.fn())
|
vi.stubGlobal('requireAdmin', vi.fn())
|
||||||
vi.stubGlobal('validateBody', vi.fn(async (event) => readBody(event)))
|
vi.stubGlobal('validateBody', vi.fn(async (event) => readBody(event)))
|
||||||
vi.stubGlobal('logActivity', vi.fn())
|
vi.stubGlobal('logActivity', vi.fn())
|
||||||
|
vi.stubGlobal('validateTagSlugs', vi.fn())
|
||||||
|
|
||||||
|
// Real server/utils that are safe to use as-is in tests
|
||||||
|
import { escapeRegex } from '../../server/utils/escapeRegex.js'
|
||||||
|
vi.stubGlobal('escapeRegex', escapeRegex)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue