parent
57460c2328
commit
e39fce5f40
|
@ -4,4 +4,6 @@
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
/frontend/public/img/.dev
|
/frontend/public/img/.dev
|
||||||
/prisma/migrations
|
/prisma/migrations
|
||||||
|
/frontend/views/themes
|
||||||
|
!/frontend/views/themes/default
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
*.ejs
|
|
@ -608,6 +608,11 @@ async function postSetting(key, value) {
|
||||||
settings[key] = value;
|
settings[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh the theme if it was a theme change
|
||||||
|
if (key === "theme") {
|
||||||
|
// Refresh the theme
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, message: e.message };
|
return { success: false, message: e.message };
|
||||||
|
@ -627,6 +632,39 @@ async function editSetting({ name, value }) {
|
||||||
return _r(true);
|
return _r(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function installTheme(url, { requester_id } = {}) {
|
||||||
|
// User is admin?
|
||||||
|
let user = await getUser({ user_id: requester_id });
|
||||||
|
if (!user.success) return _r(false, "User does not exist.");
|
||||||
|
user = user.data;
|
||||||
|
if (user.role !== "ADMIN") return _r(false, "User is not permitted.");
|
||||||
|
|
||||||
|
// TODO: Test if git repo has valid manifest.json
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
|
||||||
|
execSync(`git clone ${url}`, {
|
||||||
|
stdio: [0, 1, 2], // we need this so node will print the command output
|
||||||
|
cwd: path.resolve(__dirname, "../../frontend/views/themes"), // path to where you want to save the file
|
||||||
|
});
|
||||||
|
return _r(true);
|
||||||
|
}
|
||||||
|
async function deleteTheme(name, { requester_id } = {}) {
|
||||||
|
let user = await getUser({ user_id: requester_id });
|
||||||
|
if (!user.success) return _r(false, "User does not exist.");
|
||||||
|
user = user.data;
|
||||||
|
if (user.role !== "ADMIN") return _r(false, "User is not permitted.");
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
if (!name) _r(false, "Panic! No theme specified");
|
||||||
|
|
||||||
|
execSync(`rm -r ${name}`, {
|
||||||
|
cwd: path.resolve(__dirname, "../../frontend/views/themes"),
|
||||||
|
});
|
||||||
|
return _r(true);
|
||||||
|
}
|
||||||
function _stripPrivatePost(post) {
|
function _stripPrivatePost(post) {
|
||||||
if (!post) return;
|
if (!post) return;
|
||||||
if (post.owner) delete post.owner.password;
|
if (post.owner) delete post.owner.password;
|
||||||
|
@ -636,4 +674,4 @@ const _r = (s, m) => {
|
||||||
return { success: s, message: m };
|
return { success: s, message: m };
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { settings, newUser, getUser, editUser, getPost, newPost, editPost, deletePost, getBiography, updateBiography, uploadMedia, getTags, postSetting, getSetting };
|
module.exports = { settings, newUser, getUser, editUser, getPost, newPost, editPost, deletePost, getBiography, updateBiography, uploadMedia, getTags, postSetting, getSetting, installTheme, deleteTheme };
|
||||||
|
|
|
@ -66,5 +66,11 @@ async function patchBiography(request, response) {
|
||||||
async function patchUser(request, response) {
|
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 }));
|
return response.json(await core.editUser({ requester_id: request.session.user.id, user_id: request.body.id, user_content: request.body }));
|
||||||
}
|
}
|
||||||
|
async function postTheme(request, response) {
|
||||||
|
return response.json(await core.installTheme(request.body.url, { requester_id: request.session.user.id }));
|
||||||
|
}
|
||||||
|
async function deleteTheme(request, response) {
|
||||||
|
return response.json(await core.deleteTheme(request.body.name, { requester_id: request.session.user.id }));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = { postRegister, patchBiography, postLogin, postSetting, postImage, deleteImage, deleteBlog, patchBlog, patchUser };
|
module.exports = { postRegister, patchBiography, postLogin, postSetting, postImage, deleteImage, deleteBlog, patchBlog, patchUser, postTheme, deleteTheme };
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
const external = require("./core/external_api");
|
const external = require("./core/external_api");
|
||||||
const core = require("./core/core");
|
const core = require("./core/core");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
function _getThemePage(page_name) {
|
function _getThemePage(page_name) {
|
||||||
let manifest = require(`../frontend/views/themes/${core.settings.theme}/manifest.json`);
|
let manifest = require(`../frontend/views/themes/${core.settings.theme}/manifest.json`);
|
||||||
|
@ -99,7 +101,17 @@ async function blogEdit(req, res) {
|
||||||
res.render(_getThemePage("postNew"), { ...(await getDefaults(req)), existing_blog: existing_blog });
|
res.render(_getThemePage("postNew"), { ...(await getDefaults(req)), existing_blog: existing_blog });
|
||||||
}
|
}
|
||||||
async function admin(request, response) {
|
async function admin(request, response) {
|
||||||
response.render(_getThemePage("admin-settings"), { ...(await getDefaults(request)) });
|
let theme_data = {
|
||||||
|
installed: [],
|
||||||
|
current: core.settings.theme,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get theme list
|
||||||
|
fs.readdir(path.resolve(__dirname, "../frontend/views/themes"), (err, files) => {
|
||||||
|
files.forEach((theme) => theme_data.installed.push(theme));
|
||||||
|
});
|
||||||
|
|
||||||
|
response.render(_getThemePage("admin-settings"), { ...(await getDefaults(request)), theme_data: theme_data });
|
||||||
}
|
}
|
||||||
async function atom(req, res) {
|
async function atom(req, res) {
|
||||||
res.type("application/xml");
|
res.type("application/xml");
|
||||||
|
|
|
@ -66,6 +66,45 @@
|
||||||
.page .page-center .setting-list .setting:nth-child(even) {
|
.page .page-center .setting-list .setting:nth-child(even) {
|
||||||
background-color: rgb(250, 250, 250);
|
background-color: rgb(250, 250, 250);
|
||||||
}
|
}
|
||||||
|
.page .page-center .theme-list .add-theme-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.page .page-center .theme-list .add-theme-area input {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.page .page-center .theme-list .add-theme-area button {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
.page .page-center .theme-list .entry {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
background-color: rgb(240, 240, 240);
|
||||||
|
padding: 0.1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.page .page-center .theme-list .entry .title {
|
||||||
|
margin: auto auto auto 0;
|
||||||
|
}
|
||||||
|
.page .page-center .theme-list .entry .value {
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.page .page-center .theme-list .entry .value button {
|
||||||
|
margin: auto;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.page .page-center .theme-list .entry:nth-child(even) {
|
||||||
|
background-color: rgb(250, 250, 250);
|
||||||
|
}
|
||||||
|
.page .page-center .theme-list .entry.active {
|
||||||
|
background-color: #85c8ff;
|
||||||
|
}
|
||||||
|
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -73,6 +73,52 @@
|
||||||
background-color: rgb(250, 250, 250);
|
background-color: rgb(250, 250, 250);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.theme-list {
|
||||||
|
.add-theme-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
input {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
background-color: rgb(240, 240, 240);
|
||||||
|
padding: 0.1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: auto auto auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: auto;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.entry:nth-child(even) {
|
||||||
|
background-color: rgb(250, 250, 250);
|
||||||
|
}
|
||||||
|
.entry.active {
|
||||||
|
background-color: #85c8ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch {
|
.switch {
|
||||||
|
|
|
@ -1,74 +1,99 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<%- include("partials/document-head.ejs") %>
|
<%- include("partials/document-head.ejs") %>
|
||||||
<link rel="stylesheet" type="text/css" href="../css/settings.css" />
|
<link rel="stylesheet" type="text/css" href="../css/settings.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../css/generic.css" />
|
<link rel="stylesheet" type="text/css" href="../css/generic.css" />
|
||||||
<script src="/js/generic.js"></script>
|
<script src="/js/generic.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%- include("partials/header.ejs") %>
|
<%- include("partials/header.ejs") %>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="page-center">
|
<div class="page-center">
|
||||||
<div class="header"><%= website_name %> Admin Settings</div>
|
<div class="header"><%= website_name %> Admin Settings</div>
|
||||||
<div class="setting-list">
|
<div class="setting-list">
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<div class="title">User registration</div>
|
<div class="title">User registration</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input <% if(settings.ACCOUNT_REGISTRATION) {% /> checked <% } %> id="ACCOUNT_REGISTRATION" onchange="toggleState(this.id, this)" type="checkbox" />
|
<input <% if(settings.ACCOUNT_REGISTRATION) {%> checked <% } %> id="ACCOUNT_REGISTRATION" onchange="toggleState(this.id, this)" type="checkbox" />
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<div class="title">Hide "Login" in navigation bar</div>
|
<div class="title">Hide "Login" in navigation bar</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input <% if(settings.HIDE_LOGIN) {% /> checked <% } %> id="HIDE_LOGIN" onchange="toggleState(this.id, this)" type="checkbox" />
|
<input <% if(settings.HIDE_LOGIN) {%> checked <% } %> id="HIDE_LOGIN" onchange="toggleState(this.id, this)" type="checkbox" />
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<div class="title">Serve ATOM feed</div>
|
<div class="title">Serve ATOM feed</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input <% if(settings.CD_RSS) {% /> checked <% } %> id="CD_RSS" onchange="toggleState(this.id, this)" type="checkbox" />
|
<input <% if(settings.CD_RSS) {%> checked <% } %> id="CD_RSS" onchange="toggleState(this.id, this)" type="checkbox" />
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<div class="title">Serve JSON feed</div>
|
<div class="title">Serve JSON feed</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input <% if(settings.CD_JSON) {% /> checked <% } %> id="CD_JSON" onchange="toggleState(this.id, this)" type="checkbox" />
|
<input <% if(settings.CD_JSON) {%> checked <% } %> id="CD_JSON" onchange="toggleState(this.id, this)" type="checkbox" />
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<div class="title">Password minimum length</div>
|
<div class="title">Password minimum length</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<input id="USER_MINIMUM_PASSWORD_LENGTH" value="<%- settings.USER_MINIMUM_PASSWORD_LENGTH -%>" onchange="changeValue(this.id, this)" type="number" />
|
<input id="USER_MINIMUM_PASSWORD_LENGTH" value="<%- settings.USER_MINIMUM_PASSWORD_LENGTH -%>" onchange="changeValue(this.id, this)" type="number" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<div class="title">Website Name</div>
|
<div class="title">Website Name</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<input id="WEBSITE_NAME" value="<%- settings.WEBSITE_NAME -%>" onchange="changeValue(this.id, this)" type="text" />
|
<input id="WEBSITE_NAME" value="<%- settings.WEBSITE_NAME -%>" onchange="changeValue(this.id, this)" type="text" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting largeset">
|
<div class="setting largeset">
|
||||||
<div class="title">Custom head</div>
|
<div class="title">Custom head</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<textarea id="CUSTOM_HEADER" onchange="changeValue(this.id, this)"><%= settings.CUSTOM_HEADER -%></textarea>
|
<textarea id="CUSTOM_HEADER" onchange="changeValue(this.id, this)" ><%= settings.CUSTOM_HEADER -%></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="page-center">
|
||||||
|
<div class="header">Themes</div>
|
||||||
|
|
||||||
|
<div class="theme-list">
|
||||||
|
<div class="add-theme-area">
|
||||||
|
<input id="theme-url" placeholder="Enter a git repository URL" type="text">
|
||||||
|
<button onclick="addTheme()">Add</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% for(theme of theme_data.installed) { %>
|
||||||
|
<!-- -->
|
||||||
|
<div class="entry <% if(theme == theme_data.current) {%> active <% } %>">
|
||||||
|
<div class="title"><%= theme %></div>
|
||||||
|
<div class="value">
|
||||||
|
<button onclick="setTheme('<%= theme %>')" type="button">Use</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- -->
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<%- include("partials/footer.ejs") %>
|
<%- include("partials/footer.ejs") %>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -22,3 +22,28 @@ async function changeValue(setting_name, element) {
|
||||||
if (response.body.success) {
|
if (response.body.success) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addTheme() {
|
||||||
|
const url = qs("#theme-url").value;
|
||||||
|
|
||||||
|
if (!url || url.length == 0) return false;
|
||||||
|
|
||||||
|
const response = await request("/api/theme", "POST", { url: url });
|
||||||
|
|
||||||
|
if (response.body.success) {
|
||||||
|
alert("Added theme.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setTheme(name) {
|
||||||
|
const form = {
|
||||||
|
setting_name: "theme",
|
||||||
|
value: name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request("/setting", "POST", form);
|
||||||
|
|
||||||
|
if (response.body.success) {
|
||||||
|
alert("Changed theme.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
10
yab.js
10
yab.js
|
@ -15,9 +15,11 @@ app.set("views", path.join(__dirname, "frontend/views"));
|
||||||
app.use(express.json({ limit: "500mb" }));
|
app.use(express.json({ limit: "500mb" }));
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
|
||||||
// TODO: Does this persist previous themes? May cause security issues!
|
app.use((req, res, next) => {
|
||||||
const refreshTheme = (theme_name) => app.use(express.static(path.join(__dirname, `frontend/views/themes/${theme_name}`)));
|
let theme = require("./backend/core/core").settings.theme;
|
||||||
refreshTheme("default");
|
let middleware = express.static(path.join(__dirname, `frontend/views/themes/${theme}`));
|
||||||
|
middleware(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
|
@ -34,9 +36,11 @@ app.post("/setting", checkAuthenticated, internal.postSetting);
|
||||||
app.post("/api/web/image", checkAuthenticated, internal.postImage);
|
app.post("/api/web/image", checkAuthenticated, internal.postImage);
|
||||||
app.delete("/api/web/post/image", checkAuthenticated, internal.deleteImage);
|
app.delete("/api/web/post/image", checkAuthenticated, internal.deleteImage);
|
||||||
app.delete("/api/web/post", checkAuthenticated, internal.deleteBlog);
|
app.delete("/api/web/post", checkAuthenticated, internal.deleteBlog);
|
||||||
|
app.delete("/api/theme", checkAuthenticated, internal.deleteTheme);
|
||||||
app.patch("/api/web/post", checkAuthenticated, internal.patchBlog);
|
app.patch("/api/web/post", checkAuthenticated, internal.patchBlog);
|
||||||
app.patch("/api/web/biography", checkAuthenticated, internal.patchBiography);
|
app.patch("/api/web/biography", checkAuthenticated, internal.patchBiography);
|
||||||
app.patch("/api/web/user", checkAuthenticated, internal.patchUser);
|
app.patch("/api/web/user", checkAuthenticated, internal.patchUser);
|
||||||
|
app.post("/api/theme", checkAuthenticated, internal.postTheme);
|
||||||
|
|
||||||
// app.delete("/logout", page_scripts.logout);
|
// app.delete("/logout", page_scripts.logout);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue