diff --git a/.gitignore b/.gitignore
index f41ce1f..db2f430 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@
/data
*.code-workspace
.vscode/settings.json
-/frontend/public/img/.dev
\ No newline at end of file
+/frontend/public/img/.dev
+/prisma/migrations
\ No newline at end of file
diff --git a/backend/core/core.js b/backend/core/core.js
index 46997f2..42a5d3c 100644
--- a/backend/core/core.js
+++ b/backend/core/core.js
@@ -13,14 +13,25 @@ const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
});
const settings = require("../settings");
+const md = require("markdown-it")();
-// FIXME: This does not properly validate that a user was created
async function registerUser(username, password, options) {
const new_user = await prisma.user.create({ data: { username: username, password: password, ...options } });
- settings.act("SETUP_COMPLETE", true);
- if (new_user) return { success: true, message: `Successfully created ${new_user.username}` };
-}
+ if (new_user.id) {
+ // If the user was created as an admin, make sure that the server knows the setup process is complete.
+ if (options.role === "ADMIN") settings.act("SETUP_COMPLETE", true);
+
+ // Create a user profile page
+ const profile_page = await prisma.profilePage.create({ data: { owner: new_user.id } });
+ if (!profile_page.id) return { success: false, message: `Error creating profile page for user ${new_user.username}` };
+
+ // User has been successfully created
+ return { success: true, message: `Successfully created ${new_user.username}` };
+ }
+
+ return { success: false, message: "Unknown error" };
+}
async function getUser({ id, username } = {}) {
if (id || username) {
let user;
@@ -31,5 +42,311 @@ async function getUser({ id, username } = {}) {
else return { success: true, data: user };
}
}
+async function postBlog(blog_post, owner_id) {
+ // Check if user has permissions to upload a blog post
+ const user = await getUser({ id: owner_id });
+ if (!user.success) return { success: false, message: "User not found" };
+ if (user.data.role !== "ADMIN" && user.data.role !== "AUTHOR") return { success: false, message: "User is not permitted" };
-module.exports = { registerUser, getUser };
+ 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);
+
+ 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,
+ };
+
+ const database_blog = await prisma.blogPost.create({ data: { ...blog_post_formatted, owner: { connect: { id: owner_id } } } });
+
+ let uploaded_images = [];
+ let uploaded_thumbnail = "DEFAULT";
+
+ if (blog_post.images) {
+ // For Each image, upload to S3
+ for (let i = 0; blog_post.images.length > i; i++) {
+ const image = blog_post.images[i];
+ const image_data = Buffer.from(image.data_blob.split(",")[1], "base64");
+ const name = await _uploadImage(database_blog.id, "blog", false, image_data, image.id);
+ uploaded_images.push(name);
+ }
+ }
+
+ // Upload thumbnail to S3
+ if (blog_post.thumbnail) {
+ const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64");
+ const name = await _uploadImage(database_blog.id, "blog", true, image_data, blog_post.thumbnail.id);
+ uploaded_thumbnail = name;
+ }
+
+ await prisma.blogPost.update({ where: { id: database_blog.id }, data: { images: uploaded_images, thumbnail: uploaded_thumbnail } });
+ return { success: true, blog_id: database_blog.id };
+}
+async function deleteBlog(blog_id, requester_id) {
+ const user = await getUser({ id: requester_id });
+ const post = await getBlogList({ id: blog_id });
+
+ let can_delete = post.owner.id === user.data.id || user.data.role === "ADMIN";
+
+ if (can_delete) {
+ await prisma.blogPost.delete({ where: { id: post.id } });
+ _deleteS3Directory(post.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 getBlogList({ id: blog_post.id, raw: true });
+
+ delete blog_post.id;
+
+ let can_update = post.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);
+
+ 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,
+ };
+
+ await prisma.blogPost.update({ where: { id: post.id }, data: blog_post_formatted });
+
+ let uploaded_images = [];
+ let uploaded_thumbnail = "DEFAULT";
+
+ // For Each image, upload to S3
+ if (blog_post.images) {
+ for (let i = 0; blog_post.images.length > i; i++) {
+ const image = blog_post.images[i];
+ const image_data = Buffer.from(image.data_blob.split(",")[1], "base64");
+ const name = await _uploadImage(post.id, "blog", false, image_data, image.id);
+ uploaded_images.push(name);
+ }
+ }
+
+ let data_to_update = {
+ images: [...post.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);
+ uploaded_thumbnail = name;
+
+ data_to_update.thumbnail = uploaded_thumbnail;
+ }
+
+ await prisma.blogPost.update({ where: { id: post.id }, data: data_to_update });
+
+ return { success: true };
+}
+async function getBlogList({ id, visibility = "PUBLISHED", owner_id, raw = false } = {}, { limit = 10, page = 0 } = {}) {
+ if (id) {
+ // Get the database entry for the blog post
+ let post = await prisma.blogPost.findUnique({ where: { id: id }, include: { owner: true } });
+
+ if (!post) return null;
+
+ if (raw) {
+ // Had to do this, only God knows why.
+ post.raw_images = [];
+ post.images.forEach((image) => post.raw_images.push(image));
+
+ post.raw_thumbnail = post.thumbnail;
+ post.raw_content = post.content;
+ }
+
+ // Get the image urls for the post
+ for (i = 0; post.images.length > i; i++) {
+ post.images[i] = await _getImage(post.id, "blog", post.images[i]);
+ }
+
+ // get thumbnail URL
+ post.thumbnail = await _getImage(post.id, "blog", post.thumbnail);
+
+ // Render the markdown contents of the post
+ post.content = md.render(post.content);
+
+ // Replace custom formatting with what we want
+ post.content = _format_blog_content(post.content, post.images);
+
+ // Return the post with valid image urls
+ return post;
+ }
+
+ const where_object = {
+ OR: [
+ {
+ AND: [
+ {
+ visibility: "PUBLISHED",
+ },
+ {
+ publish_date: {
+ lte: new Date(),
+ },
+ },
+ ],
+ },
+
+ {
+ ownerid: owner_id,
+ },
+ ],
+ };
+
+ const blog_posts = await prisma.blogPost.findMany({
+ where: where_object,
+ take: limit,
+ skip: Math.max(page, 0) * limit,
+ include: { owner: true },
+ orderBy: [{ publish_date: "desc" }, { created_date: "desc" }],
+ });
+ // Get the thumbnails
+ for (i = 0; blog_posts.length > i; i++) {
+ blog_posts[i].thumbnail = await _getImage(blog_posts[i].id, "blog", blog_posts[i].thumbnail);
+
+ // Get the image urls for the post
+ for (imgindx = 0; blog_posts[i].images.length > imgindx; imgindx++) {
+ blog_posts[i].images[imgindx] = await _getImage(blog_posts[i].id, "blog", blog_posts[i].images[imgindx]);
+ }
+
+ // Render the markdown contents of the post
+ blog_posts[i].content = md.render(blog_posts[i].content);
+
+ // Replace custom formatting with what we want
+ blog_posts[i].content = _format_blog_content(blog_posts[i].content, blog_posts[i].images);
+ }
+ // Calculate pagination
+ let pagination = await prisma.blogPost.count({
+ where: where_object,
+ });
+ return { data: blog_posts, pagination: _getNavigationList(page, Math.ceil(pagination / limit)) };
+}
+async function deleteImage(image, requester_id) {
+ const user = await getUser({ id: requester_id });
+ const post = await getBlogList({ 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 _uploadImage(parent_id, parent_type, is_thumbnail, buffer, name) {
+ let size = { width: 1920, height: 1080 };
+ if (is_thumbnail) size = { width: 300, height: 300 };
+
+ const compressed_image = await sharp(buffer, { animated: true })
+ .resize({ ...size, withoutEnlargement: true, fit: "inside" })
+ .webp({ quality: 90 })
+ .toBuffer();
+
+ const params = {
+ Bucket: process.env.S3_BUCKET_NAME,
+ Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${name}.webp`,
+ Body: compressed_image,
+ ContentType: "image/webp",
+ };
+
+ const command = new PutObjectCommand(params);
+ await s3.send(command);
+
+ return name;
+}
+async function _getImage(parent_id, parent_type, name) {
+ 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` };
+
+ return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 });
+}
+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}` };
+
+ // Retrieve a list of objects in the specified directory
+ const listed_objects = await s3.send(new ListObjectsCommand(folder_params));
+
+ // If the directory is already empty, return
+ if (listed_objects?.Contents?.length === 0 || !listed_objects.Contents) return;
+
+ // Create an object to specify the bucket and objects to be deleted
+ const delete_params = {
+ Bucket: process.env.S3_BUCKET_NAME,
+ Delete: { Objects: [] },
+ };
+
+ // Iterate over each object and push its key to the deleteParams object
+ listed_objects.Contents.forEach(({ Key }) => {
+ delete_params.Delete.Objects.push({ Key });
+ });
+
+ // Delete the objects specified in deleteParams
+ await s3.send(new DeleteObjectsCommand(delete_params));
+
+ // If there are more objects to delete (truncated result), recursively call the function again
+ // if (listed_objects.IsTruncated) await emptyS3Directory(bucket, dir);
+}
+function _format_blog_content(content, images) {
+ // Replace Images
+ const image_regex = /{image:([^}]+)}/g;
+
+ // Replace Side-by-side
+ const side_by_side = /{sidebyside}(.*?){\/sidebyside}/gs;
+
+ content = content.replace(image_regex, (match, image_name) => {
+ for (image of images) {
+ if (image.includes(image_name)) {
+ return `
`;
+ }
+ }
+ });
+
+ content = content.replace(side_by_side, (match, inner_content) => {
+ return `${inner_content}
`;
+ });
+
+ // Finished formatting, return!
+ return content;
+}
+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);
+}
+
+module.exports = { registerUser, getUser, postBlog, updateBlog, getBlogList, deleteBlog, deleteImage };
diff --git a/backend/core/external_api.js b/backend/core/external_api.js
index e69de29..00df8ac 100644
--- a/backend/core/external_api.js
+++ b/backend/core/external_api.js
@@ -0,0 +1,54 @@
+const feed_lib = require("feed").Feed;
+const internal = require("./internal_api");
+
+// TODO: Expose ATOM Feed items
+function getBaseFeed() {
+ return new feed_lib({
+ title: process.env.WEBSITE_NAME,
+ description: `${process.env.S3_REGION} RSS Feed`,
+ id: process.env.BASE_URL,
+ link: process.env.BASE_URL,
+ // image: "http://example.com/image.png",
+ // favicon: "http://example.com/favicon.ico",
+ // copyright: "All rights reserved 2013, John Doe",
+ // generator: "awesome", // optional, default = 'Feed for Node.js'
+ feedLinks: {
+ json: `${process.env.BASE_URL}/json`,
+ atom: `${process.env.BASE_URL}/atom`,
+ },
+ });
+}
+
+async function getFeed({ type = "rss" }) {
+ // Get the base feed
+ let feed = getBaseFeed();
+
+ // Get posts
+ let posts = await internal.getBlogList({}, { limit: 20 });
+
+ // For each post, add a formatted object to the feed
+ posts.data.forEach((post) => {
+ let formatted = {
+ title: post.title,
+ description: post.description,
+ id: post.id,
+ link: `${process.env.BASE_URL}/blog/${post.id}`,
+ content: post.content,
+ date: new Date(post.publish_date),
+ image: post.thumbnail,
+ author: [
+ {
+ name: post.owner.username,
+ link: `${process.env.BASE_URL}/author/${post.owner.username}`,
+ },
+ ],
+ };
+
+ feed.addItem(formatted);
+ });
+ // if (type === "rss") return feed.rss2();
+ if (type === "atom") return feed.atom1();
+ // if (type === "json") return feed.json1();
+}
+
+module.exports = { getFeed };
diff --git a/backend/core/form_validation.js b/backend/core/form_validation.js
index eb6b506..c6ae3a9 100644
--- a/backend/core/form_validation.js
+++ b/backend/core/form_validation.js
@@ -1,19 +1,27 @@
const settings = require("../settings");
async function userRegistration(username, password) {
+ const active_settings = settings.getSettings();
if (!username) return { success: false, message: "No username provided" };
if (!password) return { success: false, message: "No password provided" };
- // TODO: Admin customizable minimum password length
- if (password.length < 4) return { success: false, message: "Password not long enough" };
+ if (password.length < active_settings.USER_MINIMUM_PASSWORD_LENGTH) return { success: false, message: "Password not long enough" };
// Check if username only uses URL safe characters
- if (!is_url_safe(username)) return { success: false, message: "Username is not URL safe" };
+ if (!_isUrlSafe(username)) return { success: false, message: "Username is not URL safe" };
// All good! Validation complete
return { success: true };
}
-function is_url_safe(str) {
+async function blogPost(blog_object) {
+ // TODO: Validate blog posts before upload
+ // Check title length
+ // Check description length
+ // Check content length
+ // Check valid date
+}
+
+function _isUrlSafe(str) {
const pattern = /^[A-Za-z0-9\-_.~]+$/;
return pattern.test(str);
}
diff --git a/backend/core/internal_api.js b/backend/core/internal_api.js
index 60d4ff0..0589320 100644
--- a/backend/core/internal_api.js
+++ b/backend/core/internal_api.js
@@ -4,12 +4,12 @@ const settings = require("../settings");
async function registerUser(username, password) {
// Get current and relevant settings
- const s = Promise.all([settings.act("ACCOUNT_REGISTRATION"), settings.act("SETUP_COMPLETE")]);
+ const active_settings = settings.getSettings();
const form_valid = await validate.userRegistration(username, password); // Check form for errors
// Set variables for easy reading
- const registration_allowed = s[0];
- const setup_complete = s[1];
+ const registration_allowed = active_settings.ACCOUNT_REGISTRATION;
+ const setup_complete = active_settings.SETUP_COMPLETE;
if (!registration_allowed && setup_complete) return { success: false, message: "Registration is disabled" }; // Registration disabled
if (!form_valid.success) return form_valid; // Registration details did not validate
@@ -36,4 +36,25 @@ async function loginUser(username, password) {
return { success: true, data: { username: existing_user.data.username, id: existing_user.data.id, password: existing_user.data.password } };
}
-module.exports = { registerUser, loginUser };
+async function getBlogList({ id, visibility, owner_id, raw } = {}, { page = 0, limit = 10 } = {}) {
+ const blog_list = await core.getBlogList({ id: id, visibility: visibility, owner_id: owner_id, raw: raw }, { page: page, limit: limit });
+ return blog_list;
+}
+
+async function getUser({ id } = {}) {
+ return await core.getUser({ id: id });
+}
+
+async function postBlog(blog_post, owner_id) {
+ return await core.postBlog(blog_post, owner_id);
+}
+async function deleteBlog(blog_id, owner_id) {
+ return await core.deleteBlog(blog_id, owner_id);
+}
+async function updateBlog(blog_post, requester_id) {
+ return await core.updateBlog(blog_post, requester_id);
+}
+async function deleteImage(image_data, requester_id) {
+ return await core.deleteImage(image_data, requester_id);
+}
+module.exports = { registerUser, loginUser, postBlog, getBlogList, deleteBlog, updateBlog, deleteImage, getUser };
diff --git a/backend/page_scripts.js b/backend/page_scripts.js
index 6a92fb3..f5b6c15 100644
--- a/backend/page_scripts.js
+++ b/backend/page_scripts.js
@@ -1,33 +1,81 @@
const internal = require("./core/internal_api");
+const external = require("./core/external_api");
const bcrypt = require("bcrypt");
const settings = require("./settings");
+function getDefaults(req) {
+ const active_settings = settings.getSettings();
+ return { logged_in_user: req.session.user, website_name: process.env.WEBSITE_NAME, settings: active_settings };
+}
+
async function index(request, response) {
// Check if the master admin has been created
const is_setup_complete = (await settings.act("SETUP_COMPLETE")) || false;
if (!is_setup_complete) return response.redirect("/register");
- response.render("index.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME });
+ response.redirect("/blog");
}
function register(request, response) {
- response.render("register.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME });
+ response.render("register.ejs", getDefaults(request));
}
function login(request, response) {
- response.render("login.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME });
+ response.render("login.ejs", getDefaults(request));
}
function author(request, response) {
- response.render("author.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME });
+ response.render("author.ejs", getDefaults(request));
}
-function blogList(request, response) {
- response.render("blogList.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME });
+async function blogList(req, res) {
+ const blog_list = await internal.getBlogList({ owner_id: req.session.user?.id }, { page: req.query.page || 0 });
+ res.render("blogList.ejs", {
+ ...getDefaults(req),
+ blog_list: blog_list.data,
+ pagination: blog_list.pagination,
+ current_page: req.query.page || 0,
+ loaded_page: req.path,
+ });
+}
+async function blogSingle(req, res) {
+ const blog = await internal.getBlogList({ id: req.params.blog_id });
+ if (blog === null) return res.redirect("/blog");
+ res.render("blogSingle.ejs", { ...getDefaults(req), blog_post: blog });
}
function blogNew(request, response) {
- response.render("blogNew.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME });
+ // TODO: Turn date formatting into function
+ let existing_blog = {};
+ let published_date_parts = new Date().toLocaleDateString().split("/");
+ const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`;
+ existing_blog.publish_date = formatted_date;
+
+ let published_time_parts = new Date().toLocaleTimeString([], { timeStyle: "short" }).slice(0, 4).split(":");
+ const formatted_time = `${published_time_parts[0].padStart(2, "0")}:${published_time_parts[1].padStart(2, "0")}`;
+ existing_blog.publish_time = formatted_time;
+
+ response.render("blogNew.ejs", { ...getDefaults(request), existing_blog: existing_blog });
+}
+async function blogEdit(req, res) {
+ const existing_blog = await internal.getBlogList({ id: req.params.blog_id, raw: true });
+
+ let published_date_parts = new Date(existing_blog.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")}`;
+ existing_blog.publish_date = formatted_date;
+
+ let published_time_parts = new Date(existing_blog.publish_date).toLocaleTimeString([], { timeStyle: "short" }).slice(0, 4).split(":");
+ const formatted_time = `${published_time_parts[0].padStart(2, "0")}:${published_time_parts[1].padStart(2, "0")}`;
+ existing_blog.publish_time = formatted_time;
+
+ res.render("blogNew.ejs", { ...getDefaults(req), existing_blog: existing_blog });
}
async function admin(request, response) {
- const reg_allowed = await settings.userRegistrationAllowed();
- response.render("admin.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME, settings: { registration_enabled: reg_allowed } });
+ response.render("admin.ejs", { ...getDefaults(request) });
}
+async function atom(req, res) {
+ res.type("application/xml");
+ res.send(await external.getFeed({ type: "atom" }));
+}
+// async function rss(req, res) {
+// res.type("application/rss+xml");
+// res.send(await external.getFeed({ type: "rss" }));
+// }
async function registerPost(request, response) {
const hashedPassword = await bcrypt.hash(request.body.password, 10); // Hash the password for security :^)
@@ -44,15 +92,46 @@ async function loginPost(request, response) {
request.session.user = { username: login.data.username, id: login.data.id };
response.json({ success: true });
}
-
async function settingPost(request, response) {
const user = await internal.getUser({ id: request.session.user.id });
if (!user.success) return response.json({ success: false, message: user.message });
if (user.data.role !== "ADMIN") return response.json({ success: false, message: "User is not permitted" });
- if (request.body.setting_name === "ACCOUNT_REGISTRATION") settings.setUserRegistrationAllowed(request.body.value);
+ settings.act(request.body.setting_name, request.body.value);
response.json({ success: true });
}
-module.exports = { index, register, login, author, blogList, blogNew, admin, registerPost, loginPost, settingPost };
+async function deleteImage(req, res) {
+ res.json(await internal.deleteImage(req.body, req.session.user.id));
+}
+
+async function postBlog(req, res) {
+ return res.json(await internal.postBlog(req.body, req.session.user.id));
+}
+async function deleteBlog(req, res) {
+ return res.json(await internal.deleteBlog(req.body.id, req.session.user.id));
+}
+async function updateBlog(req, res) {
+ return res.json(await internal.updateBlog(req.body, req.session.user.id));
+}
+module.exports = {
+ index,
+ register,
+ login,
+ author,
+ blogList,
+ blogNew,
+ blogEdit,
+ blogSingle,
+ admin,
+ atom,
+ // rss,
+ registerPost,
+ loginPost,
+ settingPost,
+ postBlog,
+ deleteBlog,
+ deleteImage,
+ updateBlog,
+};
diff --git a/backend/permissions.js b/backend/permissions.js
index 056e961..6de55a6 100644
--- a/backend/permissions.js
+++ b/backend/permissions.js
@@ -1 +1,3 @@
+// TODO: Permissions file
+
function checkPermissions(role, { minimum = true }) {}
diff --git a/backend/settings.js b/backend/settings.js
index ef03781..4328bdb 100644
--- a/backend/settings.js
+++ b/backend/settings.js
@@ -4,9 +4,14 @@ persistent_setting.init({ dir: "data/site/" });
let settings = {
SETUP_COMPLETE: false,
ACCOUNT_REGISTRATION: false,
+ HIDE_LOGIN: false,
BLOG_UPLOADING: false,
USER_MINIMUM_PASSWORD_LENGTH: 6,
+
+ BLOG_MINIMUM_TITLE_LENGTH: 6,
+ BLOG_MINIMUM_DESCRIPTION_LENGTH: 6,
+ BLOG_MINIMUM_CONTENT_LENGTH: 6,
};
async function act(key, value) {
diff --git a/frontend/public/css/blog-list.css b/frontend/public/css/blog-list.css
index 811ca4c..d35e84a 100644
--- a/frontend/public/css/blog-list.css
+++ b/frontend/public/css/blog-list.css
@@ -23,11 +23,16 @@
}
.blog-entry .thumbnail {
width: 150px;
+ height: 150px;
}
.blog-entry .thumbnail img {
height: 100%;
width: 100%;
}
+.blog-entry .blog-info {
+ display: flex;
+ flex-direction: column;
+}
.blog-entry .blog-info .blog-title {
font-size: 20px;
border-bottom: 1px solid #9f9f9f;
@@ -46,8 +51,40 @@
.blog-entry .blog-info .blog-description {
color: #9f9f9f;
margin-top: 10px;
+ max-height: 100px;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ word-break: none;
+ overflow: hidden;
+ text-align: left;
+}
+.blog-entry .blog-info .blog-action {
+ display: flex;
+ flex-direction: row;
+ margin-top: auto;
+}
+.blog-entry .blog-info .blog-action .date {
+ font-size: 16px;
+ color: gray;
+ margin-right: auto;
+}
+.blog-entry .blog-info .blog-action a {
+ color: white;
+ font-style: italic;
}
.blog-entry:last-of-type {
margin-bottom: inherit;
+}
+
+@media screen and (max-width: 500px) {
+ .page .blog-entry {
+ grid-template-columns: 75px auto;
+ margin-bottom: 20px;
+ }
+ .page .blog-entry .thumbnail {
+ width: 75px;
+ height: 75px;
+ }
}
\ No newline at end of file
diff --git a/frontend/public/css/blog-list.scss b/frontend/public/css/blog-list.scss
index 9ab4eb5..e4e9cbb 100644
--- a/frontend/public/css/blog-list.scss
+++ b/frontend/public/css/blog-list.scss
@@ -28,6 +28,7 @@ $quiet-text: #9f9f9f;
.thumbnail {
width: 150px;
+ height: 150px;
img {
height: 100%;
width: 100%;
@@ -35,6 +36,8 @@ $quiet-text: #9f9f9f;
}
.blog-info {
+ display: flex;
+ flex-direction: column;
.blog-title {
font-size: 20px;
border-bottom: 1px solid $quiet-text;
@@ -54,6 +57,29 @@ $quiet-text: #9f9f9f;
.blog-description {
color: $quiet-text;
margin-top: 10px;
+ max-height: 100px;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ word-break: none;
+ overflow: hidden;
+ text-align: left;
+ }
+
+ .blog-action {
+ display: flex;
+ flex-direction: row;
+ margin-top: auto;
+
+ .date {
+ font-size: 16px;
+ color: gray;
+ margin-right: auto;
+ }
+ a {
+ color: white;
+ font-style: italic;
+ }
}
}
}
@@ -61,3 +87,16 @@ $quiet-text: #9f9f9f;
.blog-entry:last-of-type {
margin-bottom: inherit;
}
+
+@media screen and (max-width: 500px) {
+ .page {
+ .blog-entry {
+ grid-template-columns: 75px auto;
+ margin-bottom: 20px;
+ .thumbnail {
+ width: 75px;
+ height: 75px;
+ }
+ }
+ }
+}
diff --git a/frontend/public/css/new-blog.css b/frontend/public/css/blogNew.css
similarity index 78%
rename from frontend/public/css/new-blog.css
rename to frontend/public/css/blogNew.css
index a17a1c8..6fd2595 100644
--- a/frontend/public/css/new-blog.css
+++ b/frontend/public/css/blogNew.css
@@ -13,7 +13,9 @@
}
.e-header .e-thumbnail img {
height: 100%;
- width: 150px;
+ width: 100%;
+ -o-object-fit: cover;
+ object-fit: cover;
}
.e-header .e-description {
width: 100%;
@@ -52,11 +54,20 @@
aspect-ratio: 16/9;
margin: auto;
display: flex;
+ position: relative;
}
.e-image-area .image img {
height: 100%;
margin: auto;
}
+.e-image-area .image div {
+ position: absolute;
+ right: 0;
+ padding: 5px 10px;
+ box-sizing: border-box;
+ background-color: darkred;
+ cursor: pointer;
+}
.e-content {
background-color: #222;
@@ -65,18 +76,19 @@
}
.e-content textarea {
font-size: 16px;
- min-height: 300px;
+ min-height: 200px;
color: white;
}
.e-settings {
- height: 40px;
+ min-height: 40px;
width: 100%;
background-color: #222;
display: flex;
flex-direction: row;
padding: 5px;
box-sizing: border-box;
+ margin-bottom: 1rem;
}
.e-settings .publish-date {
display: flex;
@@ -91,6 +103,16 @@
.e-settings textarea {
width: 200px;
}
+.e-settings .horizontal-buttons {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+.e-settings .horizontal-buttons button {
+ width: 200px;
+ height: 40px;
+}
input,
textarea {
diff --git a/frontend/public/css/new-blog.scss b/frontend/public/css/blogNew.scss
similarity index 78%
rename from frontend/public/css/new-blog.scss
rename to frontend/public/css/blogNew.scss
index 4905ec9..509640c 100644
--- a/frontend/public/css/new-blog.scss
+++ b/frontend/public/css/blogNew.scss
@@ -15,7 +15,8 @@ $background-body: #222;
img {
height: 100%;
- width: 150px;
+ width: 100%;
+ object-fit: cover;
}
}
@@ -60,11 +61,21 @@ $background-body: #222;
aspect-ratio: 16/9;
margin: auto;
display: flex;
+ position: relative;
img {
height: 100%;
margin: auto;
}
+
+ div {
+ position: absolute;
+ right: 0;
+ padding: 5px 10px;
+ box-sizing: border-box;
+ background-color: darkred;
+ cursor: pointer;
+ }
}
}
@@ -74,19 +85,20 @@ $background-body: #222;
margin-bottom: 10px;
textarea {
font-size: 16px;
- min-height: 300px;
+ min-height: 200px;
color: white;
}
}
.e-settings {
- height: 40px;
+ min-height: 40px;
width: 100%;
background-color: $background-body;
display: flex;
flex-direction: row;
padding: 5px;
box-sizing: border-box;
+ margin-bottom: 1rem;
.publish-date {
display: flex;
div {
@@ -101,6 +113,18 @@ $background-body: #222;
textarea {
width: 200px;
}
+
+ .horizontal-buttons {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+
+ button {
+ width: 200px;
+ height: 40px;
+ }
+ }
}
input,
diff --git a/frontend/public/css/blogSingle.css b/frontend/public/css/blogSingle.css
new file mode 100644
index 0000000..0d4427c
--- /dev/null
+++ b/frontend/public/css/blogSingle.css
@@ -0,0 +1,48 @@
+.page .title {
+ background-color: #222;
+ padding: 10px;
+ box-sizing: border-box;
+ font-size: 24px;
+}
+.page .image-container {
+ width: 100%;
+ margin-bottom: 4px;
+}
+.page .image-container img {
+ width: 100%;
+}
+.page .side-by-side {
+ display: flex;
+ flex-flow: row wrap;
+ place-content: space-around;
+}
+.page .side-by-side .image-container {
+ padding: 5px;
+ box-sizing: border-box;
+ width: 50%;
+ margin-bottom: 0;
+}
+.page h1 {
+ border-bottom: 1px solid #777;
+}
+.page h2 {
+ width: 50%;
+ border-bottom: 1px solid #777;
+}
+.page h3 {
+ width: 25%;
+ border-bottom: 1px solid #777;
+}
+.page h4 {
+ width: 20%;
+ border-bottom: 1px solid #777;
+}
+.page a {
+ color: white;
+}
+
+@media screen and (max-width: 500px) {
+ .page .side-by-side .image-container {
+ width: 100%;
+ }
+}
\ No newline at end of file
diff --git a/frontend/public/css/blogSingle.scss b/frontend/public/css/blogSingle.scss
new file mode 100644
index 0000000..666c0ec
--- /dev/null
+++ b/frontend/public/css/blogSingle.scss
@@ -0,0 +1,62 @@
+.page {
+ .title {
+ background-color: #222;
+ padding: 10px;
+ box-sizing: border-box;
+ font-size: 24px;
+ }
+
+ .image-container {
+ width: 100%;
+ margin-bottom: 4px;
+
+ img {
+ width: 100%;
+ }
+ }
+
+ .side-by-side {
+ display: flex;
+ flex-flow: row wrap;
+ place-content: space-around;
+
+ .image-container {
+ padding: 5px;
+ box-sizing: border-box;
+ width: 50%;
+ margin-bottom: 0;
+ }
+ }
+
+ h1 {
+ border-bottom: 1px solid #777;
+ }
+
+ h2 {
+ width: 50%;
+ border-bottom: 1px solid #777;
+ }
+ h3 {
+ width: 25%;
+ border-bottom: 1px solid #777;
+ }
+
+ h4 {
+ width: 20%;
+ border-bottom: 1px solid #777;
+ }
+
+ a {
+ color: white;
+ }
+}
+
+@media screen and (max-width: 500px) {
+ .page {
+ .side-by-side {
+ .image-container {
+ width: 100%;
+ }
+ }
+ }
+}
diff --git a/frontend/public/css/theme.css b/frontend/public/css/theme.css
index e227aaa..39fe2c3 100644
--- a/frontend/public/css/theme.css
+++ b/frontend/public/css/theme.css
@@ -2,6 +2,7 @@ body {
margin: 0;
background-color: #111;
color: white;
+ font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.header {
@@ -58,11 +59,18 @@ button:focus {
background-color: #122d57;
}
-button.good {
+button.good,
+a.good {
background-color: #015b01;
}
-button.bad {
+button.yellow,
+a.yellow {
+ background-color: #4a4a00;
+}
+
+button.bad,
+a.bad {
background-color: #8e0000;
}
@@ -75,9 +83,78 @@ button.bad {
display: flex;
flex-direction: row;
}
-.page .horizontal-button-container button {
- width: 100px;
+.page .horizontal-button-container button,
+.page .horizontal-button-container a {
+ width: 120px;
min-height: 30px;
+ text-decoration: none;
+ display: flex;
+ margin-right: 5px;
+}
+.page .horizontal-button-container button span,
+.page .horizontal-button-container a span {
+ margin: auto;
+}
+.page .horizontal-button-container button:last-of-type,
+.page .horizontal-button-container a:last-of-type {
+ margin-right: 0;
+}
+.page .blog-admin {
+ margin-bottom: 10px;
+}
+.page .pagination {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ margin: 0 auto;
+}
+.page .pagination a {
+ height: 40px;
+ width: 150px;
+ margin-right: 5px;
+ display: flex;
+ text-decoration: none;
+ background-color: #222;
+}
+.page .pagination a span {
+ margin: auto;
+ color: white;
+ font-size: 20px;
+}
+.page .pagination a:last-of-type {
+ margin-right: 0;
+ margin-left: 5px;
+}
+.page .pagination a.disabled {
+ filter: brightness(50%);
+}
+.page .pagination .page-list {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 50px;
+}
+.page .pagination .page-list a {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ text-decoration: none;
+ background-color: #222;
+ border-radius: 10px;
+ margin: 0 10px 0 0;
+}
+.page .pagination .page-list a span {
+ margin: auto;
+ color: white;
+}
+.page .pagination .page-list a:first-of-type {
+ margin: auto 10px auto auto;
+}
+.page .pagination .page-list a:last-of-type {
+ margin: auto auto auto 0;
+}
+.page .pagination .page-list a.active {
+ background-color: #993d00;
}
.hidden {
@@ -86,6 +163,6 @@ button.bad {
@media screen and (max-width: 1010px) {
.page {
- width: 100%;
+ width: 95%;
}
}
\ No newline at end of file
diff --git a/frontend/public/css/theme.scss b/frontend/public/css/theme.scss
index 96b164e..4695f31 100644
--- a/frontend/public/css/theme.scss
+++ b/frontend/public/css/theme.scss
@@ -5,6 +5,7 @@ body {
margin: 0;
background-color: #111;
color: white;
+ font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.header {
@@ -65,10 +66,16 @@ button:focus {
background-color: #122d57;
}
-button.good {
+button.good,
+a.good {
background-color: #015b01;
}
-button.bad {
+button.yellow,
+a.yellow {
+ background-color: #4a4a00;
+}
+button.bad,
+a.bad {
background-color: #8e0000;
}
@@ -81,9 +88,90 @@ button.bad {
display: flex;
flex-direction: row;
- button {
- width: 100px;
+ button,
+ a {
+ width: 120px;
min-height: 30px;
+ text-decoration: none;
+ // background-color: #222;
+ display: flex;
+ margin-right: 5px;
+
+ span {
+ margin: auto;
+ }
+ }
+
+ button:last-of-type,
+ a:last-of-type {
+ margin-right: 0;
+ }
+ }
+
+ .blog-admin {
+ margin-bottom: 10px;
+ }
+
+ .pagination {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ margin: 0 auto;
+
+ a {
+ height: 40px;
+ width: 150px;
+ margin-right: 5px;
+ display: flex;
+ text-decoration: none;
+ background-color: #222;
+
+ span {
+ margin: auto;
+ color: white;
+ font-size: 20px;
+ }
+ }
+ a:last-of-type {
+ margin-right: 0;
+ margin-left: 5px;
+ }
+
+ a.disabled {
+ filter: brightness(50%);
+ }
+
+ .page-list {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 50px;
+
+ a {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ text-decoration: none;
+ background-color: #222;
+ border-radius: 10px;
+ margin: 0 10px 0 0;
+
+ span {
+ margin: auto;
+ color: white;
+ }
+ }
+
+ a:first-of-type {
+ margin: auto 10px auto auto;
+ }
+ a:last-of-type {
+ margin: auto auto auto 0;
+ }
+
+ a.active {
+ background-color: #993d00;
+ }
}
}
}
@@ -93,6 +181,6 @@ button.bad {
}
@media screen and (max-width: 1010px) {
.page {
- width: 100%;
+ width: 95%;
}
}
diff --git a/frontend/public/js/admin.js b/frontend/public/js/admin.js
index fb1d100..4987bd6 100644
--- a/frontend/public/js/admin.js
+++ b/frontend/public/js/admin.js
@@ -2,10 +2,10 @@ async function toggleState(setting_name, new_value, element_id) {
// Show spinner
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden");
- // Send request
- const form = new FormData();
- form.append("setting_name", setting_name);
- form.append("value", new_value);
+ const form = {
+ setting_name: setting_name,
+ value: JSON.stringify(new_value),
+ };
const response = await request("/setting", "POST", form);
@@ -25,7 +25,7 @@ async function toggleState(setting_name, new_value, element_id) {
qs(`#${element_id}`).children[0].innerText = new_text;
// Function
- const add_function = new_value ? "toggleState('ACCOUNT_REGISTRATION', false, this.id)" : "toggleState('ACCOUNT_REGISTRATION', true, this.id)";
+ const add_function = new_value ? `toggleState('${setting_name}', false, this.id)` : `toggleState('${setting_name}', true, this.id)`;
qs(`#${element_id}`).removeAttribute("onclick");
qs(`#${element_id}`).setAttribute("onclick", add_function);
}
diff --git a/frontend/public/js/blogSingle.js b/frontend/public/js/blogSingle.js
new file mode 100644
index 0000000..2133cb8
--- /dev/null
+++ b/frontend/public/js/blogSingle.js
@@ -0,0 +1,8 @@
+const delete_post_btn = qs("#delete-post");
+
+if (delete_post_btn) {
+ delete_post_btn.addEventListener("click", async () => {
+ let req = await request("/api/web/blog", "DELETE", { id: window.location.href.split("/")[4] });
+ if (req.body.success) location.reload();
+ });
+}
diff --git a/frontend/public/js/generic.js b/frontend/public/js/generic.js
index 29a7906..5072956 100644
--- a/frontend/public/js/generic.js
+++ b/frontend/public/js/generic.js
@@ -5,7 +5,10 @@ const qsa = (selector) => document.querySelectorAll(selector);
async function request(url, method, body) {
const response = await fetch(url, {
method: method,
- body: body,
+ headers: {
+ "Content-Type": "application/json", // Set the Content-Type header
+ },
+ body: JSON.stringify(body),
});
return { status: response.status, body: await response.json() };
}
diff --git a/frontend/public/js/login.js b/frontend/public/js/login.js
index a18bf0c..302a288 100644
--- a/frontend/public/js/login.js
+++ b/frontend/public/js/login.js
@@ -4,11 +4,7 @@ async function requestLogin() {
password: qs("#password").value,
};
- const form = new FormData();
- form.append("username", account_information.username);
- form.append("password", account_information.password);
-
- const account_response = await request("/login", "POST", form);
+ const account_response = await request("/login", "POST", account_information);
// Check response for errors
diff --git a/frontend/public/js/newBlog.js b/frontend/public/js/newBlog.js
index 90a021f..3711683 100644
--- a/frontend/public/js/newBlog.js
+++ b/frontend/public/js/newBlog.js
@@ -1,36 +1,207 @@
-let content_images = []; // List of all images to upload
+const blog_id = window.location.href.split("/")[4];
-const image_drop_area = qs(".e-image-area");
+let existing_images = [];
+let pending_images = [];
+let pending_thumbnail = {};
-// Stylize the drop area
-image_drop_area.addEventListener("dragover", (event) => {
- event.preventDefault();
- image_drop_area.style.border = "2px dashed #333";
-});
-image_drop_area.addEventListener("dragleave", () => {
- image_drop_area.style.border = "2px dashed #ccc";
+const thumbnail_area = qs(".e-thumbnail");
+const image_area = qs(".e-image-area");
+const text_area = qs(".e-content textarea");
+
+// Style
+function stylizeDropArea(element) {
+ // Drag over start
+ element.addEventListener("dragover", (e) => {
+ e.preventDefault();
+ e.target.classList.add("drag-over");
+ });
+
+ // Drag over leave
+ element.addEventListener("dragleave", (e) => {
+ e.target.classList.remove("drag-over");
+ });
+ // Do nothing on drop
+ element.addEventListener("drop", (e) => {
+ e.preventDefault();
+ });
+}
+
+// Auto expand blog area
+qs(".e-content textarea").addEventListener("input", (e) => {
+ e.target.style.height = e.target.scrollHeight + "px";
});
-image_drop_area.addEventListener("drop", async (e) => {
- e.preventDefault();
- image_drop_area.style.border = "2px dashed #ccc";
+stylizeDropArea(thumbnail_area);
+stylizeDropArea(image_area);
+// Upload an image to the blog post
+image_area.addEventListener("drop", async (e) => {
const files = e.dataTransfer.files;
for (let i = 0; i < files.length; i++) {
- let image_object = { id: crypto.randomUUID(), data_blob: new Blob([await files[i].arrayBuffer()]) };
- content_images.push(image_object);
+ // Each dropped image will be stored in this formatted object
+ const image_object = {
+ id: crypto.randomUUID(),
+ data_blob: new Blob([await files[i].arrayBuffer()]),
+ content_type: files[i].type,
+ };
+
+ // Add the image's data to the list
+ pending_images.push(image_object);
}
+
+ // Update the displayed images
updateImages();
- console.log(content_images);
});
+// Upload an image to the blog post
+thumbnail_area.addEventListener("drop", async (e) => {
+ const file = e.dataTransfer.files[0];
+
+ // The thumbnail will be stored in this formatted object
+ const image_object = {
+ id: crypto.randomUUID(),
+ data_blob: new Blob([await file.arrayBuffer()]),
+ content_type: file.type,
+ };
+
+ // Add the image's data to the list
+ pending_thumbnail = image_object;
+
+ // Update the visible thumbnail
+ qs(".e-thumbnail img").src = URL.createObjectURL(image_object.data_blob);
+
+ // Update the displayed images
+ updateImages();
+});
+
+// Publish or Update a blog post with the new data
+async function publishBlog(unlisted, edit) {
+ // Format our request we will send to the server
+ let form_data = {
+ title: qs("#title").value,
+ description: qs("#description").value,
+ content: qs("#content").value,
+ unlisted: unlisted ? true : false,
+ date: qs("#date").value,
+ time: qs("#time").value,
+ };
+
+ // 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) };
+ }
+
+ // We have images to upload
+ if (pending_images.length + existing_images.length > 0) {
+ // Initialize the image array
+ form_data.images = [];
+
+ // Read the image, convert to base64, update the existing variable with the base64
+ for (let i = 0; pending_images.length > i; i++) {
+ form_data.images.push({ ...pending_images[i], data_blob: await _readFile(pending_images[i].data_blob) });
+ }
+ }
+
+ // We are making edits to a post, not uploading a new one
+ if (edit) {
+ form_data.id = blog_id;
+ }
+
+ // Send the request!
+ const method = edit ? "PATCH" : "post";
+
+ const res = await request("/api/web/blog", method, form_data);
+
+ if (res.body.success) {
+ window.location.href = `/blog/${res.body.blog_id}`;
+ }
+}
+
+// Send a request to delete an image
+async function deleteImage(image_id) {
+ const res = await request("/api/web/blog/image", "delete", { id: image_id, parent: blog_id, parent_type: "blog" });
+ if (res.body.success) {
+ // Remove from existing images (If it exists)
+ let image = existing_images.find((item) => item.id === image_id);
+ if (image) existing_images.splice(existing_images.indexOf(image), 1);
+
+ image = pending_images.find((item) => item.id === image_id);
+ if (image) pending_images.splice(pending_images.indexOf(image), 1);
+
+ updateImages();
+ }
+}
+
+// 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);
+ });
+}
+
+function customDragString() {
+ const images = qsa(".e-image-area .image img");
+
+ images.forEach((image) => {
+ image.addEventListener("dragstart", (event) => {
+ event.dataTransfer.setData("text/plain", event.target.getAttribute("data-image_id"));
+ });
+ });
+}
+
function updateImages() {
+ const image_div = (img_id, img_url) =>
+ ``;
+
// Clear existing listings
qsa(".e-image-area .image").forEach((entry) => entry.remove());
- // Add new entries based on saved list
- content_images.forEach((image) => {
- image_drop_area.insertAdjacentHTML("beforeend", ``);
+ // Clear placeholder text
+ if (qs(".e-image-area .placeholder")) qs(".e-image-area .placeholder").remove();
+
+ existing_images.forEach((image) => {
+ image_area.insertAdjacentHTML("beforeend", image_div(image.id, image.url));
});
+
+ // Add new entries based on saved list
+ pending_images.forEach((image) => {
+ image_area.insertAdjacentHTML("beforeend", image_div(image.id, URL.createObjectURL(image.data_blob)));
+ });
+
+ customDragString();
}
+
+text_area.addEventListener("drop", (event) => {
+ event.preventDefault();
+
+ // Get the custom string from the drag data
+ const customString = `\{image:${event.dataTransfer.getData("text/plain")}\}\n`;
+
+ // Insert the custom string at the cursor position
+ const selectionStart = text_area.selectionStart;
+ const selectionEnd = text_area.selectionEnd;
+
+ const textBefore = text_area.value.substring(0, selectionStart);
+ const textAfter = text_area.value.substring(selectionEnd);
+
+ const updatedText = textBefore + customString + textAfter;
+
+ text_area.value = updatedText;
+
+ // Set the cursor position after the custom string
+ const newPosition = selectionStart + customString.length;
+ text_area.setSelectionRange(newPosition, newPosition);
+});
+
+// Load the existing images into our existing_images variable
+qsa(".e-image-area img").forEach((image) => {
+ existing_images.push({ id: image.getAttribute("data-image_id"), url: image.src });
+});
+
+updateImages();
diff --git a/frontend/public/js/register.js b/frontend/public/js/register.js
index 3c46079..ac27160 100644
--- a/frontend/public/js/register.js
+++ b/frontend/public/js/register.js
@@ -4,11 +4,7 @@ async function requestRegister() {
password: qs("#password").value,
};
- const form = new FormData();
- form.append("username", account_information.username);
- form.append("password", account_information.password);
-
- const account_response = await request("/register", "POST", form);
+ const account_response = await request("/register", "POST", account_information);
// Check response for errors
diff --git a/frontend/views/admin.ejs b/frontend/views/admin.ejs
index 3d9b026..c6f3451 100644
--- a/frontend/views/admin.ejs
+++ b/frontend/views/admin.ejs
@@ -21,13 +21,27 @@
- <% if (!settings.registration_enabled ) { %>
+ <% if (!settings.ACCOUNT_REGISTRATION ) { %>
<% } else { %>
<%}%>
+
+
diff --git a/frontend/views/author.ejs b/frontend/views/author.ejs
index ff0d0d5..e423cca 100644
--- a/frontend/views/author.ejs
+++ b/frontend/views/author.ejs
@@ -3,19 +3,30 @@
-
+
-
- <%= website_name %> | Author
+ <%= website_name %> | <%= blog_post.title %>
<%- include("partials/header.ejs", {selected: 'home'}) %>
-
+
+ <%if(logged_in_user) {%>
+
+
+ <%}%>
+
<%= blog_post.title%>
+
+ <%- blog_post.content %>
+
<%- include("partials/footer.ejs") %>