From 78923279be97f6c167d63689d31738931e86da23 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Fri, 22 Mar 2024 09:24:19 +0000 Subject: [PATCH] Post-tags (#9) Reviewed-on: https://git.armoreddragon.com/ArmoredDragon/yet-another-blog/pulls/9 Co-authored-by: Armored Dragon Co-committed-by: Armored Dragon --- backend/core/core.js | 63 ++++++++++++++++++++------------ backend/core/internal_api.js | 12 ++++-- backend/form_validation.js | 14 +++++++ backend/page_scripts.js | 2 +- frontend/public/css/admin.css | 1 + frontend/public/css/admin.scss | 2 +- frontend/public/css/blogNew.css | 14 +++++++ frontend/public/css/blogNew.scss | 15 ++++++++ frontend/public/js/newBlog.js | 11 +++++- frontend/views/blogNew.ejs | 4 +- prisma/schema.prisma | 3 +- 11 files changed, 108 insertions(+), 33 deletions(-) diff --git a/backend/core/core.js b/backend/core/core.js index 09b2b4d..3094502 100644 --- a/backend/core/core.js +++ b/backend/core/core.js @@ -74,7 +74,7 @@ async function registerUser(username, password, options) { return { success: true, message: `Successfully created ${username}` }; } // Posts -async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, page = 0, tags = [], search_title = false, search_content = false, search }) { +async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, page = 0, search_title = false, search_content = false, search_tags = false, search }) { // If we have an ID, we want a single post if (id) { // Get the post by the id @@ -110,14 +110,22 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag ownerid: owner_id, }, ], - AND: [], + + AND: [ + { + OR: [ + + ] + } + ], }; // Build the "where_object" object - if (tags.length > 0) { + if (search){ + if (search_tags) where_object["AND"][0]["OR"].push({ tags: { hasSome: [search?.toLowerCase()] }}); + if (search_title) where_object["AND"][0]["OR"].push({ title: { contains: search, mode: "insensitive" } }); + if (search_content) where_object["AND"][0]["OR"].push({ content: { contains: search, mode: "insensitive" } }); } - if (search_title) where_object["AND"].push({ title: { contains: search, mode: "insensitive" } }); - if (search_content) where_object["AND"].push({ content: { contains: search, mode: "insensitive" } }); // Execute search const blog_posts = await prisma.blogPost.findMany({ @@ -171,6 +179,7 @@ async function postBlog(blog_post, owner_id) { content: blog_post.content, visibility: blog_post.visibility, publish_date: blog_post.publish_date, + tags: blog_post.tags, }; // Save to database @@ -203,13 +212,15 @@ async function postBlog(blog_post, owner_id) { } async function deleteBlog(blog_id, requester_id) { const user = await getUser({ id: requester_id }); - const post = await getPost({ id: blog_id }); + const post = await getBlog({ id: blog_id }); - let can_delete = post.owner.id === user.data.id || user.data.role === "ADMIN"; + if (!post.success) return { success: false, message: post.message || "Post does not exist" }; + + let can_delete = post.data.owner.id === user.data.id || user.data.role === "ADMIN"; if (can_delete) { - await prisma.blogPost.delete({ where: { id: post.id } }); - _deleteS3Directory(post.id, "blog"); + await prisma.blogPost.delete({ where: { id: post.data.id } }); + _deleteS3Directory(post.data.id, "blog"); return { success: true }; } @@ -217,27 +228,35 @@ async function deleteBlog(blog_id, requester_id) { } async function updateBlog(blog_post, requester_id) { const user = await getUser({ id: requester_id }); - const post = await getPost({ id: blog_post.id, raw: true }); + const post = await getBlog({ id: blog_post.id, raw: true }); + let publish_date = null; delete blog_post.id; - let can_update = post.owner.id === user.data.id || user.data.role === "ADMIN"; + if (!post.success) return { success: false, message: post.message || "Post not found" }; + + let can_update = post.data.owner.id === user.data.id || user.data.role === "ADMIN"; if (!can_update) return { success: false, message: "User not permitted" }; - const [year, month, day] = blog_post.date.split("-"); - const [hour, minute] = blog_post.time.split(":"); - let publish_date = new Date(year, month - 1, day, hour, minute); + // FIXME: Unsure if this actually works + // Check if we already have a formatted publish date + if (typeof blog_post.publish_date !== "object") { + const [year, month, day] = blog_post.date.split("-"); + const [hour, minute] = blog_post.time.split(":"); + publish_date = new Date(year, month - 1, day, hour, minute); + } let blog_post_formatted = { title: blog_post.title, description: blog_post.description, content: blog_post.content, visibility: blog_post.unlisted ? "UNLISTED" : "PUBLISHED", - publish_date: publish_date, + publish_date: publish_date || blog_post.publish_date, + tags: blog_post.tags, }; - await prisma.blogPost.update({ where: { id: post.id }, data: blog_post_formatted }); + await prisma.blogPost.update({ where: { id: post.data.id }, data: blog_post_formatted }); let uploaded_images = []; let uploaded_thumbnail = "DEFAULT"; @@ -253,24 +272,24 @@ async function updateBlog(blog_post, requester_id) { } let data_to_update = { - images: [...post.raw_images, ...uploaded_images], + images: [...post.data.raw_images, ...uploaded_images], }; if (blog_post.thumbnail) { const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(post.id, "blog", true, image_data, blog_post.thumbnail.id); + const name = await _uploadImage(post.data.id, "blog", true, image_data, blog_post.thumbnail.id); uploaded_thumbnail = name; data_to_update.thumbnail = uploaded_thumbnail; } - await prisma.blogPost.update({ where: { id: post.id }, data: data_to_update }); + await prisma.blogPost.update({ where: { id: post.data.id }, data: data_to_update }); return { success: true }; } async function deleteImage(image, requester_id) { const user = await getUser({ id: requester_id }); - const post = await getPost({ id: image.parent, raw: true }); + const post = await getBlog({ id: image.parent, raw: true }); // Check if post exists if (!post) return { success: false, message: "Post does not exist" }; @@ -391,9 +410,7 @@ function _format_blog_content(content, images) { const video = /{video:([^}]+)}/g; content = content.replace(video, (match, inner_content) => { - return `
`; + return `
`; }); content = content.replace(image_regex, (match, image_name) => { diff --git a/backend/core/internal_api.js b/backend/core/internal_api.js index 08729aa..38bb8fa 100644 --- a/backend/core/internal_api.js +++ b/backend/core/internal_api.js @@ -9,8 +9,7 @@ async function postRegister(req, res) { // User registration disabled? // We also check if the server was setup. If it was not set up, the server will proceed anyways. - if (!core.settings["ACCOUNT_REGISTRATION"] && core.settings["SETUP_COMPLETE"]) - return res.json({ success: false, message: "Account registrations are disabled" }); + if (!core.settings["ACCOUNT_REGISTRATION"] && core.settings["SETUP_COMPLETE"]) return res.json({ success: false, message: "Account registrations are disabled" }); // User data valid? if (!form_validation.success) return res.json({ success: false, message: form_validation.message }); @@ -70,8 +69,15 @@ async function deleteBlog(req, res) { return res.json(await core.deleteBlog(req.body.id, req.session.user.id)); } async function patchBlog(req, res) { + // FIXME: validate does not return post id + // Can user change post? + // User is admin, or user is author + + // Validate blog info + const valid = await validate.postBlog(req.body); + // TODO: Permissions for updating blog - return res.json(await core.updateBlog(req.body, req.session.user.id)); + return res.json(await core.updateBlog({ ...valid.data, id: req.body.id }, req.session.user.id)); } module.exports = { postRegister, postLogin, postSetting, deleteImage, postBlog, deleteBlog, patchBlog }; diff --git a/backend/form_validation.js b/backend/form_validation.js index c7ffe93..e65c501 100644 --- a/backend/form_validation.js +++ b/backend/form_validation.js @@ -25,6 +25,19 @@ async function postBlog(blog_object) { const [hour, minute] = blog_object.time.split(":"); let publish_date = new Date(year, month - 1, day, hour, minute); + // Go though our tags and ensure they are: + let valid_tag_array = []; + blog_object.tags.forEach((tag) => { + // Trimmed + tag = tag.trim(); + + // Lowercase + tag = tag.toLowerCase(); + + // Non-empty + if (tag.length !== 0) valid_tag_array.push(tag); + }); + // Format our data to save let blog_post_formatted = { title: blog_object.title, @@ -32,6 +45,7 @@ async function postBlog(blog_object) { content: blog_object.content, visibility: blog_object.visibility, publish_date: publish_date, + tags: valid_tag_array, images: blog_object.images, thumbnail: blog_object.thumbnail, }; diff --git a/backend/page_scripts.js b/backend/page_scripts.js index 86c31ff..2e03907 100644 --- a/backend/page_scripts.js +++ b/backend/page_scripts.js @@ -27,7 +27,7 @@ async function author(req, res) { res.render("author.ejs", { ...getDefaults(req), blog_post: profile.data }); } async function blogList(req, res) { - const blog_list = await core.getBlog({ owner_id: req.session.user?.id, page: req.query.page || 0, search: req.query.search, search_title: true }); + const blog_list = await core.getBlog({ owner_id: req.session.user?.id, page: req.query.page || 0, search: req.query.search, search_tags: true, search_title: true }); res.render("blogList.ejs", { ...getDefaults(req), blog_list: blog_list.data, diff --git a/frontend/public/css/admin.css b/frontend/public/css/admin.css index 83d1ee1..8d2be6a 100644 --- a/frontend/public/css/admin.css +++ b/frontend/public/css/admin.css @@ -221,6 +221,7 @@ a.bad { .container .setting-row .setting-toggleable input { padding: 0; margin: auto 0 auto auto; + height: 25px; width: 100px; box-sizing: border-box; text-align: center; diff --git a/frontend/public/css/admin.scss b/frontend/public/css/admin.scss index a2369b9..92eb208 100644 --- a/frontend/public/css/admin.scss +++ b/frontend/public/css/admin.scss @@ -66,7 +66,7 @@ input { padding: 0; margin: auto 0 auto auto; - // width: 100%; + height: 25px; width: 100px; box-sizing: border-box; text-align: center; diff --git a/frontend/public/css/blogNew.css b/frontend/public/css/blogNew.css index f1e8322..706206c 100644 --- a/frontend/public/css/blogNew.css +++ b/frontend/public/css/blogNew.css @@ -130,6 +130,20 @@ outline: 0; } +.e-tags { + min-height: 40px; + width: 100%; + background-color: #222; + display: flex; + flex-direction: row; + padding: 5px; + box-sizing: border-box; + margin-bottom: 1rem; +} +.e-tags input { + width: 100%; +} + .e-settings { min-height: 40px; width: 100%; diff --git a/frontend/public/css/blogNew.scss b/frontend/public/css/blogNew.scss index 6724bfe..7b3d0a9 100644 --- a/frontend/public/css/blogNew.scss +++ b/frontend/public/css/blogNew.scss @@ -145,6 +145,21 @@ $background-body: #222; } } +.e-tags { + min-height: 40px; + width: 100%; + background-color: $background-body; + display: flex; + flex-direction: row; + padding: 5px; + box-sizing: border-box; + margin-bottom: 1rem; + + input { + width: 100%; + } +} + .e-settings { min-height: 40px; width: 100%; diff --git a/frontend/public/js/newBlog.js b/frontend/public/js/newBlog.js index f25b54b..0a1245a 100644 --- a/frontend/public/js/newBlog.js +++ b/frontend/public/js/newBlog.js @@ -88,10 +88,18 @@ async function publishBlog(unlisted, edit) { description: qs("#description").value, content: qs("#content").value, visibility: unlisted ? "UNLISTED" : "PUBLISHED", + tags: [], date: qs("#date").value, time: qs("#time").value, }; + // Get our tags, trim them, then shove them into an array + const tags_value = qs("#tags").value || ""; + if (tags_value.length) { + let tags_array = qs("#tags").value.split(","); + tags_array.forEach((tag) => form_data.tags.push(tag.trim())); + } + // If we have a thumbnail, read the thumbnail image and store it if (pending_thumbnail.data_blob) { form_data.thumbnail = { ...pending_thumbnail, data_blob: await _readFile(pending_thumbnail.data_blob) }; @@ -161,8 +169,7 @@ function customDragString() { } function updateImages() { - const image_div = (img_id, img_url) => - ``; + const image_div = (img_id, img_url) => ``; // Clear existing listings qsa(".e-image-area .image").forEach((entry) => entry.remove()); diff --git a/frontend/views/blogNew.ejs b/frontend/views/blogNew.ejs index 8270f29..a446a0d 100644 --- a/frontend/views/blogNew.ejs +++ b/frontend/views/blogNew.ejs @@ -67,7 +67,9 @@ - +
+ +
Publish On
diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e81ec2b..55420fd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,8 +36,7 @@ model BlogPost { ownerid String? // Tags - organization_tag String? - keyword_tags String[] + tags String[] // Dates publish_date DateTime?