Compare commits

...

3 Commits

Author SHA1 Message Date
Armored Dragon 1d1e2795b0
Publish date autofill to now.
Fix deleteBlog.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>
2024-04-24 08:51:16 -05:00
Armored Dragon 64c49dca11
Post visibility flairs
Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>
2024-04-24 08:29:28 -05:00
Armored Dragon c1c5e5ca1d
Post drafts
Users can now only have one "unpublished" draft.
Improved password handling.
Minor cleanup.
Admin panel navigation link.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>
2024-04-24 07:30:02 -05:00
10 changed files with 131 additions and 150 deletions

View File

@ -80,7 +80,7 @@ async function newUser({ username, password, role } = {}) {
// Master user was created; server initialized
editSetting({ name: "SETUP_COMPLETE", value: true });
}
async function getUser({ user_id, username }) {
async function getUser({ user_id, username, include_password = false }) {
if (!username && !user_id) return _r(false, "Either a user_id or username is needed.");
let user;
@ -89,7 +89,11 @@ async function getUser({ user_id, username }) {
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 };
// Delete the password from responses
if (!include_password) delete user.password;
return { success: true, data: user };
}
async function editUser({ requester_id, user_id, user_content }) {
let user = await getUser({ user_id: user_id });
@ -120,11 +124,15 @@ async function deleteUser({ user_id }) {
// Posts
async function newPost({ requester_id }) {
// const user = await getUser({ id: requester_id });
const post = await prisma.post.create({ data: { owner: { connect: { id: requester_id } } } });
// TODO: Validate request (Does user have perms?)
// TODO: Does server allow new posts?
// Find if user already has a draft
let existing_post = await prisma.post.findFirst({ where: { owner: { id: requester_id }, visibility: "DRAFT" } });
if (existing_post) return existing_post.id;
const post = await prisma.post.create({ data: { owner: { connect: { id: requester_id } } } });
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 } = {}) {
@ -322,8 +330,8 @@ async function updateBiography({ requester_id, author_id, biography_content }) {
}
// TODO: Replace
async function deleteBlog(blog_id, requester_id) {
const user = await getUser({ id: requester_id });
const post = await getBlog({ id: blog_id });
const user = await getUser({ user_id: requester_id });
const post = await getPost({ post_id: blog_id });
if (!post.success) return { success: false, message: post.message || "Post does not exist" };

View File

@ -24,7 +24,7 @@ async function postLogin(req, res) {
const { username, password } = req.body; // Get the username and password from the request body
// Get the user by username
const existing_user = await core.getUser({ username: username });
const existing_user = await core.getUser({ username: username, include_password: true });
if (!existing_user.success) return res.json({ success: false, message: existing_user.message });
// Check the password

View File

@ -1,12 +1,15 @@
const external = require("./core/external_api");
const core = require("./core/core");
function getThemePage(page_name) {
function _getThemePage(page_name) {
return `themes/${core.settings.theme}/ejs/${page_name}.ejs`;
}
function getDefaults(req) {
async function getDefaults(req) {
// TODO: Fix reference to website_name
return { logged_in_user: req.session.user, website_name: core.settings.WEBSITE_NAME || "Yet-Another-Blog", settings: core.settings };
let user;
if (req.session.user) user = await core.getUser({ user_id: req.session.user.id });
if (user?.success) user = user.data;
return { logged_in_user: user, website_name: core.settings.WEBSITE_NAME || "Yet-Another-Blog", settings: core.settings };
}
async function index(request, response) {
// Check if the master admin has been created
@ -22,8 +25,8 @@ async function index(request, response) {
post.publish_date = formatted_date;
});
response.render(getThemePage("index"), {
...getDefaults(request),
response.render(_getThemePage("index"), {
...(await getDefaults(request)),
blog_list: blog_list.data,
pagination: blog_list.pagination,
current_page: request.query.page || 0,
@ -31,11 +34,11 @@ async function index(request, response) {
tags: tags,
});
}
function register(request, response) {
response.render(getThemePage("register"), getDefaults(request));
async function register(request, response) {
response.render(_getThemePage("register"), await getDefaults(request));
}
function login(request, response) {
response.render(getThemePage("login"), getDefaults(request));
async function login(request, response) {
response.render(_getThemePage("login"), await getDefaults(request));
}
async function author(req, res) {
const user = await core.getUser({ user_id: req.params.author_id });
@ -43,15 +46,14 @@ async function author(req, res) {
if (!user.success) return res.redirect("/");
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 } });
res.render(_getThemePage("author"), { ...(await 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 });
response.render(_getThemePage("authorEdit"), { ...(await getDefaults(request)), profile: author.data });
}
async function blogList(req, res) {
const blog_list = await core.getPost({ requester_id: req.session.user?.id }, { search: req.query.search, search_title: true, search_tags: true, search_content: true });
@ -62,8 +64,8 @@ async function blogList(req, res) {
post.publish_date = formatted_date;
});
res.render(getThemePage("postSearch"), {
...getDefaults(req),
res.render(_getThemePage("postSearch"), {
...(await getDefaults(req)),
blog_list: blog_list.data,
pagination: blog_list.pagination,
current_page: req.query.page || 0,
@ -73,7 +75,7 @@ async function blogList(req, res) {
async function blogSingle(req, res) {
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 });
res.render(_getThemePage("post"), { ...(await getDefaults(req)), blog_post: blog.data });
}
async function blogNew(request, response) {
const new_post = await core.newPost({ requester_id: request.session.user.id });
@ -81,7 +83,8 @@ async function blogNew(request, response) {
}
async function blogEdit(req, res) {
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
if (!existing_blog.success) return res.redirect("/");
existing_blog = existing_blog.data;
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")}`;
@ -91,10 +94,10 @@ async function blogEdit(req, res) {
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;
res.render(getThemePage("postNew"), { ...getDefaults(req), existing_blog: existing_blog });
res.render(_getThemePage("postNew"), { ...(await getDefaults(req)), existing_blog: existing_blog });
}
async function admin(request, response) {
response.render(getThemePage("admin-settings"), { ...getDefaults(request) });
response.render(_getThemePage("admin-settings"), { ...(await getDefaults(request)) });
}
async function atom(req, res) {
res.type("application/xml");

View File

@ -1,105 +0,0 @@
<!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/blogNew.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | New Blog</title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<div class="e-header">
<div class="e-thumbnail">
<%if(existing_blog?.thumbnail) {%>
<img src="<%= existing_blog?.thumbnail %>" />
<%} else {%>
<img src="/img/.dev/square.png" />
<% } %>
</div>
<div class="e-description">
<input id="title" type="text" placeholder="Title..." value="<%= existing_blog.title %>" />
<textarea id="description" placeholder="Description..."><%= existing_blog.description %></textarea>
</div>
</div>
<div class="e-image-area">
<% if(existing_blog.raw_images?.length) { %> <% for (image in existing_blog.raw_images) {%>
<div class="image">
<img data-image_id="<%=existing_blog.raw_images[image]%>" src="<%= existing_blog.images[image] %>" />
<div><a onclick="deleteImage('<%= existing_blog.raw_images[image] %>')">X</a></div>
</div>
<%}%> <% } else {%>
<div class="placeholder">Drop images here</div>
<% } %>
</div>
<div class="e-content">
<div class="text-actions">
<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="content" placeholder="Tell us about your subject..."><%= existing_blog.raw_content %></textarea>
</div>
<div class="e-tags">
<input id="tags" type="text" placeholder="Enter a comma separated list of tags" value="<%= existing_blog.tags %>" />
</div>
<div class="e-settings">
<div class="publish-date">
<div>Publish On</div>
<% if(existing_blog.publish_date) {%>
<input id="date" type="date" value="<%= existing_blog.publish_date %>" />
<%} else { %>
<input id="date" type="date" value="1990-01-01" />
<% } %>
<!-- -->
<% if(existing_blog.publish_date) {%>
<input id="time" type="time" value="<%= existing_blog.publish_time %>" />
<%} else { %>
<input id="time" type="time" value="13:00" />
<% } %>
</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>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/newBlog.js"></script>

View File

@ -1,16 +0,0 @@
<div class="blog-entry">
<a href="/blog/<%= post.id %>" class="thumbnail">
<img src="<%= post.thumbnail %>" />
</a>
<div class="blog-info">
<div class="blog-title">
<a href="/blog/<%= post.id %>"><%= post.title %> </a>
<a href="/author/<%= post.owner.id %>" class="author">By: <%= post.owner.username %></a>
</div>
<div class="blog-description"><%= post.description %></div>
<div class="blog-action">
<div class="date"><%= post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) %></div>
<a href="/blog/<%= post.id %>">Read this post -></a>
</div>
</div>
</div>

View File

@ -203,6 +203,40 @@ body {
box-sizing: border-box;
}
.info .info-blip.visibility-flag {
margin-left: auto;
font-size: 0.9rem;
display: flex;
padding: 0 0.5rem;
border-radius: 5px;
border: 2px solid;
}
.info .info-blip.visibility-flag span {
margin: auto;
color: black;
}
.info .visibility-flag.published {
border-color: #21b525;
background-color: #a0ffa0;
}
.info .visibility-flag.unlisted {
border-color: #bec10f;
background-color: #e8ffa0;
}
.info .visibility-flag.private {
border-color: #c10f0f;
background-color: #ffd7d7;
}
.info .visibility-flag.draft {
border-color: black;
background-color: rgba(0, 0, 0, 0.0588235294);
}
.info .visibility-flag.scheduled {
border-color: #0f77c1;
background-color: #d7e9ff;
margin-left: inherit;
}
@media screen and (max-width: 1280px) {
.page-center {
width: 95%;

View File

@ -217,7 +217,47 @@ body {
box-sizing: border-box;
}
}
.info {
.info-blip.visibility-flag {
margin-left: auto;
font-size: 0.9rem;
display: flex;
padding: 0 0.5rem;
border-radius: 5px;
border: 2px solid;
span {
margin: auto;
color: black;
}
}
.visibility-flag.published {
border-color: #21b525;
background-color: #a0ffa0;
}
.visibility-flag.unlisted {
border-color: #bec10f;
background-color: #e8ffa0;
}
.visibility-flag.private {
border-color: #c10f0f;
background-color: #ffd7d7;
}
.visibility-flag.draft {
border-color: black;
background-color: #0000000f;
}
.visibility-flag.scheduled {
border-color: #0f77c1;
background-color: #d7e9ff;
margin-left: inherit;
}
}
@media screen and (max-width: 1280px) {
.page-center {
width: 95%;

View File

@ -9,6 +9,7 @@
<a href="/posts">
<span>Posts</span>
</a>
<!-- -->
<% if (logged_in_user) { %>
<a href="/author/<%= logged_in_user.id %>">
<span>Account</span>
@ -18,6 +19,12 @@
<span>Login</span>
</a>
<% } %> <% } %>
<!-- -->
<% if (logged_in_user?.role == 'ADMIN') { %>
<a href="/admin">
<span>Admin</span>
</a>
<% } %>
</div>
</div>
</div>

View File

@ -4,8 +4,17 @@
<div class="description"><%= post.description %></div>
<div class="badges">
<div class="info">
<div class="info-blip icon publish-date"><%= post.publish_date ? post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) : "Null" %></div>
<div class="info-blip icon publish-date"><%= post.publish_date ? post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) : "Unknown Publish Date" %></div>
<div class="info-blip icon reading-time">Null minute read</div>
<% if (logged_in_user) { %>
<!-- -->
<div class="info-blip visibility-flag <%= post.visibility.toLowerCase() %>"><span><%= post.visibility %></span></div>
<!-- -->
<% if (new Date(post.publish_date) > new Date() && post.visibility !== 'PRIVATE') {%>
<div class="info-blip visibility-flag scheduled"><span>Scheduled</span></div>
<% } %>
<!-- -->
<% } %>
</div>
</div>
</div>

View File

@ -30,7 +30,7 @@ model Post {
description String?
content String?
media String[]
visibility PostStatus @default(UNLISTED)
visibility PostStatus @default(DRAFT)
owner User? @relation(fields: [ownerid], references: [id], onDelete: Cascade)
ownerid String?
@ -38,7 +38,7 @@ model Post {
tags Tag[]
// Dates
publish_date DateTime?
publish_date DateTime? @default(now())
created_date DateTime @default(now())
}
@ -81,6 +81,7 @@ enum Role {
}
enum PostStatus {
DRAFT
PRIVATE
UNLISTED
PUBLISHED