diff --git a/backend/core/core.js b/backend/core/core.js
index f4a84a0..8c46a9a 100644
--- a/backend/core/core.js
+++ b/backend/core/core.js
@@ -57,23 +57,20 @@ function _initS3Storage() {
}
}
-async function registerUser(username, password, options) {
- let user_database_entry;
- let user_profile_database_entry;
+// Users
+async function newUser({ username, password, role } = {}) {
+ if (!username) return _r(false, "Username not specified");
+ if (!password) return _r(false, "Password not specified");
- // Create the entry in the database
+ // Create the account
try {
- user_database_entry = await prisma.user.create({ data: { username: username, password: password, ...options } });
+ user_database_entry = await prisma.user.create({ data: { username: username, password: password, role: role } });
} catch (e) {
- let message;
-
- if (e.code === "P2002") message = "Username already exists";
- else message = "Unknown error";
-
+ let message = "Unknown error";
return { success: false, message: message };
}
- // Create a user profile page
+ // 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) {
@@ -81,27 +78,68 @@ async function registerUser(username, password, options) {
}
// Master user was created; server initialized
- postSetting("SETUP_COMPLETE", true);
-
- // User has been successfully created
- return { success: true, message: `Successfully created ${username}` };
+ editSetting({ name: "SETUP_COMPLETE", value: true });
}
+async function getUser({ user_id, username }) {
+ if (!username && !user_id) return _r(false, "Either a user_id or username is needed.");
+
+ let user;
+
+ if (user_id) user = await prisma.user.findUnique({ where: { id: user_id } });
+ else if (username) user = await prisma.user.findUnique({ where: { username: username } });
+
+ if (!user) return _r(false, "No matching user");
+ else return { success: true, data: user };
+}
+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");
+ user = user.data;
+
+ // TODO:
+ // If there was a role change, see if the acting user can make these changes
+
+ // TODO:
+ // If there was a password change,
+ // check to see if the user can make these changes
+ // Hash the password
+
+ // FIXME: Not secure. ASAP!
+ let formatted = {};
+ formatted[user_content.setting_name] = user_content.value;
+
+ await prisma.user.update({ where: { id: user.id }, data: formatted });
+ return _r(true);
+}
+async function deleteUser({ user_id }) {
+ if (!user_id) return _r(false, "User_id not specified.");
+
+ await prisma.user.delete({ where: { id: user_id } }); // TODO: Test
+ return _r(true, `User ${user_id} deleted`);
+}
+
// Posts
-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
- let post = await prisma.blogPost.findUnique({ where: { id: id }, include: { owner: true } });
- if (!post) return { success: false, message: "Post does not exist" };
+async function newPost({ requester_id }) {
+ // const user = await getUser({ id: requester_id });
+ const post = await prisma.post.create({ data: { owner: { connect: { id: requester_id } } } });
- // Render the post
- const rendered_post = await _renderPost(post, true);
-
- // Return the post with valid image urls
- return { data: rendered_post, success: true };
+ // TODO: Validate request (Does user have perms?)
+ // TODO: Does server allow new posts?
+ 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 } = {}) {
+ // Get a single post
+ if (post_id) {
+ let post;
+ post = await prisma.post.findUnique({ where: { id: post_id }, include: { owner: true } });
+ if (!post) return _r(false, "Post does not exist");
+ post = _stripPrivatePost(post);
+ // Render post
+ return { success: true, data: await _renderPost(post) };
}
+
// Otherwise build WHERE_OBJECT using data we do have
- let rendered_post_list = [];
+ let post_list = [];
let where_object = {
OR: [
// Standard discovery: Public, and after the publish date
@@ -120,7 +158,7 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag
// User owns the post
{
- ownerid: owner_id,
+ ownerid: requester_id,
},
],
@@ -130,16 +168,14 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag
},
],
};
-
// Build the "where_object" object
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" } });
}
-
// Execute search
- const blog_posts = await prisma.blogPost.findMany({
+ let posts = await prisma.post.findMany({
where: where_object,
take: limit,
skip: Math.max(page, 0) * limit,
@@ -147,59 +183,116 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag
orderBy: [{ publish_date: "desc" }, { created_date: "desc" }],
});
- // Render each of the posts in the list
- for (post of blog_posts) {
- rendered_post_list.push(await _renderPost(post, true));
+ for (post of posts) {
+ post = _stripPrivatePost(post);
+ post = await _renderPost(post);
+ post_list.push(post);
}
+
// Calculate pagination
- let pagination = await prisma.blogPost.count({
+ let post_count = await prisma.post.count({
where: where_object,
});
- return { data: rendered_post_list, pagination: _getNavigationList(page, Math.ceil(pagination / limit)), success: true };
+
+ return { data: post_list, pagination: _getNavigationList(page, Math.ceil(post_count / limit)), success: true };
+
+ function _getNavigationList(current_page, max_page) {
+ current_page = Number(current_page);
+ max_page = Number(max_page);
+
+ const pageList = [current_page - 2, current_page - 1, current_page, current_page + 1, current_page + 2].filter((num) => num >= 0 && num < max_page);
+ return pageList.slice(0, 5);
+ }
}
-async function getAuthorPage({ author_id }) {
- // Get the post by the id
+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;
- let post = await prisma.profilePage.findUnique({ where: { ownerid: author_id }, include: { owner: true } });
- if (!post) return { success: false, message: "Post does not exist" };
+ 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;
- // Render the post
- const rendered_post = await _renderPost(post, true);
+ // Check to see if the requester can update the post
+ // TODO: Permissions
+ let can_update = post.owner.id === user.id || user.role === "ADMIN";
- // Return the post with valid image urls
- return { data: rendered_post, success: true };
-}
-async function getUser({ id, username } = {}) {
- let user;
- if (id) user = await prisma.user.findUnique({ where: { id: id } });
- else if (username) user = await prisma.user.findUnique({ where: { username: username } });
+ // 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);
+ }
- if (!user) return { success: false, message: "No matching user" };
- else return { success: true, data: user };
-}
-async function postBlog(blog_post, owner_id) {
- const user = await getUser({ id: owner_id });
- // Check if user has permissions to upload a blog post
-
- if (user.data.role !== "ADMIN" && user.data.role !== "AUTHOR") return { success: false, message: "User is not permitted" };
-
- // Create object without image data to store in the database
- let blog_post_formatted = {
- title: blog_post.title,
- description: blog_post.description,
- content: blog_post.content,
- visibility: blog_post.visibility,
- publish_date: blog_post.publish_date,
- tags: blog_post.tags,
+ // 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,
+ tags: post_content.tags,
+ media: [...post.raw_media, ...post_content.media],
};
- // Save to database
- const database_blog = await prisma.blogPost.create({ data: { ...blog_post_formatted, owner: { connect: { id: owner_id } } } });
+ // Save the updated post to the database
+ await prisma.post.update({ where: { id: post.id }, data: post_formatted });
- // Update the blog post to include references to our images
- await prisma.blogPost.update({ where: { id: database_blog.id }, data: { images: uploaded_images, thumbnail: uploaded_thumbnail } });
- return { success: true, blog_id: database_blog.id };
+ return _r(true);
}
+async function deletePost({ requester_id, post_id }) {}
+// User Profiles
+async function getBiography({ requester_id, author_id }) {
+ if (!author_id) return _r(false, "No Author specified.");
+ let post = await prisma.profilePage.findFirst({ where: { ownerid: author_id }, include: { owner: true } });
+
+ // Check if it is private
+ // TODO
+
+ // HACK:
+ // When we render the post and reading from S3, we want the post id
+ // The problem is when a user views the biography page, the page shows the account id opposed to the "profile page" id.
+ // This causes a incorrect parent_id value and an incorrect key.
+ // Replace the "id" to the value it's expecting.
+ const original_post_id = post.id;
+ let rendering_formatted_post = {};
+
+ rendering_formatted_post = post;
+ rendering_formatted_post.id = author_id;
+
+ // Render
+ post = _stripPrivatePost(post);
+ post = await _renderPost(rendering_formatted_post);
+
+ post.id = original_post_id;
+
+ return { success: true, data: post };
+}
+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;
+
+ if (!biography.success) return _r(false, biography.message || "Post not found");
+ biography = biography.data;
+
+ let can_update = biography.owner.id === user.id || user.role === "ADMIN";
+ if (!can_update) return _r(false, "User not permitted");
+
+ let formatted = {
+ content: biography_content.content,
+ media: [...biography.raw_media, ...biography_content.media],
+ };
+
+ await prisma.profilePage.update({ where: { id: biography.id }, data: formatted });
+
+ return _r(true);
+}
+// TODO: Replace
async function deleteBlog(blog_id, requester_id) {
const user = await getUser({ id: requester_id });
const post = await getBlog({ id: blog_id });
@@ -209,85 +302,30 @@ async function deleteBlog(blog_id, requester_id) {
let can_delete = post.data.owner.id === user.data.id || user.data.role === "ADMIN";
if (can_delete) {
- await prisma.blogPost.delete({ where: { id: post.data.id } });
+ await prisma.post.delete({ where: { id: post.data.id } });
_deleteS3Directory(post.data.id, "blog");
return { success: true };
}
return { success: false, message: "Action not permitted" };
}
-async function updateBlog(blog_post, requester_id) {
- const user = await getUser({ id: requester_id });
- const post = await getBlog({ id: blog_post.id, raw: true });
- let publish_date = null;
-
- delete blog_post.id;
-
- 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" };
-
- // 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 || blog_post.publish_date,
- tags: blog_post.tags,
- images: [...post.data.raw_images, ...blog_post.images],
- };
-
- await prisma.blogPost.update({ where: { id: post.data.id }, data: blog_post_formatted });
-
- return { success: true };
-}
-async function deleteImage(image, requester_id) {
- const user = await getUser({ id: requester_id });
- const post = await getBlog({ id: image.parent, raw: true });
-
- // Check if post exists
- if (!post) return { success: false, message: "Post does not exist" };
-
- // Check for permissions
- if (post.owner.id !== user.data.id || user.data.role !== "ADMIN") return { success: false, message: "User is not permitted" };
-
- let image_index = post.raw_images.indexOf(image.id);
-
- post.raw_images.splice(image_index, 1);
-
- await prisma.blogPost.update({ where: { id: post.id }, data: { images: post.raw_images } });
-
- const request_params = {
- Bucket: process.env.S3_BUCKET_NAME,
- Key: `${process.env.ENVIRONMENT}/${image.parent_type}/${image.parent}/${image.id}.webp`,
- };
-
- const command = new DeleteObjectCommand(request_params);
- await s3.send(command);
-
- return { success: true };
-}
-async function postImage(post_id, buffer) {
+async function uploadMedia({ parent_id, file_buffer, file_extension }) {
if (!use_s3_storage) return null;
- let size = { width: 1920, height: 1080 };
- const image_name = crypto.randomUUID();
+ const content_name = crypto.randomUUID();
+ let maximum_image_resolution = { width: 1920, height: 1080 };
- const compressed_image = await sharp(Buffer.from(buffer.split(",")[1], "base64"), { animated: true })
- .resize({ ...size, withoutEnlargement: true, fit: "inside" })
+ // const image_extensions = ["png", "webp", "jpg", "jpeg"];
+ // const video_extensions = ["mp4", "webm", "mkv", "avi"];
+
+ // Images
+ const compressed_image = await sharp(Buffer.from(file_buffer.split(",")[1], "base64"), { animated: true })
+ .resize({ ...maximum_image_resolution, withoutEnlargement: true, fit: "inside" })
.webp({ quality: 90, animated: true })
.toBuffer();
+
const params = {
Bucket: process.env.S3_BUCKET_NAME,
- Key: `${process.env.ENVIRONMENT}/posts/${post_id}/${image_name}.webp`,
+ Key: `${process.env.ENVIRONMENT}/posts/${parent_id}/${content_name}.webp`,
Body: compressed_image,
ContentType: "image/webp",
};
@@ -295,20 +333,45 @@ async function postImage(post_id, buffer) {
const command = new PutObjectCommand(params);
await s3.send(command);
- return image_name;
+ return content_name;
}
-async function _getImage(parent_id, parent_type, name) {
+async function getMedia({ parent_id, file_name }) {
if (!use_s3_storage) return null;
- let params;
- // Default image
- if (name === "DEFAULT") params = { Bucket: process.env.S3_BUCKET_NAME, Key: `defaults/thumbnail.webp` };
- // Named image
- else params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${name}.webp` };
-
+ const params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/posts/${parent_id}/${file_name}.webp` };
return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 });
}
+// TODO:
+// Will be done automatically in the background.
+// Unreferenced images and media will be deleted
+async function deleteMedia({ parent_id, file_name }) {}
+
+// async function deleteImage(image, requester_id) {
+// const user = await getUser({ id: requester_id });
+// const post = await getBlog({ id: image.parent, raw: true });
+
+// // Check if post exists
+// if (!post) return { success: false, message: "Post does not exist" };
+
+// // Check for permissions
+// if (post.owner.id !== user.data.id || user.data.role !== "ADMIN") return { success: false, message: "User is not permitted" };
+
+// let image_index = post.raw_images.indexOf(image.id);
+
+// post.raw_images.splice(image_index, 1);
+
+// await prisma.post.update({ where: { id: post.id }, data: { images: post.raw_images } });
+
+// const request_params = {
+// Bucket: process.env.S3_BUCKET_NAME,
+// Key: `${process.env.ENVIRONMENT}/${image.parent_type}/${image.parent}/${image.id}.webp`,
+// };
+
+// const command = new DeleteObjectCommand(request_params);
+// await s3.send(command);
+
+// return { success: true };
+// }
async function _deleteS3Directory(id, type) {
- // logger.verbose(`Deleting entire S3 image directory`);
// Erase database images from S3 server
const folder_params = { Bucket: process.env.S3_BUCKET_NAME, Prefix: `${process.env.ENVIRONMENT}/${type}/${id}` };
@@ -335,89 +398,82 @@ async function _deleteS3Directory(id, type) {
// If there are more objects to delete (truncated result), recursively call the function again
// if (listed_objects.IsTruncated) await emptyS3Directory(bucket, dir);
}
-async function _renderPost(blog_post, raw) {
- if (raw) {
- // Had to do this, only God knows why.
- blog_post.raw_images = [];
- if (blog_post.images) blog_post.images.forEach((image) => blog_post.raw_images.push(image));
- blog_post.raw_thumbnail = blog_post.thumbnail;
- blog_post.raw_content = blog_post.content;
- }
- if (blog_post.images) {
- // Get the image urls for the post
- for (i = 0; blog_post.images.length > i; i++) {
- blog_post.images[i] = await _getImage(blog_post.id, "posts", blog_post.images[i]);
+async function _renderPost(post) {
+ post.raw_media = [];
+ post.raw_content = post.content;
+
+ // For some reason Node does not like to set a variable and leave it.
+ post.media.forEach((media) => post.raw_media.push(media));
+
+ if (post.media) {
+ for (i = 0; post.media.length > i; i++) {
+ post.media[i] = await getMedia({ parent_id: post.id, file_name: post.media[i] });
}
}
- if (blog_post.content) {
+ if (post.content) {
// Render the markdown contents of the post
- blog_post.content = md.render(blog_post.content);
+ post.content = md.render(post.content);
// Replace custom formatting with what we want
- blog_post.content = _format_blog_content(blog_post.content, blog_post.images);
+ post.content = _formatBlogContent(post.content, post.media);
}
+ return post;
- return blog_post;
-}
-function _format_blog_content(content, images) {
- // Replace Images
- const image_regex = /{image:([^}]+)}/g;
+ function _formatBlogContent(content, media_list) {
+ // Replace Images
+ const image_regex = /{image:([^}]+)}/g;
- // Replace Side-by-side
- const side_by_side = /{sidebyside}(.*?){\/sidebyside}/gs;
+ // Replace Side-by-side
+ const side_by_side = /{sidebyside}(.*?){\/sidebyside}/gs;
- // Replace video links
- const video = /{video:([^}]+)}/g;
+ // Replace video links
+ const video = /{video:([^}]+)}/g;
- content = content.replace(video, (match, inner_content) => {
- return `
`;
- });
+ content = content.replace(video, (match, inner_content) => {
+ return ``;
+ });
- content = content.replace(image_regex, (match, image_name) => {
- for (image of images) {
- if (image.includes(image_name)) {
- return ``;
+ // Replace Images
+ content = content.replace(image_regex, (match, image_name) => {
+ for (media of media_list) {
+ if (media.includes(image_name)) {
+ return ``;
+ }
+ }
+
+ // Unknown image (Image was probably deleted)
+ return "";
+ });
+
+ content = content.replace(side_by_side, (match, inner_content) => {
+ return `${inner_content}
`;
+ });
+
+ // Finished formatting, return!
+ return content;
+
+ function _getVideoEmbed(video_url) {
+ // YouTube
+ if (video_url.includes("youtu.be")) {
+ return `https://youtube.com/embed/${video_url.split("/")[3]}`;
+ }
+ if (video_url.includes("youtube")) {
+ let video_id = video_url.split("/")[3];
+ video_id = video_id.split("watch?v=").pop();
+ return `https://youtube.com/embed/${video_id}`;
+ }
+
+ // Odysee
+ if (video_url.includes("://odysee.com")) {
+ let video_link = `https://${video_url.split("/")[2]}/$/embed/${video_url.split("/")[3]}/${video_url.split("/")[4]}`;
+ return video_link;
}
}
-
- // Unknown image (Image was probably deleted)
- return "";
- });
-
- content = content.replace(side_by_side, (match, inner_content) => {
- return `${inner_content}
`;
- });
-
- // Finished formatting, return!
- return content;
-
- function _getVideoEmbed(video_url) {
- // YouTube
- if (video_url.includes("youtu.be")) {
- return `https://youtube.com/embed/${video_url.split("/")[3]}`;
- }
- if (video_url.includes("youtube")) {
- let video_id = video_url.split("/")[3];
- video_id = video_id.split("watch?v=").pop();
- return `https://youtube.com/embed/${video_id}`;
- }
-
- // Odysee
- if (video_url.includes("://odysee.com")) {
- let video_link = `https://${video_url.split("/")[2]}/$/embed/${video_url.split("/")[3]}/${video_url.split("/")[4]}`;
- return video_link;
- }
}
}
-function _getNavigationList(current_page, max_page) {
- current_page = Number(current_page);
- max_page = Number(max_page);
- const pageList = [current_page - 2, current_page - 1, current_page, current_page + 1, current_page + 2].filter((num) => num >= 0 && num < max_page);
- return pageList.slice(0, 5);
-}
async function _getSettings() {
// Go though each object key in our settings to get the value if it exists
Object.keys(settings).forEach(async (key) => {
@@ -435,13 +491,7 @@ async function _getSettings() {
return (settings[key] = value);
});
}
-// Create a new empty "post".
-// Used so uploaded images know where to go
-async function newPost(owner_id) {
- const post = await prisma.blogPost.create({ data: { owner: { connect: { id: owner_id } } } });
- return post.id;
-}
-
+// TODO: Replace
async function getSetting(key, { parse = true }) {
if (!settings[key]) return null;
@@ -450,6 +500,7 @@ async function getSetting(key, { parse = true }) {
}
return settings[key];
}
+// TODO: Replace
async function postSetting(key, value) {
try {
if (!Object.keys(settings).includes(key)) return { success: false, message: "Setting not valid" };
@@ -466,5 +517,27 @@ async function postSetting(key, value) {
return { success: false, message: e.message };
}
}
+// TODO: Replace
+async function editSetting({ name, value }) {
+ if (!Object.keys(settings).includes(name)) return _r(false, "Setting is not valid");
-module.exports = { settings, newPost, registerUser, getUser, getAuthorPage, postBlog, updateBlog, getBlog, deleteBlog, postImage, deleteImage, postSetting, getSetting };
+ await prisma.setting.upsert({ where: { id: key }, update: { value: value }, create: { id: key, value: value } });
+ try {
+ settings[key] = JSON.parse(value);
+ } catch {
+ settings[key] = value;
+ }
+
+ return _r(true);
+}
+
+function _stripPrivatePost(post) {
+ if (!post) return;
+ if (post.owner) delete post.owner.password;
+ return post;
+}
+const _r = (s, m) => {
+ return { success: s, message: m };
+};
+
+module.exports = { settings, newUser, getUser, editUser, getPost, newPost, editPost, getBiography, updateBiography, uploadMedia, deleteBlog, postSetting, getSetting };
diff --git a/backend/core/external_api.js b/backend/core/external_api.js
index 83f61c2..986c085 100644
--- a/backend/core/external_api.js
+++ b/backend/core/external_api.js
@@ -23,7 +23,7 @@ async function getFeed({ type = "rss" }) {
let feed = getBaseFeed();
// Get posts
- let posts = await core.getBlog({ limit: 20 }); // internal.getBlogList({}, { limit: 20 });
+ let posts = await core.getPost(null, null, { limit: 20 });
// For each post, add a formatted object to the feed
posts.data.forEach((post) => {
diff --git a/backend/core/internal_api.js b/backend/core/internal_api.js
index 597991e..5ecc1d9 100644
--- a/backend/core/internal_api.js
+++ b/backend/core/internal_api.js
@@ -18,7 +18,7 @@ async function postRegister(req, res) {
const role = core.settings["SETUP_COMPLETE"] ? undefined : "ADMIN";
const hashed_password = await bcrypt.hash(password, 10); // Hash the password for security :^)
- res.json(await core.registerUser(username, hashed_password, { role: role }));
+ res.json(await core.newUser({ username: username, password: hashed_password, role: role }));
}
async function postLogin(req, res) {
const { username, password } = req.body; // Get the username and password from the request body
@@ -46,7 +46,7 @@ async function postSetting(request, response) {
async function postImage(request, response) {
// TODO: Permissions for uploading images
// TODO: Verification for image uploading
- return response.json(await core.postImage(request.body.post_id, request.body.buffer));
+ return response.json(await core.uploadMedia({ parent_id: request.body.post_id, file_buffer: request.body.buffer }));
}
async function deleteImage(req, res) {
// TODO: Permissions for deleting image
@@ -78,10 +78,19 @@ async function patchBlog(req, res) {
// User is admin, or user is author
// Validate blog info
- const valid = await validate.postBlog(req.body);
+ 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.updateBlog({ ...valid.data, id: req.body.id }, req.session.user.id));
+ 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) {
+ // TODO: Validate
+ return response.json(await core.updateBiography({ requester_id: request.session.user.id, author_id: request.body.id, biography_content: request.body }));
+}
+async function patchUser(request, response) {
+ return response.json(await core.editUser({ requester_id: request.session.user.id, user_id: request.body.id, user_content: request.body }));
}
-module.exports = { postRegister, postLogin, postSetting, postImage, deleteImage, postBlog, deleteBlog, patchBlog };
+module.exports = { postRegister, patchBiography, postLogin, postSetting, postImage, deleteImage, postBlog, deleteBlog, patchBlog, patchUser };
diff --git a/backend/form_validation.js b/backend/form_validation.js
index e65c501..de46b88 100644
--- a/backend/form_validation.js
+++ b/backend/form_validation.js
@@ -46,7 +46,7 @@ async function postBlog(blog_object) {
visibility: blog_object.visibility,
publish_date: publish_date,
tags: valid_tag_array,
- images: blog_object.images,
+ media: blog_object.media,
thumbnail: blog_object.thumbnail,
};
diff --git a/backend/page_scripts.js b/backend/page_scripts.js
index 43068af..c0d84b8 100644
--- a/backend/page_scripts.js
+++ b/backend/page_scripts.js
@@ -13,7 +13,14 @@ async function index(request, response) {
// const is_setup_complete = core.settings["SETUP_COMPLETE"];
// if (!is_setup_complete) return response.redirect("/register");
- const blog_list = await core.getBlog({ owner_id: request.session.user?.id, page: request.query.page || 0 });
+ const blog_list = await core.getPost({ requester_id: request.session.user?.id, page: request.query.page || 0 });
+
+ blog_list.data.forEach((post) => {
+ let published_date_parts = new Date(post.publish_date).toLocaleDateString().split("/");
+ const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`;
+ post.publish_date = formatted_date;
+ });
+
response.render(getThemePage("index"), {
...getDefaults(request),
blog_list: blog_list.data,
@@ -29,15 +36,30 @@ function login(request, response) {
response.render(getThemePage("login"), getDefaults(request));
}
async function author(req, res) {
- const user = await core.getUser({ id: req.params.author_id });
+ const user = await core.getUser({ user_id: req.params.author_id });
// FIXME: Bandage fix for author get error
if (!user.success) return res.redirect("/");
- const profile = await core.getAuthorPage({ author_id: user.data.id });
- console.log(profile.data);
- res.render(getThemePage("author"), { ...getDefaults(req), post: profile.data });
+ const profile = await core.getBiography({ author_id: user.data.id });
+ // TODO: Check for success
+ // const posts = await core.getBlog({ owner_id: user.data.id, raw: true });
+ const posts = await core.getPost({ requester_id: user.data.id });
+
+ res.render(getThemePage("author"), { ...getDefaults(req), post: { ...profile.data, post_count: posts.data.length } });
+}
+async function authorEdit(request, response) {
+ let author = await core.getBiography({ author_id: request.params.author_id });
+ if (!author.success) return response.redirect("/");
+ response.render(getThemePage("authorEdit"), { ...getDefaults(request), profile: author.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_tags: true, search_title: true });
+ const blog_list = await core.getPost({ requester_id: req.session.user?.id }, { search: req.query.search, search_title: true });
+
+ blog_list.data.forEach((post) => {
+ let published_date_parts = new Date(post.publish_date).toLocaleDateString().split("/");
+ const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`;
+ post.publish_date = formatted_date;
+ });
+
res.render(getThemePage("postSearch"), {
...getDefaults(req),
blog_list: blog_list.data,
@@ -47,16 +69,16 @@ async function blogList(req, res) {
});
}
async function blogSingle(req, res) {
- const blog = await core.getBlog({ id: req.params.blog_id });
+ const blog = await core.getPost({ post_id: req.params.blog_id });
if (blog.success === false) return res.redirect("/");
res.render(getThemePage("post"), { ...getDefaults(req), blog_post: blog.data });
}
async function blogNew(request, response) {
- const new_post = await core.newPost(request.session.user.id);
+ const new_post = await core.newPost({ requester_id: request.session.user.id });
return response.redirect(`/post/${new_post}/edit`);
}
async function blogEdit(req, res) {
- let existing_blog = await core.getBlog({ id: req.params.blog_id, raw: true });
+ let existing_blog = await core.getPost({ post_id: req.params.blog_id });
if (existing_blog.success) existing_blog = existing_blog.data; // FIXME: Quickfix for .success/.data issue
let published_time_parts = new Date(existing_blog.publish_date).toLocaleTimeString([], { timeStyle: "short" }).slice(0, 4).split(":");
@@ -94,4 +116,5 @@ module.exports = {
admin,
atom,
jsonFeed,
+ authorEdit,
};
diff --git a/frontend/views/themes/default/css/author.css b/frontend/views/themes/default/css/author.css
index 6307c3d..dafda4b 100644
--- a/frontend/views/themes/default/css/author.css
+++ b/frontend/views/themes/default/css/author.css
@@ -2,7 +2,14 @@
display: flex;
flex-direction: row;
min-height: 50px;
- margin-top: 4rem;
+ background-color: white;
+ box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px;
+ margin-top: 2rem;
+ padding: 1rem;
+ box-sizing: border-box;
+ width: 1080px;
+ max-width: 1080px;
+ border-radius: 5px;
}
.page .page-center .biography {
width: 66.6666666667%;
@@ -12,14 +19,22 @@
padding: 1rem;
box-sizing: border-box;
}
+.page .page-center .biography .image-container {
+ max-width: 100%;
+}
+.page .page-center .biography .image-container img {
+ max-width: 100%;
+}
.page .page-center .about {
width: 33.3333333333%;
background-color: white;
min-height: 50px;
- margin: 0 1rem;
+ margin: 0 0 0 1rem;
padding: 1rem;
box-sizing: border-box;
box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px;
+ height: -moz-fit-content;
+ height: fit-content;
}
.page .page-center .about .profile-picture {
margin: auto auto 1.5rem auto;
@@ -49,4 +64,10 @@
text-decoration: none;
color: black;
margin: auto;
+}
+
+.page .nobackground {
+ background-color: transparent;
+ box-shadow: none;
+ padding: 0;
}
\ No newline at end of file
diff --git a/frontend/views/themes/default/css/author.scss b/frontend/views/themes/default/css/author.scss
index 0264b3d..a66b16f 100644
--- a/frontend/views/themes/default/css/author.scss
+++ b/frontend/views/themes/default/css/author.scss
@@ -2,7 +2,14 @@
display: flex;
flex-direction: row;
min-height: 50px;
- margin-top: 4rem;
+ background-color: white;
+ box-shadow: #0000001c 0 0px 5px;
+ margin-top: 2rem;
+ padding: 1rem;
+ box-sizing: border-box;
+ width: 1080px;
+ max-width: 1080px;
+ border-radius: 5px;
.biography {
width: calc(100% * (2 / 3));
@@ -11,15 +18,24 @@
box-shadow: #0000001c 0 0px 5px;
padding: 1rem;
box-sizing: border-box;
+
+ .image-container {
+ max-width: 100%;
+
+ img {
+ max-width: 100%;
+ }
+ }
}
.about {
width: calc(100% * (1 / 3));
background-color: white;
min-height: 50px;
- margin: 0 1rem;
+ margin: 0 0 0 1rem;
padding: 1rem;
box-sizing: border-box;
box-shadow: #0000001c 0 0px 5px;
+ height: fit-content;
.profile-picture {
margin: auto auto 1.5rem auto;
@@ -57,3 +73,9 @@
}
}
}
+
+.page .nobackground {
+ background-color: transparent;
+ box-shadow: none;
+ padding: 0;
+}
diff --git a/frontend/views/themes/default/css/generic.css b/frontend/views/themes/default/css/generic.css
index c478a5a..f62a0bc 100644
--- a/frontend/views/themes/default/css/generic.css
+++ b/frontend/views/themes/default/css/generic.css
@@ -74,6 +74,9 @@ body {
min-width: 130px;
border: transparent;
transition: filter ease-in-out 0.1s;
+ padding: 0.3rem;
+ box-sizing: border-box;
+ text-decoration: none;
}
.button:hover {
@@ -148,6 +151,58 @@ body {
margin-bottom: 1rem;
}
+.rich-text-editor .controls {
+ width: 100%;
+ min-height: 10px;
+ background-color: #dbd8d8;
+ display: flex;
+ flex-direction: row;
+}
+.rich-text-editor .controls a {
+ box-sizing: border-box;
+ margin: 0.1rem;
+ cursor: pointer;
+ background-color: #dbd8d8;
+ display: flex;
+ flex-direction: row;
+ width: 2rem;
+ height: 2rem;
+}
+.rich-text-editor .controls a span {
+ margin: auto;
+}
+.rich-text-editor .controls a:hover,
+.rich-text-editor .controls a:focus {
+ filter: brightness(80%);
+}
+.rich-text-editor .controls .left {
+ margin: 0 auto 0 0;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+}
+.rich-text-editor .controls .right {
+ margin: 0 0 0 auto;
+ display: flex;
+ flex-direction: row;
+}
+.rich-text-editor .controls .right a {
+ padding: 2px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+}
+.rich-text-editor .controls .right a img {
+ height: 20px;
+ margin: auto;
+}
+.rich-text-editor textarea {
+ border-radius: 0 0 5px 5px;
+ min-width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
@media screen and (max-width: 1280px) {
.page-center {
width: 95%;
diff --git a/frontend/views/themes/default/css/generic.scss b/frontend/views/themes/default/css/generic.scss
index 2430c7a..e7949a7 100644
--- a/frontend/views/themes/default/css/generic.scss
+++ b/frontend/views/themes/default/css/generic.scss
@@ -81,6 +81,9 @@ body {
min-width: 130px;
border: transparent;
transition: filter ease-in-out 0.1s;
+ padding: 0.3rem;
+ box-sizing: border-box;
+ text-decoration: none;
}
.button:hover {
cursor: pointer;
@@ -153,6 +156,68 @@ body {
margin-bottom: 1rem;
}
+.rich-text-editor {
+ .controls {
+ width: 100%;
+ min-height: 10px;
+ background-color: #dbd8d8;
+ display: flex;
+ flex-direction: row;
+
+ a {
+ box-sizing: border-box;
+ margin: 0.1rem;
+ cursor: pointer;
+ background-color: #dbd8d8;
+ display: flex;
+ flex-direction: row;
+ width: 2rem;
+ height: 2rem;
+
+ span {
+ margin: auto;
+ }
+ }
+
+ a:hover,
+ a:focus {
+ filter: brightness(80%);
+ }
+
+ .left {
+ margin: 0 auto 0 0;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ }
+
+ .right {
+ margin: 0 0 0 auto;
+ display: flex;
+ flex-direction: row;
+
+ a {
+ padding: 2px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+
+ img {
+ height: 20px;
+ margin: auto;
+ }
+ }
+ }
+ }
+
+ textarea {
+ border-radius: 0 0 5px 5px;
+ min-width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+ }
+}
+
@media screen and (max-width: 1280px) {
.page-center {
width: 95%;
diff --git a/frontend/views/themes/default/css/newPost.css b/frontend/views/themes/default/css/newPost.css
index 23c673f..40e8cf4 100644
--- a/frontend/views/themes/default/css/newPost.css
+++ b/frontend/views/themes/default/css/newPost.css
@@ -47,53 +47,4 @@
padding: 0.5rem;
box-sizing: border-box;
margin: auto;
-}
-
-.rich-text-editor .controls {
- width: 100%;
- min-height: 10px;
- background-color: #dbd8d8;
- display: flex;
- flex-direction: row;
-}
-.rich-text-editor .controls a {
- box-sizing: border-box;
- margin: 0.1rem;
- cursor: pointer;
- background-color: #dbd8d8;
- display: flex;
- flex-direction: row;
- width: 2rem;
- height: 2rem;
-}
-.rich-text-editor .controls a span {
- margin: auto;
-}
-.rich-text-editor .controls a:hover,
-.rich-text-editor .controls a:focus {
- filter: brightness(80%);
-}
-.rich-text-editor .controls .left {
- margin: 0 auto 0 0;
- height: 100%;
- display: flex;
- flex-direction: row;
-}
-.rich-text-editor .controls .right {
- margin: 0 0 0 auto;
- display: flex;
- flex-direction: row;
-}
-.rich-text-editor .controls .right a {
- padding: 2px;
- box-sizing: border-box;
- display: flex;
- flex-direction: row;
-}
-.rich-text-editor .controls .right a img {
- height: 20px;
- margin: auto;
-}
-.rich-text-editor textarea {
- border-radius: 0 0 5px 5px;
}
\ No newline at end of file
diff --git a/frontend/views/themes/default/css/newPost.scss b/frontend/views/themes/default/css/newPost.scss
index 683a54c..542b2f9 100644
--- a/frontend/views/themes/default/css/newPost.scss
+++ b/frontend/views/themes/default/css/newPost.scss
@@ -58,62 +58,3 @@
}
}
}
-
-.rich-text-editor {
- .controls {
- width: 100%;
- min-height: 10px;
- background-color: #dbd8d8;
- display: flex;
- flex-direction: row;
-
- a {
- box-sizing: border-box;
- margin: 0.1rem;
- cursor: pointer;
- background-color: #dbd8d8;
- display: flex;
- flex-direction: row;
- width: 2rem;
- height: 2rem;
-
- span {
- margin: auto;
- }
- }
-
- a:hover,
- a:focus {
- filter: brightness(80%);
- }
-
- .left {
- margin: 0 auto 0 0;
- height: 100%;
- display: flex;
- flex-direction: row;
- }
-
- .right {
- margin: 0 0 0 auto;
- display: flex;
- flex-direction: row;
-
- a {
- padding: 2px;
- box-sizing: border-box;
- display: flex;
- flex-direction: row;
-
- img {
- height: 20px;
- margin: auto;
- }
- }
- }
- }
-
- textarea {
- border-radius: 0 0 5px 5px;
- }
-}
diff --git a/frontend/views/themes/default/css/settings.css b/frontend/views/themes/default/css/settings.css
index 0bd99b9..315aecc 100644
--- a/frontend/views/themes/default/css/settings.css
+++ b/frontend/views/themes/default/css/settings.css
@@ -42,6 +42,10 @@
text-align: right;
font-size: 1rem;
}
+.page .page-center .setting-list .setting.fit-column {
+ flex-direction: column;
+ width: 100%;
+}
.page .page-center .setting-list .setting:nth-child(even) {
background-color: rgb(250, 250, 250);
}
diff --git a/frontend/views/themes/default/css/settings.scss b/frontend/views/themes/default/css/settings.scss
index 0f09d3c..0b90ee5 100644
--- a/frontend/views/themes/default/css/settings.scss
+++ b/frontend/views/themes/default/css/settings.scss
@@ -47,6 +47,11 @@
}
}
+ .setting.fit-column {
+ flex-direction: column;
+ width: 100%;
+ }
+
.setting:nth-child(even) {
background-color: rgb(250, 250, 250);
}
diff --git a/frontend/views/themes/default/ejs/author.ejs b/frontend/views/themes/default/ejs/author.ejs
index 4ca46a3..19a0cac 100644
--- a/frontend/views/themes/default/ejs/author.ejs
+++ b/frontend/views/themes/default/ejs/author.ejs
@@ -10,19 +10,28 @@
<%- include("partials/header.ejs") %>
+ <%if(logged_in_user) {%>
+ <%}%>
+
+
<%= post.title %>
<%- post.content %>
-
-
DISPLAYNAME
-
Registered REGISTRATIONDATE
-
NUMPOSTS Posts
+
+
<%= post.owner.display_name || post.owner.username %>
+
+
Registered <%= post.created_date.toLocaleString('en-US', { dateStyle:'medium' }) || "Null" %>
+
<%= post.post_count %> Posts
diff --git a/frontend/views/themes/default/ejs/authorEdit.ejs b/frontend/views/themes/default/ejs/authorEdit.ejs
new file mode 100644
index 0000000..6482852
--- /dev/null
+++ b/frontend/views/themes/default/ejs/authorEdit.ejs
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
Yet-Another-Blog
+
+
+ <%- include("partials/header.ejs") %>
+
+
+
+
+
+
+
Change Password
+
+
+
+
Change Profile Picture
+
+
+
+
+
+
+
+
+ <%- include("partials/richTextEditor.ejs", {text_selector: 'post-content', prefill: profile.raw_content}) %>
+
+
+
+ <%- include("partials/footer.ejs") %>
+
+
+
diff --git a/frontend/views/themes/default/ejs/partials/post.ejs b/frontend/views/themes/default/ejs/partials/post.ejs
index 10f4695..b6b8390 100644
--- a/frontend/views/themes/default/ejs/partials/post.ejs
+++ b/frontend/views/themes/default/ejs/partials/post.ejs
@@ -1,6 +1,6 @@
<%= post.title ? post.title : "Untitled Post" %>
-
+
<%= post.description %>
diff --git a/frontend/views/themes/default/ejs/partials/richTextEditor.ejs b/frontend/views/themes/default/ejs/partials/richTextEditor.ejs
new file mode 100644
index 0000000..c456836
--- /dev/null
+++ b/frontend/views/themes/default/ejs/partials/richTextEditor.ejs
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/views/themes/default/ejs/post.ejs b/frontend/views/themes/default/ejs/post.ejs
index 828898d..1f898a6 100644
--- a/frontend/views/themes/default/ejs/post.ejs
+++ b/frontend/views/themes/default/ejs/post.ejs
@@ -20,7 +20,7 @@
<%}%>
-
<%= blog_post.title%>
+
<%= blog_post.title %>
<%- blog_post.content %>
diff --git a/frontend/views/themes/default/ejs/postNew.ejs b/frontend/views/themes/default/ejs/postNew.ejs
index 491d276..aebec79 100644
--- a/frontend/views/themes/default/ejs/postNew.ejs
+++ b/frontend/views/themes/default/ejs/postNew.ejs
@@ -27,36 +27,7 @@
Content
-
-
-
-
-
-
-
-
-
-
+ <%- include("partials/richTextEditor.ejs", {text_selector: 'post-content', prefill: existing_blog.raw_content}) %>
Post Date
@@ -67,8 +38,9 @@
-
-
+
+
+
diff --git a/frontend/views/themes/default/js/editAuthor.js b/frontend/views/themes/default/js/editAuthor.js
new file mode 100644
index 0000000..6856e07
--- /dev/null
+++ b/frontend/views/themes/default/js/editAuthor.js
@@ -0,0 +1,12 @@
+async function changeValue(setting_name, element) {
+ const form = {
+ setting_name: setting_name,
+ value: element.value,
+ id: window.location.href.split("/")[4],
+ };
+ const response = await request(`/api/web/user`, "PATCH", form);
+
+ // TODO: On failure, notify the user
+ if (response.body.success) {
+ }
+}
diff --git a/frontend/views/themes/default/js/newPost.js b/frontend/views/themes/default/js/newPost.js
index 743b556..b7e9bd3 100644
--- a/frontend/views/themes/default/js/newPost.js
+++ b/frontend/views/themes/default/js/newPost.js
@@ -1,86 +1,12 @@
let blog_id = window.location.href.split("/")[4];
-const post_content_area = qs("#post-content");
-let images = [];
-// TODO: Support videos
-
-// Text area custom text editor
-qs("#insert-sidebyside").addEventListener("click", () => textareaAction("{sidebyside}{/sidebyside}", 12));
-qs("#insert-video").addEventListener("click", () => textareaAction("{video:}", 7));
-qs("#insert-h1").addEventListener("click", () => textareaAction("# "));
-qs("#insert-h2").addEventListener("click", () => textareaAction("## "));
-qs("#insert-h3").addEventListener("click", () => textareaAction("### "));
-qs("#insert-h4").addEventListener("click", () => textareaAction("#### "));
-qs("#insert-underline").addEventListener("click", () => textareaAction("_", undefined, true));
-qs("#insert-italics").addEventListener("click", () => textareaAction("*", undefined, true));
-qs("#insert-bold").addEventListener("click", () => textareaAction("__", undefined, true));
-qs("#insert-strike").addEventListener("click", () => textareaAction("~~", undefined, true));
-qs("#insert-sup").addEventListener("click", () => textareaAction("^", undefined, true));
-
-function textareaAction(insert, cursor_position, dual_side) {
- // Insert the custom string at the cursor position
- const selectionStart = post_content_area.selectionStart;
- const selectionEnd = post_content_area.selectionEnd;
-
- const textBefore = post_content_area.value.substring(0, selectionStart);
- const textAfter = post_content_area.value.substring(selectionEnd);
- const selectedText = post_content_area.value.substring(selectionStart, selectionEnd);
-
- let updatedText;
-
- if (dual_side) updatedText = `${textBefore}${insert}${selectedText}${insert}${textAfter}`;
- else updatedText = `${textBefore}${insert}${selectedText}${textAfter}`;
-
- post_content_area.value = updatedText;
-
- // Set the cursor position after the custom string
- post_content_area.focus();
- const newPosition = selectionStart + (cursor_position || insert.length);
- post_content_area.setSelectionRange(newPosition, newPosition);
-}
-
-// Upload an image to the blog post
-post_content_area.addEventListener("drop", async (event) => {
- event.preventDefault();
- const files = event.dataTransfer.files;
- // let image_queue = [];
-
- for (let i = 0; i < files.length; i++) {
- // Each dropped image will be stored in this formatted object
- const image_object = {
- data_blob: new Blob([await files[i].arrayBuffer()]),
- content_type: files[i].type,
- };
- let form_data = {
- buffer: await _readFile(image_object.data_blob),
- post_id: blog_id,
- };
-
- const image_uploading_request = await request("/api/web/image", "POST", form_data);
-
- if (image_uploading_request.status == 200) {
- textareaAction(`{image:${image_uploading_request.body}}`);
- images.push(image_uploading_request.body);
- }
- }
-});
-
-// We need to read the file contents in order to convert it to base64 to send to the server
-function _readFile(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result);
- reader.onerror = reject;
- reader.readAsDataURL(file);
- });
-}
-
-async function publish() {
+async function publish(visibility) {
let form_data = {
title: qs("#post-title").value,
description: qs("#post-description").value,
tags: [],
- images: images,
+ media: media,
+ visibility: visibility,
content: qs("#post-content").value,
date: qs("#date").value,
time: qs("#time").value,
@@ -93,19 +19,9 @@ async function publish() {
let tags_array = qs("#post-tags").value.split(",");
tags_array.forEach((tag) => form_data.tags.push(tag.trim()));
}
-
const post_response = await request("/api/web/post", "PATCH", form_data);
if (post_response.body.success) {
window.location.href = `/post/${post_response.body.post_id}`;
}
}
-
-// Auto resize on page load
-post_content_area.style.height = post_content_area.scrollHeight + "px";
-post_content_area.style.minHeight = post_content_area.scrollHeight + "px";
-// Auto expand blog area
-post_content_area.addEventListener("input", (e) => {
- post_content_area.style.height = post_content_area.scrollHeight + "px";
- post_content_area.style.minHeight = e.target.scrollHeight + "px";
-});
diff --git a/frontend/views/themes/default/js/richTextEditor.js b/frontend/views/themes/default/js/richTextEditor.js
new file mode 100644
index 0000000..d890d68
--- /dev/null
+++ b/frontend/views/themes/default/js/richTextEditor.js
@@ -0,0 +1,98 @@
+const rich_text_editors = qsa(".rich-text-editor");
+let media = [];
+
+function textareaAction(textarea, insert, cursor_position, dual_side = false) {
+ // textarea = textarea.querySelector("textarea");
+ const selectionStart = textarea.selectionStart;
+ const selectionEnd = textarea.selectionEnd;
+
+ const textBefore = textarea.value.substring(0, selectionStart);
+ const textAfter = textarea.value.substring(selectionEnd);
+ const selectedText = textarea.value.substring(selectionStart, selectionEnd);
+
+ let updatedText;
+
+ if (dual_side) updatedText = `${textBefore}${insert}${selectedText}${insert}${textAfter}`;
+ else updatedText = `${textBefore}${insert}${selectedText}${textAfter}`;
+
+ textarea.value = updatedText;
+
+ // Set the cursor position after the custom string
+ textarea.focus();
+ const newPosition = selectionStart + (cursor_position || insert.length);
+ textarea.setSelectionRange(newPosition, newPosition);
+}
+
+// Go though rich editors and apply image uploading script
+rich_text_editors.forEach((editor) => {
+ editor.querySelector("#insert-sidebyside").addEventListener("click", () => textareaAction(editor, "{sidebyside}{/sidebyside}", 12));
+ editor.querySelector("#insert-video").addEventListener("click", () => textareaAction(editor, "{video:}", 7));
+ editor.querySelector("#insert-h1").addEventListener("click", () => textareaAction(editor, "# "));
+ editor.querySelector("#insert-h2").addEventListener("click", () => textareaAction(editor, "## "));
+ editor.querySelector("#insert-h3").addEventListener("click", () => textareaAction(editor, "### "));
+ editor.querySelector("#insert-h4").addEventListener("click", () => textareaAction(editor, "#### "));
+ editor.querySelector("#insert-underline").addEventListener("click", () => textareaAction(editor, "_", undefined, true));
+ editor.querySelector("#insert-italics").addEventListener("click", () => textareaAction(editor, "*", undefined, true));
+ editor.querySelector("#insert-bold").addEventListener("click", () => textareaAction(editor, "__", undefined, true));
+ editor.querySelector("#insert-strike").addEventListener("click", () => textareaAction(editor, "~~", undefined, true));
+ editor.querySelector("#insert-sup").addEventListener("click", () => textareaAction(editor, "^", undefined, true));
+
+ editor.addEventListener("drop", async (event) => {
+ event.preventDefault();
+ const files = event.dataTransfer.files;
+ // let image_queue = [];
+
+ for (let i = 0; i < files.length; i++) {
+ // Each dropped image will be stored in this formatted object
+ const image_object = {
+ data_blob: new Blob([await files[i].arrayBuffer()]),
+ content_type: files[i].type,
+ };
+ let form_data = {
+ buffer: await _readFile(image_object.data_blob),
+ post_id: window.location.href.split("/")[4],
+ };
+
+ const image_uploading_request = await request("/api/web/image", "POST", form_data);
+
+ if (image_uploading_request.status == 200) {
+ textareaAction(editor, `{image:${image_uploading_request.body}}`);
+ media.push(image_uploading_request.body);
+ }
+ }
+ });
+
+ let textarea = editor.querySelector("textarea");
+ textarea.addEventListener("input", (e) => {
+ textarea.style.height = textarea.scrollHeight + "px";
+ textarea.style.minHeight = e.target.scrollHeight + "px";
+ });
+
+ // Auto resize on page load
+ textarea.style.height = textarea.scrollHeight + "px";
+ textarea.style.minHeight = textarea.scrollHeight + "px";
+});
+
+// We need to read the file contents in order to convert it to base64 to send to the server
+function _readFile(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+}
+
+async function updateBiography() {
+ let form_data = {
+ media: media,
+ content: qs("#post-content").value,
+ id: window.location.href.split("/")[4],
+ };
+
+ const post_response = await request("/api/web/biography", "PATCH", form_data);
+
+ if (post_response.body.success) {
+ window.location.href = `/post/${post_response.body.post_id}`;
+ }
+}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 55420fd..1d8d508 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -11,32 +11,31 @@ datasource db {
}
model User {
- id String @id @unique @default(uuid())
- username String @unique
- password String
+ id String @id @unique @default(uuid())
+ username String @unique
+ password String
+ display_name String?
- role Role @default(USER)
- group String?
+ role Role @default(USER)
- blog_posts BlogPost[]
+ blog_posts Post[]
profile_page ProfilePage?
@@index([username, role])
}
-model BlogPost {
+model Post {
id String @id @unique @default(uuid())
title String?
description String?
content String?
- thumbnail String?
- images String[]
+ media String[]
visibility PostStatus @default(UNLISTED)
owner User? @relation(fields: [ownerid], references: [id], onDelete: Cascade)
ownerid String?
// Tags
- tags String[]
+ tags String[]
// Dates
publish_date DateTime?
@@ -44,12 +43,13 @@ model BlogPost {
}
model ProfilePage {
- id String @id @unique @default(uuid())
- content String?
- images String[]
- visibility PostStatus @default(UNLISTED)
- owner User @relation(fields: [ownerid], references: [id], onDelete: Cascade)
- ownerid String @unique
+ id String @id @unique @default(uuid())
+ content String?
+ media String[]
+ visibility PostStatus @default(UNLISTED)
+ owner User @relation(fields: [ownerid], references: [id], onDelete: Cascade)
+ ownerid String @unique
+ created_date DateTime @default(now())
}
model Setting {
@@ -69,6 +69,7 @@ enum Role {
}
enum PostStatus {
+ PRIVATE
UNLISTED
PUBLISHED
}
diff --git a/yab.js b/yab.js
index f75ae9f..47f93df 100644
--- a/yab.js
+++ b/yab.js
@@ -36,6 +36,8 @@ app.post("/api/web/image", checkAuthenticated, internal.postImage);
app.delete("/api/web/post/image", checkAuthenticated, internal.deleteImage);
app.delete("/api/web/post", checkAuthenticated, internal.deleteBlog);
app.patch("/api/web/post", checkAuthenticated, internal.patchBlog);
+app.patch("/api/web/biography", checkAuthenticated, internal.patchBiography);
+app.patch("/api/web/user", checkAuthenticated, internal.patchUser);
// app.delete("/logout", page_scripts.logout);
@@ -44,6 +46,7 @@ app.get("/", page_scripts.index);
app.get("/login", page_scripts.login);
app.get("/register", checkNotAuthenticated, page_scripts.register);
app.get("/author/:author_id", page_scripts.author);
+app.get("/author/:author_id/edit", checkAuthenticated, page_scripts.authorEdit);
app.get("/admin", checkAuthenticated, page_scripts.admin);
app.get("/posts", page_scripts.blogList);
app.get("/post/new", checkAuthenticated, page_scripts.blogNew);