From 37e582ac1d87e1ced9192a62f4adb6703403eefa Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Thu, 2 May 2024 14:22:11 +0000 Subject: [PATCH] Sanity Checks (#3) * New user registration check. Signed-off-by: Armored Dragon * Post updating permission check. Moved validation action from internal_api to core. Updated form validation to delete unneeded data. Signed-off-by: Armored Dragon * Permission check for author editing. Fixed manifest.json. Signed-off-by: Armored Dragon * Moved checks from core to form_validation. Signed-off-by: Armored Dragon --------- Signed-off-by: Armored Dragon --- backend/core/core.js | 63 +++++++-------- backend/core/internal_api.js | 13 +--- backend/form_validation.js | 86 +++++++++++++-------- backend/permissions.js | 27 ++++++- frontend/views/themes/default/manifest.json | 1 + 5 files changed, 112 insertions(+), 78 deletions(-) diff --git a/backend/core/core.js b/backend/core/core.js index 0ac804e..0c35ea1 100644 --- a/backend/core/core.js +++ b/backend/core/core.js @@ -5,6 +5,8 @@ const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListO const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); let s3; const crypto = require("crypto"); +const validate = require("../form_validation"); +const permissions = require("../permissions"); const md = require("markdown-it")() .use(require("markdown-it-underline")) .use(require("markdown-it-footnote")) @@ -59,22 +61,23 @@ function _initS3Storage() { // Users async function newUser({ username, password, role } = {}) { - if (!username) return _r(false, "Username not specified"); - if (!password) return _r(false, "Password not specified"); + // Sanity check on user registration. + const valid = validate.newUser({ username: username, password: password }); + if (!valid.success) return _r(false, valid.message); // Create the account try { user_database_entry = await prisma.user.create({ data: { username: username, password: password, role: role } }); } catch (e) { let message = "Unknown error"; - return { success: false, message: message }; + return _r(false, message); } // Create the profile page and link try { user_profile_database_entry = await prisma.profilePage.create({ data: { owner: { connect: { id: user_database_entry.id } } } }); } catch (e) { - return { success: false, message: `Error creating profile page for user ${username}` }; + return _r(false, `Error creating profile page for user ${username}`); } // Master user was created; server initialized @@ -95,6 +98,7 @@ async function getUser({ user_id, username, include_password = false }) { return { success: true, data: user }; } +// TODO: Rename patchUser async function editUser({ requester_id, user_id, user_content }) { let user = await getUser({ user_id: user_id }); if (!user.success) return _r(false, "User not found"); @@ -226,29 +230,24 @@ async function getPost({ requester_id, post_id, visibility = "PUBLISHED" } = {}, return pageList.slice(0, 5); } } +// TODO: Rename patchPost async function editPost({ requester_id, post_id, post_content }) { let user = await getUser({ user_id: requester_id }); let post = await getPost({ post_id: post_id }); - let publish_date = null; - if (!user.success) return _r(false, post.message || "User not found"); - user = user.data; - if (!post.success) return _r(false, post.message || "Post not found"); - post = post.data; + // Validate the post content + let validated_post = validate.patchPost(post_content, user, post); + if (!validated_post.success) return _r(false, validated_post.message); - // Check to see if the requester can update the post - // TODO: Permissions - let can_update = post.owner.id === user.id || user.role === "ADMIN"; + user = validated_post.data.user; + post = validated_post.data.post; + validated_post = validated_post.data.post_formatted; - // FIXME: Unsure if this actually works - // Check if we already have a formatted publish date - if (typeof post.publish_date !== "object") { - const [year, month, day] = post.date.split("-"); - const [hour, minute] = post.time.split(":"); - publish_date = new Date(year, month - 1, day, hour, minute); - } + // Check if the user can preform the action + const can_act = permissions.patchPost(post, user); + if (!can_act.success) return _r(false, can_act.message); - // Handle tags ---- + // Handle tags ---------- let database_tag_list = []; const existing_tags = post.tags?.map((tag) => ({ name: tag })) || []; @@ -264,12 +263,10 @@ async function editPost({ requester_id, post_id, post_content }) { // Rebuild the post to save let post_formatted = { - title: post_content.title, - description: post_content.description, - content: post_content.content, - visibility: post_content.visibility || "PRIVATE", - publish_date: publish_date || post_content.publish_date, + ...validated_post, + // Handle tag changes tags: { disconnect: [...existing_tags], connect: [...database_tag_list] }, + // Handle media changes media: [...post.raw_media, ...post_content.media], }; @@ -327,18 +324,22 @@ async function getBiography({ requester_id, author_id }) { return { success: true, data: post }; } +// TODO: Rename to patchBiography async function updateBiography({ requester_id, author_id, biography_content }) { let user = await getUser({ user_id: requester_id }); let biography = await getBiography({ author_id: author_id }); - if (!user.success) return _r(false, user.message || "Author not found"); - user = user.data; + // Validate post ---------- + let formatted_biography = validate.patchBiography(biography_content, user, biography); + if (!formatted_biography.success) return _r(false, formatted_biography.message); - if (!biography.success) return _r(false, biography.message || "Post not found"); - biography = biography.data; + user = formatted_biography.data.user; + biography = formatted_biography.data.biography; + biography_content = formatted_biography.data.biography_content; - let can_update = biography.owner.id === user.id || user.role === "ADMIN"; - if (!can_update) return _r(false, "User not permitted"); + // Permission check ---------- + const can_act = permissions.patchBiography(biography_content, user, biography); + if (!can_act.success) return _r(false, "User not permitted"); let formatted = { content: biography_content.content, diff --git a/backend/core/internal_api.js b/backend/core/internal_api.js index 8bc5354..51ee1f2 100644 --- a/backend/core/internal_api.js +++ b/backend/core/internal_api.js @@ -57,18 +57,7 @@ async function deleteBlog(req, res) { return res.json(await core.deletePost({ post_id: req.body.id, requester_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 - let valid = await validate.postBlog(req.body); - - if (!valid.success) return { success: false, message: valid.message || "Post failed validation" }; - valid = valid.data; - - // TODO: Permissions for updating blog - return res.json(await core.editPost({ requester_id: req.session.user.id, post_id: req.body.id, post_content: valid })); + return res.json(await core.editPost({ requester_id: req.session.user.id, post_id: req.body.id, post_content: req.body })); } async function patchBiography(request, response) { // TODO: Validate diff --git a/backend/form_validation.js b/backend/form_validation.js index de46b88..3e5f38c 100644 --- a/backend/form_validation.js +++ b/backend/form_validation.js @@ -1,33 +1,39 @@ +// +// Form validation +// +// Preform sanity checks on content +// Format given data in an accessible way +// + const core = require("./core/core"); -async function registerUser(username, password) { - if (!username) return { success: false, message: "No username provided" }; - if (!password) return { success: false, message: "No password provided" }; - if (password.length < core.settings["USER_MINIMUM_PASSWORD_LENGTH"]) return { success: false, message: "Password not long enough" }; +// Make sure the user registration data is safe and valid. +function newUser({ username, password } = {}) { + if (!username) return _r(false, "No username provided"); + if (!password) return _r(false, "No password provided"); + if (password.length < core.settings["USER_MINIMUM_PASSWORD_LENGTH"]) return _r(false, `Password is not long enough. Minimum length is ${core.settings["USER_MINIMUM_PASSWORD_LENGTH"]}`); - // Check if username only uses URL safe characters - if (!_isUrlSafe(username)) return { success: false, message: "Username is not URL safe" }; + // TODO: Method to block special characters + if (!_isUrlSafe(username)) return _r(false, "Invalid Username. Please only use a-z A-Z 0-9"); - // All good! Validation complete - return { success: true }; + return _r(true); } -async function postBlog(blog_object) { - // TODO: Validate blog posts before upload - // Check title length - // Check description length - // Check content length - // Check valid date - // Return formatted object +function patchPost(post_content, user, post) { + let post_formatted = {}; // The formatted post content object that will be returned upon success + let publish_date; // Time and date the post should be made public + let tags = []; // An array of tags for the post + + if (!user.success) return _r(false, "User not found"); + if (!post.success) return _r(false, "Post not found"); // Get the publish date in a standard format - const [year, month, day] = blog_object.date.split("-"); - const [hour, minute] = blog_object.time.split(":"); - let publish_date = new Date(year, month - 1, day, hour, minute); + const [year, month, day] = post_content.date.split("-"); + const [hour, minute] = post_content.time.split(":"); + 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) => { + // Go though tags list, and turn into a pretty array + post_content.tags.forEach((tag) => { // Trimmed tag = tag.trim(); @@ -35,27 +41,41 @@ async function postBlog(blog_object) { tag = tag.toLowerCase(); // Non-empty - if (tag.length !== 0) valid_tag_array.push(tag); + if (tag.length !== 0) tags.push(tag); }); - // Format our data to save - let blog_post_formatted = { - title: blog_object.title, - description: blog_object.description, - content: blog_object.content, - visibility: blog_object.visibility, + delete post_content.date; + delete post_content.time; + + // Format the post content + post_formatted = { + // Autofill the given data + ...post_content, + + // Update tags to our pretty array + tags: tags, + + // Update date publish_date: publish_date, - tags: valid_tag_array, - media: blog_object.media, - thumbnail: blog_object.thumbnail, }; - return { success: true, data: blog_post_formatted }; + return _r(true, null, { post_formatted: post_formatted, user: user.data, post: post.data }); } +function patchBiography(biography_content, user, biography) { + if (!user.success) return _r(false, "User not found"); + if (!biography.success) return _r(false, "Post not found"); + + return _r(true, null, { biography_content: biography_content, user: user.data, biography: biography.data }); +} + +// Helper functions -------------------- function _isUrlSafe(str) { const pattern = /^[A-Za-z0-9\-_.~]+$/; return pattern.test(str); } +function _r(s, m, d) { + return { success: s, message: m ? m || "Unknown error" : undefined, data: d }; +} -module.exports = { registerUser, postBlog }; +module.exports = { newUser, patchPost, patchBiography }; diff --git a/backend/permissions.js b/backend/permissions.js index b8117a0..cbb57f8 100644 --- a/backend/permissions.js +++ b/backend/permissions.js @@ -1,3 +1,26 @@ -function postBlog(user) {} +// +// Permissions +// +// Check if a given user has permissions to preform an action +// -module.exports = { postBlog }; +// Updating a blog post +function patchPost(post_content, user) { + // Admins can always update any post + if (user.role === "ADMIN") return _r(true); + + // User owns the post + if (post_content.owner.id === user.id) return _r(true); + + // User is not permitted + return _r(false, "User is not permitted to preform action."); +} +function patchBiography(biography, user) { + // Biographies are just fancy posts right now. + return patchPost(biography, user); +} + +function _r(s, m, d) { + return { success: s, message: m ? m || "Unknown error" : undefined, data: d }; +} +module.exports = { patchPost, patchBiography }; diff --git a/frontend/views/themes/default/manifest.json b/frontend/views/themes/default/manifest.json index 0d22884..6dab71f 100644 --- a/frontend/views/themes/default/manifest.json +++ b/frontend/views/themes/default/manifest.json @@ -7,6 +7,7 @@ "login": "/ejs/login.ejs", "register": "/ejs/register.ejs", "author": "/ejs/author.ejs", + "authorEdit": "/ejs/authorEdit.ejs", "post": "/ejs/post.ejs", "postSearch": "/ejs/postSearch.ejs", "postNew": "/ejs/postNew.ejs",