Blog Uploading support (#2)
Provides basic functionality when uploading blog posts. Pending cleanup. Reviewed-on: #2 Co-authored-by: Armored-Dragon <forgejo3829105@armoreddragon.com> Co-committed-by: Armored-Dragon <forgejo3829105@armoreddragon.com>pull/2/head
parent
f5cab625ec
commit
624b46e345
|
@ -4,3 +4,4 @@
|
|||
*.code-workspace
|
||||
.vscode/settings.json
|
||||
/frontend/public/img/.dev
|
||||
/prisma/migrations
|
|
@ -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 `<div class='image-container'><img src='${image}'></div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
content = content.replace(side_by_side, (match, inner_content) => {
|
||||
return `<div class='side-by-side'>${inner_content}</div>`;
|
||||
});
|
||||
|
||||
// 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 };
|
||||
|
|
|
@ -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 };
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
// TODO: Permissions file
|
||||
|
||||
function checkPermissions(role, { minimum = true }) {}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
|
@ -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,
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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() };
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
image_drop_area.addEventListener("drop", async (e) => {
|
||||
// Style
|
||||
function stylizeDropArea(element) {
|
||||
// Drag over start
|
||||
element.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
image_drop_area.style.border = "2px dashed #ccc";
|
||||
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";
|
||||
});
|
||||
|
||||
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) =>
|
||||
`<div class="image"><img data-image_id="${img_id}" src="${img_url}" /><div><a onclick="deleteImage('${img_id}')">X</a></div></div>`;
|
||||
|
||||
// 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", `<div class="image"><img src="${URL.createObjectURL(image.data_blob)}" /></div>`);
|
||||
// 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();
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -21,13 +21,27 @@
|
|||
<div class="spinner hidden">
|
||||
<div>↺</div>
|
||||
</div>
|
||||
<% if (!settings.registration_enabled ) { %>
|
||||
<% if (!settings.ACCOUNT_REGISTRATION ) { %>
|
||||
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', true, this.id)" class="bad"><div>Disabled</div></button>
|
||||
<% } else { %>
|
||||
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', false, this.id)" class="good"><div>Enabled</div></button>
|
||||
<%}%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="label">Hide Login</div>
|
||||
<div class="button-group">
|
||||
<div class="spinner hidden">
|
||||
<div>↺</div>
|
||||
</div>
|
||||
<% if (!settings.HIDE_LOGIN ) { %>
|
||||
<button id="hidelogin_toggle" onclick="toggleState('HIDE_LOGIN', true, this.id)" class="bad"><div>Disabled</div></button>
|
||||
<% } else { %>
|
||||
<button id="hidelogin_toggle" onclick="toggleState('HIDE_LOGIN', false, this.id)" class="good"><div>Enabled</div></button>
|
||||
<%}%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,19 +3,30 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/index.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/blogSingle.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/blog-list.css" />
|
||||
<script src="/js/generic.js"></script>
|
||||
<title><%= website_name %> | Author</title>
|
||||
<title><%= website_name %> | <%= blog_post.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<%- include("partials/header.ejs", {selected: 'home'}) %>
|
||||
|
||||
<div class="page"></div>
|
||||
<div class="page">
|
||||
<%if(logged_in_user) {%>
|
||||
<div class="blog-admin">
|
||||
<div class="horizontal-button-container">
|
||||
<a class="yellow" href=""><span>Edit Profile</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%}%>
|
||||
<div class="title"><%= blog_post.title%></div>
|
||||
|
||||
<%- blog_post.content %>
|
||||
</div>
|
||||
|
||||
<%- include("partials/footer.ejs") %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script defer src="/js/login.js"></script>
|
||||
<script defer src="/js/blogSingle.js"></script>
|
||||
|
|
|
@ -13,12 +13,15 @@
|
|||
<%- include("partials/header.ejs", {selected: 'home'}) %>
|
||||
|
||||
<div class="page">
|
||||
<%- include("partials/blog-admin.ejs") %> <%- include("partials/blog-entry.ejs", {thumbnail: '/img/.dev/square.png', title: 'Title', description:
|
||||
'Description', author: 'Author'}) %>
|
||||
<%if(logged_in_user) {%> <%- include("partials/blog-admin.ejs") %> <%}%>
|
||||
<!-- -->
|
||||
<% for(post of blog_list) { %>
|
||||
<!-- -->
|
||||
<%- include("partials/blog-entry.ejs", {post:post}) %>
|
||||
<!-- -->
|
||||
<% } %> <%- include("partials/pagination.ejs") %>
|
||||
</div>
|
||||
|
||||
<%- include("partials/footer.ejs") %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script defer src="/js/login.js"></script>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/new-blog.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/blogNew.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
|
||||
<script src="/js/generic.js"></script>
|
||||
<title><%= website_name %> | New Blog</title>
|
||||
|
@ -14,27 +14,57 @@
|
|||
<div class="page">
|
||||
<div class="e-header">
|
||||
<div class="e-thumbnail">
|
||||
<%if(existing_blog?.thumbnail) {%>
|
||||
<img src="<%= existing_blog?.thumbnail %>" />
|
||||
<%} else {%>
|
||||
<img src="/img/.dev/square.png" />
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="e-description">
|
||||
<input type="text" placeholder="Title..." />
|
||||
<textarea placeholder="Description..."></textarea>
|
||||
<input id="title" type="text" placeholder="Title..." value="<%= existing_blog.title %>" />
|
||||
<textarea id="description" placeholder="Description..."><%= existing_blog.description %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="e-image-area">
|
||||
<% if(existing_blog.raw_images?.length) { %> <% for (image in existing_blog.raw_images) {%>
|
||||
<div class="image">
|
||||
<img data-image_id="<%=existing_blog.raw_images[image]%>" src="<%= existing_blog.images[image] %>" />
|
||||
<div><a onclick="deleteImage('<%= existing_blog.raw_images[image] %>')">X</a></div>
|
||||
</div>
|
||||
<%}%> <% } else {%>
|
||||
<div class="placeholder">Drop images here</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="e-content">
|
||||
<textarea placeholder="Tell us about your subject..."></textarea>
|
||||
<textarea id="content" placeholder="Tell us about your subject..."><%= existing_blog.raw_content %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="e-settings">
|
||||
<div class="publish-date">
|
||||
<div>Publish On</div>
|
||||
<input type="date" value="2023-09-26" />
|
||||
<input type="time" value="13:00" step="3600" />
|
||||
<% if(existing_blog.publish_date) {%>
|
||||
<input id="date" type="date" value="<%= existing_blog.publish_date %>" />
|
||||
<%} else { %>
|
||||
<input id="date" type="date" value="1990-01-01" />
|
||||
<% } %>
|
||||
<!-- -->
|
||||
<% if(existing_blog.publish_date) {%>
|
||||
<input id="time" type="time" step="3600" value="<%= existing_blog.publish_time %>" />
|
||||
<%} else { %>
|
||||
<input id="time" type="time" value="13:00" step="3600" />
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="e-settings">
|
||||
<div class="horizontal-buttons">
|
||||
<button onclick="publishBlog(true)" class="yellow"><span>Unlisted</span></button>
|
||||
<% if(existing_blog.id){%>
|
||||
<button onclick="publishBlog(false, true)"><span>Edit</span></button>
|
||||
<% } else {%>
|
||||
<button onclick="publishBlog()"><span>Publish</span></button>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/blogSingle.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
|
||||
<script src="/js/generic.js"></script>
|
||||
<title><%= website_name %> | <%= blog_post.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<%- include("partials/header.ejs", {selected: 'home'}) %>
|
||||
|
||||
<div class="page">
|
||||
<!-- TODO: Check if user is the owner or an admin -->
|
||||
<!-- TODO: Confirmation before deleting post -->
|
||||
<%if(logged_in_user) {%>
|
||||
<div class="blog-admin">
|
||||
<div class="horizontal-button-container">
|
||||
<a id="delete-post" href="#" class="bad"><span>Delete Post</span></a>
|
||||
<a class="yellow" href="/blog/<%=blog_post.id %>/edit"><span>Edit Post</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%}%>
|
||||
<div class="title"><%= blog_post.title%></div>
|
||||
|
||||
<%- blog_post.content %>
|
||||
</div>
|
||||
|
||||
<%- include("partials/footer.ejs") %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script defer src="/js/blogSingle.js"></script>
|
|
@ -1,5 +1,5 @@
|
|||
<div class="blog-admin">
|
||||
<div class="horizontal-button-container">
|
||||
<a href="/blog/new">New Post</a>
|
||||
<a href="/blog/new"><span>New Post</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
<div class="blog-entry">
|
||||
<a src="#" class="thumbnail">
|
||||
<img src="<%= thumbnail %>" />
|
||||
<a href="/blog/<%= post.id %>" class="thumbnail">
|
||||
<img src="<%= post.thumbnail %>" />
|
||||
</a>
|
||||
<div class="blog-info">
|
||||
<div class="blog-title"><a href="#"><%= title %></a> <a href="#" class="author">By: <%= author %></a></div>
|
||||
<div class="blog-description"><%= description %></div>
|
||||
<div class="blog-title">
|
||||
<a href="/blog/<%= post.id %>"><%= post.title %> </a>
|
||||
<a href="/author/<%= post.owner.id %>" class="author">By: <%= post.owner.username %></a>
|
||||
</div>
|
||||
<div class="blog-description"><%= post.description %></div>
|
||||
<div class="blog-action">
|
||||
<div class="date"><%= post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) %></div>
|
||||
<a href="/blog/<%= post.id %>">Read this post -></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
<a class="nav-button" href="/blog">
|
||||
<div>Blog</div>
|
||||
</a>
|
||||
<% if (user) { %>
|
||||
<a class="nav-button" href="/author/<%= user.username %>">
|
||||
<% if (logged_in_user) { %>
|
||||
<a class="nav-button" href="/author/<%= logged_in_user.username %>">
|
||||
<div>Profile</div>
|
||||
</a>
|
||||
<% } else {%>
|
||||
<% } else {%> <% if(!settings.HIDE_LOGIN) {%>
|
||||
<a class="nav-button" href="/login">
|
||||
<div>Login</div>
|
||||
</a>
|
||||
<% } %>
|
||||
<% } %> <% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<div class="pagination">
|
||||
<% if(pagination.includes(Number(current_page) - 1)) {%>
|
||||
<a href="<%= loaded_page %>?page=<%= Number(current_page) - 1 %>">
|
||||
<span>Previous</span>
|
||||
</a>
|
||||
<% } else {%>
|
||||
<a class="disabled">
|
||||
<span>Previous</span>
|
||||
</a>
|
||||
<%}%>
|
||||
|
||||
<div class="page-list">
|
||||
<% for(page of pagination) { %> <% if (page == current_page) {%>
|
||||
<a href="#" class="active"><span><%=page + 1%></span></a>
|
||||
<% } else { %>
|
||||
<a href="<%= loaded_page %>?page=<%=page %>" class=""><span><%=page + 1%></span></a>
|
||||
<% } %> <% } %>
|
||||
</div>
|
||||
|
||||
<% if(pagination.includes(Number(current_page) + 1)) {%>
|
||||
<a href="<%= loaded_page %>?page=<%= Number(current_page) + 1 %>">
|
||||
<span>Next</span>
|
||||
</a>
|
||||
<% } else {%>
|
||||
<a class="disabled">
|
||||
<span>Next</span>
|
||||
</a>
|
||||
<%}%>
|
||||
</div>
|
|
@ -17,6 +17,7 @@
|
|||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"feed": "^4.2.2",
|
||||
"markdown-it": "^13.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-persist": "^3.1.3",
|
||||
|
@ -2358,6 +2359,17 @@
|
|||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/feed": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz",
|
||||
"integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==",
|
||||
"dependencies": {
|
||||
"xml-js": "^1.6.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
|
@ -3459,6 +3471,11 @@
|
|||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
|
||||
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
|
@ -3993,6 +4010,17 @@
|
|||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"feed": "^4.2.2",
|
||||
"markdown-it": "^13.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-persist": "^3.1.3",
|
||||
|
|
|
@ -17,7 +17,7 @@ model User {
|
|||
role Role @default(VISITOR)
|
||||
|
||||
blog_posts BlogPost[]
|
||||
edits Edit[]
|
||||
profile_page ProfilePage?
|
||||
|
||||
@@index([username, role])
|
||||
}
|
||||
|
@ -25,29 +25,26 @@ model User {
|
|||
model BlogPost {
|
||||
id String @id @unique @default(uuid())
|
||||
title String?
|
||||
short_description String?
|
||||
description String?
|
||||
content String?
|
||||
thumbnail String?
|
||||
tags String[]
|
||||
status PostStatus @default(DRAFT)
|
||||
group Role @default(AUTHOR)
|
||||
images String[]
|
||||
visibility PostStatus @default(UNLISTED)
|
||||
owner User? @relation(fields: [ownerid], references: [id])
|
||||
ownerid String?
|
||||
edits Edit[]
|
||||
|
||||
// Dates
|
||||
publish_date DateTime?
|
||||
created_date DateTime @default(now())
|
||||
}
|
||||
|
||||
model Edit {
|
||||
model ProfilePage {
|
||||
id String @id @unique @default(uuid())
|
||||
content String?
|
||||
images String[]
|
||||
visibility PostStatus @default(UNLISTED)
|
||||
owner User @relation(fields: [ownerid], references: [id])
|
||||
ownerid String
|
||||
reason String? @default("No reason provided.")
|
||||
|
||||
blogpost BlogPost? @relation(fields: [blogpost_id], references: [id], onDelete: Cascade)
|
||||
blogpost_id String?
|
||||
ownerid String @unique
|
||||
}
|
||||
|
||||
enum Role {
|
||||
|
@ -58,13 +55,6 @@ enum Role {
|
|||
}
|
||||
|
||||
enum PostStatus {
|
||||
DRAFT
|
||||
UNLISTED
|
||||
PUBLISHED
|
||||
}
|
||||
|
||||
enum ProjectStatus {
|
||||
DELAYED
|
||||
IN_PROGRESS
|
||||
SUCCESS
|
||||
FAILURE
|
||||
}
|
||||
|
|
32
yab.js
32
yab.js
|
@ -1,9 +1,3 @@
|
|||
// Multer file handling
|
||||
// REVIEW: Look for a way to drop this dependency?
|
||||
const multer = require("multer");
|
||||
const multer_storage = multer.memoryStorage();
|
||||
const upload = multer({ storage: multer_storage });
|
||||
|
||||
// Express
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
|
@ -11,10 +5,6 @@ const app = express();
|
|||
|
||||
const path = require("path");
|
||||
|
||||
// Security and encryption
|
||||
const bcrypt = require("bcrypt");
|
||||
const crypto = require("crypto");
|
||||
|
||||
// Local modules
|
||||
const page_scripts = require("./backend/page_scripts");
|
||||
|
||||
|
@ -22,14 +12,12 @@ const page_scripts = require("./backend/page_scripts");
|
|||
app.set("view-engine", "ejs");
|
||||
app.set("views", path.join(__dirname, "frontend/views"));
|
||||
app.use(express.static(path.join(__dirname, "frontend/public")));
|
||||
const bodyParser = require("body-parser");
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(upload.array());
|
||||
app.use(express.json({ limit: "500mb" }));
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: crypto.randomBytes(128).toString("base64"),
|
||||
secret: require("crypto").randomBytes(128).toString("base64"),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
})
|
||||
|
@ -44,19 +32,21 @@ app.post("/register", checkNotAuthenticated, page_scripts.registerPost);
|
|||
// Account Required Endpoints
|
||||
app.post("/setting", checkAuthenticated, page_scripts.settingPost);
|
||||
app.get("/blog/new", checkAuthenticated, page_scripts.blogNew);
|
||||
// app.post("/blog", checkAuthenticated, page_scripts.postBlog);
|
||||
app.post("/api/web/blog", checkAuthenticated, page_scripts.postBlog);
|
||||
app.delete("/api/web/blog", checkAuthenticated, page_scripts.deleteBlog);
|
||||
app.patch("/api/web/blog", checkAuthenticated, page_scripts.updateBlog);
|
||||
app.delete("/api/web/blog/image", checkAuthenticated, page_scripts.deleteImage);
|
||||
// app.delete("/logout", page_scripts.logout);
|
||||
|
||||
// Image Endpoints
|
||||
// app.post("/api/image", checkAuthenticated, upload.fields([{ name: "image" }]), page_scripts.uploadImage);
|
||||
|
||||
// Endpoints
|
||||
app.get("/", page_scripts.index);
|
||||
app.get("/author/:author_username", page_scripts.author);
|
||||
app.get("/admin", checkAuthenticated, page_scripts.admin);
|
||||
app.get("/blog", page_scripts.blogList);
|
||||
// app.get("/blog/:id", page_scripts.blogSingle);
|
||||
// app.get("/projects", page_scripts.projectList);
|
||||
app.get("/blog/:blog_id", page_scripts.blogSingle);
|
||||
app.get("/blog/:blog_id/edit", checkAuthenticated, page_scripts.blogEdit);
|
||||
app.get("/atom", page_scripts.atom);
|
||||
// app.get("/rss", page_scripts.rss);
|
||||
|
||||
function checkAuthenticated(req, res, next) {
|
||||
if (req.session.user) return next();
|
||||
|
|
Loading…
Reference in New Issue