Compare commits

..

No commits in common. "0dd6a7fa00dd0835481008ca8f2d010730295e1f" and "3203fea2f89179d4e68384449878da60070e8332" have entirely different histories.

8 changed files with 108 additions and 165 deletions

View File

@ -5,8 +5,6 @@ const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListO
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
let s3; let s3;
const crypto = require("crypto"); const crypto = require("crypto");
const validate = require("../form_validation");
const permissions = require("../permissions");
const md = require("markdown-it")() const md = require("markdown-it")()
.use(require("markdown-it-underline")) .use(require("markdown-it-underline"))
.use(require("markdown-it-footnote")) .use(require("markdown-it-footnote"))
@ -61,23 +59,22 @@ function _initS3Storage() {
// Users // Users
async function newUser({ username, password, role } = {}) { async function newUser({ username, password, role } = {}) {
// Sanity check on user registration. if (!username) return _r(false, "Username not specified");
const valid = validate.newUser({ username: username, password: password }); if (!password) return _r(false, "Password not specified");
if (!valid.success) return _r(false, valid.message);
// Create the account // Create the account
try { try {
user_database_entry = await prisma.user.create({ data: { username: username, password: password, role: role } }); user_database_entry = await prisma.user.create({ data: { username: username, password: password, role: role } });
} catch (e) { } catch (e) {
let message = "Unknown error"; let message = "Unknown error";
return _r(false, message); return { success: false, message: message };
} }
// Create the profile page and link // Create the profile page and link
try { try {
user_profile_database_entry = await prisma.profilePage.create({ data: { owner: { connect: { id: user_database_entry.id } } } }); user_profile_database_entry = await prisma.profilePage.create({ data: { owner: { connect: { id: user_database_entry.id } } } });
} catch (e) { } catch (e) {
return _r(false, `Error creating profile page for user ${username}`); return { success: false, message: `Error creating profile page for user ${username}` };
} }
// Master user was created; server initialized // Master user was created; server initialized
@ -98,7 +95,6 @@ async function getUser({ user_id, username, include_password = false }) {
return { success: true, data: user }; return { success: true, data: user };
} }
// TODO: Rename patchUser
async function editUser({ requester_id, user_id, user_content }) { async function editUser({ requester_id, user_id, user_content }) {
let user = await getUser({ user_id: user_id }); let user = await getUser({ user_id: user_id });
if (!user.success) return _r(false, "User not found"); if (!user.success) return _r(false, "User not found");
@ -140,6 +136,28 @@ async function newPost({ requester_id }) {
return post.id; return post.id;
} }
async function getPost({ requester_id, post_id, visibility = "PUBLISHED" } = {}, { search, search_title, search_content, search_tags } = {}, { limit = 10, page = 0, pagination = true } = {}) { async function getPost({ requester_id, post_id, visibility = "PUBLISHED" } = {}, { search, search_title, search_content, search_tags } = {}, { limit = 10, page = 0, pagination = true } = {}) {
// Get a single post
if (post_id) {
let post;
post = await prisma.post.findUnique({ where: { id: post_id }, include: { owner: true, tags: true } });
if (!post) return _r(false, "Post does not exist");
post = _stripPrivatePost(post);
// Tags
let post_tags = [];
post.raw_tags = [];
post.tags.forEach((tag) => {
post_tags.push(tag.name);
post.raw_tags.push();
});
post.tags = post_tags;
// Render post
return { success: true, data: await _renderPost(post) };
}
// Otherwise build WHERE_OBJECT using data we do have
let post_list = [];
let where_object = { let where_object = {
OR: [ OR: [
// Standard discovery: Public, and after the publish date // Standard discovery: Public, and after the publish date
@ -168,46 +186,6 @@ async function getPost({ requester_id, post_id, visibility = "PUBLISHED" } = {},
}, },
], ],
}; };
// Admins can view everything at any point
let user;
if (requester_id) {
user = await getUser({ user_id: requester_id });
if (user.success) user = user.data;
if (user.role === "ADMIN") where_object["OR"].push({ NOT: { id: "" } });
}
// Get a single post
if (post_id) {
let post;
// We can view unlisted posts, but we don't want them to show up otherwise in search.
// Inject a "unlisted" inclusion into where_object to allow direct viewing of unlisted posts
where_object["OR"].push({ visibility: "UNLISTED" });
// Allow getting drafts if the requesting user owns the draft
where_object["OR"].push({ AND: [{ visibility: "DRAFT" }, { ownerid: requester_id }] });
post = await prisma.post.findUnique({ where: { ...where_object, id: post_id }, include: { owner: true, tags: true } });
if (!post) return _r(false, "Post does not exist");
post = _stripPrivatePost(post);
// Tags
let post_tags = [];
post.raw_tags = [];
post.tags.forEach((tag) => {
post_tags.push(tag.name);
post.raw_tags.push();
});
post.tags = post_tags;
// Render post
return { success: true, data: await _renderPost(post) };
}
// Otherwise build WHERE_OBJECT using data we do have
let post_list = [];
// Build the "where_object" object // Build the "where_object" object
if (search) { if (search) {
if (search_tags) where_object["AND"][0]["OR"].push({ tags: { some: { name: search?.toLowerCase() } } }); if (search_tags) where_object["AND"][0]["OR"].push({ tags: { some: { name: search?.toLowerCase() } } });
@ -248,24 +226,29 @@ async function getPost({ requester_id, post_id, visibility = "PUBLISHED" } = {},
return pageList.slice(0, 5); return pageList.slice(0, 5);
} }
} }
// TODO: Rename patchPost
async function editPost({ requester_id, post_id, post_content }) { async function editPost({ requester_id, post_id, post_content }) {
let user = await getUser({ user_id: requester_id }); let user = await getUser({ user_id: requester_id });
let post = await getPost({ post_id: post_id }); let post = await getPost({ post_id: post_id });
let publish_date = null;
// Validate the post content if (!user.success) return _r(false, post.message || "User not found");
let validated_post = validate.patchPost(post_content, user, post); user = user.data;
if (!validated_post.success) return _r(false, validated_post.message); if (!post.success) return _r(false, post.message || "Post not found");
post = post.data;
user = validated_post.data.user; // Check to see if the requester can update the post
post = validated_post.data.post; // TODO: Permissions
validated_post = validated_post.data.post_formatted; let can_update = post.owner.id === user.id || user.role === "ADMIN";
// Check if the user can preform the action // FIXME: Unsure if this actually works
const can_act = permissions.patchPost(post, user); // Check if we already have a formatted publish date
if (!can_act.success) return _r(false, can_act.message); 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);
}
// Handle tags ---------- // Handle tags ----
let database_tag_list = []; let database_tag_list = [];
const existing_tags = post.tags?.map((tag) => ({ name: tag })) || []; const existing_tags = post.tags?.map((tag) => ({ name: tag })) || [];
@ -281,10 +264,12 @@ async function editPost({ requester_id, post_id, post_content }) {
// Rebuild the post to save // Rebuild the post to save
let post_formatted = { let post_formatted = {
...validated_post, title: post_content.title,
// Handle tag changes description: post_content.description,
content: post_content.content,
visibility: post_content.visibility || "PRIVATE",
publish_date: publish_date || post_content.publish_date,
tags: { disconnect: [...existing_tags], connect: [...database_tag_list] }, tags: { disconnect: [...existing_tags], connect: [...database_tag_list] },
// Handle media changes
media: [...post.raw_media, ...post_content.media], media: [...post.raw_media, ...post_content.media],
}; };
@ -342,22 +327,18 @@ async function getBiography({ requester_id, author_id }) {
return { success: true, data: post }; return { success: true, data: post };
} }
// TODO: Rename to patchBiography
async function updateBiography({ requester_id, author_id, biography_content }) { async function updateBiography({ requester_id, author_id, biography_content }) {
let user = await getUser({ user_id: requester_id }); let user = await getUser({ user_id: requester_id });
let biography = await getBiography({ author_id: author_id }); let biography = await getBiography({ author_id: author_id });
// Validate post ---------- if (!user.success) return _r(false, user.message || "Author not found");
let formatted_biography = validate.patchBiography(biography_content, user, biography); user = user.data;
if (!formatted_biography.success) return _r(false, formatted_biography.message);
user = formatted_biography.data.user; if (!biography.success) return _r(false, biography.message || "Post not found");
biography = formatted_biography.data.biography; biography = biography.data;
biography_content = formatted_biography.data.biography_content;
// Permission check ---------- let can_update = biography.owner.id === user.id || user.role === "ADMIN";
const can_act = permissions.patchBiography(biography_content, user, biography); if (!can_update) return _r(false, "User not permitted");
if (!can_act.success) return _r(false, "User not permitted");
let formatted = { let formatted = {
content: biography_content.content, content: biography_content.content,
@ -617,7 +598,7 @@ async function postSetting(key, value) {
async function editSetting({ name, value }) { async function editSetting({ name, value }) {
if (!Object.keys(settings).includes(name)) return _r(false, "Setting is not valid"); if (!Object.keys(settings).includes(name)) return _r(false, "Setting is not valid");
await prisma.setting.upsert({ where: { id: name }, update: { value: value }, create: { id: name, value: value } }); await prisma.setting.upsert({ where: { id: key }, update: { value: value }, create: { id: key, value: value } });
try { try {
settings[key] = JSON.parse(value); settings[key] = JSON.parse(value);
} catch { } catch {

View File

@ -23,7 +23,7 @@ async function getFeed({ type = "rss" }) {
let feed = getBaseFeed(); let feed = getBaseFeed();
// Get posts // Get posts
let posts = await core.getPost(undefined, undefined, { limit: 20 }); let posts = await core.getPost(null, null, { limit: 20 });
// For each post, add a formatted object to the feed // For each post, add a formatted object to the feed
posts.data.forEach((post) => { posts.data.forEach((post) => {
@ -45,6 +45,7 @@ async function getFeed({ type = "rss" }) {
feed.addItem(formatted); feed.addItem(formatted);
}); });
// if (type === "rss") return feed.rss2();
if (type === "atom") return feed.atom1(); if (type === "atom") return feed.atom1();
if (type === "json") return feed.json1(); if (type === "json") return feed.json1();
} }

View File

@ -5,7 +5,7 @@ const validate = require("../form_validation");
async function postRegister(req, res) { async function postRegister(req, res) {
const { username, password } = req.body; // Get the username and password from the request body const { username, password } = req.body; // Get the username and password from the request body
const form_validation = await validate.newUser({ username: username, password: password }); // Check form for errors const form_validation = await validate.registerUser(username, password); // Check form for errors
// User registration disabled? // User registration disabled?
// We also check if the server was setup. If it was not set up, the server will proceed anyways. // We also check if the server was setup. If it was not set up, the server will proceed anyways.
@ -57,7 +57,18 @@ async function deleteBlog(req, res) {
return res.json(await core.deletePost({ post_id: req.body.id, requester_id: req.session.user.id })); return res.json(await core.deletePost({ post_id: req.body.id, requester_id: req.session.user.id }));
} }
async function patchBlog(req, res) { async function patchBlog(req, res) {
return res.json(await core.editPost({ requester_id: req.session.user.id, post_id: req.body.id, post_content: req.body })); // 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 }));
} }
async function patchBiography(request, response) { async function patchBiography(request, response) {
// TODO: Validate // TODO: Validate

View File

@ -1,39 +1,33 @@
// const core = require("./core/core");
// Form validation
//
// Preform sanity checks on content
// Format given data in an accessible way
//
// Make sure the user registration data is safe and valid. async function registerUser(username, password) {
function newUser({ username, password } = {}) { if (!username) return { success: false, message: "No username provided" };
const core = require("./core/core"); // HACK: Need to require the core module here because the settings don't get set otherwise. 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" };
if (!username) return _r(false, "No username provided"); // Check if username only uses URL safe characters
if (!password) return _r(false, "No password provided"); if (!_isUrlSafe(username)) return { success: false, message: "Username is not URL safe" };
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"]}`);
// TODO: Method to block special characters // All good! Validation complete
if (!_isUrlSafe(username)) return _r(false, "Invalid Username. Please only use a-z A-Z 0-9"); return { success: true };
return _r(true);
} }
function patchPost(post_content, user, post) { async function postBlog(blog_object) {
let post_formatted = {}; // The formatted post content object that will be returned upon success // TODO: Validate blog posts before upload
let publish_date; // Time and date the post should be made public // Check title length
let tags = []; // An array of tags for the post // Check description length
// Check content length
if (!user.success) return _r(false, "User not found"); // Check valid date
if (!post.success) return _r(false, "Post not found"); // Return formatted object
// Get the publish date in a standard format // Get the publish date in a standard format
const [year, month, day] = post_content.date.split("-"); const [year, month, day] = blog_object.date.split("-");
const [hour, minute] = post_content.time.split(":"); const [hour, minute] = blog_object.time.split(":");
publish_date = new Date(year, month - 1, day, hour, minute); let publish_date = new Date(year, month - 1, day, hour, minute);
// Go though tags list, and turn into a pretty array // Go though our tags and ensure they are:
post_content.tags.forEach((tag) => { let valid_tag_array = [];
blog_object.tags.forEach((tag) => {
// Trimmed // Trimmed
tag = tag.trim(); tag = tag.trim();
@ -41,41 +35,27 @@ function patchPost(post_content, user, post) {
tag = tag.toLowerCase(); tag = tag.toLowerCase();
// Non-empty // Non-empty
if (tag.length !== 0) tags.push(tag); if (tag.length !== 0) valid_tag_array.push(tag);
}); });
delete post_content.date; // Format our data to save
delete post_content.time; let blog_post_formatted = {
title: blog_object.title,
// Format the post content description: blog_object.description,
post_formatted = { content: blog_object.content,
// Autofill the given data visibility: blog_object.visibility,
...post_content,
// Update tags to our pretty array
tags: tags,
// Update date
publish_date: publish_date, publish_date: publish_date,
tags: valid_tag_array,
media: blog_object.media,
thumbnail: blog_object.thumbnail,
}; };
return _r(true, null, { post_formatted: post_formatted, user: user.data, post: post.data }); return { success: true, data: blog_post_formatted };
} }
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) { function _isUrlSafe(str) {
const pattern = /^[A-Za-z0-9\-_.~]+$/; const pattern = /^[A-Za-z0-9\-_.~]+$/;
return pattern.test(str); return pattern.test(str);
} }
function _r(s, m, d) {
return { success: s, message: m ? m || "Unknown error" : undefined, data: d };
}
module.exports = { newUser, patchPost, patchBiography }; module.exports = { registerUser, postBlog };

View File

@ -75,7 +75,7 @@ async function blogList(req, res) {
}); });
} }
async function blogSingle(req, res) { async function blogSingle(req, res) {
const blog = await core.getPost({ requester_id: req.session.user?.id, post_id: req.params.blog_id }); const blog = await core.getPost({ post_id: req.params.blog_id });
if (blog.success === false) return res.redirect("/"); if (blog.success === false) return res.redirect("/");
res.render(_getThemePage("post"), { ...(await getDefaults(req)), blog_post: blog.data }); res.render(_getThemePage("post"), { ...(await getDefaults(req)), blog_post: blog.data });
} }

View File

@ -1,32 +1,3 @@
// function postBlog(user) {}
// Permissions
//
// Check if a given user has permissions to preform an action
//
// Updating a blog post module.exports = { postBlog };
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) {
// Admins can always update any post
if (user.role === "ADMIN") return _r(true);
// User edits their own account
if (biography.id === user.id) return _r(true);
// User is not permitted
return _r(false, "User is not permitted to preform action.");
}
function _r(s, m, d) {
return { success: s, message: m ? m || "Unknown error" : undefined, data: d };
}
module.exports = { patchPost, patchBiography };

View File

@ -1,8 +1,8 @@
<div class="footer"> <div class="footer">
<div class="page-center"> <div class="page-center">
<div class="resources"> <div class="resources">
<a class="atom-feed icon" href="/atom"><span>ATOM Feed</span> </a> <a class="atom-feed icon" href="#"><span>ATOM Feed</span> </a>
<a class="json icon" href="/json"><span>JSON Feed</span></a> <a class="json icon" href="#"><span>JSON Feed</span></a>
</div> </div>
<div class="info"> <div class="info">
<a class="json icon" href="https://github.com/armored-dragon/yet-another-blog">Built using Yet-Another-Blog</a> <a class="json icon" href="https://github.com/armored-dragon/yet-another-blog">Built using Yet-Another-Blog</a>

View File

@ -7,7 +7,6 @@
"login": "/ejs/login.ejs", "login": "/ejs/login.ejs",
"register": "/ejs/register.ejs", "register": "/ejs/register.ejs",
"author": "/ejs/author.ejs", "author": "/ejs/author.ejs",
"authorEdit": "/ejs/authorEdit.ejs",
"post": "/ejs/post.ejs", "post": "/ejs/post.ejs",
"postSearch": "/ejs/postSearch.ejs", "postSearch": "/ejs/postSearch.ejs",
"postNew": "/ejs/postNew.ejs", "postNew": "/ejs/postNew.ejs",