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