From e39fce5f407a3fe73eb84061c1e37ef5a02721ca Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Mon, 8 Jul 2024 18:24:31 +0000 Subject: [PATCH] Theme support (#6) Signed-off-by: Armored Dragon --- .gitignore | 4 +- .prettierignore | 1 + backend/core/core.js | 40 ++++- backend/core/internal_api.js | 8 +- backend/page_scripts.js | 14 +- .../views/themes/default/css/settings.css | 39 +++++ .../views/themes/default/css/settings.scss | 46 +++++ .../themes/default/ejs/admin-settings.ejs | 163 ++++++++++-------- frontend/views/themes/default/js/admin.js | 25 +++ yab.js | 10 +- 10 files changed, 274 insertions(+), 76 deletions(-) create mode 100644 .prettierignore diff --git a/.gitignore b/.gitignore index db2f430..c000006 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ *.code-workspace .vscode/settings.json /frontend/public/img/.dev -/prisma/migrations \ No newline at end of file +/prisma/migrations +/frontend/views/themes +!/frontend/views/themes/default diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..82435b1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.ejs \ No newline at end of file diff --git a/backend/core/core.js b/backend/core/core.js index 7d48cfd..03c3bfc 100644 --- a/backend/core/core.js +++ b/backend/core/core.js @@ -608,6 +608,11 @@ async function postSetting(key, value) { settings[key] = value; } + // Refresh the theme if it was a theme change + if (key === "theme") { + // Refresh the theme + } + return { success: true }; } catch (e) { return { success: false, message: e.message }; @@ -627,6 +632,39 @@ async function editSetting({ name, value }) { 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) { if (!post) return; if (post.owner) delete post.owner.password; @@ -636,4 +674,4 @@ const _r = (s, 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 }; diff --git a/backend/core/internal_api.js b/backend/core/internal_api.js index 3adf840..21b12f3 100644 --- a/backend/core/internal_api.js +++ b/backend/core/internal_api.js @@ -66,5 +66,11 @@ async function patchBiography(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 })); } +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 }; diff --git a/backend/page_scripts.js b/backend/page_scripts.js index 58943ee..b85d8bb 100644 --- a/backend/page_scripts.js +++ b/backend/page_scripts.js @@ -1,5 +1,7 @@ const external = require("./core/external_api"); const core = require("./core/core"); +const fs = require("fs"); +const path = require("path"); function _getThemePage(page_name) { 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 }); } 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) { res.type("application/xml"); diff --git a/frontend/views/themes/default/css/settings.css b/frontend/views/themes/default/css/settings.css index 3e651c2..fe0b93c 100644 --- a/frontend/views/themes/default/css/settings.css +++ b/frontend/views/themes/default/css/settings.css @@ -66,6 +66,45 @@ .page .page-center .setting-list .setting:nth-child(even) { 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 { position: relative; diff --git a/frontend/views/themes/default/css/settings.scss b/frontend/views/themes/default/css/settings.scss index 3a0803e..f931841 100644 --- a/frontend/views/themes/default/css/settings.scss +++ b/frontend/views/themes/default/css/settings.scss @@ -73,6 +73,52 @@ 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 { diff --git a/frontend/views/themes/default/ejs/admin-settings.ejs b/frontend/views/themes/default/ejs/admin-settings.ejs index 60317b4..454a77e 100644 --- a/frontend/views/themes/default/ejs/admin-settings.ejs +++ b/frontend/views/themes/default/ejs/admin-settings.ejs @@ -1,74 +1,99 @@ - - <%- include("partials/document-head.ejs") %> - - - - - - <%- include("partials/header.ejs") %> -
-
-
<%= website_name %> Admin Settings
-
-
-
User registration
-
- -
-
-
-
Hide "Login" in navigation bar
-
- -
-
-
-
Serve ATOM feed
-
- -
-
-
-
Serve JSON feed
-
- -
-
-
-
Password minimum length
-
- -
-
-
-
Website Name
-
- -
-
-
-
Custom head
-
- -
-
-
-
-
+ + <%- include("partials/document-head.ejs") %> + + + + + + <%- include("partials/header.ejs") %> +
+
+
<%= website_name %> Admin Settings
+
+
+
User registration
+
+ +
+
+
+
Hide "Login" in navigation bar
+
+ +
+
+
+
Serve ATOM feed
+
+ +
+
+
+
Serve JSON feed
+
+ +
+
+
+
Password minimum length
+
+ +
+
+
+
Website Name
+
+ +
+
+
+
Custom head
+
+ +
+
+
+
+ +
+
Themes
+ +
+
+ + +
+ + <% for(theme of theme_data.installed) { %> + +
+
<%= theme %>
+
+ +
+
+ + <% } %> + + +
+
+
+ <%- include("partials/footer.ejs") %> diff --git a/frontend/views/themes/default/js/admin.js b/frontend/views/themes/default/js/admin.js index 577dc31..bf0274c 100644 --- a/frontend/views/themes/default/js/admin.js +++ b/frontend/views/themes/default/js/admin.js @@ -22,3 +22,28 @@ async function changeValue(setting_name, element) { 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."); + } +} diff --git a/yab.js b/yab.js index 780cb67..a16cbd2 100644 --- a/yab.js +++ b/yab.js @@ -15,9 +15,11 @@ app.set("views", path.join(__dirname, "frontend/views")); app.use(express.json({ limit: "500mb" })); app.use(express.urlencoded({ extended: false })); -// TODO: Does this persist previous themes? May cause security issues! -const refreshTheme = (theme_name) => app.use(express.static(path.join(__dirname, `frontend/views/themes/${theme_name}`))); -refreshTheme("default"); +app.use((req, res, next) => { + let theme = require("./backend/core/core").settings.theme; + let middleware = express.static(path.join(__dirname, `frontend/views/themes/${theme}`)); + middleware(req, res, next); +}); app.use( session({ @@ -34,9 +36,11 @@ app.post("/setting", checkAuthenticated, internal.postSetting); 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.delete("/api/theme", checkAuthenticated, internal.deleteTheme); 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.post("/api/theme", checkAuthenticated, internal.postTheme); // app.delete("/logout", page_scripts.logout);