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
|
*.code-workspace
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
/frontend/public/img/.dev
|
/frontend/public/img/.dev
|
||||||
|
/prisma/migrations
|
|
@ -13,14 +13,25 @@ const s3 = new S3Client({
|
||||||
endpoint: process.env.S3_ENDPOINT,
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
});
|
});
|
||||||
const settings = require("../settings");
|
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) {
|
async function registerUser(username, password, options) {
|
||||||
const new_user = await prisma.user.create({ data: { username: username, password: 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 } = {}) {
|
async function getUser({ id, username } = {}) {
|
||||||
if (id || username) {
|
if (id || username) {
|
||||||
let user;
|
let user;
|
||||||
|
@ -31,5 +42,311 @@ async function getUser({ id, username } = {}) {
|
||||||
else return { success: true, data: user };
|
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");
|
const settings = require("../settings");
|
||||||
|
|
||||||
async function userRegistration(username, password) {
|
async function userRegistration(username, password) {
|
||||||
|
const active_settings = settings.getSettings();
|
||||||
if (!username) return { success: false, message: "No username provided" };
|
if (!username) return { success: false, message: "No username provided" };
|
||||||
if (!password) return { success: false, message: "No password provided" };
|
if (!password) return { success: false, message: "No password provided" };
|
||||||
// TODO: Admin customizable minimum password length
|
if (password.length < active_settings.USER_MINIMUM_PASSWORD_LENGTH) return { success: false, message: "Password not long enough" };
|
||||||
if (password.length < 4) return { success: false, message: "Password not long enough" };
|
|
||||||
|
|
||||||
// Check if username only uses URL safe characters
|
// 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
|
// All good! Validation complete
|
||||||
return { success: true };
|
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\-_.~]+$/;
|
const pattern = /^[A-Za-z0-9\-_.~]+$/;
|
||||||
return pattern.test(str);
|
return pattern.test(str);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@ const settings = require("../settings");
|
||||||
|
|
||||||
async function registerUser(username, password) {
|
async function registerUser(username, password) {
|
||||||
// Get current and relevant settings
|
// 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
|
const form_valid = await validate.userRegistration(username, password); // Check form for errors
|
||||||
|
|
||||||
// Set variables for easy reading
|
// Set variables for easy reading
|
||||||
const registration_allowed = s[0];
|
const registration_allowed = active_settings.ACCOUNT_REGISTRATION;
|
||||||
const setup_complete = s[1];
|
const setup_complete = active_settings.SETUP_COMPLETE;
|
||||||
|
|
||||||
if (!registration_allowed && setup_complete) return { success: false, message: "Registration is disabled" }; // Registration disabled
|
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
|
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 } };
|
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 internal = require("./core/internal_api");
|
||||||
|
const external = require("./core/external_api");
|
||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
const settings = require("./settings");
|
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) {
|
async function index(request, response) {
|
||||||
// Check if the master admin has been created
|
// Check if the master admin has been created
|
||||||
const is_setup_complete = (await settings.act("SETUP_COMPLETE")) || false;
|
const is_setup_complete = (await settings.act("SETUP_COMPLETE")) || false;
|
||||||
if (!is_setup_complete) return response.redirect("/register");
|
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) {
|
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) {
|
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) {
|
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) {
|
async function blogList(req, res) {
|
||||||
response.render("blogList.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME });
|
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) {
|
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) {
|
async function admin(request, response) {
|
||||||
const reg_allowed = await settings.userRegistrationAllowed();
|
response.render("admin.ejs", { ...getDefaults(request) });
|
||||||
response.render("admin.ejs", { user: request.session.user || null, website_name: process.env.WEBSITE_NAME, settings: { registration_enabled: reg_allowed } });
|
|
||||||
}
|
}
|
||||||
|
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) {
|
async function registerPost(request, response) {
|
||||||
const hashedPassword = await bcrypt.hash(request.body.password, 10); // Hash the password for security :^)
|
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 };
|
request.session.user = { username: login.data.username, id: login.data.id };
|
||||||
response.json({ success: true });
|
response.json({ success: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function settingPost(request, response) {
|
async function settingPost(request, response) {
|
||||||
const user = await internal.getUser({ id: request.session.user.id });
|
const user = await internal.getUser({ id: request.session.user.id });
|
||||||
|
|
||||||
if (!user.success) return response.json({ success: false, message: user.message });
|
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 (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 });
|
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 }) {}
|
function checkPermissions(role, { minimum = true }) {}
|
||||||
|
|
|
@ -4,9 +4,14 @@ persistent_setting.init({ dir: "data/site/" });
|
||||||
let settings = {
|
let settings = {
|
||||||
SETUP_COMPLETE: false,
|
SETUP_COMPLETE: false,
|
||||||
ACCOUNT_REGISTRATION: false,
|
ACCOUNT_REGISTRATION: false,
|
||||||
|
HIDE_LOGIN: false,
|
||||||
BLOG_UPLOADING: false,
|
BLOG_UPLOADING: false,
|
||||||
|
|
||||||
USER_MINIMUM_PASSWORD_LENGTH: 6,
|
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) {
|
async function act(key, value) {
|
||||||
|
|
|
@ -23,11 +23,16 @@
|
||||||
}
|
}
|
||||||
.blog-entry .thumbnail {
|
.blog-entry .thumbnail {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
}
|
}
|
||||||
.blog-entry .thumbnail img {
|
.blog-entry .thumbnail img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.blog-entry .blog-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
.blog-entry .blog-info .blog-title {
|
.blog-entry .blog-info .blog-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
border-bottom: 1px solid #9f9f9f;
|
border-bottom: 1px solid #9f9f9f;
|
||||||
|
@ -46,8 +51,40 @@
|
||||||
.blog-entry .blog-info .blog-description {
|
.blog-entry .blog-info .blog-description {
|
||||||
color: #9f9f9f;
|
color: #9f9f9f;
|
||||||
margin-top: 10px;
|
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 {
|
.blog-entry:last-of-type {
|
||||||
margin-bottom: inherit;
|
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 {
|
.thumbnail {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -35,6 +36,8 @@ $quiet-text: #9f9f9f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blog-info {
|
.blog-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.blog-title {
|
.blog-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
border-bottom: 1px solid $quiet-text;
|
border-bottom: 1px solid $quiet-text;
|
||||||
|
@ -54,6 +57,29 @@ $quiet-text: #9f9f9f;
|
||||||
.blog-description {
|
.blog-description {
|
||||||
color: $quiet-text;
|
color: $quiet-text;
|
||||||
margin-top: 10px;
|
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 {
|
.blog-entry:last-of-type {
|
||||||
margin-bottom: inherit;
|
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 {
|
.e-header .e-thumbnail img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 150px;
|
width: 100%;
|
||||||
|
-o-object-fit: cover;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.e-header .e-description {
|
.e-header .e-description {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -52,11 +54,20 @@
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.e-image-area .image img {
|
.e-image-area .image img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: auto;
|
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 {
|
.e-content {
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
|
@ -65,18 +76,19 @@
|
||||||
}
|
}
|
||||||
.e-content textarea {
|
.e-content textarea {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
min-height: 300px;
|
min-height: 200px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.e-settings {
|
.e-settings {
|
||||||
height: 40px;
|
min-height: 40px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
.e-settings .publish-date {
|
.e-settings .publish-date {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -91,6 +103,16 @@
|
||||||
.e-settings textarea {
|
.e-settings textarea {
|
||||||
width: 200px;
|
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,
|
input,
|
||||||
textarea {
|
textarea {
|
|
@ -15,7 +15,8 @@ $background-body: #222;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 150px;
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,11 +61,21 @@ $background-body: #222;
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: auto;
|
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;
|
margin-bottom: 10px;
|
||||||
textarea {
|
textarea {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
min-height: 300px;
|
min-height: 200px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.e-settings {
|
.e-settings {
|
||||||
height: 40px;
|
min-height: 40px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: $background-body;
|
background-color: $background-body;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 1rem;
|
||||||
.publish-date {
|
.publish-date {
|
||||||
display: flex;
|
display: flex;
|
||||||
div {
|
div {
|
||||||
|
@ -101,6 +113,18 @@ $background-body: #222;
|
||||||
textarea {
|
textarea {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.horizontal-buttons {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 200px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
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;
|
margin: 0;
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
color: white;
|
color: white;
|
||||||
|
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -58,11 +59,18 @@ button:focus {
|
||||||
background-color: #122d57;
|
background-color: #122d57;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.good {
|
button.good,
|
||||||
|
a.good {
|
||||||
background-color: #015b01;
|
background-color: #015b01;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.bad {
|
button.yellow,
|
||||||
|
a.yellow {
|
||||||
|
background-color: #4a4a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.bad,
|
||||||
|
a.bad {
|
||||||
background-color: #8e0000;
|
background-color: #8e0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,9 +83,78 @@ button.bad {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.page .horizontal-button-container button {
|
.page .horizontal-button-container button,
|
||||||
width: 100px;
|
.page .horizontal-button-container a {
|
||||||
|
width: 120px;
|
||||||
min-height: 30px;
|
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 {
|
.hidden {
|
||||||
|
@ -86,6 +163,6 @@ button.bad {
|
||||||
|
|
||||||
@media screen and (max-width: 1010px) {
|
@media screen and (max-width: 1010px) {
|
||||||
.page {
|
.page {
|
||||||
width: 100%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,6 +5,7 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
color: white;
|
color: white;
|
||||||
|
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -65,10 +66,16 @@ button:focus {
|
||||||
background-color: #122d57;
|
background-color: #122d57;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.good {
|
button.good,
|
||||||
|
a.good {
|
||||||
background-color: #015b01;
|
background-color: #015b01;
|
||||||
}
|
}
|
||||||
button.bad {
|
button.yellow,
|
||||||
|
a.yellow {
|
||||||
|
background-color: #4a4a00;
|
||||||
|
}
|
||||||
|
button.bad,
|
||||||
|
a.bad {
|
||||||
background-color: #8e0000;
|
background-color: #8e0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,9 +88,90 @@ button.bad {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
button {
|
button,
|
||||||
width: 100px;
|
a {
|
||||||
|
width: 120px;
|
||||||
min-height: 30px;
|
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) {
|
@media screen and (max-width: 1010px) {
|
||||||
.page {
|
.page {
|
||||||
width: 100%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ async function toggleState(setting_name, new_value, element_id) {
|
||||||
// Show spinner
|
// Show spinner
|
||||||
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden");
|
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden");
|
||||||
|
|
||||||
// Send request
|
const form = {
|
||||||
const form = new FormData();
|
setting_name: setting_name,
|
||||||
form.append("setting_name", setting_name);
|
value: JSON.stringify(new_value),
|
||||||
form.append("value", new_value);
|
};
|
||||||
|
|
||||||
const response = await request("/setting", "POST", form);
|
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;
|
qs(`#${element_id}`).children[0].innerText = new_text;
|
||||||
|
|
||||||
// Function
|
// 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}`).removeAttribute("onclick");
|
||||||
qs(`#${element_id}`).setAttribute("onclick", add_function);
|
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) {
|
async function request(url, method, body) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: method,
|
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() };
|
return { status: response.status, body: await response.json() };
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,7 @@ async function requestLogin() {
|
||||||
password: qs("#password").value,
|
password: qs("#password").value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const form = new FormData();
|
const account_response = await request("/login", "POST", account_information);
|
||||||
form.append("username", account_information.username);
|
|
||||||
form.append("password", account_information.password);
|
|
||||||
|
|
||||||
const account_response = await request("/login", "POST", form);
|
|
||||||
|
|
||||||
// Check response for errors
|
// 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
|
const thumbnail_area = qs(".e-thumbnail");
|
||||||
image_drop_area.addEventListener("dragover", (event) => {
|
const image_area = qs(".e-image-area");
|
||||||
event.preventDefault();
|
const text_area = qs(".e-content textarea");
|
||||||
image_drop_area.style.border = "2px dashed #333";
|
|
||||||
});
|
|
||||||
image_drop_area.addEventListener("dragleave", () => {
|
|
||||||
image_drop_area.style.border = "2px dashed #ccc";
|
|
||||||
});
|
|
||||||
|
|
||||||
image_drop_area.addEventListener("drop", async (e) => {
|
// Style
|
||||||
|
function stylizeDropArea(element) {
|
||||||
|
// Drag over start
|
||||||
|
element.addEventListener("dragover", (e) => {
|
||||||
e.preventDefault();
|
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;
|
const files = e.dataTransfer.files;
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
let image_object = { id: crypto.randomUUID(), data_blob: new Blob([await files[i].arrayBuffer()]) };
|
// Each dropped image will be stored in this formatted object
|
||||||
content_images.push(image_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();
|
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() {
|
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
|
// Clear existing listings
|
||||||
qsa(".e-image-area .image").forEach((entry) => entry.remove());
|
qsa(".e-image-area .image").forEach((entry) => entry.remove());
|
||||||
|
|
||||||
// Add new entries based on saved list
|
// Clear placeholder text
|
||||||
content_images.forEach((image) => {
|
if (qs(".e-image-area .placeholder")) qs(".e-image-area .placeholder").remove();
|
||||||
image_drop_area.insertAdjacentHTML("beforeend", `<div class="image"><img src="${URL.createObjectURL(image.data_blob)}" /></div>`);
|
|
||||||
|
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,
|
password: qs("#password").value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const form = new FormData();
|
const account_response = await request("/register", "POST", account_information);
|
||||||
form.append("username", account_information.username);
|
|
||||||
form.append("password", account_information.password);
|
|
||||||
|
|
||||||
const account_response = await request("/register", "POST", form);
|
|
||||||
|
|
||||||
// Check response for errors
|
// Check response for errors
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,27 @@
|
||||||
<div class="spinner hidden">
|
<div class="spinner hidden">
|
||||||
<div>↺</div>
|
<div>↺</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>
|
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', true, this.id)" class="bad"><div>Disabled</div></button>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', false, this.id)" class="good"><div>Enabled</div></button>
|
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', false, this.id)" class="good"><div>Enabled</div></button>
|
||||||
<%}%>
|
<%}%>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,19 +3,30 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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/theme.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/css/blog-list.css" />
|
|
||||||
<script src="/js/generic.js"></script>
|
<script src="/js/generic.js"></script>
|
||||||
<title><%= website_name %> | Author</title>
|
<title><%= website_name %> | <%= blog_post.title %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%- include("partials/header.ejs", {selected: 'home'}) %>
|
<%- 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") %>
|
<%- include("partials/footer.ejs") %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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'}) %>
|
<%- include("partials/header.ejs", {selected: 'home'}) %>
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<%- include("partials/blog-admin.ejs") %> <%- include("partials/blog-entry.ejs", {thumbnail: '/img/.dev/square.png', title: 'Title', description:
|
<%if(logged_in_user) {%> <%- include("partials/blog-admin.ejs") %> <%}%>
|
||||||
'Description', author: 'Author'}) %>
|
<!-- -->
|
||||||
|
<% for(post of blog_list) { %>
|
||||||
|
<!-- -->
|
||||||
|
<%- include("partials/blog-entry.ejs", {post:post}) %>
|
||||||
|
<!-- -->
|
||||||
|
<% } %> <%- include("partials/pagination.ejs") %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%- include("partials/footer.ejs") %>
|
<%- include("partials/footer.ejs") %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<script defer src="/js/login.js"></script>
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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" />
|
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
|
||||||
<script src="/js/generic.js"></script>
|
<script src="/js/generic.js"></script>
|
||||||
<title><%= website_name %> | New Blog</title>
|
<title><%= website_name %> | New Blog</title>
|
||||||
|
@ -14,27 +14,57 @@
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="e-header">
|
<div class="e-header">
|
||||||
<div class="e-thumbnail">
|
<div class="e-thumbnail">
|
||||||
|
<%if(existing_blog?.thumbnail) {%>
|
||||||
|
<img src="<%= existing_blog?.thumbnail %>" />
|
||||||
|
<%} else {%>
|
||||||
<img src="/img/.dev/square.png" />
|
<img src="/img/.dev/square.png" />
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<div class="e-description">
|
<div class="e-description">
|
||||||
<input type="text" placeholder="Title..." />
|
<input id="title" type="text" placeholder="Title..." value="<%= existing_blog.title %>" />
|
||||||
<textarea placeholder="Description..."></textarea>
|
<textarea id="description" placeholder="Description..."><%= existing_blog.description %></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="e-image-area">
|
<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 class="placeholder">Drop images here</div>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="e-content">
|
<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>
|
||||||
|
|
||||||
<div class="e-settings">
|
<div class="e-settings">
|
||||||
<div class="publish-date">
|
<div class="publish-date">
|
||||||
<div>Publish On</div>
|
<div>Publish On</div>
|
||||||
<input type="date" value="2023-09-26" />
|
<% if(existing_blog.publish_date) {%>
|
||||||
<input type="time" value="13:00" step="3600" />
|
<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>
|
</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="blog-admin">
|
||||||
<div class="horizontal-button-container">
|
<div class="horizontal-button-container">
|
||||||
<a href="/blog/new">New Post</a>
|
<a href="/blog/new"><span>New Post</span></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
<div class="blog-entry">
|
<div class="blog-entry">
|
||||||
<a src="#" class="thumbnail">
|
<a href="/blog/<%= post.id %>" class="thumbnail">
|
||||||
<img src="<%= thumbnail %>" />
|
<img src="<%= post.thumbnail %>" />
|
||||||
</a>
|
</a>
|
||||||
<div class="blog-info">
|
<div class="blog-info">
|
||||||
<div class="blog-title"><a href="#"><%= title %></a> <a href="#" class="author">By: <%= author %></a></div>
|
<div class="blog-title">
|
||||||
<div class="blog-description"><%= description %></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
<a class="nav-button" href="/blog">
|
<a class="nav-button" href="/blog">
|
||||||
<div>Blog</div>
|
<div>Blog</div>
|
||||||
</a>
|
</a>
|
||||||
<% if (user) { %>
|
<% if (logged_in_user) { %>
|
||||||
<a class="nav-button" href="/author/<%= user.username %>">
|
<a class="nav-button" href="/author/<%= logged_in_user.username %>">
|
||||||
<div>Profile</div>
|
<div>Profile</div>
|
||||||
</a>
|
</a>
|
||||||
<% } else {%>
|
<% } else {%> <% if(!settings.HIDE_LOGIN) {%>
|
||||||
<a class="nav-button" href="/login">
|
<a class="nav-button" href="/login">
|
||||||
<div>Login</div>
|
<div>Login</div>
|
||||||
</a>
|
</a>
|
||||||
<% } %>
|
<% } %> <% } %>
|
||||||
</div>
|
</div>
|
||||||
</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",
|
"ejs": "^3.1.9",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"feed": "^4.2.2",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-persist": "^3.1.3",
|
"node-persist": "^3.1.3",
|
||||||
|
@ -2358,6 +2359,17 @@
|
||||||
"fxparser": "src/cli/cli.js"
|
"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": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"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": {
|
"node_modules/semver": {
|
||||||
"version": "7.5.4",
|
"version": "7.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"ejs": "^3.1.9",
|
"ejs": "^3.1.9",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"feed": "^4.2.2",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-persist": "^3.1.3",
|
"node-persist": "^3.1.3",
|
||||||
|
|
|
@ -17,7 +17,7 @@ model User {
|
||||||
role Role @default(VISITOR)
|
role Role @default(VISITOR)
|
||||||
|
|
||||||
blog_posts BlogPost[]
|
blog_posts BlogPost[]
|
||||||
edits Edit[]
|
profile_page ProfilePage?
|
||||||
|
|
||||||
@@index([username, role])
|
@@index([username, role])
|
||||||
}
|
}
|
||||||
|
@ -25,29 +25,26 @@ model User {
|
||||||
model BlogPost {
|
model BlogPost {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
title String?
|
title String?
|
||||||
short_description String?
|
description String?
|
||||||
content String?
|
content String?
|
||||||
thumbnail String?
|
thumbnail String?
|
||||||
tags String[]
|
images String[]
|
||||||
status PostStatus @default(DRAFT)
|
visibility PostStatus @default(UNLISTED)
|
||||||
group Role @default(AUTHOR)
|
|
||||||
owner User? @relation(fields: [ownerid], references: [id])
|
owner User? @relation(fields: [ownerid], references: [id])
|
||||||
ownerid String?
|
ownerid String?
|
||||||
edits Edit[]
|
|
||||||
|
|
||||||
// Dates
|
// Dates
|
||||||
publish_date DateTime?
|
publish_date DateTime?
|
||||||
created_date DateTime @default(now())
|
created_date DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Edit {
|
model ProfilePage {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
|
content String?
|
||||||
|
images String[]
|
||||||
|
visibility PostStatus @default(UNLISTED)
|
||||||
owner User @relation(fields: [ownerid], references: [id])
|
owner User @relation(fields: [ownerid], references: [id])
|
||||||
ownerid String
|
ownerid String @unique
|
||||||
reason String? @default("No reason provided.")
|
|
||||||
|
|
||||||
blogpost BlogPost? @relation(fields: [blogpost_id], references: [id], onDelete: Cascade)
|
|
||||||
blogpost_id String?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
|
@ -58,13 +55,6 @@ enum Role {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PostStatus {
|
enum PostStatus {
|
||||||
DRAFT
|
UNLISTED
|
||||||
PUBLISHED
|
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
|
// Express
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const session = require("express-session");
|
const session = require("express-session");
|
||||||
|
@ -11,10 +5,6 @@ const app = express();
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
// Security and encryption
|
|
||||||
const bcrypt = require("bcrypt");
|
|
||||||
const crypto = require("crypto");
|
|
||||||
|
|
||||||
// Local modules
|
// Local modules
|
||||||
const page_scripts = require("./backend/page_scripts");
|
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("view-engine", "ejs");
|
||||||
app.set("views", path.join(__dirname, "frontend/views"));
|
app.set("views", path.join(__dirname, "frontend/views"));
|
||||||
app.use(express.static(path.join(__dirname, "frontend/public")));
|
app.use(express.static(path.join(__dirname, "frontend/public")));
|
||||||
const bodyParser = require("body-parser");
|
app.use(express.json({ limit: "500mb" }));
|
||||||
app.use(bodyParser.json());
|
app.use(express.urlencoded({ extended: false }));
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
|
||||||
app.use(upload.array());
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
secret: crypto.randomBytes(128).toString("base64"),
|
secret: require("crypto").randomBytes(128).toString("base64"),
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
})
|
})
|
||||||
|
@ -44,19 +32,21 @@ app.post("/register", checkNotAuthenticated, page_scripts.registerPost);
|
||||||
// Account Required Endpoints
|
// Account Required Endpoints
|
||||||
app.post("/setting", checkAuthenticated, page_scripts.settingPost);
|
app.post("/setting", checkAuthenticated, page_scripts.settingPost);
|
||||||
app.get("/blog/new", checkAuthenticated, page_scripts.blogNew);
|
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);
|
// app.delete("/logout", page_scripts.logout);
|
||||||
|
|
||||||
// Image Endpoints
|
|
||||||
// app.post("/api/image", checkAuthenticated, upload.fields([{ name: "image" }]), page_scripts.uploadImage);
|
|
||||||
|
|
||||||
// Endpoints
|
// Endpoints
|
||||||
app.get("/", page_scripts.index);
|
app.get("/", page_scripts.index);
|
||||||
app.get("/author/:author_username", page_scripts.author);
|
app.get("/author/:author_username", page_scripts.author);
|
||||||
app.get("/admin", checkAuthenticated, page_scripts.admin);
|
app.get("/admin", checkAuthenticated, page_scripts.admin);
|
||||||
app.get("/blog", page_scripts.blogList);
|
app.get("/blog", page_scripts.blogList);
|
||||||
// app.get("/blog/:id", page_scripts.blogSingle);
|
app.get("/blog/:blog_id", page_scripts.blogSingle);
|
||||||
// app.get("/projects", page_scripts.projectList);
|
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) {
|
function checkAuthenticated(req, res, next) {
|
||||||
if (req.session.user) return next();
|
if (req.session.user) return next();
|
||||||
|
|
Loading…
Reference in New Issue