From 384d3197ce02c05ac745babfa7756b06d8a3bd36 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Thu, 21 May 2026 17:51:01 +0100 Subject: [PATCH] feat(admin-tags): POST /api/admin/tags to create craft/cooperative tags Admin-only route that validates a label + pool via zod, slugifies the label, returns the existing tag if the slug already exists, otherwise creates a new active Tag document. --- server/api/admin/tags/index.post.js | 40 +++++++++++++++++++++++++++++ server/utils/schemas.js | 5 ++++ 2 files changed, 45 insertions(+) create mode 100644 server/api/admin/tags/index.post.js diff --git a/server/api/admin/tags/index.post.js b/server/api/admin/tags/index.post.js new file mode 100644 index 0000000..70e0624 --- /dev/null +++ b/server/api/admin/tags/index.post.js @@ -0,0 +1,40 @@ +import Tag from '../../../models/tag.js' +import { adminTagCreateSchema } from '../../../utils/schemas.js' + +const slugify = (s) => + s + .toLowerCase() + .trim() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, '') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + +export default defineEventHandler(async (event) => { + await requireAdmin(event) + await connectDB() + + const body = await validateBody(event, adminTagCreateSchema) + const slug = slugify(body.label) + + if (!slug) { + throw createError({ + statusCode: 400, + statusMessage: 'Tag label must contain at least one alphanumeric character' + }) + } + + const existing = await Tag.findOne({ slug }) + if (existing) { + return { tag: { slug: existing.slug, label: existing.label, pool: existing.pool } } + } + + const tag = await Tag.create({ + slug, + label: body.label, + pool: body.pool, + active: true + }) + + return { tag: { slug: tag.slug, label: tag.label, pool: tag.pool } } +}) diff --git a/server/utils/schemas.js b/server/utils/schemas.js index 8e7416b..322bbd4 100644 --- a/server/utils/schemas.js +++ b/server/utils/schemas.js @@ -383,6 +383,11 @@ export const tagSuggestionSchema = z.object({ pool: z.enum(['craft', 'cooperative']) }) +export const adminTagCreateSchema = z.object({ + label: z.string().trim().min(1).max(100), + pool: z.enum(['craft', 'cooperative']) +}) + // --- Board post / channel schemas --- export const boardPostCreateSchema = z.object({